use crate::common;
use camber::RuntimeError;
use camber::http::{Next, Rejection, RejectionContext, Request, Response, Router};
use camber::runtime;
use common::{
COMPLETION_MESSAGE, CountedOutcome, DECLARED_MESSAGE, MIDDLE_CAUSE, REJECTION_MESSAGE,
ROOT_CAUSE, UNREPRESENTABLE_HEADER, WIRE_TIMEOUT, assert_counted, assert_field_value,
assert_fields, assert_message_is_fixed, assert_no_private_text, counted_rows, forbidden_values,
one_level_failure, only_event, two_level_failure,
};
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[test]
fn built_in_internal_error_wire_response_is_redacted() {
common::test_runtime()
.run(|| {
let mut router = Router::new();
router.get("/rejection-redaction", |_req: &Request| async {
Err::<Response, RuntimeError>(two_level_failure())
});
let addr = common::spawn_server(router);
let response =
common::request(addr, "GET", "/rejection-redaction", &[], &[], WIRE_TIMEOUT)
.expect("the fixture request completed");
assert_eq!(response.status, 500);
assert_eq!(
response.text().as_ref(),
common::REDACTED_BODY,
"a built-in 500 body is fixed text, not the error it came from"
);
common::request_id_of(&response, "built-in redaction");
assert_no_private_text(
&response,
&[ROOT_CAUSE, MIDDLE_CAUSE, "io error"],
"built-in redaction",
);
runtime::request_shutdown();
})
.expect("the fixture runtime ran to completion");
}
struct ChainRow {
label: &'static str,
path: &'static str,
failure: fn() -> RuntimeError,
causes: &'static [&'static str],
}
const CHAIN_ROWS: [ChainRow; 2] = [
ChainRow {
label: "one-level chain",
path: "/rejection-chain-one",
failure: one_level_failure,
causes: &[ROOT_CAUSE],
},
ChainRow {
label: "two-level chain",
path: "/rejection-chain-two",
failure: two_level_failure,
causes: &[MIDDLE_CAUSE, ROOT_CAUSE],
},
];
const _: () = assert!(!CHAIN_ROWS.is_empty());
fn register_chain_routes(router: &mut Router) {
for row in &CHAIN_ROWS {
let failure = row.failure;
router.get(row.path, move |_req: &Request| {
std::future::ready(Err::<Response, RuntimeError>(failure()))
});
}
}
fn assert_causes_in_field(event: &str, causes: &[&str], label: &str) {
assert!(
!causes.is_empty(),
"{label}: no private cause was declared, so this record proves nothing: {event}"
);
let cause_at = event
.find("cause=")
.unwrap_or_else(|| panic!("{label}: the event carries no cause field: {event}"));
for cause in causes {
let at = event
.find(cause)
.unwrap_or_else(|| panic!("{label}: the chain lost {cause:?}: {event}"));
assert!(
at > cause_at,
"{label}: {cause:?} appears outside the cause field: {event}"
);
}
}
fn assert_private_chain(row: &ChainRow, event: &str) {
assert_causes_in_field(event, row.causes, row.label);
assert_message_is_fixed(event, REJECTION_MESSAGE, row.label);
}
fn assert_chain_event(row: &ChainRow, capture: &common::TraceCapture, request_id: &str) {
let events = capture.events();
let event = only_event(&events, REJECTION_MESSAGE, row.label);
assert_field_value(event, "status", "500", row.label);
assert_field_value(event, "request_id", request_id, row.label);
assert_field_value(event, "kind", "internal_service", row.label);
assert_field_value(event, "raw_path", row.path, row.label);
assert_field_value(event, "route", row.path, row.label);
assert_fields(
event,
&[
"method=GET",
"protocol=ordinary_http",
"remote_addr=127.0.0.1",
],
row.label,
);
assert_private_chain(row, event);
}
#[test]
fn rejection_event_fields_preserve_private_source_chain() {
let captures: Box<[common::TraceCapture]> = CHAIN_ROWS
.iter()
.map(|row| common::capture_events(row.path))
.collect();
common::test_runtime()
.with_tracing()
.run(|| {
let mut router = Router::new();
register_chain_routes(&mut router);
let addr = common::spawn_server(router);
let mut rows = 0_usize;
for (row, capture) in CHAIN_ROWS.iter().zip(captures.iter()) {
let response = common::request(addr, "GET", row.path, &[], &[], WIRE_TIMEOUT)
.unwrap_or_else(|error| {
panic!("{}: the request did not complete: {error}", row.label)
});
assert_eq!(response.status, 500, "{}: wire status", row.label);
assert_eq!(
response.text().as_ref(),
common::REDACTED_BODY,
"{}: wire body",
row.label
);
let request_id = common::request_id_of(&response, row.label);
assert_no_private_text(&response, row.causes, row.label);
assert_chain_event(row, capture, &request_id);
rows += 1;
}
assert_eq!(rows, CHAIN_ROWS.len(), "every declared chain row ran");
runtime::request_shutdown();
})
.expect("the fixture runtime ran to completion");
}
const CUSTOM_STATUS: u16 = 418;
const MAPPED_BODY: &str = "policy answered";
const FALLBACK_PATH: &str = "/completion-fallback";
const DISPLACED_PATH: &str = "/completion-displaced";
const CUSTOM_PATH: &str = "/completion-custom";
const HEAD_PATH: &str = "/completion-head";
const COMPLETION_CUSTOM_PATHS: &[&str] = &[CUSTOM_PATH, HEAD_PATH];
const DISPLACEMENT_MESSAGE: &str = "message=mapped rejection response could not be represented";
fn displacement_needle() -> String {
format!("mapped_status={CUSTOM_STATUS}")
}
struct CompletionRow {
label: &'static str,
method: &'static str,
path: &'static str,
status: u16,
body: &'static str,
displaced: bool,
}
const COMPLETION_ROWS: [CompletionRow; 4] = [
CompletionRow {
label: "custom mapped status",
method: "GET",
path: CUSTOM_PATH,
status: CUSTOM_STATUS,
body: MAPPED_BODY,
displaced: false,
},
CompletionRow {
label: "policy fallback",
method: "GET",
path: FALLBACK_PATH,
status: 500,
body: common::REDACTED_BODY,
displaced: false,
},
CompletionRow {
label: "head suppression",
method: "HEAD",
path: HEAD_PATH,
status: CUSTOM_STATUS,
body: "",
displaced: false,
},
CompletionRow {
label: "displaced mapped response",
method: "GET",
path: DISPLACED_PATH,
status: 500,
body: common::REDACTED_BODY,
displaced: true,
},
];
const _: () = assert!(!COMPLETION_ROWS.is_empty());
enum PolicyArm {
Unanswerable,
Displaced,
Mapped,
PassedThrough,
}
impl PolicyArm {
fn of(path: &str, fallback: &str, custom: &[&str], displaced: &str) -> Self {
match path {
named if named == fallback => Self::Unanswerable,
named if named == displaced => Self::Displaced,
named if custom.iter().any(|declared| *declared == named) => Self::Mapped,
_ => Self::PassedThrough,
}
}
}
fn refusing_policy(
fallback: &'static str,
custom: &'static [&'static str],
displaced: &'static str,
status: u16,
) -> impl Fn(&Rejection, &RejectionContext) -> Result<Response, RuntimeError> + Send + Sync + 'static
{
move |rejection: &Rejection, context: &RejectionContext| {
let path = context.raw_path();
let answered = match PolicyArm::of(path, fallback, custom, displaced) {
PolicyArm::Unanswerable => {
return Err(RuntimeError::Http("this policy cannot answer".into()));
}
PolicyArm::Displaced => Response::text(status, MAPPED_BODY)
.map(|response| response.with_header(UNREPRESENTABLE_HEADER, "present")),
PolicyArm::Mapped => Response::text(status, MAPPED_BODY),
PolicyArm::PassedThrough => Response::text(rejection.status(), rejection.message()),
};
common::naming(answered, context)
}
}
fn register_completion_routes(router: &mut Router) {
for row in &COMPLETION_ROWS {
router.get(row.path, |_req: &Request| {
std::future::ready(Err::<Response, RuntimeError>(two_level_failure()))
});
}
}
fn assert_completion_events(row: &CompletionRow, capture: &common::TraceCapture, request_id: &str) {
let events = capture.events();
let sent = row.status.to_string();
let rejected = only_event(&events, REJECTION_MESSAGE, row.label);
assert_field_value(rejected, "request_id", request_id, row.label);
assert_field_value(rejected, "status", &sent, row.label);
let completed = only_event(&events, COMPLETION_MESSAGE, row.label);
assert_field_value(completed, "request_id", request_id, row.label);
assert_field_value(completed, "status", &sent, row.label);
assert_message_is_fixed(completed, COMPLETION_MESSAGE, row.label);
}
fn assert_displacement_event(capture: &common::TraceCapture, request_id: &str, label: &str) {
let events = capture.events();
let event = only_event(&events, DISPLACEMENT_MESSAGE, label);
assert_field_value(event, "request_id", request_id, label);
assert_field_value(event, "kind", "internal_service", label);
assert_field_value(event, "mapped_status", &CUSTOM_STATUS.to_string(), label);
assert_field_value(event, "status", "500", label);
assert_message_is_fixed(event, DISPLACEMENT_MESSAGE, label);
}
#[test]
fn completion_event_uses_final_sent_status() {
let captures: Box<[common::TraceCapture]> = COMPLETION_ROWS
.iter()
.map(|row| common::capture_events(row.path))
.collect();
let displacement = common::capture_events(&displacement_needle());
common::test_runtime()
.with_tracing()
.run(|| {
let mut router = Router::new();
register_completion_routes(&mut router);
let addr = common::spawn_server(router.rejection_mapper(refusing_policy(
FALLBACK_PATH,
COMPLETION_CUSTOM_PATHS,
DISPLACED_PATH,
CUSTOM_STATUS,
)));
let mut rows = 0_usize;
for (row, capture) in COMPLETION_ROWS.iter().zip(captures.iter()) {
let response = common::request(addr, row.method, row.path, &[], &[], WIRE_TIMEOUT)
.unwrap_or_else(|error| {
panic!("{}: the request did not complete: {error}", row.label)
});
assert_eq!(response.status, row.status, "{}: wire status", row.label);
assert_eq!(
response.text().as_ref(),
row.body,
"{}: wire body",
row.label
);
let request_id = common::request_id_of(&response, row.label);
assert_completion_events(row, capture, &request_id);
if row.displaced {
assert_displacement_event(&displacement, &request_id, row.label);
}
rows += 1;
}
assert_eq!(
rows,
COMPLETION_ROWS.len(),
"every declared completion row ran"
);
assert_eq!(
COMPLETION_ROWS.iter().filter(|row| row.displaced).count(),
1,
"exactly one declared row has its mapped answer displaced"
);
assert!(
COMPLETION_ROWS.iter().any(|row| !row.displaced),
"at least one declared row is answered with what policy settled on"
);
runtime::request_shutdown();
})
.expect("the fixture runtime ran to completion");
}
const METRIC_BODY_LIMIT: usize = 1024;
const METRIC_CUSTOM_STATUS: u16 = 599;
const METRIC_CUSTOM_PATH: &str = "/metric-custom";
const METRIC_FALLBACK_PATH: &str = "/metric-mapper-fails";
const METRIC_MIDDLEWARE_PATH: &str = "/metric-middleware";
const METRIC_REPLACED_PATH: &str = "/metric-replaced";
const METRIC_DISPLACED_PATH: &str = "/metric-displaced";
const METRIC_BOUNDARY: &str = "----metricboundary";
const MULTIPART_REPRESENTATION: &str = "multipart";
#[derive(Eq, PartialEq)]
enum BodyShape {
Stated,
Oversized,
Unreadable,
}
struct MetricRow {
label: &'static str,
method: &'static str,
path: &'static str,
content_type: &'static str,
body: &'static str,
shape: BodyShape,
kind: &'static str,
status: u16,
counted: bool,
}
const METRIC_ROWS: [MetricRow; 15] = [
MetricRow {
label: "unmatched path",
method: "GET",
path: "/metric-absent",
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "routing",
status: 404,
counted: true,
},
MetricRow {
label: "head on an unmatched path",
method: "HEAD",
path: "/metric-absent",
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "routing",
status: 404,
counted: true,
},
MetricRow {
label: "wrong method",
method: "POST",
path: "/metric-ordinary",
content_type: "text/plain",
body: "",
shape: BodyShape::Stated,
kind: "method_selection",
status: 405,
counted: true,
},
MetricRow {
label: "oversized body",
method: "POST",
path: "/metric-upload",
content_type: "text/plain",
body: "",
shape: BodyShape::Oversized,
kind: "body_limit",
status: 413,
counted: true,
},
MetricRow {
label: "unreadable body",
method: "POST",
path: "/metric-upload",
content_type: "application/octet-stream",
body: "",
shape: BodyShape::Unreadable,
kind: "body_unreadable",
status: 400,
counted: true,
},
MetricRow {
label: "malformed json",
method: "POST",
path: "/metric-json",
content_type: "application/json",
body: common::MALFORMED_JSON,
shape: BodyShape::Stated,
kind: "malformed_body",
status: 400,
counted: true,
},
MetricRow {
label: "malformed multipart",
method: "POST",
path: "/metric-multipart",
content_type: MULTIPART_REPRESENTATION,
body: common::MALFORMED_MULTIPART,
shape: BodyShape::Stated,
kind: "multipart",
status: 400,
counted: true,
},
MetricRow {
label: "declared application refusal",
method: "GET",
path: "/metric-declared",
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "application",
status: 400,
counted: true,
},
MetricRow {
label: "middleware refusal",
method: "GET",
path: METRIC_MIDDLEWARE_PATH,
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "middleware",
status: 500,
counted: true,
},
MetricRow {
label: "unrepresentable response head",
method: "GET",
path: "/metric-invalid-header",
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "invalid_header",
status: 500,
counted: true,
},
MetricRow {
label: "no admissible backend",
method: "GET",
path: "/metric-proxy/upstream",
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "proxy",
status: 503,
counted: true,
},
MetricRow {
label: "custom mapped status",
method: "GET",
path: METRIC_CUSTOM_PATH,
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "internal_service",
status: METRIC_CUSTOM_STATUS,
counted: true,
},
MetricRow {
label: "policy fallback",
method: "GET",
path: METRIC_FALLBACK_PATH,
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "internal_service",
status: 500,
counted: true,
},
MetricRow {
label: "displaced mapped response",
method: "GET",
path: METRIC_DISPLACED_PATH,
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "internal_service",
status: 500,
counted: true,
},
MetricRow {
label: "replaced mapped response",
method: "GET",
path: METRIC_REPLACED_PATH,
content_type: "",
body: "",
shape: BodyShape::Stated,
kind: "internal_service",
status: REPLACED_STATUS,
counted: false,
},
];
const _: () = assert!(!METRIC_ROWS.is_empty());
fn guarding_middleware(router: &mut Router) {
router.use_middleware(
|req: &Request,
next: Next|
-> Pin<Box<dyn Future<Output = Result<Response, RuntimeError>> + Send>> {
match req.path() == METRIC_MIDDLEWARE_PATH {
true => Box::pin(async {
Err(RuntimeError::Http("this frame refused the request".into()))
}),
false => {
let replaced = req.path() == METRIC_REPLACED_PATH;
let entered = next.call(req);
Box::pin(async move { replace_or_pass(entered.await, replaced) })
}
}
},
);
}
fn replace_or_pass(entered: Response, replaced: bool) -> Result<Response, RuntimeError> {
match replaced {
true => Response::text(REPLACED_STATUS, REPLACED_BODY),
false => Ok(entered),
}
}
fn register_metric_routes(router: &mut Router, handled: &Arc<AtomicUsize>) {
guarding_middleware(router);
router.get(METRIC_MIDDLEWARE_PATH, |_req: &Request| {
std::future::ready(Response::text(200, "guarded"))
});
router.get("/metric-ordinary", |_req: &Request| {
std::future::ready(Response::text(200, "ordinary"))
});
router.post("/metric-upload", |_req: &Request| {
std::future::ready(Response::text(200, "stored"))
});
router.post("/metric-json", common::ticket_handler());
router.post("/metric-multipart", common::multipart_count_handler());
router.get("/metric-declared", |_req: &Request| {
std::future::ready(Err::<Response, RuntimeError>(RuntimeError::BadRequest(
DECLARED_MESSAGE.into(),
)))
});
router.get(
"/metric-invalid-header",
common::unrepresentable_handler(handled),
);
for path in [
METRIC_CUSTOM_PATH,
METRIC_FALLBACK_PATH,
METRIC_DISPLACED_PATH,
METRIC_REPLACED_PATH,
] {
router.get(path, |_req: &Request| {
std::future::ready(Err::<Response, RuntimeError>(two_level_failure()))
});
}
router.proxy_checked(
"/metric-proxy",
"http://127.0.0.1:1",
Arc::new(AtomicBool::new(false)),
);
}
const METRIC_CUSTOM_PATHS: &[&str] = &[METRIC_CUSTOM_PATH];
fn send_broken_framing(addr: SocketAddr, row: &MetricRow, content_type: &str) -> u16 {
let label = row.label;
let (refused, _socket) = common::send_unreadable_body(
addr,
common::CLOSE_AFTER_RESPONSE,
row.method,
row.path,
content_type,
)
.unwrap_or_else(|error| panic!("{label}: the broken framing was not answered: {error}"));
refused.status
}
fn send_metric_row(addr: SocketAddr, row: &MetricRow, oversized: &[u8]) -> u16 {
let multipart = common::multipart_content_type(METRIC_BOUNDARY);
let content_type = match row.content_type {
MULTIPART_REPRESENTATION => multipart.as_ref(),
stated => stated,
};
if row.shape == BodyShape::Unreadable {
return send_broken_framing(addr, row, content_type);
}
let headers: Box<[(&str, &str)]> = match content_type.is_empty() {
true => Box::new([]),
false => Box::new([("Content-Type", content_type)]),
};
let body: &[u8] = match row.shape {
BodyShape::Oversized => oversized,
_ => row.body.as_bytes(),
};
common::request(addr, row.method, row.path, &headers, body, WIRE_TIMEOUT)
.unwrap_or_else(|error| panic!("{}: the request did not complete: {error}", row.label))
.status
}
fn scrape(addr: SocketAddr) -> common::HttpResponse {
common::request(addr, "GET", "/metrics", &[], &[], WIRE_TIMEOUT)
.expect("the metrics endpoint answered")
}
fn assert_counted_rows(before: &common::RejectionCounters, after: &common::RejectionCounters) {
for row in &METRIC_ROWS {
assert_counted(
before,
after,
&CountedOutcome {
label: row.label,
kind: row.kind,
status: row.status,
rejections: counted_rows(&METRIC_ROWS, |other| {
other.counted && other.kind == row.kind && other.status == row.status
}),
completions: counted_rows(&METRIC_ROWS, |other| other.status == row.status),
},
);
}
}
#[test]
fn rejection_metric_labels_are_bounded() {
common::test_runtime()
.with_metrics()
.run(|| {
let mut router = Router::new();
let handled = Arc::new(AtomicUsize::new(0));
register_metric_routes(&mut router, &handled);
let router =
router
.max_request_body(METRIC_BODY_LIMIT)
.rejection_mapper(refusing_policy(
METRIC_FALLBACK_PATH,
METRIC_CUSTOM_PATHS,
METRIC_DISPLACED_PATH,
METRIC_CUSTOM_STATUS,
));
let addr = common::spawn_server(router);
let named = common::request(addr, "GET", "/metric-absent", &[], &[], WIRE_TIMEOUT)
.expect("the naming request completed");
let request_id = common::request_id_of(&named, "metric identity");
let before = common::rejection_counters(|| scrape(addr));
let oversized = vec![b'x'; METRIC_BODY_LIMIT * 2].into_boxed_slice();
let mut rows = 0_usize;
for row in &METRIC_ROWS {
let status = send_metric_row(addr, row, &oversized);
assert_eq!(status, row.status, "{}: wire status", row.label);
rows += 1;
}
assert_eq!(rows, METRIC_ROWS.len(), "every declared metric row ran");
assert_eq!(
handled.load(Ordering::SeqCst),
1,
"the unrepresentable head was produced by a handler that ran, so \
its refusal is one the wire decided and not one that preceded it"
);
assert!(
METRIC_ROWS.iter().any(|row| row.counted),
"at least one declared row moves the rejection counter"
);
assert!(
METRIC_ROWS.iter().any(|row| !row.counted),
"at least one declared row declares the counter's silence"
);
let after = common::rejection_counters(|| scrape(addr));
common::assert_bounded_rejection_labels(
&after.rejections,
&forbidden_values(
&request_id,
&[DECLARED_MESSAGE, ROOT_CAUSE, MIDDLE_CAUSE],
METRIC_ROWS.iter().map(|row| row.path),
),
);
assert_counted_rows(&before, &after);
runtime::request_shutdown();
})
.expect("the fixture runtime ran to completion");
}
const REPLACED_PATH: &str = "/rejection-replaced";
const REPLACED_STATUS: u16 = 202;
const REPLACED_BODY: &str = "the frame answered";
const DISCARDED_MESSAGE: &str = "message=rejection response was not sent";
fn replacing_middleware(router: &mut Router) {
router.use_middleware(
|req: &Request,
next: Next|
-> Pin<Box<dyn Future<Output = Result<Response, RuntimeError>> + Send>> {
let entered = next.call(req);
Box::pin(async move {
drop(entered.await);
Response::text(REPLACED_STATUS, REPLACED_BODY)
})
},
);
}
fn assert_replaced_events(capture: &common::TraceCapture) {
let label = "replaced rejection response";
let events = capture.events();
let discarded = only_event(&events, DISCARDED_MESSAGE, label);
assert_field_value(discarded, "kind", "internal_service", label);
assert_field_value(discarded, "mapped_status", "500", label);
assert_field_value(discarded, "default_status", "500", label);
let raw_path = format!("raw_path={REPLACED_PATH}");
let route = format!("route={REPLACED_PATH}");
assert_fields(
discarded,
&[
"method=GET",
raw_path.as_str(),
route.as_str(),
"protocol=ordinary_http",
"remote_addr=127.0.0.1",
],
label,
);
assert_causes_in_field(discarded, &[MIDDLE_CAUSE, ROOT_CAUSE], label);
assert!(
common::field_value(discarded, "status").is_none(),
"{label}: a refusal no peer received recorded a sent status: {discarded}"
);
assert_message_is_fixed(discarded, DISCARDED_MESSAGE, label);
assert!(
!capture.recorded(&[REJECTION_MESSAGE]),
"{label}: a refusal the peer never received was recorded as one it did"
);
let completed = only_event(&events, COMPLETION_MESSAGE, label);
assert_field_value(completed, "status", &REPLACED_STATUS.to_string(), label);
let request_id = common::field_value(discarded, "request_id")
.unwrap_or_else(|| panic!("{label}: the discarded record names no request: {discarded}"));
assert_field_value(completed, "request_id", request_id, label);
}
#[test]
fn replaced_rejection_response_reaches_the_operator_record() {
let capture = common::capture_events(REPLACED_PATH);
common::test_runtime()
.with_tracing()
.run(|| {
let mut router = Router::new();
replacing_middleware(&mut router);
router.get(REPLACED_PATH, |_req: &Request| {
std::future::ready(Err::<Response, RuntimeError>(two_level_failure()))
});
let addr = common::spawn_server(router);
let response = common::request(addr, "GET", REPLACED_PATH, &[], &[], WIRE_TIMEOUT)
.expect("the replaced request completed");
assert_eq!(
response.status, REPLACED_STATUS,
"the frame's own answer reached the peer"
);
assert_eq!(response.text().as_ref(), REPLACED_BODY, "wire body");
assert_no_private_text(
&response,
&[MIDDLE_CAUSE, ROOT_CAUSE],
"replaced rejection response",
);
assert_replaced_events(&capture);
runtime::request_shutdown();
})
.expect("the fixture runtime ran to completion");
}
const DEADLINE_PATH: &str = "/deadline-upload";
const ANSWER_BOUND: Duration = Duration::from_secs(300);
const ANSWER_ARMINGS: usize = 4;
fn assert_deadline_event(capture: &common::TraceCapture) {
let label = "body deadline";
let events = capture.events();
let event = only_event(&events, REJECTION_MESSAGE, label);
assert_field_value(event, "status", "408", label);
let raw_path = format!("raw_path={DEADLINE_PATH}");
let route = format!("route={DEADLINE_PATH}");
assert_fields(
event,
&[
"kind=body_timeout",
"default_status=408",
"method=POST",
raw_path.as_str(),
route.as_str(),
"protocol=ordinary_http",
],
label,
);
assert_message_is_fixed(event, REJECTION_MESSAGE, label);
}
#[tokio::test(start_paused = true)]
async fn body_deadline_refusal_reaches_the_operator_record() {
let capture = common::capture_events(DEADLINE_PATH);
let _context = camber::runtime_test_support::install_runtime_context();
let mut router = Router::new();
router.post(DEADLINE_PATH, |_req: &Request| {
std::future::ready(Response::text(200, "stored"))
});
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("the deadline fixture bound a listener");
let addr = listener.local_addr().expect("the listener named its port");
let server = camber::http::serve_background(listener, router);
let mut peer = tokio::net::TcpStream::connect(addr)
.await
.expect("the stalled peer connected");
let head = common::stalled_request_head(None, "POST", DEADLINE_PATH);
peer.write_all(head.as_bytes())
.await
.expect("the stalled head was sent");
peer.flush().await.expect("the stalled head flushed");
let mut answer = Vec::new();
common::bounded_under_pause(
peer.read_to_end(&mut answer),
ANSWER_BOUND,
ANSWER_ARMINGS,
"the stalled body's answer",
)
.await
.expect("the stalled peer read its answer");
drop(peer);
server.shutdown();
let joined = common::bounded_under_pause(
server.join(),
ANSWER_BOUND,
ANSWER_ARMINGS,
"the deadline fixture's server",
)
.await;
common::assert_server_joined(Ok(joined));
let text = String::from_utf8_lossy(&answer);
assert!(
text.starts_with("HTTP/1.1 408 "),
"the stalled body was answered with {text}"
);
assert_deadline_event(&capture);
}