use crate::{ZaiError, ZaiResult, client::error::codes};
pub const JSON_CONTENT_TYPES: &[&str] = &["application/json", "application/"];
pub const SSE_CONTENT_TYPE: &str = "text/event-stream";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpectedKind {
Json,
Sse,
Binary(&'static str),
}
pub fn validate_content_type(raw: &str, expected: ExpectedKind) -> ZaiResult<()> {
let lower = raw.to_ascii_lowercase();
let ok = match expected {
ExpectedKind::Json => JSON_CONTENT_TYPES
.iter()
.any(|p| lower == *p || lower.starts_with(p) || lower.ends_with("+json")),
ExpectedKind::Sse => lower.starts_with(SSE_CONTENT_TYPE),
ExpectedKind::Binary(mime) => {
let m = mime.to_ascii_lowercase();
lower == m || lower.starts_with(&m)
},
};
if ok {
Ok(())
} else {
Err(ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: format!("unexpected content-type: {raw:?} (expected {expected:?})"),
})
}
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum WireEnvelope {
Nested {
error: WireError,
},
Flat {
code: serde_json::Value,
message: String,
},
}
#[derive(serde::Deserialize)]
pub(crate) struct WireError {
code: serde_json::Value,
#[allow(dead_code)]
message: String,
}
impl WireEnvelope {
pub(crate) fn is_error(&self) -> bool {
match self {
WireEnvelope::Nested { .. } => true,
WireEnvelope::Flat { code, .. } => !is_success_code(code),
}
}
}
fn is_success_code(code: &serde_json::Value) -> bool {
match code {
serde_json::Value::Number(n) => n.as_u64() == Some(200),
serde_json::Value::String(s) => s == "200",
_ => false,
}
}
pub fn probe_error_envelope(body: &str) -> bool {
serde_json::from_str::<WireEnvelope>(body)
.ok()
.is_some_and(|env| env.is_error())
}
#[derive(Debug, Clone)]
pub struct BusinessError {
pub code: Option<serde_json::Value>,
pub message: String,
}
pub fn extract_error_envelope(body: &str) -> Option<BusinessError> {
let env = serde_json::from_str::<WireEnvelope>(body).ok()?;
if !env.is_error() {
return None;
}
match env {
WireEnvelope::Nested { error } => Some(BusinessError {
code: Some(error.code),
message: error.message,
}),
WireEnvelope::Flat { code, message } => Some(BusinessError {
code: Some(code),
message,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_content_type_accepted() {
assert!(validate_content_type("application/json", ExpectedKind::Json).is_ok());
assert!(
validate_content_type("application/json; charset=utf-8", ExpectedKind::Json).is_ok()
);
assert!(validate_content_type("application/vnd.api+json", ExpectedKind::Json).is_ok());
assert!(validate_content_type("text/plain", ExpectedKind::Json).is_err());
}
#[test]
fn sse_content_type_accepted() {
assert!(validate_content_type("text/event-stream", ExpectedKind::Sse).is_ok());
assert!(
validate_content_type("text/event-stream; charset=utf-8", ExpectedKind::Sse).is_ok()
);
assert!(validate_content_type("application/json", ExpectedKind::Sse).is_err());
}
#[test]
fn binary_content_type_matches_manifest_mime() {
assert!(validate_content_type("audio/pcm", ExpectedKind::Binary("audio/pcm")).is_ok());
assert!(
validate_content_type(
"application/octet-stream",
ExpectedKind::Binary("application/octet-stream")
)
.is_ok()
);
assert!(validate_content_type("text/html", ExpectedKind::Binary("audio/pcm")).is_err());
}
#[test]
fn probe_finds_business_error_envelopes() {
assert!(probe_error_envelope(r#"{"code":500,"message":"x"}"#));
assert!(probe_error_envelope(
r#"{"error":{"code":1302,"message":"x"}}"#
));
assert!(!probe_error_envelope(r#"{"code":200,"message":"ok"}"#));
assert!(!probe_error_envelope(r#"{"id":"x","choices":[]}"#));
}
}