You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The gateway's error.message field (OpenAI-shape envelope) and error.type field both leak internal-taxonomy / upstream-provider details to the caller. This bleeds across both the streaming and non-streaming paths uniformly, so this is a system-wide pre-existing gap rather than a streaming-specific issue.
Where it leaks
Both paths funnel BridgeError through err.to_string() for error.message and err.error_type() for error.type:
Non-streaming: crates/aisix-proxy/src/error.rs:127 — ProxyError::envelope() calls self.to_string() which, for ProxyError::Bridge(err), transparently forwards to BridgeError's Display impl. That Display formats include raw upstream body fragments and parser detail:
BridgeError::UpstreamDecode(s) → "upstream returned an unparseable body: {s}" — s is serde_json parser detail (e.g. "expected value at line 1 column 1").
BridgeError::Transport(s) → "transport error: {s}" — s is hyper/reqwest internals.
The error.type taxonomy from BridgeError::error_type() (crates/aisix-gateway/src/bridge.rs:109-118) emits upstream_decode_error, transport_error, stream_aborted, config_error — none of which appear in OpenAI's enumerated error.type values. Clients that switch on error.type will hit unknown values.
Why this matters
Sensitive information bleed: upstream provider error messages can carry account-shape hints, internal model names, prompt-fragment echoes, or rate-limit-policy detail that the gateway should not expose to its own callers.
Internal taxonomy leak: upstream_decode_error / stream_aborted / config_error reveal aisix-internal failure categorization. Callers should see a stable OpenAI-shape taxonomy.
Spec deviation: clients that key off the OpenAI taxonomy (e.g. retry policies based on error.type === "rate_limit_error") get unexpected types.
Scope
Affects every endpoint that wraps BridgeError into the wire envelope:
/v1/chat/completions (streaming + non-streaming)
/v1/embeddings
/v1/completions
/v1/responses (streaming + non-streaming)
/v1/images/generations
All of the above when fronted by failover or direct dispatch
Proposed fix
Two parts, both touching crates/aisix-proxy/src/error.rs:
Map BridgeError::error_type() to the OpenAI taxonomy at the proxy boundary (not the bridge layer — bridge taxonomy is fine for internal logs / metrics):
Timeout → api_error (or new explicit timeout if we want to extend OpenAI)
Sanitize error.message at the proxy boundary by mapping each BridgeError variant to a generic client-safe string:
UpstreamDecode(_) → "upstream returned an unparseable response"
Transport(_) → "upstream transport error"
Timeout { .. } → "upstream timed out"
StreamAborted → "upstream cancelled the response mid-stream"
UpstreamStatus { .. } → "upstream returned an error" (status is already conveyed via HTTP code)
Config(_) → "gateway misconfiguration"
Internal logs (via tracing::warn!(error = %err, ...)) keep the rich original detail for operator debugging.
This change must apply uniformly to both the streaming SSE error frame and the non-streaming JSON envelope so the two paths emit identical taxonomy + sanitized messages.
#153 covers the related case where an output-keyword guardrail leaks the matched literal back to the caller. The fix shape is similar (redact at the wire boundary, keep rich detail in logs) but the source is ProxyError::ContentFiltered rather than ProxyError::Bridge. Worth fixing in the same audit pass.
Surfaced by
Independent audit on PR #198 (HIGH-1 sensitive-info leakage in message, HIGH-2 internal taxonomy leak in type). Recommended both findings be filed as a separate issue rather than widening #198's scope, since the gap pre-exists and spans every endpoint that wraps BridgeError.
Summary
The gateway's
error.messagefield (OpenAI-shape envelope) anderror.typefield both leak internal-taxonomy / upstream-provider details to the caller. This bleeds across both the streaming and non-streaming paths uniformly, so this is a system-wide pre-existing gap rather than a streaming-specific issue.Where it leaks
Both paths funnel
BridgeErrorthrougherr.to_string()forerror.messageanderr.error_type()forerror.type:crates/aisix-proxy/src/error.rs:127—ProxyError::envelope()callsself.to_string()which, forProxyError::Bridge(err), transparently forwards toBridgeError's Display impl. That Display formats include raw upstream body fragments and parser detail:BridgeError::UpstreamStatus { status, message }→"upstream returned HTTP {status}: {message}"—messagecarries upstream provider error text.BridgeError::UpstreamDecode(s)→"upstream returned an unparseable body: {s}"—sisserde_jsonparser detail (e.g."expected value at line 1 column 1").BridgeError::Transport(s)→"transport error: {s}"—sis hyper/reqwest internals.crates/aisix-proxy/src/chat.rs::build_sse_stream(post-fix(proxy): omit terminal [DONE] when stream terminates abnormally #198) emits the sameerr.to_string()payload inside the SSE error frame'smessagefield viaerror_frame_payload(etype, &err.to_string()).The
error.typetaxonomy fromBridgeError::error_type()(crates/aisix-gateway/src/bridge.rs:109-118) emitsupstream_decode_error,transport_error,stream_aborted,config_error— none of which appear in OpenAI's enumerated error.type values. Clients that switch onerror.typewill hit unknown values.Why this matters
upstream_decode_error/stream_aborted/config_errorreveal aisix-internal failure categorization. Callers should see a stable OpenAI-shape taxonomy.error.type === "rate_limit_error") get unexpected types.Scope
Affects every endpoint that wraps
BridgeErrorinto the wire envelope:/v1/chat/completions(streaming + non-streaming)/v1/embeddings/v1/completions/v1/responses(streaming + non-streaming)/v1/images/generationsProposed fix
Two parts, both touching
crates/aisix-proxy/src/error.rs:Map
BridgeError::error_type()to the OpenAI taxonomy at the proxy boundary (not the bridge layer — bridge taxonomy is fine for internal logs / metrics):Timeout→api_error(or new explicittimeoutif we want to extend OpenAI)UpstreamStatus/UpstreamDecode/Transport/StreamAborted→api_errorConfig→server_errorSanitize
error.messageat the proxy boundary by mapping eachBridgeErrorvariant to a generic client-safe string:UpstreamDecode(_)→"upstream returned an unparseable response"Transport(_)→"upstream transport error"Timeout { .. }→"upstream timed out"StreamAborted→"upstream cancelled the response mid-stream"UpstreamStatus { .. }→"upstream returned an error"(status is already conveyed via HTTP code)Config(_)→"gateway misconfiguration"Internal logs (via
tracing::warn!(error = %err, ...)) keep the rich original detail for operator debugging.This change must apply uniformly to both the streaming SSE error frame and the non-streaming JSON envelope so the two paths emit identical taxonomy + sanitized messages.
Companion: #153
#153 covers the related case where an output-keyword guardrail leaks the matched literal back to the caller. The fix shape is similar (redact at the wire boundary, keep rich detail in logs) but the source is
ProxyError::ContentFilteredrather thanProxyError::Bridge. Worth fixing in the same audit pass.Surfaced by
Independent audit on PR #198 (HIGH-1 sensitive-info leakage in
message, HIGH-2 internal taxonomy leak intype). Recommended both findings be filed as a separate issue rather than widening #198's scope, since the gap pre-exists and spans every endpoint that wrapsBridgeError.