use std::any::Any;
use std::backtrace::{Backtrace, BacktraceStatus};
use std::cell::RefCell;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::{Arc, Once};
use std::task::{Context, Poll};
use axum::extract::MatchedPath;
use axum::http::{Request, StatusCode};
use axum::response::{IntoResponse, Response};
use futures::FutureExt;
use pin_project_lite::pin_project;
use tower::{Layer, Service};
use crate::middleware::RequestId;
use crate::middleware::exception_filter::AutumnErrorInfo;
pub type ReportFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ErrorEvent {
pub status: StatusCode,
pub message: String,
pub problem_type: Option<String>,
pub request_id: Option<String>,
pub route: Option<String>,
pub method: Option<String>,
pub panic: Option<PanicInfo>,
pub capsule: Option<crate::capsule::CapsuleRef>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct PanicInfo {
pub payload: String,
pub backtrace: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CaughtPanic {
pub payload: String,
}
pub trait ErrorReporter: Send + Sync + 'static {
fn report<'a>(&'a self, event: &'a ErrorEvent) -> ReportFuture<'a>;
}
#[derive(Debug, Clone, Default)]
pub struct LogReporter;
impl ErrorReporter for LogReporter {
fn report<'a>(&'a self, event: &'a ErrorEvent) -> ReportFuture<'a> {
Box::pin(async move {
if let Some(panic) = event.panic.as_ref() {
tracing::error!(
status = %event.status,
method = event.method.as_deref().unwrap_or("-"),
route = event.route.as_deref().unwrap_or("-"),
request_id = event.request_id.as_deref().unwrap_or("-"),
backtrace = panic.backtrace.as_deref().unwrap_or("(set RUST_BACKTRACE=1 to capture)"),
"handler panic captured: {}",
panic.payload
);
} else {
tracing::error!(
status = %event.status,
method = event.method.as_deref().unwrap_or("-"),
route = event.route.as_deref().unwrap_or("-"),
request_id = event.request_id.as_deref().unwrap_or("-"),
problem_type = event.problem_type.as_deref().unwrap_or("-"),
"server error captured: {}",
event.message
);
}
})
}
}
#[derive(Clone, Default)]
pub(crate) struct RegisteredReporters(pub(crate) Vec<Arc<dyn ErrorReporter>>);
struct ReporterChain {
reporters: Vec<Arc<dyn ErrorReporter>>,
enabled: bool,
sample_rate: f64,
}
impl ReporterChain {
fn dispatch(self: &Arc<Self>, event: ErrorEvent, capture: Option<CaptureContext>) {
let deliver = self.enabled && sampled(self.sample_rate);
if !deliver && capture.is_none() {
return;
}
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let chain = Arc::clone(self);
handle.spawn(async move {
let mut event = event;
let _pin = match capture {
Some(capture) => {
let written = persist_capsule(capture).await;
written.map(|(reference, pin)| {
event.capsule = Some(reference);
pin
})
}
None => None,
};
if deliver {
chain.report_all(&event).await;
}
});
}
}
async fn report_all(&self, event: &ErrorEvent) {
for reporter in &self.reporters {
match std::panic::catch_unwind(AssertUnwindSafe(|| reporter.report(event))) {
Ok(future) => {
if AssertUnwindSafe(future).catch_unwind().await.is_err() {
tracing::warn!("error reporter panicked while reporting; ignoring");
}
}
Err(_panic) => {
tracing::warn!("error reporter panicked constructing report future; ignoring");
}
}
}
}
}
struct CaptureContext {
handle: crate::capsule::CaptureHandle,
outcome: crate::capsule::CapsuleOutcome,
}
async fn persist_capsule(
capture: CaptureContext,
) -> Option<(
crate::capsule::CapsuleRef,
crate::capsule::persist::ReportingPin,
)> {
let written = tokio::task::spawn_blocking(move || {
crate::capsule::persist::persist_pinned(capture.handle.scope(), capture.outcome)
})
.await;
match written {
Ok(reference) => reference,
Err(error) => {
tracing::error!(
%error,
"failure capsule could not be written on the blocking pool; \
the failure itself is still reported"
);
None
}
}
}
thread_local! {
static RNG_STATE: std::cell::Cell<u64> = std::cell::Cell::new(seed_rng());
}
fn seed_rng() -> u64 {
let mut buf = [0u8; 8];
if getrandom::getrandom(&mut buf).is_ok() {
let seed = u64::from_ne_bytes(buf);
if seed != 0 {
return seed;
}
}
0x5555_5555_5555_5555
}
fn next_u64() -> u64 {
RNG_STATE.with(|cell| {
let mut x = cell.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
cell.set(x);
x
})
}
#[allow(clippy::cast_precision_loss)]
fn sampled(rate: f64) -> bool {
if rate >= 1.0 {
return true;
}
if rate <= 0.0 {
return false;
}
let draw = next_u64() >> 11;
let value = draw as f64 / (1u64 << 53) as f64;
value < rate
}
thread_local! {
static LAST_PANIC: RefCell<Option<CapturedPanic>> = const { RefCell::new(None) };
}
struct CapturedPanic {
backtrace: Option<String>,
}
static HOOK_INSTALLED: Once = Once::new();
fn ensure_panic_hook() {
HOOK_INSTALLED.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let backtrace = Backtrace::capture();
let backtrace =
(backtrace.status() == BacktraceStatus::Captured).then(|| backtrace.to_string());
LAST_PANIC.with(|cell| {
*cell.borrow_mut() = Some(CapturedPanic { backtrace });
});
previous(info);
}));
});
}
fn format_panic_payload(payload: &(dyn Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_owned())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "handler panicked".to_owned())
}
#[derive(Clone)]
struct RequestContext {
method: String,
route: Option<String>,
request_id: Option<String>,
capture: Option<crate::capsule::CaptureHandle>,
}
#[derive(Clone)]
pub struct ReportingLayer {
chain: Arc<ReporterChain>,
}
impl ReportingLayer {
#[must_use]
pub(crate) fn new(
reporters: Vec<Arc<dyn ErrorReporter>>,
enabled: bool,
sample_rate: f64,
) -> Self {
ensure_panic_hook();
let reporters = if reporters.is_empty() {
vec![Arc::new(LogReporter) as Arc<dyn ErrorReporter>]
} else {
reporters
};
Self {
chain: Arc::new(ReporterChain {
reporters,
enabled,
sample_rate,
}),
}
}
}
impl<S> Layer<S> for ReportingLayer {
type Service = ReportingService<S>;
fn layer(&self, inner: S) -> Self::Service {
ReportingService {
inner,
chain: Arc::clone(&self.chain),
}
}
}
#[derive(Clone)]
pub struct ReportingService<S> {
inner: S,
chain: Arc<ReporterChain>,
}
impl<S, ReqBody> Service<Request<ReqBody>> for ReportingService<S>
where
S: Service<Request<ReqBody>, Response = Response>,
{
type Response = Response;
type Error = S::Error;
type Future = ReportingFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let method = req.method().as_str().to_owned();
let route = req
.extensions()
.get::<MatchedPath>()
.map(|m| m.as_str().to_owned());
let request_id = req
.extensions()
.get::<RequestId>()
.map(std::string::ToString::to_string);
let capture = req
.extensions()
.get::<crate::capsule::CaptureHandle>()
.cloned();
let context = Some(RequestContext {
method,
route,
request_id,
capture,
});
let inner = &mut self.inner;
match std::panic::catch_unwind(AssertUnwindSafe(|| inner.call(req))) {
Ok(future) => ReportingFuture {
inner: Some(future),
pending_panic: None,
context,
chain: Arc::clone(&self.chain),
},
Err(panic) => ReportingFuture {
inner: None,
pending_panic: Some(panic),
context,
chain: Arc::clone(&self.chain),
},
}
}
}
pin_project! {
pub struct ReportingFuture<F> {
#[pin]
inner: Option<F>,
pending_panic: Option<Box<dyn Any + Send>>,
context: Option<RequestContext>,
chain: Arc<ReporterChain>,
}
}
impl<F, E> Future for ReportingFuture<F>
where
F: Future<Output = Result<Response, E>>,
{
type Output = Result<Response, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if let Some(panic) = this.pending_panic.take() {
let context = this.context.take();
return Poll::Ready(Ok(handle_panic(&*panic, context, this.chain)));
}
let Some(inner) = this.inner.as_pin_mut() else {
return Poll::Pending;
};
match std::panic::catch_unwind(AssertUnwindSafe(move || inner.poll(cx))) {
Ok(Poll::Pending) => Poll::Pending,
Ok(Poll::Ready(Ok(mut response))) => {
if let Some(context) = this.context.take() {
report_response(&mut response, context, this.chain);
}
Poll::Ready(Ok(response))
}
Ok(Poll::Ready(Err(error))) => Poll::Ready(Err(error)),
Err(panic) => {
let context = this.context.take();
let response = handle_panic(&*panic, context, this.chain);
Poll::Ready(Ok(response))
}
}
}
}
fn report_response(response: &mut Response, context: RequestContext, chain: &Arc<ReporterChain>) {
if !response.status().is_server_error() {
return;
}
let info = response.extensions().get::<AutumnErrorInfo>();
let (message, problem_type) = info.map_or_else(
|| {
(
response
.status()
.canonical_reason()
.unwrap_or("server error")
.to_owned(),
None,
)
},
|info| (info.message.clone(), info.problem_type.map(str::to_owned)),
);
let body_is_materialized = context.capture.is_some() && materialize_body(response);
let capture = context.capture.map(|handle| {
if !body_is_materialized {
handle.scope().note(
"the failing response body was still being produced when the response \
head resolved; effects produced while the body streams are not recorded, \
so the capsule is marked truncated",
);
handle.scope().mark_truncated();
}
handle.scope().close();
CaptureContext {
handle,
outcome: crate::capsule::CapsuleOutcome::Status {
code: response.status().as_u16(),
message: message.clone(),
problem_type: problem_type.clone(),
},
}
});
chain.dispatch(
ErrorEvent {
status: response.status(),
message,
problem_type,
request_id: context.request_id,
route: context.route,
method: Some(context.method),
panic: None,
capsule: None,
},
capture,
);
}
const PROBE_FRAMES: usize = 8;
const PROBE_BYTES: usize = 64 * 1024;
fn materialize_body(response: &mut Response) -> bool {
use axum::body::Body;
use bytes::Bytes;
use http_body::Body as _;
let body = response.body_mut();
if body.is_end_stream() {
return true;
}
if body.size_hint().exact().is_none() {
return false;
}
let mut body = std::mem::replace(response.body_mut(), Body::empty());
let mut frames: std::collections::VecDeque<http_body::Frame<Bytes>> =
std::collections::VecDeque::new();
let mut collected = 0usize;
let mut data_only = true;
let waker = std::task::Waker::noop();
let mut cx = Context::from_waker(waker);
for _ in 0..PROBE_FRAMES {
match Pin::new(&mut body).poll_frame(&mut cx) {
Poll::Ready(None) => {
*response.body_mut() = rebuild_probed_body(frames, None, None, data_only);
return true;
}
Poll::Ready(Some(Err(error))) => {
*response.body_mut() = rebuild_probed_body(frames, Some(error), None, data_only);
return false;
}
Poll::Ready(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
collected = collected.saturating_add(data.len());
} else {
data_only = false;
}
frames.push_back(frame);
if collected > PROBE_BYTES {
break;
}
}
Poll::Pending => break,
}
}
*response.body_mut() = rebuild_probed_body(frames, None, Some(body), data_only);
false
}
fn rebuild_probed_body(
frames: std::collections::VecDeque<http_body::Frame<bytes::Bytes>>,
error: Option<axum::Error>,
rest: Option<axum::body::Body>,
data_only: bool,
) -> axum::body::Body {
use axum::body::Body;
if frames.is_empty() && error.is_none() {
return rest.unwrap_or_else(Body::empty);
}
if data_only && error.is_none() && rest.is_none() {
return Body::from(concat_frames(frames));
}
Body::new(ProbedBody {
frames,
error,
rest,
})
}
fn concat_frames(
frames: std::collections::VecDeque<http_body::Frame<bytes::Bytes>>,
) -> bytes::Bytes {
use bytes::Bytes;
let mut chunks = frames
.into_iter()
.filter_map(|frame| frame.into_data().ok());
let Some(first) = chunks.next() else {
return Bytes::new();
};
let Some(second) = chunks.next() else {
return first;
};
let mut joined = Vec::with_capacity(first.len().saturating_add(second.len()));
joined.extend_from_slice(&first);
joined.extend_from_slice(&second);
for chunk in chunks {
joined.extend_from_slice(&chunk);
}
Bytes::from(joined)
}
struct ProbedBody {
frames: std::collections::VecDeque<http_body::Frame<bytes::Bytes>>,
error: Option<axum::Error>,
rest: Option<axum::body::Body>,
}
impl http_body::Body for ProbedBody {
type Data = bytes::Bytes;
type Error = axum::Error;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
if let Some(frame) = self.frames.pop_front() {
return Poll::Ready(Some(Ok(frame)));
}
if let Some(error) = self.error.take() {
return Poll::Ready(Some(Err(error)));
}
self.rest
.as_mut()
.map_or_else(|| Poll::Ready(None), |rest| Pin::new(rest).poll_frame(cx))
}
fn is_end_stream(&self) -> bool {
self.frames.is_empty()
&& self.error.is_none()
&& self
.rest
.as_ref()
.is_none_or(http_body::Body::is_end_stream)
}
}
fn handle_panic(
payload: &(dyn Any + Send),
context: Option<RequestContext>,
chain: &Arc<ReporterChain>,
) -> Response {
let message = format_panic_payload(payload);
let backtrace = LAST_PANIC
.with(|cell| cell.borrow_mut().take())
.and_then(|captured| captured.backtrace);
if let Some(context) = context {
let capture = context.capture.map(|handle| {
handle.scope().close();
CaptureContext {
handle,
outcome: crate::capsule::CapsuleOutcome::Panic {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
payload: message.clone(),
backtrace: backtrace.clone(),
},
}
});
chain.dispatch(
ErrorEvent {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: message.clone(),
problem_type: None,
request_id: context.request_id,
route: context.route,
method: Some(context.method),
panic: Some(PanicInfo {
payload: message,
backtrace,
}),
capsule: None,
},
capture,
);
}
let mut response =
crate::error::AutumnError::internal_server_error_msg("Internal server error")
.into_response();
response.extensions_mut().insert(CaughtPanic {
payload: format_panic_payload(payload),
});
response
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[test]
fn sampled_extremes_are_deterministic() {
assert!(sampled(1.0));
assert!(sampled(2.0));
assert!(!sampled(0.0));
assert!(!sampled(-1.0));
}
#[test]
fn sampled_full_rate_always_true_over_many_draws() {
for _ in 0..1000 {
assert!(sampled(1.0));
}
}
#[test]
fn format_panic_payload_handles_str_and_string() {
let s: &str = "boom";
assert_eq!(format_panic_payload(&s), "boom");
let owned: String = "kaboom".to_owned();
assert_eq!(format_panic_payload(&owned), "kaboom");
let other: u32 = 7;
assert_eq!(format_panic_payload(&other), "handler panicked");
}
#[tokio::test]
async fn a_body_is_materialized_only_when_it_finishes_without_waiting() {
use axum::body::Body;
use http_body_util::BodyExt as _;
fn probe(body: Body) -> (bool, Response) {
let mut response = Response::new(body);
let materialized = materialize_body(&mut response);
(materialized, response)
}
struct ExactButLazy(u8);
impl http_body::Body for ExactButLazy {
type Data = bytes::Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
self.0 = self.0.saturating_add(1);
match self.0 {
1 => {
cx.waker().wake_by_ref();
Poll::Pending
}
2 => Poll::Ready(Some(Ok(http_body::Frame::data(bytes::Bytes::from_static(
b"late",
))))),
_ => Poll::Ready(None),
}
}
fn size_hint(&self) -> http_body::SizeHint {
http_body::SizeHint::with_exact(4)
}
}
assert!(probe(Body::empty()).0);
let (materialized, response) = probe(Body::from("boom details"));
assert!(materialized);
assert_eq!(
response
.into_body()
.collect()
.await
.expect("collect")
.to_bytes(),
"boom details",
"a materialized body must be handed on byte for byte"
);
let streaming = Body::from_stream(futures::stream::once(async {
Ok::<_, std::convert::Infallible>(bytes::Bytes::from_static(b"chunk"))
}));
assert!(!probe(streaming).0);
let (materialized, response) = probe(Body::new(ExactButLazy(0)));
assert!(
!materialized,
"an exact size hint does not mean the bytes exist yet"
);
assert_eq!(
response
.into_body()
.collect()
.await
.expect("collect")
.to_bytes(),
"late",
"a probed body must still deliver everything it produces"
);
}
#[tokio::test]
async fn a_probed_trailer_frame_is_put_back() {
use axum::body::Body;
use http_body_util::BodyExt as _;
struct DataThenTrailers(u8);
impl http_body::Body for DataThenTrailers {
type Data = bytes::Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
self.0 = self.0.saturating_add(1);
match self.0 {
1 => Poll::Ready(Some(Ok(http_body::Frame::data(bytes::Bytes::from_static(
b"body",
))))),
2 => {
let mut trailers = axum::http::HeaderMap::new();
trailers.insert("x-checksum", axum::http::HeaderValue::from_static("42"));
Poll::Ready(Some(Ok(http_body::Frame::trailers(trailers))))
}
_ => Poll::Ready(None),
}
}
fn size_hint(&self) -> http_body::SizeHint {
http_body::SizeHint::with_exact(4)
}
}
let mut response = Response::new(Body::new(DataThenTrailers(0)));
assert!(
materialize_body(&mut response),
"the body finished without waiting, so the capsule is complete"
);
let collected = response
.into_body()
.collect()
.await
.expect("collect the rebuilt body");
let trailers = collected
.trailers()
.cloned()
.expect("the trailer frame must survive the probe");
assert_eq!(trailers.get("x-checksum").expect("checksum"), "42");
assert_eq!(collected.to_bytes(), "body");
}
#[tokio::test]
async fn a_probed_body_error_is_preserved_and_marks_the_capsule_incomplete() {
use axum::body::Body;
use http_body_util::BodyExt as _;
struct DataThenError(bool);
impl http_body::Body for DataThenError {
type Data = bytes::Bytes;
type Error = std::io::Error;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
if self.0 {
self.0 = false;
return Poll::Ready(Some(Ok(http_body::Frame::data(
bytes::Bytes::from_static(b"partial"),
))));
}
Poll::Ready(Some(Err(std::io::Error::other("upstream went away"))))
}
fn size_hint(&self) -> http_body::SizeHint {
http_body::SizeHint::with_exact(7)
}
}
let mut response = Response::new(Body::new(DataThenError(true)));
assert!(
!materialize_body(&mut response),
"a body that failed did not finish, so the capsule must not claim to be complete"
);
let error = response
.into_body()
.collect()
.await
.expect_err("the body error must reach the client, not be collapsed into EOF");
assert!(
error.to_string().contains("upstream went away"),
"the original error must survive the probe: {error}"
);
}
#[tokio::test]
async fn probed_frames_are_put_back_in_front_of_a_stalling_body() {
use axum::body::Body;
use http_body_util::BodyExt as _;
struct ReadyThenStall(u8);
impl http_body::Body for ReadyThenStall {
type Data = bytes::Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
self.0 = self.0.saturating_add(1);
match self.0 {
1 => Poll::Ready(Some(Ok(http_body::Frame::data(bytes::Bytes::from_static(
b"first",
))))),
2 => {
cx.waker().wake_by_ref();
Poll::Pending
}
3 => Poll::Ready(Some(Ok(http_body::Frame::data(bytes::Bytes::from_static(
b"-rest",
))))),
_ => Poll::Ready(None),
}
}
fn size_hint(&self) -> http_body::SizeHint {
http_body::SizeHint::with_exact(10)
}
}
let mut response = Response::new(Body::new(ReadyThenStall(0)));
assert!(!materialize_body(&mut response), "the body stalls");
assert_eq!(
response
.into_body()
.collect()
.await
.expect("collect")
.to_bytes(),
"first-rest",
"the probed frame must not be lost"
);
}
#[test]
fn capture_context_can_cross_to_the_blocking_pool() {
const fn assert_send_static<T: Send + 'static>() {}
assert_send_static::<CaptureContext>();
}
#[test]
fn log_reporter_is_the_default_when_empty() {
let layer = ReportingLayer::new(Vec::new(), true, 1.0);
assert_eq!(layer.chain.reporters.len(), 1);
}
#[tokio::test]
async fn panic_in_inner_call_is_caught_as_500() {
use axum::body::Body;
use std::convert::Infallible;
use tower::ServiceExt;
#[derive(Clone)]
struct PanicInCall;
impl Service<Request<Body>> for PanicInCall {
type Response = Response;
type Error = Infallible;
type Future = std::future::Ready<Result<Response, Infallible>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<Body>) -> Self::Future {
panic!("boom in call");
}
}
let service = ReportingLayer::new(Vec::new(), true, 1.0).layer(PanicInCall);
let response = service
.oneshot(Request::new(Body::empty()))
.await
.expect("panic in call must be converted to a response, not propagated");
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn a_streaming_5xx_marks_its_capsule_truncated() {
use std::convert::Infallible;
use std::sync::Arc;
use axum::body::Body;
use tower::ServiceExt;
use crate::capsule::{CaptureHandle, CaptureLayer, CaptureSettings};
use crate::log::filter::ParameterFilter;
let dir = tempfile::tempdir().expect("tempdir");
let seen: Arc<Mutex<Option<CaptureHandle>>> = Arc::new(Mutex::new(None));
let inner = {
let seen = Arc::clone(&seen);
tower::service_fn(move |req: Request<Body>| {
let seen = Arc::clone(&seen);
async move {
*seen.lock().expect("lock") = req.extensions().get::<CaptureHandle>().cloned();
let stream = futures::stream::once(async {
Ok::<_, Infallible>(bytes::Bytes::from_static(b"partial error page"))
});
let mut response = Response::new(Body::from_stream(stream));
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Ok::<_, Infallible>(response)
}
})
};
let reporting = ReportingLayer::new(Vec::new(), true, 1.0).layer(inner);
let service = CaptureLayer::new(
CaptureSettings {
dir: dir.path().to_string_lossy().into_owned(),
..CaptureSettings::default()
},
Arc::new(ParameterFilter::new(&[], &[])),
)
.layer(reporting);
let response = service
.oneshot(Request::new(Body::empty()))
.await
.expect("infallible");
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let handle = seen
.lock()
.expect("lock")
.clone()
.expect("the capture layer inserts a handle");
assert!(
handle.scope().is_truncated(),
"a streaming 5xx body means the capsule cannot vouch for completeness"
);
assert!(
handle
.scope()
.notes()
.iter()
.any(|note| note.contains("still being produced")),
"the truncation must be explained in the notes: {:?}",
handle.scope().notes()
);
}
#[tokio::test]
async fn disabled_chain_does_not_dispatch() {
#[derive(Clone)]
struct Counter(Arc<Mutex<u32>>);
impl ErrorReporter for Counter {
fn report<'a>(&'a self, _event: &'a ErrorEvent) -> ReportFuture<'a> {
let count = self.0.clone();
Box::pin(async move {
*count.lock().unwrap() += 1;
})
}
}
let count = Arc::new(Mutex::new(0));
let chain = Arc::new(ReporterChain {
reporters: vec![Arc::new(Counter(count.clone()))],
enabled: false,
sample_rate: 1.0,
});
chain.dispatch(
ErrorEvent {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "x".into(),
problem_type: None,
request_id: None,
route: None,
method: None,
panic: None,
capsule: None,
},
None,
);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert_eq!(*count.lock().unwrap(), 0);
}
fn server_error_event() -> ErrorEvent {
ErrorEvent {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "boom".into(),
problem_type: Some("https://autumn.dev/problems/x".into()),
request_id: Some("req-1".into()),
route: Some("/x".into()),
method: Some("GET".into()),
panic: None,
capsule: None,
}
}
fn panic_event() -> ErrorEvent {
ErrorEvent {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: "kaboom".into(),
problem_type: None,
request_id: None,
route: None,
method: None,
panic: Some(PanicInfo {
payload: "kaboom".into(),
backtrace: Some("<backtrace>".into()),
}),
capsule: None,
}
}
#[tokio::test]
async fn log_reporter_reports_both_event_kinds() {
let reporter = LogReporter;
reporter.report(&server_error_event()).await;
reporter.report(&panic_event()).await;
}
#[test]
fn sampled_fractional_uses_prng_and_varies() {
let mut trues = 0;
for _ in 0..10_000 {
if sampled(0.5) {
trues += 1;
}
}
assert!(
trues > 0 && trues < 10_000,
"fractional sampling should produce a mix of decisions, got {trues}"
);
}
#[tokio::test]
async fn reporter_panicking_while_constructing_future_is_swallowed() {
struct PanicOnConstruct;
impl ErrorReporter for PanicOnConstruct {
fn report<'a>(&'a self, _event: &'a ErrorEvent) -> ReportFuture<'a> {
panic!("panic before returning the future");
}
}
let chain = ReporterChain {
reporters: vec![Arc::new(PanicOnConstruct)],
enabled: true,
sample_rate: 1.0,
};
chain.report_all(&server_error_event()).await;
}
#[test]
fn dispatch_without_a_runtime_is_a_noop() {
let chain = Arc::new(ReporterChain {
reporters: vec![Arc::new(LogReporter)],
enabled: true,
sample_rate: 1.0,
});
chain.dispatch(server_error_event(), None);
}
}