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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user