From e9405aa07ea1ec06541304ba1152fb65310cff19 Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Wed, 22 Apr 2026 11:38:31 +0100 Subject: [PATCH 1/2] Log request deserialization errors in driver API Axum returns a terse 400 when a request body or query string fails to deserialize, which makes malformed caller payloads invisible in production. Add `LoggingJson` and `LoggingQuery` extractors that emit a `warn` with the serde error and the type being decoded, then delegate to axum's stock rejection so the HTTP response shape is unchanged. Wire the new extractors into `/settle`, `/reveal`, and `/quote`. Also warn on `/solve` body parse failures in `pre_processing.rs`. --- .../src/domain/competition/pre_processing.rs | 6 +- crates/driver/src/infra/api/extract.rs | 65 +++++++++++++++++++ crates/driver/src/infra/api/mod.rs | 1 + .../driver/src/infra/api/routes/quote/mod.rs | 6 +- .../driver/src/infra/api/routes/reveal/mod.rs | 4 +- .../driver/src/infra/api/routes/settle/mod.rs | 4 +- 6 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 crates/driver/src/infra/api/extract.rs diff --git a/crates/driver/src/domain/competition/pre_processing.rs b/crates/driver/src/domain/competition/pre_processing.rs index 44f3effee5..f45971a74e 100644 --- a/crates/driver/src/domain/competition/pre_processing.rs +++ b/crates/driver/src/domain/competition/pre_processing.rs @@ -235,7 +235,11 @@ impl Utilities { observe::metrics::metrics().on_auction_overhead_start("driver", "parse_dto"); // deserialization takes tens of milliseconds so run it on a blocking task tokio::task::spawn_blocking(move || { - serde_json::from_slice(&solve_request).context("could not parse solve request") + serde_json::from_slice(&solve_request) + .inspect_err(|err| { + tracing::warn!(?err, "failed to parse /solve request body"); + }) + .context("could not parse solve request") }) .await .context("failed to await blocking task")?? diff --git a/crates/driver/src/infra/api/extract.rs b/crates/driver/src/infra/api/extract.rs new file mode 100644 index 0000000000..97f79cb2f6 --- /dev/null +++ b/crates/driver/src/infra/api/extract.rs @@ -0,0 +1,65 @@ +//! Axum extractors that emit a `warn` log when request deserialization +//! fails, then delegate to the stock extractor's rejection so the HTTP +//! response shape is unchanged. + +use { + axum::{ + extract::{ + FromRequest, + FromRequestParts, + Request, + rejection::{JsonRejection, QueryRejection}, + }, + http::request::Parts, + }, + serde::de::DeserializeOwned, +}; + +pub struct LoggingJson(pub T); + +impl FromRequest for LoggingJson +where + S: Send + Sync, + T: DeserializeOwned, +{ + type Rejection = JsonRejection; + + async fn from_request(req: Request, state: &S) -> Result { + match axum::Json::::from_request(req, state).await { + Ok(axum::Json(value)) => Ok(Self(value)), + Err(rejection) => { + tracing::warn!( + err = %rejection, + target = std::any::type_name::(), + "failed to deserialize JSON request body", + ); + Err(rejection) + } + } + } +} + +pub struct LoggingQuery(pub T); + +impl FromRequestParts for LoggingQuery +where + S: Send + Sync, + T: DeserializeOwned, +{ + type Rejection = QueryRejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + match axum::extract::Query::::from_request_parts(parts, state).await { + Ok(axum::extract::Query(value)) => Ok(Self(value)), + Err(rejection) => { + tracing::warn!( + err = %rejection, + target = std::any::type_name::(), + query = parts.uri.query().unwrap_or_default(), + "failed to deserialize query string", + ); + Err(rejection) + } + } + } +} diff --git a/crates/driver/src/infra/api/mod.rs b/crates/driver/src/infra/api/mod.rs index e9d0ce022b..0ba4ad0441 100644 --- a/crates/driver/src/infra/api/mod.rs +++ b/crates/driver/src/infra/api/mod.rs @@ -28,6 +28,7 @@ use { }; mod error; +mod extract; pub mod routes; pub struct Api { diff --git a/crates/driver/src/infra/api/routes/quote/mod.rs b/crates/driver/src/infra/api/routes/quote/mod.rs index 054cbc8819..5fb791b2ac 100644 --- a/crates/driver/src/infra/api/routes/quote/mod.rs +++ b/crates/driver/src/infra/api/routes/quote/mod.rs @@ -1,6 +1,6 @@ use { crate::infra::{ - api::{Error, State}, + api::{Error, State, extract::LoggingQuery}, observe, }, tracing::Instrument, @@ -16,10 +16,10 @@ pub(in crate::infra::api) fn quote(router: axum::Router) -> axum::Router< async fn route( state: axum::extract::State, - order: axum::extract::Query, + LoggingQuery(order): LoggingQuery, ) -> Result, (axum::http::StatusCode, axum::Json)> { let handle_request = async { - let order = order.0.into_domain(); + let order = order.into_domain(); observe::quoting(&order); let quote = order .quote( diff --git a/crates/driver/src/infra/api/routes/reveal/mod.rs b/crates/driver/src/infra/api/routes/reveal/mod.rs index 08e388d761..0475649f20 100644 --- a/crates/driver/src/infra/api/routes/reveal/mod.rs +++ b/crates/driver/src/infra/api/routes/reveal/mod.rs @@ -4,7 +4,7 @@ use { crate::{ domain::competition::auction, infra::{ - api::{self, Error, State}, + api::{self, Error, State, extract::LoggingJson}, observe, }, }, @@ -17,7 +17,7 @@ pub(in crate::infra::api) fn reveal(router: axum::Router) -> axum::Router async fn route( state: axum::extract::State, - req: axum::Json, + LoggingJson(req): LoggingJson, ) -> Result, (axum::http::StatusCode, axum::Json)> { let auction_id = auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?; diff --git a/crates/driver/src/infra/api/routes/settle/mod.rs b/crates/driver/src/infra/api/routes/settle/mod.rs index 047b062e01..3c7bec8a54 100644 --- a/crates/driver/src/infra/api/routes/settle/mod.rs +++ b/crates/driver/src/infra/api/routes/settle/mod.rs @@ -4,7 +4,7 @@ use { crate::{ domain::competition::auction, infra::{ - api::{self, Error, State}, + api::{self, Error, State, extract::LoggingJson}, observe, }, }, @@ -17,7 +17,7 @@ pub(in crate::infra::api) fn settle(router: axum::Router) -> axum::Router async fn route( state: axum::extract::State, - req: axum::Json, + LoggingJson(req): LoggingJson, ) -> Result<(), (axum::http::StatusCode, axum::Json)> { let auction_id = auction::Id::try_from(req.auction_id).map_err(api::routes::AuctionError::from)?; From 6ab5b6bcefc98ed77a71b52d5191839b420bbe5f Mon Sep 17 00:00:00 2001 From: squadgazzz Date: Mon, 27 Apr 2026 11:07:14 +0100 Subject: [PATCH 2/2] Nits --- crates/driver/src/infra/api/extract.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/driver/src/infra/api/extract.rs b/crates/driver/src/infra/api/extract.rs index 97f79cb2f6..74663d8921 100644 --- a/crates/driver/src/infra/api/extract.rs +++ b/crates/driver/src/infra/api/extract.rs @@ -15,6 +15,8 @@ use { serde::de::DeserializeOwned, }; +/// JSON extractor that wraps Axum's native one and logs deserialization +/// errors. pub struct LoggingJson(pub T); impl FromRequest for LoggingJson @@ -39,6 +41,8 @@ where } } +/// Query extractor that wraps Axum's native one and logs deserialization +/// errors. pub struct LoggingQuery(pub T); impl FromRequestParts for LoggingQuery