Skip to content
Open
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
40 changes: 40 additions & 0 deletions crates/orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ paths:
description: No route was found quoting the order.
"422":
description: Unable to parse request body as valid JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"429":
description: Too many order placements.
"500":
Expand Down Expand Up @@ -239,6 +243,10 @@ paths:
description: Order was not found.
"422":
description: Unable to parse request body as valid JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"/api/v1/orders/{UID}/status":
get:
operationId: getOrderStatus
Expand Down Expand Up @@ -505,6 +513,10 @@ paths:
description: No route was found for the specified order.
"422":
description: Unable to parse request body as valid JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"429":
description: Too many order quotes.
"500":
Expand Down Expand Up @@ -706,6 +718,10 @@ paths:
description: Error validating full `appData`
"422":
description: Unable to parse request body as valid JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: Error storing the full `appData`
/api/v1/app_data:
Expand Down Expand Up @@ -739,6 +755,10 @@ paths:
description: Error validating full `appData`
"422":
description: Unable to parse request body as valid JSON.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: Error storing the full `appData`
"/api/v1/users/{address}/total_surplus":
Expand Down Expand Up @@ -795,6 +815,10 @@ paths:
description: >
Request body failed schema validation: missing required field,
wrong field type, zero `sellAmount`, or unrecognised `kind` value.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
description: Internal error.
"/restricted/api/v1/debug/simulation/{uid}":
Expand Down Expand Up @@ -1801,6 +1825,22 @@ components:
description: Empty signature bytes. Used for "presign" signatures.
type: string
example: 0x
Error:
description: >
Error response returned when a request body cannot be deserialized into
the expected type, for example malformed JSON or a missing required
field.
type: object
properties:
errorType:
type: string
description: An identifier for the kind of error.
description:
type: string
description: A human-readable description of the error.
required:
- errorType
- description
OrderPostError:
type: object
properties:
Expand Down
1 change: 1 addition & 0 deletions crates/orderbook/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod cancel_order;
mod cancel_orders;
mod debug_order;
mod debug_simulation;
mod extract;
mod get_app_data;
mod get_auction;
mod get_native_price;
Expand Down
6 changes: 4 additions & 2 deletions crates/orderbook/src/api/cancel_order.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use {
crate::{api::AppState, orderbook::OrderCancellationError},
crate::{
api::{AppState, extract::Json},
orderbook::OrderCancellationError,
},
axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Response},
Expand Down
7 changes: 5 additions & 2 deletions crates/orderbook/src/api/debug_simulation.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use {
crate::{api::AppState, dto::OrderSimulationRequest, orderbook::OrderSimulationError},
crate::{
api::{AppState, extract::Json},
dto::OrderSimulationRequest,
orderbook::OrderSimulationError,
},
axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Response},
Expand Down
114 changes: 114 additions & 0 deletions crates/orderbook/src/api/extract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Axum extractors that wrap the stock extractor and, when request
//! deserialization fails, respond with this API's structured error format
//! (`{ errorType, description }`) instead of the stock plain-text rejection, so
//! clients can parse every response from the API as JSON.

use {
super::error,
axum::{
extract::{FromRequest, Request},
response::{IntoResponse, Response},
},
serde::{Serialize, de::DeserializeOwned},
};

/// JSON extractor that wraps Axum's native one and renders deserialization
/// errors as this API's structured error response. Also serves as a response
/// type so it can fully replace [`axum::Json`] where both are used.
pub struct Json<T>(pub T);

impl<S, T> FromRequest<S> for Json<T>
where
S: Send + Sync,
T: DeserializeOwned,
{
type Rejection = Response;

async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
match axum::Json::<T>::from_request(req, state).await {
Ok(axum::Json(value)) => Ok(Self(value)),
Err(rejection) => Err((
rejection.status(),
error("InvalidJson", rejection.body_text()),
)
.into_response()),
}
}
}

impl<T: Serialize> IntoResponse for Json<T> {
fn into_response(self) -> Response {
axum::Json(self.0).into_response()
}
}

#[cfg(test)]
mod tests {
use {
super::*,
axum::{
body::{Body, to_bytes},
http::{Request, StatusCode, header::CONTENT_TYPE},
},
serde::Deserialize,
};

#[derive(Deserialize)]
struct Dummy {
_required: u32,
}

async fn structured_error(body: &'static str) -> (StatusCode, serde_json::Value) {
let request = Request::builder()
.method("POST")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(body))
.unwrap();

let response = match Json::<Dummy>::from_request(request, &()).await {
Ok(_) => panic!("malformed body should have been rejected"),
Err(response) => response,
};

let status = response.status();
assert_eq!(
response.headers().get(CONTENT_TYPE).unwrap(),
"application/json",
"error response must be JSON"
);
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
(status, serde_json::from_slice(&bytes).unwrap())
}

// Reproduces cowprotocol/services#4439: an empty JSON object misses a
// required field and must yield a structured JSON error, not plain text.
#[tokio::test]
async fn missing_field_returns_structured_json_error() {
let (status, json) = structured_error("{}").await;
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(json["errorType"], "InvalidJson");
assert!(
json["description"]
.as_str()
.unwrap()
.contains("missing field")
);
}

// Reproduces cowprotocol/services#4440: a field whose value cannot be
// deserialized into the target type (e.g. an invalid token address) must
// also yield a structured JSON error rather than plain text.
#[tokio::test]
async fn invalid_field_value_returns_structured_json_error() {
let (status, json) = structured_error(r#"{"_required": "not-a-number"}"#).await;
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(json["errorType"], "InvalidJson");
}

#[tokio::test]
async fn invalid_syntax_returns_structured_json_error() {
let (status, json) = structured_error("not json").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(json["errorType"], "InvalidJson");
}
}
3 changes: 1 addition & 2 deletions crates/orderbook/src/api/post_order.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use {
crate::{
api::{AppState, error},
api::{AppState, error, extract::Json},
orderbook::{AddOrderError, OrderReplacementError},
},
axum::{
Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Expand Down
3 changes: 1 addition & 2 deletions crates/orderbook/src/api/post_quote.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use {
super::post_order::{AppDataValidationErrorWrapper, PartialValidationErrorWrapper},
crate::{
api::{AppState, error, rich_error},
api::{AppState, error, extract::Json, rich_error},
quoter::OrderQuoteError,
},
axum::{
Json,
extract::State,
response::{IntoResponse, Response},
},
Expand Down
4 changes: 2 additions & 2 deletions crates/orderbook/src/api/put_app_data.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use {
crate::api::{AppState, internal_error_reply},
crate::api::{AppState, extract::Json, internal_error_reply},
app_data::{AppDataDocument, AppDataHash},
axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Json, Response},
response::{IntoResponse, Response},
},
std::sync::Arc,
};
Expand Down
Loading