Skip to content

fix(services): route under the public /services prefix without a stripping proxy - #6702

Open
mmabrouk wants to merge 1 commit into
release/v0.115.4from
fix/services-prefix-strip
Open

fix(services): route under the public /services prefix without a stripping proxy#6702
mmabrouk wants to merge 1 commit into
release/v0.115.4from
fix/services-prefix-strip

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 8, 2026

Copy link
Copy Markdown
Member

Why

The services app is published under /services on the shared host. Traefik and the AWS ALB strip that prefix before the request reaches the container, so the routes live at root. A managed ingress such as GKE cannot rewrite paths and forwards /services/... verbatim. On the GKE staging stage every services route answered 404, including /services/agent/v0/invoke, the endpoint the playground and the release gate drive.

The api already solves this with ApiPrefixStripMiddleware. The services app had nothing equivalent.

What changed

  • New services/entrypoints/prefix.py: an ASGI middleware that strips the prefix inbound, in a loop so a double prefix still routes, and never issues a redirect. The prefix comes from AGENTA_SERVICES_PATH_PREFIX, default /services; an empty value turns the strip off.
  • Registered last in services/entrypoints/main.py, so it runs before auth and routing.
  • Unit tests for root, prefixed, double-prefixed, bare-prefix and disabled cases.

Behavior behind Traefik is unchanged: a request that arrives already stripped has no prefix to remove.

How to verify

cd services && uv run python -m pytest -q oss/tests/pytest/unit/entrypoints/test_prefix_strip.py

Live: with this image the GKE staging stage answers /services/agent/v0/invoke and the release gate's chat journey runs.

https://claude.ai/code/session_01GZqpvjwLHgVnMrpKH8hzq8

…pping proxy

Traefik and the AWS ALB strip /services before a request reaches the
services container, so its routes live at root. A managed ingress such as
GKE cannot rewrite paths and forwards /services/... verbatim, which answered
404 on every route, including the agent invoke endpoint the playground uses.

Add an ASGI middleware that strips the prefix inbound, in a loop and without
redirects, the same way the api's ApiPrefixStripMiddleware does. The prefix
comes from AGENTA_SERVICES_PATH_PREFIX, default /services; an empty value
turns the strip off. Registered last so it runs before auth and routing.

Claude-Session: https://claude.ai/code/session_01GZqpvjwLHgVnMrpKH8hzq8
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 8, 2026 9:08pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved request routing when the /services path prefix is forwarded by managed ingress configurations.
    • Added support for repeated prefixes and configurable prefix handling without redirecting requests.

Walkthrough

The change adds configurable /services prefix stripping for HTTP and WebSocket requests. The middleware rewrites request paths before routing, supports repeated prefixes, preserves bare-prefix behavior, and is registered as the outermost FastAPI middleware.

Changes

Services prefix handling

Layer / File(s) Summary
Prefix stripping middleware
services/entrypoints/prefix.py
Adds ServicesPrefixStripMiddleware with environment and constructor configuration. It strips repeated prefixes, rewrites path and raw_path, and does not redirect requests.
Application integration and validation
services/entrypoints/main.py, services/oss/tests/pytest/unit/entrypoints/test_prefix_strip.py
Registers the middleware as the outermost middleware. Tests cover prefixed routes, repeated prefixes, bare prefixes, disabled stripping, and redirect-free responses.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🔵 Low · up to d19df

The new middleware enables services routes under a forwarded prefix, but encoded request paths may be altered after normalization and default configuration tests can be environment-dependent. These are bounded issues that should be addressed before relying on encoded-path behavior and test coverage.

Sequence Diagram(s)

sequenceDiagram
  participant Ingress
  participant ServicesPrefixStripMiddleware
  participant FastAPI
  Ingress->>ServicesPrefixStripMiddleware: Forward request with /services prefix
  ServicesPrefixStripMiddleware->>ServicesPrefixStripMiddleware: Strip configured prefix
  ServicesPrefixStripMiddleware->>FastAPI: Pass normalized request scope
  FastAPI-->>Ingress: Return routed response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: enabling services routes under the public /services prefix when the proxy does not strip it.
Description check ✅ Passed The description directly explains the ingress problem, middleware behavior, registration order, compatibility, tests, and verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/services-prefix-strip

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Advanced

Run ID: 0b94556f-dda7-4b29-bdf4-6ea613286bef

📥 Commits

Reviewing files that changed from the base of the PR and between 0c85a59 and d19dfa0.

📒 Files selected for processing (4)
  • services/entrypoints/main.py
  • services/entrypoints/prefix.py
  • services/oss/tests/pytest/unit/entrypoints/__init__.py
  • services/oss/tests/pytest/unit/entrypoints/test_prefix_strip.py

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.

scope = dict(scope)
scope["path"] = path
if isinstance(scope.get("raw_path"), (bytes, bytearray)):
scope["raw_path"] = path.encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🤖 get_repo_knowledge executed:

get_repo_knowledge Agenta-AI/agenta /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/conventions /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/learnings

Length of output: 12008


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
sed -n '1,140p' services/entrypoints/prefix.py
printf '%s\n' '--- raw_path references ---'
rg -n -C 3 'raw_path|scope\["path"\]|scope\.get\(["'\'']path|scope\.get\(["'\'']raw_path' services
printf '%s\n' '--- relevant project files ---'
git ls-files | rg '(^|/)(prefix|middleware|entrypoint|router|routing|asgi|websocket)' | head -80

Repository: Agenta-AI/agenta

Length of output: 6401


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- analogous API middleware ---'
sed -n '1,140p' api/oss/src/middlewares/prefix.py
printf '%s\n' '--- services prefix tests ---'
sed -n '1,240p' services/oss/tests/pytest/unit/entrypoints/test_prefix_strip.py
printf '%s\n' '--- API prefix tests ---'
sed -n '1,220p' api/oss/tests/pytest/unit/middlewares/test_prefix.py
printf '%s\n' '--- services raw_path consumers ---'
rg -n -C 4 'raw_path' services api/oss/src | head -160

Repository: Agenta-AI/agenta

Length of output: 10445


🤖 get_repo_knowledge executed:

get_repo_knowledge Agenta-AI/agenta /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/conventions /tmp/coderabbit-repo-knowledge/agenta-ai-agenta-4b53879a/learnings

Length of output: 14662


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- services entrypoint wiring ---'
sed -n '1,220p' services/entrypoints/main.py
printf '%s\n' '--- services middleware and raw_path references ---'
rg -n -C 5 'ServicesPrefixStripMiddleware|raw_path|scope\[["'\'']path["'\'']\]|scope\.get\(' services --glob '*.py'
printf '%s\n' '--- relevant diff ---'
git diff -- services/entrypoints/prefix.py services/oss/tests/pytest/unit/entrypoints/test_prefix_strip.py

Repository: Agenta-AI/agenta

Length of output: 12634


🌐 Web query:

official ASGI HTTP scope raw_path original encoded bytes specification

💡 Result:

According to the official ASGI specification, the raw_path key in the HTTP connection scope is defined as a byte string [1][2][3]. Its technical specification includes the following details: - Type: It must be a byte string [1][3]. - Content: It represents the original HTTP path component, excluding any query string [1][2][3]. It remains unmodified from the bytes as they were received by the web server [1][3]. - Status: It is an optional field [1][3]. If an underlying web server implementation is unable to provide the raw path, this field may be missing, in which case it defaults to None [1][3]. Unlike the path field, which is a Unicode string that has been processed to decode percent-encoded sequences and UTF-8 byte sequences, raw_path is intended to preserve the original byte-level representation of the path as received on the wire [1][2]. This allows applications to recover information that might otherwise be lost due to automatic unquoting by the server [4]. Note that it does not include the query string [1][3].

Citations:


Preserve the request encoding in raw_path.

When _strip removes the prefix, line 42 rebuilds raw_path from decoded path. For /services/caf%C3%A9, downstream receives b"/caf\xc3\xa9" instead of b"/caf%C3%A9". Update raw_path from its existing bytes, remove the bytes for the consumed prefix, and preserve the remaining encoded bytes.

from entrypoints.prefix import ServicesPrefixStripMiddleware


def _app(prefix=None) -> TestClient:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate default-prefix tests from AGENTA_SERVICES_PATH_PREFIX.

_app() passes None, so the middleware reads the process environment. If the test process sets AGENTA_SERVICES_PATH_PREFIX to another value, the default-prefix tests fail or validate the wrong prefix.

Clear this variable with monkeypatch for the default tests. Add a separate test for environment-based configuration.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-6702.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-6702-3938be2
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-09-08T21:17:59.269Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant