Skip to content

fix: add ROOT_PATH_SKIP_PREFIXES to exempt sibling-service links from root_path rewriting 2#198

Merged
alukach merged 4 commits into
mainfrom
fix/scope-root-path-link-rewrite-local
Jul 23, 2026
Merged

fix: add ROOT_PATH_SKIP_PREFIXES to exempt sibling-service links from root_path rewriting 2#198
alukach merged 4 commits into
mainfrom
fix/scope-root-path-link-rewrite-local

Conversation

@lhoupert

Copy link
Copy Markdown
Contributor

closes #196

🤖 Claude text below 🤖

On shared-host deployments (e.g. eoapi-k8s defaults: this proxy at /stac with titiler at /raster, /vector, /browser on the same hostname), STAC items carry same-host links to sibling services (rel=xyz, tilejson, viewer, ...). ProcessLinksMiddleware prepends ROOT_PATH to every same-host link, so https://host/raster/... is served as https://host/stac/raster/... → 404. Netloc can't disambiguate these: with OVERRIDE_HOST=false the upstream honours forwarded headers and generates its own links with the request netloc too, and those must keep getting ROOT_PATH (per test_transform_with_forwarded_headers).

This adds an opt-in ROOT_PATH_SKIP_PREFIXES setting (comma-separated path prefixes, e.g. /raster,/vector,/browser): same-host links whose path matches a configured prefix are left untouched. Matching is path-segment-aware (/raster matches /raster and /raster/..., not /rasterfoo); trailing slashes are normalized. Unset (the default) leaves behaviour byte-identical to today — all existing tests pass unchanged; regression tests and docs added.

We hit this in production at EOPF-Explorer/platform-deploy#343 and currently run this guard as a patched image.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

AI-assisted: implementation and tests drafted with Claude, reviewed by me, and validated against the full test suite (478 passed) and pre-commit hooks (ruff, isort, mypy).

lhoupert and others added 2 commits July 22, 2026 16:08
… root_path rewriting

On shared-host deployments (e.g. eoapi-k8s defaults: proxy at /stac,
titiler at /raster, /vector, /browser on one hostname), STAC responses
carry same-host links to sibling services. ProcessLinksMiddleware cannot
distinguish these from its own links by netloc — with OVERRIDE_HOST=false
the upstream generates its links with the request netloc too — so it
prepends ROOT_PATH to them, producing broken links (/stac/raster/... 404).

Add an opt-in ROOT_PATH_SKIP_PREFIXES setting: same-host links whose path
matches a configured prefix (path-segment-aware) are left untouched.
Unset (default) keeps behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… prefix

- add a regression test asserting upstream-host links are still rewritten
  even when their path matches a skip prefix (the setting describes sibling
  services on the proxy's public hostname)
- raise on a bare '/' prefix instead of silently dropping it (it would
  otherwise read as 'skip everything' but do nothing)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the fix label Jul 23, 2026
@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @pantierra's task in 2m 57s —— View job


Claude's Task List

  • Gather context (PR diff, changed files, CLAUDE.md)
  • Review diff for correctness bugs, security issues, best-practice problems
  • Apply ponytail-review lens for over-engineering
  • Post final review

✅ No blocking issues — safe to merge.

Traced _update_link end-to-end for the new skip-prefix behavior in src/stac_auth_proxy/middleware/ProcessLinksMiddleware.py:61-126:

  • Same-host sibling links (e.g. /raster/...) correctly skip the ROOT_PATH prepend while still getting the host swapped to the client-facing netloc when applicable.
  • Segment-aware matching (path == prefix or path.startswith(f"{prefix}/")) correctly avoids /raster matching /rasterfoo.
  • test_transform_with_forwarded_headers (existing test, untouched) still passes unaffected since it doesn't set root_path_skip_prefixes — empty tuple means _matches_skip_prefix is always False, preserving prior behavior byte-for-byte.
  • The NoDecode annotation + pydantic-settings>=2.7.0 bump in config.py:82-84 is justified, not incidental: pydantic-settings' EnvSettingsSource attempts JSON-decoding Sequence fields before validators run, but only for values sourced from the environment (not direct kwargs) — which is exactly why test_root_path_skip_prefixes_from_environment (using monkeypatch.setenv) is the test that actually exercises this path. Without NoDecode, ROOT_PATH_SKIP_PREFIXES=/raster,/vector would fail JSON parsing before reaching parse_root_path_skip_prefixes.
  • Validator correctly rejects a bare / and non-/-prefixed values, and silently drops empty comma entries ("/raster,,"("/raster",)).
  • New middleware/config logic is scoped only to outbound link rewriting (app.py:173-179); RemoveRootPathMiddleware (incoming request path stripping) is untouched, which is correct since sibling-service links only ever appear in response bodies.
  • Docs (configuration.md, tips.md) and Helm schema/values are consistent with the new setting and its default.

No security issues (no new attack surface — this only narrows which links get a path prepended, never adds new redirection targets).

Simplify (ponytail): none — the latest commit ("Love you, too, ponytail.") already extracted the duplicated inline any(...) matching logic from both call sites into _matches_skip_prefix, so there's nothing left to cut.


💰 Estimated review cost: $1.02 · 2m57s · 32 turns

@pantierra
pantierra force-pushed the fix/scope-root-path-link-rewrite-local branch from f383dbd to 0a13430 Compare July 23, 2026 15:08
@pantierra
pantierra force-pushed the fix/scope-root-path-link-rewrite-local branch from 935f20c to 9b0361c Compare July 23, 2026 15:32
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.67%. Comparing base (6825f23) to head (9b0361c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #198      +/-   ##
==========================================
+ Coverage   89.42%   89.67%   +0.25%     
==========================================
  Files          30       30              
  Lines        1314     1337      +23     
  Branches      176      182       +6     
==========================================
+ Hits         1175     1199      +24     
  Misses         96       96              
+ Partials       43       42       -1     
Flag Coverage Δ
unittests 89.67% <100.00%> (+0.25%) ⬆️

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.

@alukach alukach left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @lhoupert & @pantierra!

@alukach
alukach merged commit 349986b into main Jul 23, 2026
12 checks passed
@alukach
alukach deleted the fix/scope-root-path-link-rewrite-local branch July 23, 2026 21:23
alukach pushed a commit that referenced this pull request Jul 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.2.0](v1.1.1...v1.2.0)
(2026-07-23)


### Features

* add optional prometheus metrics endpoint.
([#186](#186))
([6825f23](6825f23))


### Bug Fixes

* add ROOT_PATH_SKIP_PREFIXES to exempt sibling-service links from
root_path rewriting 2
([#198](#198))
([349986b](349986b))
* allow , delimited string in configuration
([#191](#191))
([df530a2](df530a2))
* allow Any type for classInput
([#182](#182))
([56aa3f5](56aa3f5))
* configurable upstream timeout.
([#174](#174))
([533bac6](533bac6))
* **helm:** make health path take root path into account.
([#189](#189))
([5236a0e](5236a0e))
* publish helm chart only on release
([#178](#178))
([c89f650](c89f650))
* **tests:** fix flaky health-check retry test caused by global
asyncio.sleep patch
([#193](#193))
([fec33b6](fec33b6))

---
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.

ProcessLinksMiddleware prepends ROOT_PATH to same-host links it does not serve (e.g. titiler on a shared host)

3 participants