From dd0f821b3b1c3e6f336e506bb94e417bab30ebb1 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 28 Aug 2026 18:46:24 +0200 Subject: [PATCH 1/4] fix: allow text in json response for CQl2RewriteLinksFilterMiddleware --- .../Cql2RewriteLinksFilterMiddleware.py | 57 ++++++++++--------- ...st_cql2_rewrite_links_filter_middleware.py | 19 +++++++ 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py index dff36fb..4e54dc8 100644 --- a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py +++ b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py @@ -124,35 +124,36 @@ async def _process_and_send_response( await send({"type": "http.response.body", "body": body, "more_body": False}) return - links = data.get("links") - if isinstance(links, list): - for link in links: - # Handle filter in query string - if "href" in link: - url = urlparse(link["href"]) - qs = parse_qs(url.query) - if "filter" in qs: - if user_filter is not None: - qs["filter"] = [user_filter.to_text()] - else: - qs.pop("filter", None) - qs.pop("filter-lang", None) - new_query = urlencode(qs, doseq=True) - link["href"] = urlunparse(url._replace(query=new_query)) - - # Handle filter in body (for POST links). The spec only - # requires cql2-json for POST bodies, but if the link advertises - # cql2-text we preserve that lang on the way out. - if "body" in link and isinstance(link["body"], dict): - if "filter" in link["body"]: - if user_filter is not None: - if link["body"].get("filter-lang") == "cql2-text": - link["body"]["filter"] = user_filter.to_text() + if isinstance(data, dict): + links = data.get("links") + if isinstance(links, list): + for link in links: + # Handle filter in query string + if "href" in link: + url = urlparse(link["href"]) + qs = parse_qs(url.query) + if "filter" in qs: + if user_filter is not None: + qs["filter"] = [user_filter.to_text()] else: - link["body"]["filter"] = user_filter.to_json() - else: - link["body"].pop("filter", None) - link["body"].pop("filter-lang", None) + qs.pop("filter", None) + qs.pop("filter-lang", None) + new_query = urlencode(qs, doseq=True) + link["href"] = urlunparse(url._replace(query=new_query)) + + # Handle filter in body (for POST links). The spec only + # requires cql2-json for POST bodies, but if the link advertises + # cql2-text we preserve that lang on the way out. + if "body" in link and isinstance(link["body"], dict): + if "filter" in link["body"]: + if user_filter is not None: + if link["body"].get("filter-lang") == "cql2-text": + link["body"]["filter"] = user_filter.to_text() + else: + link["body"]["filter"] = user_filter.to_json() + else: + link["body"].pop("filter", None) + link["body"].pop("filter-lang", None) # Send the modified response new_body = json.dumps(data).encode("utf-8") diff --git a/tests/test_cql2_rewrite_links_filter_middleware.py b/tests/test_cql2_rewrite_links_filter_middleware.py index 4dabda5..69097b8 100644 --- a/tests/test_cql2_rewrite_links_filter_middleware.py +++ b/tests/test_cql2_rewrite_links_filter_middleware.py @@ -93,6 +93,25 @@ async def test_endpoint(request: Request): data = response.json() assert data == {"links": "not a list"} + def test_text_in_json(self): + """Test text encoded as JSON.""" + app = FastAPI() + app.add_middleware(Cql2RewriteLinksFilterMiddleware) + + @app.get("/plain") + async def plain(): + return Response(content="not json", media_type="text/plain") + + @app.get("/text", response_model=str) + async def plain_text(): + return "text in json" + + client = TestClient(app) + response = client.get("/text") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + assert response.text == '"text in json"' + class TestMiddlewareStackSimulation: """Test middleware behavior by simulating the full middleware stack.""" From f48a501bc90496d015813410c243b6344359c448 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Mon, 21 Sep 2026 16:04:05 -0700 Subject: [PATCH 2/4] Update tests/test_cql2_rewrite_links_filter_middleware.py --- tests/test_cql2_rewrite_links_filter_middleware.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_cql2_rewrite_links_filter_middleware.py b/tests/test_cql2_rewrite_links_filter_middleware.py index 69097b8..04e624b 100644 --- a/tests/test_cql2_rewrite_links_filter_middleware.py +++ b/tests/test_cql2_rewrite_links_filter_middleware.py @@ -98,10 +98,6 @@ def test_text_in_json(self): app = FastAPI() app.add_middleware(Cql2RewriteLinksFilterMiddleware) - @app.get("/plain") - async def plain(): - return Response(content="not json", media_type="text/plain") - @app.get("/text", response_model=str) async def plain_text(): return "text in json" From 76ed7f6703e6f268b6203a6af49b98833b18cc43 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Mon, 21 Sep 2026 16:09:11 -0700 Subject: [PATCH 3/4] Refactor link filtering logic to reduce indentation --- .../Cql2RewriteLinksFilterMiddleware.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py index 4e54dc8..005ae62 100644 --- a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py +++ b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py @@ -124,36 +124,34 @@ async def _process_and_send_response( await send({"type": "http.response.body", "body": body, "more_body": False}) return - if isinstance(data, dict): - links = data.get("links") - if isinstance(links, list): - for link in links: - # Handle filter in query string - if "href" in link: - url = urlparse(link["href"]) - qs = parse_qs(url.query) - if "filter" in qs: - if user_filter is not None: - qs["filter"] = [user_filter.to_text()] - else: - qs.pop("filter", None) - qs.pop("filter-lang", None) - new_query = urlencode(qs, doseq=True) - link["href"] = urlunparse(url._replace(query=new_query)) - - # Handle filter in body (for POST links). The spec only - # requires cql2-json for POST bodies, but if the link advertises - # cql2-text we preserve that lang on the way out. - if "body" in link and isinstance(link["body"], dict): - if "filter" in link["body"]: - if user_filter is not None: - if link["body"].get("filter-lang") == "cql2-text": - link["body"]["filter"] = user_filter.to_text() - else: - link["body"]["filter"] = user_filter.to_json() - else: - link["body"].pop("filter", None) - link["body"].pop("filter-lang", None) + links = data.get("links") if isinstance(data, dict) else None + for link in links if isinstance(links, list) else []: + # Handle filter in query string + if "href" in link: + url = urlparse(link["href"]) + qs = parse_qs(url.query) + if "filter" in qs: + if user_filter is not None: + qs["filter"] = [user_filter.to_text()] + else: + qs.pop("filter", None) + qs.pop("filter-lang", None) + new_query = urlencode(qs, doseq=True) + link["href"] = urlunparse(url._replace(query=new_query)) + + # Handle filter in body (for POST links). The spec only requires + # cql2-json for POST bodies, but if the link advertises cql2-text + # we preserve that lang on the way out. + link_body = link["body"] if "body" in link else None + if not isinstance(link_body, dict) or "filter" not in link_body: + continue + if user_filter is None: + link_body.pop("filter", None) + link_body.pop("filter-lang", None) + elif link_body.get("filter-lang") == "cql2-text": + link_body["filter"] = user_filter.to_text() + else: + link_body["filter"] = user_filter.to_json() # Send the modified response new_body = json.dumps(data).encode("utf-8") From bbf83e337bc98873843ad9117b9aa251ca43b67c Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Mon, 21 Sep 2026 17:22:36 -0700 Subject: [PATCH 4/4] fix: make test_text_in_json exercise the filter path; use dict.get Address PR review: the test never set a cql2 filter, so the middleware passed through and the isinstance(data, dict) guard was never hit. Co-Authored-By: Claude Fable 5.1 --- .../middleware/Cql2RewriteLinksFilterMiddleware.py | 2 +- tests/test_cql2_rewrite_links_filter_middleware.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py index 005ae62..bd2be2f 100644 --- a/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py +++ b/src/stac_auth_proxy/middleware/Cql2RewriteLinksFilterMiddleware.py @@ -142,7 +142,7 @@ async def _process_and_send_response( # Handle filter in body (for POST links). The spec only requires # cql2-json for POST bodies, but if the link advertises cql2-text # we preserve that lang on the way out. - link_body = link["body"] if "body" in link else None + link_body = link.get("body") if not isinstance(link_body, dict) or "filter" not in link_body: continue if user_filter is None: diff --git a/tests/test_cql2_rewrite_links_filter_middleware.py b/tests/test_cql2_rewrite_links_filter_middleware.py index 04e624b..60d0b05 100644 --- a/tests/test_cql2_rewrite_links_filter_middleware.py +++ b/tests/test_cql2_rewrite_links_filter_middleware.py @@ -96,7 +96,7 @@ async def test_endpoint(request: Request): def test_text_in_json(self): """Test text encoded as JSON.""" app = FastAPI() - app.add_middleware(Cql2RewriteLinksFilterMiddleware) + _install_middlewares(app, "id='foo'") @app.get("/text", response_model=str) async def plain_text():