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..74663d8921 --- /dev/null +++ b/crates/driver/src/infra/api/extract.rs @@ -0,0 +1,69 @@ +//! 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, +}; + +/// JSON extractor that wraps Axum's native one and logs deserialization +/// errors. +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) + } + } + } +} + +/// Query extractor that wraps Axum's native one and logs deserialization +/// errors. +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)?;