| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- """A refused header's value never leaves the API in a JSON response.
- An HTTP client refusing a header (a stored token with a stray space or line
- break) quotes the value in its error, and that text reaches responses from many
- places: stored provider and backup errors (including rows written before this
- was masked anywhere), restore and connection-test results, status endpoints.
- ``RefusedHeaderResponseMask`` masks it in every JSON body on the way out.
- """
- from __future__ import annotations
- import json
- import pytest
- from fastapi import FastAPI, HTTPException
- from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, StreamingResponse
- from httpx import ASGITransport, AsyncClient
- from backend.app.core.response_redaction import RefusedHeaderResponseMask, redact_refused_headers_json
- # Made up. Never paste a value from a real log here.
- SECRET = "resp-value-5"
- QUOTE = f"Illegal header value b'Bearer {SECRET} '"
- def _app(manifest=None) -> FastAPI:
- app = FastAPI()
- @app.api_route("/manifest.json", methods=["GET", "HEAD"])
- async def manifest_file():
- return FileResponse(manifest, media_type="application/manifest+json")
- @app.get("/model")
- async def model():
- return {"last_error": QUOTE, "other": "kept", "n": 3}
- @app.get("/nested")
- async def nested():
- return [{"message": f"LocalProtocolError: {QUOTE}"}, {"message": f'with "quotes" {QUOTE}'}]
- @app.get("/raised")
- async def raised():
- raise HTTPException(status_code=502, detail=f"Could not reach it: {QUOTE}")
- @app.get("/explicit")
- async def explicit():
- return JSONResponse({"error": QUOTE}, status_code=400)
- @app.get("/text")
- async def text():
- return PlainTextResponse(QUOTE)
- @app.get("/stream")
- async def stream():
- async def body():
- yield b'{"error": "'
- yield QUOTE.encode()
- yield b'"}'
- return StreamingResponse(body(), media_type="application/json")
- app.add_middleware(RefusedHeaderResponseMask)
- return app
- async def _get(path: str):
- async with AsyncClient(transport=ASGITransport(app=_app()), base_url="http://test") as client:
- return await client.get(path)
- @pytest.mark.asyncio
- async def test_a_head_request_keeps_the_length_of_the_body_it_omits(tmp_path):
- """HEAD sends no body; the length still describes the GET body."""
- manifest = tmp_path / "manifest.json"
- manifest.write_text('{"name": "Bambuddy"}')
- async with AsyncClient(transport=ASGITransport(app=_app(manifest)), base_url="http://test") as client:
- head = await client.head("/manifest.json")
- get = await client.get("/manifest.json")
- assert head.headers["content-length"] == get.headers["content-length"] == str(len('{"name": "Bambuddy"}'))
- @pytest.mark.asyncio
- @pytest.mark.parametrize("path", ["/model", "/nested", "/raised", "/explicit"])
- async def test_json_responses_carry_no_header_value(path):
- response = await _get(path)
- assert SECRET not in response.text
- assert "Illegal header value [REDACTED]" in response.text
- json.loads(response.text) # still valid JSON
- assert int(response.headers["content-length"]) == len(response.content)
- @pytest.mark.asyncio
- async def test_the_rest_of_the_body_is_kept():
- body = (await _get("/model")).json()
- assert body == {"last_error": "Illegal header value [REDACTED]", "other": "kept", "n": 3}
- @pytest.mark.asyncio
- async def test_a_streamed_json_body_is_masked_too():
- response = await _get("/stream")
- assert SECRET not in response.text
- @pytest.mark.asyncio
- async def test_other_content_types_pass_untouched():
- """Only JSON is the API's; a text body is left as its route wrote it."""
- assert (await _get("/text")).text == QUOTE
- @pytest.mark.parametrize(
- ("body", "expected"),
- [
- (b'{"a": "x Illegal header name b\'X-1\' y", "b": 1}', b'{"a": "x Illegal header name [REDACTED]", "b": 1}'),
- (b'{"a": "Illegal header value b\'\\"q\\" s\'"}', b'{"a": "Illegal header value [REDACTED]"}'),
- (b'{"a": "nothing here"}', b'{"a": "nothing here"}'),
- ],
- )
- def test_the_mask_ends_with_the_json_string(body, expected):
- assert redact_refused_headers_json(body) == expected
- def test_main_installs_the_mask_next_to_the_routes():
- """Innermost: outer middleware re-stream the body in chunks."""
- from backend.app.main import app
- assert app.user_middleware[-1].cls is RefusedHeaderResponseMask
|