fix(dataflows): keep vendor API keys out of request errors

- FRED and Alpha Vantage authenticate with a query parameter, and requests
  quotes the full URL in HTTP, connection and timeout errors, so the key was
  written into any log or traceback that recorded one
- route both vendors through one request helper that re-raises a requests error
  as the same class with the key removed, carrying no request, response or
  exception chain, each of which still held the URL #1324
This commit is contained in:
Yijia-Xiao
2026-09-14 22:38:19 +00:00
parent 241638da68
commit b20c8e60a4
5 changed files with 110 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ import unittest
from unittest import mock
import pytest
import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
@@ -233,3 +234,53 @@ class FredRoutingTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
_KEY = "abcdef0123456789abcdef0123456789"
@pytest.mark.unit
class TestKeyKeptOutOfErrors:
"""The key travels as a query parameter, and requests quotes the full URL in
its error messages, so any log or traceback would carry it (#1324)."""
def _raises(self, side_effect):
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=side_effect), \
pytest.raises(requests.RequestException) as caught:
fred._request("series", {"series_id": "DGS10"})
return caught.value
def test_http_error_message_carries_no_key(self):
response = mock.Mock(status_code=502)
response.raise_for_status.side_effect = requests.HTTPError(
f"502 Server Error for url: https://api.stlouisfed.org/fred/series?api_key={_KEY}",
response=response,
)
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", return_value=response), \
pytest.raises(requests.HTTPError) as caught:
fred._request("series", {"series_id": "DGS10"})
exc = caught.value
assert _KEY not in str(exc) and _KEY not in repr(exc)
# The response and request carry the full URL, so they are not attached.
assert exc.response is None and exc.request is None
assert exc.__cause__ is None and exc.__context__ is None # no chain holds the key
def test_connection_error_before_any_response_carries_no_key(self):
exc = self._raises(requests.ConnectionError(
f"Max retries exceeded with url: /fred/series?series_id=DGS10&api_key={_KEY}"))
assert isinstance(exc, requests.ConnectionError)
assert _KEY not in str(exc) and exc.__context__ is None
@pytest.mark.unit
def test_error_without_the_key_in_its_message_still_drops_the_request():
# Some timeout messages omit the URL, but the attached request still has it.
import requests as rq
req = rq.Request("GET", f"https://api.stlouisfed.org/fred/series?api_key={_KEY}").prepare()
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=rq.Timeout("Read timed out.", request=req)), \
pytest.raises(rq.Timeout) as caught:
fred._request("series", {"series_id": "DGS10"})
assert caught.value.request is None