-
Notifications
You must be signed in to change notification settings - Fork 30
[dont merge me!] fix(gateway): stop following upstream redirects on dispatched requests #951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,7 +58,12 @@ pub const DEFAULT_UPSTREAM_TIMEOUT: Duration = Duration::from_secs(30); | |
| /// MCP server behind an enterprise CA is exactly as common as a model | ||
| /// endpoint behind one, and the PEM has to be re-parsed rather than | ||
| /// reused because rmcp's `Certificate` is a different crate version's | ||
| /// type than the one `upstream_tls` caches for the workspace line. | ||
| /// type than the one `upstream_tls` caches for the workspace line; | ||
| /// - redirects are refused, as they are on every other client that | ||
| /// carries a caller's payload under a gateway-held credential. A | ||
| /// `tools/call` POSTs the caller's arguments under the MCP server's | ||
| /// `x-api-key` or bearer, and reqwest does not strip either on a hop | ||
| /// to whatever host a `Location` names. | ||
| /// | ||
| /// One client for all MCP upstreams = one shared pool, matching how the | ||
| /// provider bridges share theirs (auth is injected per-request by the | ||
|
|
@@ -69,6 +74,7 @@ fn shared_http_client() -> rmcp_reqwest::Client { | |
| .get_or_init(|| { | ||
| let cfg = aisix_gateway::upstream_http::config(); | ||
| let mut b = rmcp_reqwest::Client::builder() | ||
| .redirect(rmcp_reqwest::redirect::Policy::none()) | ||
| .pool_idle_timeout(cfg.pool_idle_timeout) | ||
| .tcp_keepalive(cfg.tcp_keepalive); | ||
| if let Some(d) = cfg.connect_timeout { | ||
|
|
@@ -112,7 +118,15 @@ fn shared_http_client() -> rmcp_reqwest::Client { | |
| if !cfg.tls.verify { | ||
| b = b.danger_accept_invalid_certs(true); | ||
| } | ||
| b.build().unwrap_or_else(|_| rmcp_reqwest::Client::new()) | ||
| // The connection settings are lost if the build fails; the | ||
| // redirect refusal must not be, which `Client::new()` would | ||
| // silently give back. | ||
| b.build().unwrap_or_else(|_| { | ||
| rmcp_reqwest::Client::builder() | ||
| .redirect(rmcp_reqwest::redirect::Policy::none()) | ||
| .build() | ||
| .expect("a client with only a redirect policy always builds") | ||
| }) | ||
|
Comment on lines
+121
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 \
'shared_http_client|extra_ca_pem|client_identity|danger_accept_invalid_certs|unwrap_or_else|\.build\(\)' \
crates/aisix-mcp/src/bridge.rs
curl -fsSL 'https://docs.rs/crate/reqwest/0.13.0/source/src/async_impl/client.rs' |
rg -n -C 6 'pub fn build|This method fails|tls_certs_merge|identity'Repository: api7/aisix Length of output: 50367 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- bridge.rs ---'
sed -n '45,135p' crates/aisix-mcp/src/bridge.rs
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 'rmcp|reqwest' Cargo.toml Cargo.lock crates/aisix-mcp/Cargo.toml
printf '%s\n' '--- related upstream configuration ---'
rg -n -C 5 'struct .*Tls|extra_ca_pem|client_identity|danger_accept_invalid_certs|pool_idle_timeout|tcp_keepalive|timeout' crates/aisix-gateway crates/aisix-mcp -g '*.rs' -g '*.toml' 2>/dev/null | head -240
printf '%s\n' '--- focused reqwest documentation ---'
curl -fsSL 'https://docs.rs/reqwest/0.13.0/src/reqwest/async_impl/client.rs.html' |
python3 -c '
import sys
from html import unescape
from re import sub
text = unescape(sys.stdin.read())
text = sub(r"<[^>]+>", "", text)
for needle in ("pub fn build", "This method fails if a TLS backend", "pub fn add_root_certificate", "pub fn identity", "pub fn danger_accept_invalid_certs"):
pos = text.find(needle)
print(f"\n--- {needle} ---")
print(text[max(0,pos-500):pos+1000] if pos >= 0 else "not found")
'Repository: api7/aisix Length of output: 42208 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
source = Path("crates/aisix-mcp/src/bridge.rs").read_text()
start = source.index("fn shared_http_client()")
end = source.index("\n}\n\n/// Header carrying", start)
function = source[start:end]
primary, fallback = function.split("b.build().unwrap_or_else", 1)
fallback = fallback.split("})", 1)[0]
required = [
"pool_idle_timeout",
"tcp_keepalive",
"connect_timeout",
"tcp_keepalive_interval",
"tcp_keepalive_retries",
"pool_max_idle_per_host",
"add_root_certificate",
"identity",
"danger_accept_invalid_certs",
]
print("Primary builder settings:")
for name in required:
print(f" {name}: {'present' if name in primary else 'absent'}")
print("Fallback builder settings:")
for name in required:
print(f" {name}: {'present' if name in fallback else 'absent'}")
assert "redirect(rmcp_reqwest::redirect::Policy::none())" in primary
assert "redirect(rmcp_reqwest::redirect::Policy::none())" in fallback
assert all(name not in fallback for name in required)
# The fallback expression contains an unconditional panic path.
assert '.expect("a client with only a redirect policy always builds")' in function
print("Result: fallback retains only the redirect policy and contains an unconditional expect panic.")
PY
printf '%s\n' '--- locked rmcp reqwest version and build documentation ---'
curl -fsSL 'https://docs.rs/reqwest/0.13.4/src/reqwest/async_impl/client.rs.html' |
python3 -c '
import sys, re
from html import unescape
text = unescape(sys.stdin.read())
text = re.sub(r"<[^>]+>", "", text)
for needle in (
"This method fails if a TLS backend",
"pub fn build(self)",
"pub fn identity",
"pub fn danger_accept_invalid_certs",
):
pos = text.find(needle)
print(f"\n--- {needle} ---")
print(text[max(0, pos-180):pos+420] if pos >= 0 else "not found")
'Repository: api7/aisix Length of output: 3437 Do not silently downgrade the MCP upstream client on build failure. When Propagate the original build error, or preserve the required configuration and report both failures. Remove the claim that a minimal client “always builds.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| }) | ||
| .clone() | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: api7/aisix
Length of output: 50366
🏁 Script executed:
Repository: api7/aisix
Length of output: 5564
Correct the
reqwestheader-behavior claim.reqwest0.13.4 removesAuthorizationon cross-host, cross-port, or cross-scheme redirects, but it does not remove arbitraryx-api-keyheaders. Keep the no-redirect rationale and explain that it prevents the request body and custom headers from reaching a new host. (source)🤖 Prompt for AI Agents
Source: Coding guidelines