fix: forward Google's exact error messages to client

Root cause: errors from Google were being swallowed, replaced with
placeholders like 'Google API returned HTTP 400' or '[Timeout waiting
for response]', or silently converted to fake 'incomplete' responses.

Changes across all endpoints (/v1/chat/completions, /v1/responses,
/v1/gemini, /v1/search):

Error message fidelity:
- UpstreamError message now includes Google's status prefix: [STATUS] msg
- Falls back to raw body if JSON parsing fails (protobuf, HTML, etc.)
- ErrorDetail gains optional code and param fields

Timeout handling:
- poll_for_response returns UpstreamError(504, DEADLINE_EXCEEDED) on timeout
  instead of '[Timeout waiting for AI response]' placeholder text
- Streaming timeouts emit proper error events, not fake content
- Sync bypass timeouts return 504 Gateway Timeout, not 200 incomplete

Missing error checks added:
- responses.rs sync bypass: added upstream_error check in polling loop
- gemini.rs sync bypass: added upstream_error check in polling loop
- gemini.rs streaming: added upstream_error check in polling loop
  (was completely missing — errors only handled in sync path)

DRY helpers:
- upstream_error_message(): shared exact message extraction
- upstream_error_type(): shared Google→OpenAI error type mapping
- All streaming handlers use these instead of inline formatting
This commit is contained in:
Nikketryhard
2026-02-16 19:30:32 -06:00
parent 931e1cc5a1
commit a47c572e48
6 changed files with 171 additions and 106 deletions

View File

@@ -20,17 +20,18 @@ pub(crate) fn err_response(
error: ErrorDetail {
message,
error_type: error_type.to_string(),
code: Some(status.as_u16()),
param: None,
},
};
(status, Json(body)).into_response()
}
/// Convert a MITM-captured upstream error from Google into an HTTP response.
/// Maps Google's HTTP status codes and preserves the error message.
/// Forwards Google's exact error message and HTTP status code to the client.
pub(crate) fn upstream_err_response(
err: &crate::mitm::store::UpstreamError,
) -> axum::response::Response {
// Map Google's status code to HTTP status
let status = StatusCode::from_u16(err.status).unwrap_or(StatusCode::BAD_GATEWAY);
// Map Google error status to OpenAI-style error type
@@ -43,12 +44,75 @@ pub(crate) fn upstream_err_response(
_ => "upstream_error",
};
let message = err
.message
.clone()
.unwrap_or_else(|| format!("Google API returned HTTP {}", err.status));
// Use Google's exact error message. Try parsed message first, then raw body.
let message = if let Some(ref msg) = err.message {
// Include Google's error status for context if available
if let Some(ref gstatus) = err.error_status {
format!("[{gstatus}] {msg}")
} else {
msg.clone()
}
} else if !err.body.is_empty() {
// No parsed message — forward the raw body as-is so the client
// sees exactly what Google returned (protobuf, HTML, etc.)
err.body.clone()
} else {
format!("Google API error: HTTP {}", err.status)
};
err_response(status, message, error_type)
// Extract param hint from Google's error details if available
let param = serde_json::from_str::<serde_json::Value>(&err.body)
.ok()
.and_then(|v| {
v["error"]["details"]
.as_array()
.and_then(|details| {
details.iter().find_map(|d| {
d["fieldViolations"]
.as_array()
.and_then(|fv| fv.first())
.and_then(|v| v["field"].as_str().map(|s| s.to_string()))
})
})
});
let body = ErrorResponse {
error: ErrorDetail {
message,
error_type: error_type.to_string(),
code: Some(err.status),
param,
},
};
(status, Json(body)).into_response()
}
/// Extract the exact error message from a MITM-captured upstream error.
/// Preserves Google's original message verbatim. Used by streaming handlers.
pub(crate) fn upstream_error_message(err: &crate::mitm::store::UpstreamError) -> String {
if let Some(ref msg) = err.message {
if let Some(ref gstatus) = err.error_status {
format!("[{gstatus}] {msg}")
} else {
msg.clone()
}
} else if !err.body.is_empty() {
err.body.clone()
} else {
format!("Google API error: HTTP {}", err.status)
}
}
/// Map Google's error status to OpenAI-compatible error type string.
pub(crate) fn upstream_error_type(err: &crate::mitm::store::UpstreamError) -> &'static str {
match err.error_status.as_deref() {
Some("INVALID_ARGUMENT") => "invalid_request_error",
Some("RESOURCE_EXHAUSTED") => "rate_limit_error",
Some("PERMISSION_DENIED") | Some("UNAUTHENTICATED") => "authentication_error",
Some("NOT_FOUND") => "not_found_error",
Some("INTERNAL") | Some("UNAVAILABLE") => "server_error",
_ => "upstream_error",
}
}
pub(crate) fn now_unix() -> u64 {