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
84 changes: 39 additions & 45 deletions src/handlers/response_inspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,63 +152,57 @@ pub async fn response_headers_get_handler(
return Ok(builder.body(body.clone()));
}

// Build response with iterative header reflection like httpbin
let mut iteration_count = 0;
const MAX_ITERATIONS: usize = 10; // Prevent infinite loops

loop {
let mut response_builder = HttpResponse::Ok();

// Add query parameters as actual response headers
for (key, values) in &multi_value_params {
for value in values {
response_builder.append_header((key.as_str(), value.as_str()));
}
}

// Prepare the headers map for JSON response
let mut headers_map: BTreeMap<String, Value> = BTreeMap::new();

// Add query parameters to the JSON response
for (key, values) in &multi_value_params {
if values.len() == 1 {
headers_map.insert(key.clone(), Value::String(values[0].clone()));
} else {
headers_map.insert(
key.clone(),
Value::Array(values.iter().map(|v| Value::String(v.clone())).collect()),
);
}
// Prepare the headers map for the JSON response
let mut headers_map: BTreeMap<String, Value> = BTreeMap::new();
for (key, values) in &multi_value_params {
if values.len() == 1 {
headers_map.insert(key.clone(), Value::String(values[0].clone()));
} else {
headers_map.insert(
key.clone(),
Value::Array(values.iter().map(|v| Value::String(v.clone())).collect()),
);
}
}

// Add standard headers that would be set by the framework
// Only supply the JSON default when no Content-Type was requested.
let has_content_type = multi_value_params
.keys()
.any(|key| key.eq_ignore_ascii_case("Content-Type"));
if !has_content_type {
headers_map.insert(
"Content-Type".to_string(),
Value::String("application/json".to_string()),
);
}

// Calculate content length for the JSON
let json_string = serde_json::to_string(&headers_map)?;
// The body reports its own Content-Length, so re-serialize until the
// value matches the body size (like httpbin does). This settles within a
// couple of rounds; the cap is just a safety net.
const MAX_ITERATIONS: usize = 10;
let mut json_string = serde_json::to_string(&headers_map)?;
for _ in 0..MAX_ITERATIONS {
let content_length = json_string.len().to_string();
headers_map.insert(
"Content-Length".to_string(),
Value::String(content_length.clone()),
);

// Set Content-Length header on response
response_builder.append_header(("Content-Length", content_length));

// Check if we need another iteration (like httpbin does)
let _current_json = serde_json::to_string(&headers_map)?;
if headers_map.get("Content-Length") == Some(&Value::String(content_length.clone())) {
break;
}
headers_map.insert("Content-Length".to_string(), Value::String(content_length));
json_string = serde_json::to_string(&headers_map)?;
}

iteration_count += 1;
let mut response_builder = HttpResponse::Ok();

// For simplicity, we'll do a fixed number of iterations like httpbin's logic
// In httpbin, it iterates until response data doesn't change
if iteration_count >= 2 || iteration_count >= MAX_ITERATIONS {
return Ok(response_builder.json(headers_map));
// Add query parameters as actual response headers
for (key, values) in &multi_value_params {
for value in values {
response_builder.append_header((key.as_str(), value.as_str()));
}
}

if !has_content_type {
response_builder.content_type("application/json");
}
Ok(response_builder.body(json_string))
}

pub async fn response_headers_post_handler(
Expand Down
82 changes: 82 additions & 0 deletions src/tests/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,40 @@ async fn response_headers_post_sets_headers() {
);
}

/// The JSON body of /response-headers reports its own Content-Length, so the
/// value must match the actual body size (and the header).
#[actix_web::test]
async fn response_headers_content_length_matches_body() {
let app = test::init_service(create_app(cfg())).await;
for uri in [
"/response-headers",
"/response-headers?X-Test=bar&X-Test=baz",
"/response-headers?Content-Type=text/plain",
"/response-headers?content-type=text/plain",
"/response-headers?CoNtEnT-TyPe=text/plain",
] {
for method in [actix_web::http::Method::GET, actix_web::http::Method::POST] {
let req = test::TestRequest::default()
.method(method.clone())
.uri(uri)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let header = resp
.headers()
.get("content-length")
.map(|v| v.to_str().unwrap().to_string());
let body = test::read_body(resp).await;
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
let reported = json["Content-Length"].as_str().unwrap().to_string();
assert_eq!(reported, body.len().to_string(), "{method} {uri}");
if let Some(header) = header {
assert_eq!(header, reported, "{method} {uri}");
}
}
}
}

/// Codes outside 100-999 can't be sent as a real status, so they are
/// rejected like any other invalid code instead of falling back to 200.
#[actix_web::test]
Expand All @@ -143,3 +177,51 @@ async fn status_out_of_range_code_is_bad_request() {
}
}
}

#[actix_web::test]
async fn response_headers_get_preserves_content_type() {
let app = test::init_service(create_app(cfg())).await;
for (uri, expected) in [
("/response-headers", "application/json"),
("/response-headers?Content-Type=text/plain", "text/plain"),
("/response-headers?content-type=text/plain", "text/plain"),
("/response-headers?CoNtEnT-TyPe=text/plain", "text/plain"),
] {
let req = test::TestRequest::get().uri(uri).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
expected,
"{uri}"
);
}
}

#[actix_web::test]
async fn response_headers_post_preserves_content_type() {
let app = test::init_service(create_app(cfg())).await;
for (uri, expected) in [
("/response-headers", "application/json"),
("/response-headers?Content-Type=text/plain", "text/plain"),
("/response-headers?content-type=text/plain", "text/plain"),
("/response-headers?CoNtEnT-TyPe=text/plain", "text/plain"),
] {
let req = test::TestRequest::post().uri(uri).to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap(),
expected,
"{uri}"
);
}
}
Loading