#![cfg(all(not(target_arch = "wasm32"), feature = "reqwest"))]
use std::future::Future;
use allama::Error;
use allama_conformance::{MockHttpResponse, TestHttpServer};
use allama_http::{
HttpRequest, HttpResponse, HttpTransport, ReqwestTransport, TransportError, TransportErrorKind,
status_error,
};
async fn assert_redirect_not_followed<F, Fut, T>(status: u16, send: F) -> T
where
F: FnOnce(String) -> Fut,
Fut: Future<Output = T>,
{
let target = TestHttpServer::spawn([MockHttpResponse::json(200, r#"{"stolen":true}"#)]).await;
let location = format!(
"{}/v1/messages",
target.url().replace("127.0.0.1", "localhost")
);
let origin =
TestHttpServer::spawn([MockHttpResponse::text(status, "").header("location", location)])
.await;
let result = send(origin.url().to_owned()).await;
assert_eq!(origin.recorded_requests().await.len(), 1);
let leaked = target.recorded_requests().await;
assert!(
leaked.is_empty(),
"the redirect was followed: the target got {:?}",
leaked
.iter()
.map(|request| (request.header("x-api-key"), request.body_text().len()))
.collect::<Vec<_>>()
);
result
}
fn map_http(status: u16, _: &allama_http::Headers, _: &str) -> Error {
status_error(status, format!("HTTP {status}")).authored()
}
#[tokio::test]
async fn the_default_transport_never_follows_a_redirect() {
for status in [301, 302, 303, 307, 308] {
let response = assert_redirect_not_followed(status, |url| async move {
ReqwestTransport::new()
.send(
HttpRequest::post(format!("{url}/v1/messages"), r#"{"prompt":"secret"}"#)
.header("x-api-key", "SECRET-KEY-123"),
)
.await
.unwrap()
})
.await;
assert_eq!(response.status, status);
let err = response.map_error(map_http).await;
assert!(
matches!(err, Error::UnexpectedResponse { .. }),
"{status}: {err:?}"
);
assert_eq!(err.status(), Some(status));
assert!(!err.is_retryable());
assert!(
err.as_log().to_string().contains("`localhost`"),
"{}",
err.as_log()
);
}
}
#[tokio::test]
async fn a_client_without_redirects_keeps_the_guarantee() {
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let response = assert_redirect_not_followed(307, |url| async move {
ReqwestTransport::from_client(client)
.send(HttpRequest::post(url, "{}"))
.await
.unwrap()
})
.await;
assert_eq!(response.status, 307);
}
#[tokio::test]
async fn success_bodies_are_read_under_a_limit() {
let big = "x".repeat(1024 * 1024);
let server = TestHttpServer::spawn([
MockHttpResponse::text(200, big.clone()),
MockHttpResponse::text(200, big),
])
.await;
let transport = ReqwestTransport::new();
let send = || transport.send(HttpRequest::get(server.url()));
let err = send()
.await
.unwrap()
.bytes_limited(64 * 1024)
.await
.unwrap_err();
assert_eq!(err.kind(), TransportErrorKind::TooLarge);
assert!(matches!(err.into_error(), Error::UnexpectedResponse { .. }));
let whole = send()
.await
.unwrap()
.bytes_limited(allama_http::DEFAULT_MAX_RESPONSE_SIZE)
.await
.unwrap();
assert_eq!(whole.len(), 1024 * 1024);
}
#[tokio::test]
async fn error_bodies_are_capped() {
let server =
TestHttpServer::spawn([MockHttpResponse::text(500, "e".repeat(1024 * 1024))]).await;
let response = ReqwestTransport::new()
.send(HttpRequest::get(server.url()))
.await
.unwrap();
let mut seen = 0;
let err = response
.map_error(|status, headers, body| {
seen = body.len();
map_http(status, headers, body)
})
.await;
assert_eq!(seen, allama_http::MAX_ERROR_BODY_SIZE);
assert!(matches!(err, Error::Provider { .. }), "{err:?}");
}
#[tokio::test]
async fn an_endless_error_body_ends_the_call() {
let endless = HttpResponse::new(
503,
allama_http::Headers::new(),
Box::pin(futures_util::stream::repeat_with(|| {
Ok::<_, TransportError>(allama_http::Bytes::from(vec![b'x'; 64 * 1024]))
})),
);
let err = tokio::time::timeout(
std::time::Duration::from_secs(5),
endless.map_error(map_http),
)
.await
.expect("the error body read did not end");
assert!(matches!(err, Error::Overloaded { .. }), "{err:?}");
}