Skip to content

fix: fetch existing record in-process for transaction validation - #214

Merged
alukach merged 2 commits into
mainfrom
fix/inprocess-fetch-existing
Oct 3, 2026
Merged

alukach merged 2 commits into
mainfrom
fix/inprocess-fetch-existing

Conversation

@alukach

@alukach alukach commented Sep 30, 2026

Copy link
Copy Markdown
Member

Problem

Cql2ValidateTransactionMiddleware fetches the existing record for PUT/PATCH/DELETE with its own httpx client pointed at upstream_url. When stac-auth-proxy is used as middleware around a STAC API, upstream_url points back at the same app, so that GET re-enters EnforceAuthMiddleware without credentials and fails (a 401 surfaced to the caller as a 502).

#201 fixes this by forwarding the caller's headers onto the GET, but forwarding everything also carries If-None-Match/Range (→ 304/206 with no JSON body → uncaught 500) and Accept-Encoding (e.g. zstd → undecodable body).

Change

Issue the GET in-process to the downstream ASGI app (self.app) instead of over HTTP:

  • Middleware mode: reaches the STAC API's routes directly. No network hop, no auth needed.
  • Proxy mode: goes through ReverseProxyHandler to the upstream exactly like any other request (same client, timeout, Forwarded headers).
  • The sub-request sits below the auth middleware in both modes, so no credentials are forwarded. Only host and accept: application/json are sent, which closes off the conditional and encoding problems above.
  • Removes the middleware's own httpx.AsyncClient and its upstream_url argument.
  • Non-200/404 responses and non-JSON bodies raise UpstreamError, which is mapped to the existing 502 UpstreamError response.

Behavior note: in the full stack the sub-request passes through Cql2ValidateResponseBodyMiddleware with the caller's filter, so a record the caller can't read is reported as 404, matching the existing "existing record doesn't match filter → 404" behavior.

Tests

  • TestFetchExistingMiddlewareMode: an outer auth stand-in rejects requests without Authorization. PUT/DELETE succeed, and the in-process GET arrives with no authorization/if-none-match/accept-encoding. Also covers denied-existing → 404, missing → 404, and downstream 500 → 502.
  • TestFetchExistingProxyMode: real ReverseProxyHandler over httpx.MockTransport. The upstream sees a clean GET followed by the PUT (with the caller's auth). An upstream 503 on the GET → 502 and the PUT is never forwarded.
  • Full suite: 489 passed, 92% coverage.

Supersedes #201.

🤖 Generated with Claude Code

Cql2ValidateTransactionMiddleware fetched the existing record for PUT/PATCH/
DELETE with its own httpx client pointed at upstream_url. When stac-auth-proxy
is used as middleware around a STAC API, upstream_url points back at the same
app, so that GET re-entered the auth middleware without credentials and failed
(401 surfaced as 502).

Issue the GET in-process to the downstream ASGI app instead. As middleware this
hits the STAC API's routes directly; as a proxy it goes through
ReverseProxyHandler to the upstream. Either way it never re-enters the auth
layer, so no credentials need forwarding. Only host/accept are sent, so caller
conditional (If-None-Match, Range) and Accept-Encoding headers can't produce a
304/206/undecodable body.

Supersedes #201.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 23s —— View job


✅ No blocking issues — safe to merge.

I read the diff and traced the sub-request through the middleware stack.

  • The in-process GET sits below EnforceAuthMiddleware in both modes, so it needs no credentials. This fixes the 401→502 problem.
  • Only host and accept: application/json are sent. That closes off the 304/206 and Accept-Encoding failures described in the PR.
  • Non-200/404 responses, non-JSON bodies and downstream exceptions all become UpstreamError, which is returned as a 502.
  • Cql2ValidateResponseBodyMiddleware returns 404 for a record that doesn't match the filter (line 111), so the "denied existing → 404" behavior holds.
  • The sub-scope's state is copied, so downstream writes don't leak into the caller's request. The method is GET, so this middleware can't recurse.

Non-blocking notes:

  • In proxy mode the pre-flight GET reaches upstream without the caller's Authorization. The old httpx client did the same, so this isn't a regression. An upstream that requires auth on GET will still produce a 502 on PUT/PATCH/DELETE. That's worth a line in the docs.
  • _fetch_existing buffers the whole response body without a size cap. Records are small, so this is fine in practice.

Simplify (ponytail)

  • Cql2ValidateTransactionMiddleware.py:~215-260: the 502 UpstreamError block is copy-pasted in _handle_update and _handle_delete, and the 404 block appears four times. Replace them with one _not_found() / _bad_gateway() helper, or fold the None/no-match check into a single if existing is None or not cql2_filter.matches(existing).
  • Cql2ValidateTransactionMiddleware.py UpstreamError: the exception is raised and caught inside the same file. Have _fetch_existing return a sentinel or the error response instead of introducing the exception class. This is optional, since the class reads clearly.

💰 Estimated review cost: $0.16 · 0m22s · 6 turns

@github-actions github-actions Bot added the fix label Sep 30, 2026
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.22%. Comparing base (be008ba) to head (8dd65a6).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #214      +/-   ##
==========================================
+ Coverage   89.76%   90.22%   +0.46%     
==========================================
  Files          30       30              
  Lines        1348     1361      +13     
  Branches      180      182       +2     
==========================================
+ Hits         1210     1228      +18     
+ Misses         97       92       -5     
  Partials       41       41              
Flag Coverage Δ
unittests 90.22% <100.00%> (+0.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Address review feedback on #214:
- Copy scope state for the in-process GET so downstream writes to
  request.state can't leak into the caller's request.
- Wrap the downstream call so a raised exception becomes UpstreamError (502)
  instead of an unhandled 500.
- Cover raised-exception, non-JSON body, and state isolation in tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review October 3, 2026 03:21
@alukach
alukach merged commit 3ffdd1d into main Oct 3, 2026
13 checks passed
@alukach
alukach deleted the fix/inprocess-fetch-existing branch October 3, 2026 03:22
alukach pushed a commit that referenced this pull request Oct 6, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.3.0](v1.2.0...v1.3.0)
(2026-10-06)


### Features

* add HTTPException handling in CQL2BuildFilter middleware
([#202](#202))
([5aeee7c](5aeee7c))
* **helm:** add servicemonitor support.
([#209](#209))
([979ceda](979ceda))


### Bug Fixes

* allow queryables endpoints for collections
([#204](#204))
([8b3d3c9](8b3d3c9))
* allow text in json response for CQl2RewriteLinksFilterMiddleware
([#205](#205))
([e55d85f](e55d85f))
* fetch existing record in-process for transaction validation
([#214](#214))
([3ffdd1d](3ffdd1d))
* keep headers from HTTPException responses built by middleware
([#212](#212))
([bd13264](bd13264))
* key OPA filter cache on full request context, not just Authorization
(GHSA-rm58-963w-252r)
([2a1c989](2a1c989))
* normalize root_path_skip_prefixes in ProcessLinksMiddleware
([#199](#199))
([1193600](1193600))
* reject ambiguous request paths before path-based checks
(GHSA-c42p-7w4w-p877)
([2a1c989](2a1c989))
* update reverse_proxy.py to return correct status code for upstream
request timeout
([#210](#210))
([be008ba](be008ba))
* URL-encode query string values when injecting CQL2 filter
(GHSA-c2p2-r6vx-2qc8)
([2a1c989](2a1c989))


### Documentation

* add security policy
([#218](#218))
([ed3a6d7](ed3a6d7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: ds-release-bot[bot] <116609932+ds-release-bot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant