Skip to content

enhancement: optional strict-mode error envelope sanitization (OpenAI-conformant taxonomy) #199

Description

@moonming

Summary

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:127ProxyError::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::UpstreamStatus { status, message }"upstream returned HTTP {status}: {message}"message carries upstream provider error text.
    • 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.
  • Streaming: crates/aisix-proxy/src/chat.rs::build_sse_stream (post-fix(proxy): omit terminal [DONE] when stream terminates abnormally #198) emits the same err.to_string() payload inside the SSE error frame's message field via error_frame_payload(etype, &err.to_string()).

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:

  1. Map BridgeError::error_type() to the OpenAI taxonomy at the proxy boundary (not the bridge layer — bridge taxonomy is fine for internal logs / metrics):

    • Timeoutapi_error (or new explicit timeout if we want to extend OpenAI)
    • UpstreamStatus/UpstreamDecode/Transport/StreamAbortedapi_error
    • Configserver_error
  2. 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-value differentiatorbugSomething isn't workingenhancementNew feature or requestvulnerability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions