use std::time::Duration;
use aimux_core::{AiMuxError, ApiCallError};
use crate::handle_fetch_error::handle_fetch_error;
use crate::http::{HttpBody, HttpMethod, HttpRequest, PreparedRequest};
use crate::multipart::MultipartForm;
use crate::response_handler::{ResponseHandler, ResponseHandlerInput, ResponseHandlerOutput};
pub async fn post_json_to_api<T>(
request: HttpRequest,
body: serde_json::Value,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
let request_body_values = crate::logging::redact_error_context(body.clone());
call_to_api(
request.prepare(HttpMethod::Post, HttpBody::Json(body)),
request_body_values,
successful_response_handler,
failed_response_handler,
)
.await
}
pub async fn post_form_data_to_api<T>(
request: HttpRequest,
form_data: MultipartForm,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
let (content, content_type, values) = form_data.into_parts();
call_to_api(
request.prepare(HttpMethod::Post, HttpBody::Bytes(content, content_type)),
crate::logging::redact_error_context(values),
successful_response_handler,
failed_response_handler,
)
.await
}
pub async fn post_to_api<T>(
request: HttpRequest,
body: HttpBody,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
let request_body_values = crate::logging::redact_request_values(&body);
call_to_api(
request.prepare(HttpMethod::Post, body),
request_body_values,
successful_response_handler,
failed_response_handler,
)
.await
}
const EXCHANGE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) async fn call_to_api<T>(
request: PreparedRequest,
request_body_values: serde_json::Value,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
let response_timeout = request
.response_timeout
.unwrap_or(EXCHANGE_RESPONSE_TIMEOUT);
call_to_api_with_deadline(
request,
request_body_values,
successful_response_handler,
failed_response_handler,
response_timeout,
)
.await
}
pub(crate) async fn call_to_api_with_deadline<T>(
request: PreparedRequest,
request_body_values: serde_json::Value,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
response_timeout: Duration,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
let url = crate::http::sanitized_request_url(&request.url);
if successful_response_handler.is_streaming() {
return execute_exchange(
request,
request_body_values,
successful_response_handler,
failed_response_handler,
)
.await;
}
let exchange = execute_exchange(
request,
request_body_values.clone(),
successful_response_handler,
failed_response_handler,
);
match tokio::time::timeout(response_timeout, exchange).await {
Ok(result) => result,
Err(_) => Err(AiMuxError::ApiCall(Box::new(ApiCallError {
is_retryable: true,
..ApiCallError::new(
format!(
"Request exceeded the {}s exchange response timeout",
response_timeout.as_secs()
),
url,
request_body_values,
)
}))),
}
}
async fn execute_exchange<T>(
request: PreparedRequest,
request_body_values: serde_json::Value,
successful_response_handler: ResponseHandler<T>,
failed_response_handler: ResponseHandler<AiMuxError>,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
crate::logging::auto_init_from_env();
let url = crate::http::sanitized_request_url(&request.url);
let response = crate::http::send_request_once(&request)
.await
.map_err(|error| handle_fetch_error(error, &url, &request_body_values))?;
let status = response.status().as_u16();
let response_headers =
crate::extract_response_headers::extract_response_headers(response.headers());
let input = ResponseHandlerInput {
url: url.clone(),
request_body_values: request_body_values.clone(),
response,
abort_signal: request.abort_signal.clone(),
max_json_response_bytes: request.max_json_response_bytes,
};
if !(200..300).contains(&status) {
return match failed_response_handler.handle(input).await {
Ok(output) => Err(output.value),
Err(
error @ (AiMuxError::ApiCall(_) | AiMuxError::Aborted(_) | AiMuxError::Timeout(_)),
) => Err(error),
Err(error) => Err(AiMuxError::ApiCall(Box::new(ApiCallError {
status_code: Some(status),
response_headers: Some(response_headers),
is_retryable: aimux_core::error::is_retryable_status(status),
..ApiCallError::new(
format!("Failed to process error response: {error}"),
url,
request_body_values,
)
}))),
};
}
match successful_response_handler.handle(input).await {
Ok(output) => Ok(output),
Err(error @ (AiMuxError::ApiCall(_) | AiMuxError::Aborted(_) | AiMuxError::Timeout(_))) => {
Err(error)
}
Err(error) => Err(AiMuxError::ApiCall(Box::new(ApiCallError {
status_code: Some(status),
response_headers: Some(response_headers),
..ApiCallError::new(
format!("Failed to process successful response: {error}"),
url,
request_body_values,
)
}))),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
use crate::response_handler::{
create_event_source_response_handler, create_json_response_handler,
create_status_code_error_response_handler,
};
#[tokio::test]
async fn slow_response_trips_the_exchange_timeout_as_retryable() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"ok": true}))
.set_delay(Duration::from_millis(500)),
)
.mount(&server)
.await;
let request = HttpRequest {
url: server.uri(),
headers: vec![],
abort_signal: None,
call_id: None,
recording_context: None,
..Default::default()
};
let error = call_to_api_with_deadline(
request.prepare(HttpMethod::Post, HttpBody::Json(serde_json::json!({}))),
serde_json::json!({}),
create_json_response_handler::<serde_json::Value>(),
create_status_code_error_response_handler(),
Duration::from_millis(50),
)
.await
.unwrap_err();
match error {
AiMuxError::ApiCall(detail) => {
assert!(detail.is_retryable, "exchange timeout must be retryable");
assert!(detail.message.contains("exchange response timeout"));
}
other => panic!("expected retryable ApiCall, got {other:?}"),
}
}
#[test]
fn event_source_handler_is_marked_streaming() {
let handler = create_event_source_response_handler::<serde_json::Value>();
assert!(handler.is_streaming());
let plain = create_json_response_handler::<serde_json::Value>();
assert!(!plain.is_streaming());
}
}