Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/anthropic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ pub mod error;
pub mod schema;
pub mod sse;

pub const MAX_ANTHROPIC_REQUEST_BYTES: usize = 64 * 1024 * 1024;

pub use self::error::{ErrorDetail, ErrorEnvelope, json_error};
pub use self::schema::{CountTokensResponse, Message, MessagesRequest};
pub use self::sse::{
Expand Down
13 changes: 7 additions & 6 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
anthropic::json_error,
anthropic::{MAX_ANTHROPIC_REQUEST_BYTES, json_error},
logging::{Logger, REDACT_KEYS, create_logger},
monitor::{EndpointKind, MonitorHandle},
openai_compat::{
Expand Down Expand Up @@ -1412,13 +1412,14 @@ async fn dispatch_request(
}
let request_guard = RequestMonitorGuard::new(state.monitor.clone(), req_id.clone());
let now = current_millis();
let body_bytes = match axum::body::to_bytes(req.into_body(), MAX_OPENAI_REQUEST_BYTES).await {
let body_bytes = match axum::body::to_bytes(req.into_body(), MAX_ANTHROPIC_REQUEST_BYTES).await
{
Ok(bytes) => bytes,
Err(err) => {
Err(_) => {
let response = json_error(
StatusCode::BAD_REQUEST,
"invalid_request_error",
format!("Invalid JSON: {err}"),
StatusCode::PAYLOAD_TOO_LARGE,
"request_too_large",
"Request body exceeded the size limit",
);
log_request_completed(
&log,
Expand Down
102 changes: 102 additions & 0 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axum::http::{Method, Request, StatusCode};
use axum::response::IntoResponse;
use claude_code_proxy::{
MessagesRequest,
anthropic::MAX_ANTHROPIC_REQUEST_BYTES,
config::AliasProvider,
monitor::{MonitorHandle, RequestStatus},
provider::{CliHandlers, Generation, GenerationBody, Provider, ProviderError, RequestContext},
Expand Down Expand Up @@ -610,6 +611,107 @@ async fn missing_model_returns_400() {
assert_eq!(error_type, "invalid_request_error");
}

async fn error_body(response: axum::response::Response) -> Value {
axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.ok()
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap()
}

// Builds a valid JSON /v1/messages body of exactly `total_len` bytes that has
// no "model", so the handler parses it and then fails on the missing model.
// That failure proves the body cleared the size gate without touching a
// provider.
fn padded_messages_body_without_model(total_len: usize) -> String {
let prefix = r#"{"messages":[{"role":"user","content":"hello"}],"padding":""#;
let suffix = r#""}"#;
let padding = total_len - prefix.len() - suffix.len();
let mut body = String::with_capacity(total_len);
body.push_str(prefix);
body.extend(std::iter::repeat_n('a', padding));
body.push_str(suffix);
assert_eq!(body.len(), total_len);
body
}

#[tokio::test]
async fn messages_body_over_16mib_clears_size_gate() {
const OLD_LIMIT: usize = 16 * 1024 * 1024;
const { assert!(MAX_ANTHROPIC_REQUEST_BYTES > OLD_LIMIT) };
let app = app(Arc::new(Registry::with_default_alias()));
let response = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/messages")
.header("content-type", "application/json")
.body(Body::from(padded_messages_body_without_model(
OLD_LIMIT + 1,
)))
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = error_body(response).await;
let message = body["error"]["message"].as_str().unwrap_or("");
assert!(
message.starts_with("Missing \"model\""),
"body over 16 MiB should reach model validation, got: {message}"
);
}

#[tokio::test]
async fn messages_body_at_limit_clears_size_gate() {
let app = app(Arc::new(Registry::with_default_alias()));
let response = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/messages")
.header("content-type", "application/json")
.body(Body::from(padded_messages_body_without_model(
MAX_ANTHROPIC_REQUEST_BYTES,
)))
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = error_body(response).await;
let message = body["error"]["message"].as_str().unwrap_or("");
assert!(
message.starts_with("Missing \"model\""),
"body at the limit should reach model validation, got: {message}"
);
}

#[tokio::test]
async fn anthropic_bodies_over_limit_return_request_too_large() {
for path in ["/v1/messages", "/v1/messages/count_tokens"] {
let response = app(Arc::new(Registry::with_default_alias()))
.oneshot(
Request::builder()
.method(Method::POST)
.uri(path)
.header("content-type", "application/json")
.body(Body::from(padded_messages_body_without_model(
MAX_ANTHROPIC_REQUEST_BYTES + 1,
)))
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
let body = error_body(response).await;
assert_eq!(body["error"]["type"], "request_too_large");
}
}

#[tokio::test]
async fn known_model_reaches_codex_provider() {
let app = app(Arc::new(Registry::with_default_alias()));
Expand Down