Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(proxy): entry-level URL rewriting via proxy.url_rewrites by jarvis9443 · Pull Request #881 · api7/aisix · GitHub
Skip to content

feat(proxy): entry-level URL rewriting via proxy.url_rewrites - #881

Merged
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites
Aug 4, 2026
Merged

feat(proxy): entry-level URL rewriting via proxy.url_rewrites#881
jarvis9443 merged 8 commits into
mainfrom
feat/url-rewrites

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Replaces #878, which GitHub auto-closed when its stacked base branch (feat/mcp-scoped-endpoint, #875) was deleted on merge; same branch, now based on main.

What

Adds proxy.url_rewrites: an ordered list of entry-level URL rewrite rules applied to every proxy-listener request before route matching (the admin and metrics listeners are unaffected).

proxy:
url_rewrites:
- name: per-server-mcp-compatmatch: "^/mcp-servers/([^/]+)/mcp$"rewrite: "/mcp/$1"
  • The first rule whose match regex matches the request path rewrites it — once, no cascading — and the request then flows through the normal endpoint (auth, ACL, quota, metrics labelling) exactly as if the client had sent the rewritten path. A miss leaves the request untouched.
  • match runs against the raw, percent-encoded path (no decoding, no normalization — documented; unlike gateways that match a decoded $uri). rewrite replaces the matched portion; $1/${name} expand capture groups; the query string is preserved as sent.
  • Startup validation rejects what would otherwise fail silently at runtime: invalid regexes, references to capture groups the pattern doesn't define (the engine expands those to empty — traffic would land on the wrong endpoint), ?/#/whitespace in the template (query absorption / fragment truncation), and patterns matching the empty string (would fire on every request). A template that still assembles an invalid path at runtime logs a warning naming the rule and serves the original path.
  • Env-only deployments (the chart injects config purely through AISIX_* vars, which cannot express a structured list) set the whole list as one JSON array: AISIX_PROXY__URL_REWRITES='[{"match":"...","rewrite":"..."}]'.
  • Rules compile once at boot; the middleware wrapper is only built when rules are configured, so deployments without rules pay nothing. Request ids cover the rewrite layer, so its fired/failed log lines carry the request span.

Implementation note: axum's Router::layer middleware runs after route matching, so a URI rewritten there could never change which route matches. The rewrite therefore wraps the whole router as the fallback of an outer router, giving it a genuine pre-routing seat.

Why

Lets operators map legacy URL shapes onto AISIX endpoints without client changes. The flagship scenario (api7/AISIX-Cloud#1219): clients migrating from gateways that expose one URL per MCP server (/mcp-servers/{service}/{path}) keep their configured URLs and original tool names — one rule maps the URL onto the /mcp/{server} endpoint from #875, and the whole existing governance chain applies unchanged.

Design comparison (per repo rule): mainstream gateways all ship a regex path-rewrite primitive with matched-portion replacement and capture-group templates (route-plugin, per-route rewrite, or middleware forms). Ours differs in placement only — a gateway-global ordered rule list instead of per-route config — because AISIX's routes are fixed built-in endpoints and the layer's purpose is mapping external URL space onto them; first-match-wins order replaces per-route attachment. LiteLLM offers no operator-configurable equivalent (its per-server MCP alias route is an internal fixed rewrite of the same shape), so route-rewrite plugins of general-purpose gateways are the reference baseline.

Rewriting cannot bypass governance: it only re-targets which proxy endpoint serves the request, and every endpoint enforces its own auth/ACL/quota after the rewrite; the admin surface lives on a separate listener the layer never touches.

Follow-up (tracked): the public DP Helm chart should surface urlRewrites as first-class values at the next release sync, so chart users don't need the env JSON form — api7/api7-helm-chart#331.

Tests

  • crates/aisix-proxy/src/rewrite.rs — unit + router-level: capture groups, matched-portion + first-occurrence semantics, named/braced references, query preservation, raw percent-encoded matching, warn-fallback serving the original path, first-rule-wins through the real router, no-rules passthrough.
  • crates/aisix-core/src/config.rs — config load, env JSON-string form, and boot rejection of invalid regexes, unknown group references, forbidden template characters, and empty-matching patterns.
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts — real binary + etcd + real MCP upstream: the full migration scenario (legacy per-server URL + original tool name end to end), generic non-MCP mapping, routing with a query present, miss-passthrough (canonical paths intact, unmatched legacy tails 404).

config.example.yaml / config.managed.yaml document the block. Fixes api7/AISIX-Cloud#1219.

The path names a registered mcp_servers entry; the endpoint presents that
server alone: initialize reports its name, tools/list returns the
upstream's original (un-namespaced) tool names, and tools/call accepts
both the bare and the namespaced spelling. A name prefixed with a
different registered server fails closed; an unregistered prefix stays a
bare name. Unknown/disabled servers 404 after auth, with no fallback to
the aggregate.
Governance is the same pipeline as /mcp: per-tool ACL still evaluates
the namespaced form (a grant means one thing on every endpoint, and the
scoped surface cannot widen a key's grant), per-server rate limits and
usage attribution key on the path's server rather than the tool-name
prefix, and guardrails run both directions. /mcp itself is unchanged.
Part of AISIX-Cloud#1219: gives per-server-URL clients a native scoped
endpoint; the entry-level URL rewrite that maps legacy URL shapes onto
it ships separately.
Independent review findings on the scoped endpoint:
- tools/list stripped the namespace prefix unconditionally, so an
upstream tool whose literal name starts with a registered server's
prefix was advertised under a spelling tools/call would re-strip
(mis-dispatch) or fail closed on. Strip only when the bare name
round-trips; colliding names stay namespaced — that spelling is the
callable one.
- The proxy's attribution peek parsed the name with its own logic,
which diverged from the gateway for server names ending in '_'.
Both sides now share one primitive (strip_server_prefix), which is
whole-string-prefix based, so such names namespace cleanly too.
- Pin the remaining review gaps in tests: the namespaced spelling and
the aggregated endpoint share the per-server rate-limit bucket, and
/mcp/{server} vs /mcp/ metric labels can't regress by arm reorder.
An ordered list of {match, rewrite} regex rules applied to every
proxy-listener request before route matching (admin/metrics listeners
unaffected): the first matching rule rewrites the path once — no
cascading — and the request then flows through the normal endpoint
(auth, ACL, quota, metrics labelling) as if the client had sent the
rewritten path. Replacement substitutes the matched portion with
$1/${name} capture-group expansion; the query string is preserved; a
miss leaves the request untouched. Invalid regexes fail startup.
Because axum's Router::layer middleware runs after route matching, the
rewrite gets its pre-routing seat by wrapping the whole router as the
fallback of an outer router; the wrapper is only built when rules are
configured, so the default path pays nothing.
Lets operators map legacy URL shapes onto AISIX endpoints without
client changes — e.g. per-server MCP paths like /mcp-servers/{svc}/mcp
onto the /mcp/{server} endpoint, completing the migration scenario of
api7/AISIX-Cloud#1219 together with the scoped-endpoint PR.
Review follow-up: the foreign set excluded disabled entries, so toggling
another server's enabled flag changed which names a scope would serve
bare vs fail closed on. Reserve every other registered name regardless
of enabled state; the round-trip listing keeps the colliding literal
names namespaced, so they stay callable. Also assert the ACL rejection
wording in the scoped e2e denial case.
Independent review findings on the rewrite layer:
- proxy.url_rewrites is a struct list, which AISIX_* env vars — the only
config channel in chart-driven deployments — cannot express. The field
now also accepts one JSON array in a string, so
AISIX_PROXY__URL_REWRITES='[{...}]' works.
- Only the match regex was validated; the template could reference an
unknown capture group (expands to empty — every legacy request lands
on the wrong endpoint, silently), or carry '?'/'#'/whitespace (absorbs
the caller's query / truncates the path as a fragment). Config::validate
now rejects unknown group references, forbidden template characters,
and patterns that match the empty string (which would fire on every
request).
- Request ids now cover the rewrite layer: ensure_request_id moved
outside the wrapper, so fired/failed rewrite logs carry the request
span.
- Pin the remaining review gaps: warn-fallback serves the original path
(router-level), raw percent-encoded matching (no decode/normalize),
and first-occurrence replacement for unanchored patterns. Document the
raw-path semantics and the env JSON form.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in:2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 511263a5-ef35-481b-ba87-9240d14122c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5bddcf9 and 1483221.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • config.example.yaml
  • config.managed.yaml
  • crates/aisix-admin/src/playground_handler.rs
  • crates/aisix-core/Cargo.toml
  • crates/aisix-core/src/config.rs
  • crates/aisix-core/src/lib.rs
  • crates/aisix-proxy/Cargo.toml
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/models.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/rewrite.rs
  • crates/aisix-proxy/src/state.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts
  • tests/e2e/src/harness/app.ts

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

@jarvis9443
jarvis9443 merged commit 0b0cfac into mainAug 4, 2026
9 checks passed
@jarvis9443
jarvis9443 deleted the feat/url-rewrites branch August 4, 2026 10:08
Sign up for freeto 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

@jarvis9443