include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
use alien_error::{AlienError, GenericError, HumanLayerPresentation};
pub trait SdkResultExt<T> {
fn into_sdk_error(self) -> Result<T, AlienError<GenericError>>;
}
impl<T> SdkResultExt<ResponseValue<T>> for Result<ResponseValue<T>, Error<types::ApiError>> {
fn into_sdk_error(self) -> Result<ResponseValue<T>, AlienError<GenericError>> {
self.map_err(convert_sdk_error)
}
}
pub fn convert_sdk_error(err: Error<types::ApiError>) -> AlienError<GenericError> {
match err {
Error::ErrorResponse(response) => {
let status = response.status().as_u16();
let api_error = response.into_inner();
let context = context_with_request_id(api_error.context, None);
AlienError {
code: api_error.code.to_string(),
message: api_error.message.to_string(),
context,
hint: None,
retryable: api_error.retryable,
internal: false, http_status_code: Some(status),
source: api_error.source.and_then(parse_source_error),
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError {
message: api_error.message.to_string(),
}),
}
}
Error::CommunicationError(reqwest_err) => {
let retryable =
reqwest_err.is_connect() || reqwest_err.is_timeout() || reqwest_err.is_request();
let message = reqwest_failure_message("HTTP request", &reqwest_err);
AlienError {
code: "COMMUNICATION_ERROR".to_string(),
message: message.clone(),
context: reqwest_failure_context(&reqwest_err),
hint: None,
retryable,
internal: false,
http_status_code: reqwest_err.status().map(|s| s.as_u16()),
source: build_reqwest_source(&reqwest_err),
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message }),
}
}
Error::InvalidRequest(msg) => AlienError {
code: "INVALID_REQUEST".to_string(),
message: format!("Invalid Request: {}", msg),
context: None,
hint: None,
retryable: false,
internal: false,
http_status_code: Some(400),
source: None,
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError {
message: format!("Invalid Request: {}", msg),
}),
},
Error::ResponseBodyError(reqwest_err) => {
let message = reqwest_failure_message("HTTP response body read", &reqwest_err);
AlienError {
code: "RESPONSE_BODY_ERROR".to_string(),
message: message.clone(),
context: reqwest_failure_context(&reqwest_err),
hint: None,
retryable: true, internal: false,
http_status_code: reqwest_err.status().map(|s| s.as_u16()),
source: build_reqwest_source(&reqwest_err),
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message }),
}
}
Error::InvalidResponsePayload(bytes, json_err) => {
let raw_body = String::from_utf8_lossy(&bytes);
let truncated = if raw_body.len() > 1000 {
format!(
"{}...(truncated {} bytes)",
&raw_body[..1000],
raw_body.len() - 1000
)
} else {
raw_body.to_string()
};
AlienError {
code: "INVALID_RESPONSE_PAYLOAD".to_string(),
message: format!("Failed to parse response: {}", json_err),
context: Some(serde_json::json!({
"parseError": json_err.to_string(),
"responseBody": truncated,
})),
hint: None,
retryable: false,
internal: false,
http_status_code: None,
source: Some(Box::new(AlienError::new(GenericError {
message: json_err.to_string(),
}))),
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError {
message: format!("Failed to parse response: {}", json_err),
}),
}
}
Error::InvalidUpgrade(reqwest_err) => {
let message = reqwest_failure_message("HTTP connection upgrade", &reqwest_err);
AlienError {
code: "INVALID_UPGRADE".to_string(),
message: message.clone(),
context: reqwest_failure_context(&reqwest_err),
hint: None,
retryable: false,
internal: false,
http_status_code: reqwest_err.status().map(|s| s.as_u16()),
source: build_reqwest_source(&reqwest_err),
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message }),
}
}
Error::UnexpectedResponse(response) => {
let status = response.status().as_u16();
AlienError {
code: "UNEXPECTED_RESPONSE".to_string(),
message: format!(
"Unexpected response: {} {}",
status,
response.status().canonical_reason().unwrap_or("Unknown")
),
context: Some(serde_json::json!({
"status": status,
"url": response.url().to_string(),
})),
hint: None,
retryable: status >= 500, internal: false,
http_status_code: Some(status),
source: None,
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError {
message: format!("Unexpected response status: {}", status),
}),
}
}
Error::Custom(msg) => AlienError {
code: "SDK_HOOK_ERROR".to_string(),
message: msg.clone(),
context: None,
hint: None,
retryable: false,
internal: false,
http_status_code: None,
source: None,
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message: msg }),
},
}
}
fn context_with_request_id(
context: Option<serde_json::Value>,
request_id: Option<&str>,
) -> Option<serde_json::Value> {
let Some(request_id) = request_id else {
return context;
};
match context {
Some(serde_json::Value::Object(mut object)) => {
object
.entry("requestId")
.or_insert_with(|| serde_json::Value::String(request_id.to_string()));
Some(serde_json::Value::Object(object))
}
Some(value) => Some(serde_json::json!({
"requestId": request_id,
"context": value,
})),
None => Some(serde_json::json!({ "requestId": request_id })),
}
}
fn reqwest_failure_message(operation: &str, err: &reqwest::Error) -> String {
match err.url() {
Some(url) => format!("{operation} {} failed: {err}", url),
None => format!("{operation} failed: {err}"),
}
}
fn reqwest_failure_context(err: &reqwest::Error) -> Option<serde_json::Value> {
err.url().map(|url| {
serde_json::json!({
"url": url.to_string(),
})
})
}
fn build_reqwest_source(err: &reqwest::Error) -> Option<Box<AlienError<GenericError>>> {
use std::error::Error;
let mut sources = Vec::new();
let mut current: Option<&(dyn Error + 'static)> = err.source();
while let Some(src) = current {
sources.push(src.to_string());
current = src.source();
}
if sources.is_empty() {
return None;
}
let mut result: Option<Box<AlienError<GenericError>>> = None;
for msg in sources.into_iter().rev() {
let error = AlienError {
code: "GENERIC_ERROR".to_string(),
message: msg.clone(),
context: None,
hint: None,
retryable: false,
internal: false,
http_status_code: None,
source: result,
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message: msg }),
};
result = Some(Box::new(error));
}
result
}
fn parse_source_error(value: serde_json::Value) -> Option<Box<AlienError<GenericError>>> {
let obj = value.as_object()?;
let code = obj
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("NESTED_ERROR")
.to_string();
let message = obj
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Nested error")
.to_string();
let context = obj.get("context").cloned();
let retryable = obj
.get("retryable")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let nested_source = obj.get("source").cloned().and_then(parse_source_error);
Some(Box::new(AlienError {
code,
message: message.clone(),
context,
hint: None,
retryable,
internal: false,
http_status_code: None,
source: nested_source,
human_layer_presentation: HumanLayerPresentation::Normal,
error: Some(GenericError { message }),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_code_deref() {
let code = types::ApiErrorCode::try_from("TEST_ERROR").unwrap();
assert_eq!(code.as_str(), "TEST_ERROR");
}
#[test]
fn context_with_request_id_adds_request_id_to_empty_context() {
let context = super::context_with_request_id(None, Some("req_123")).unwrap();
assert_eq!(context["requestId"], "req_123");
}
#[test]
fn context_with_request_id_preserves_existing_context() {
let context = super::context_with_request_id(
Some(serde_json::json!({ "workspace": "demo" })),
Some("req_123"),
)
.unwrap();
assert_eq!(context["workspace"], "demo");
assert_eq!(context["requestId"], "req_123");
}
#[tokio::test]
async fn communication_error_includes_url_in_message_and_context() {
let reqwest_err = reqwest::Client::new()
.get("http://127.0.0.1:9/v1/whoami")
.send()
.await
.expect_err("localhost discard port should refuse the connection");
let error = super::convert_sdk_error(Error::CommunicationError(reqwest_err));
assert_eq!(error.code, "COMMUNICATION_ERROR");
assert!(error
.message
.starts_with("HTTP request http://127.0.0.1:9/v1/whoami failed:"));
assert_eq!(
error.context.as_ref().unwrap()["url"],
"http://127.0.0.1:9/v1/whoami"
);
}
}