From 300522aac10b37d90b6dee67da8867b8dfb1be69 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2026 19:32:45 -0600 Subject: [PATCH] fix(backend): default GET list endpoints to JSON per spec 6.2 Spec 6.2 says: 'Default JSON response wraps the same data in a {items, total, returned, has_more} envelope so the frontend can paginate uniformly.' Previously the GET list endpoints defaulted to NDJSON (consistent with the parse endpoints), which forced every curl call to set Accept: application/json. Now NDJSON is strictly opt-in via Accept: application/x-ndjson; JSON is the default. Parse endpoints keep their existing NDJSON default (browser uploads). T8 test 'test_claims_default_accept_returns_ndjson' updated to assert the new default. --- backend/src/cyclone/api.py | 20 +++++++++----------- backend/tests/test_api_gets.py | 11 ++++++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index 265d6b3..244cca2 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -143,21 +143,19 @@ def _client_wants_json(request: Request) -> bool: def _wants_ndjson(request: Request) -> bool: - """Content negotiation for list endpoints: NDJSON wins ties, NDJSON is the - default when no Accept header is set (consistent with parse endpoints). + """Content negotiation for list endpoints: NDJSON is an opt-in, JSON is the + default (per spec 6.2: "Default JSON response wraps the same data in a + {items, total, returned, has_more} envelope so the frontend can paginate + uniformly"). Used by the GET list routes (/api/batches, /api/claims, /api/remittances, - /api/providers, /api/activity). When both ``application/x-ndjson`` and - ``application/json`` appear in Accept, NDJSON wins. When only JSON is - asked for, JSON wins. When neither is asked for (e.g. browser default - ``*/*`` or an empty Accept), NDJSON wins. + /api/providers, /api/activity). NDJSON is returned only when the client + explicitly sends ``Accept: application/x-ndjson`` (with or without + ``application/json``). Bare ``*/*``, an empty Accept, or an explicit + ``Accept: application/json`` all return the JSON envelope. """ accept = request.headers.get("accept", "") - if "application/x-ndjson" in accept: - return True - if "application/json" in accept: - return False - return True + return "application/x-ndjson" in accept def _ndjson_stream_list( diff --git a/backend/tests/test_api_gets.py b/backend/tests/test_api_gets.py index 4f07268..5f53df9 100644 --- a/backend/tests/test_api_gets.py +++ b/backend/tests/test_api_gets.py @@ -278,7 +278,12 @@ def test_claims_ndjson_wins_over_json_when_both_accepted(seeded_store): assert resp.headers["content-type"].startswith("application/x-ndjson") -def test_claims_default_accept_returns_ndjson(seeded_store): - """No Accept header → NDJSON (consistent with parse endpoints' default).""" +def test_claims_default_accept_returns_json(seeded_store): + """No Accept header → JSON envelope (per spec 6.2: JSON is the default for + list endpoints; NDJSON is the opt-in).""" resp = seeded_store.get("/api/claims") - assert resp.headers["content-type"].startswith("application/x-ndjson") + body = resp.json() + assert "items" in body + assert "total" in body + assert "returned" in body + assert "has_more" in body