1use axum::{
45 extract::{FromRequest, Request},
46 response::{IntoResponse, Response},
47 Json,
48};
49use serde::de::DeserializeOwned;
50#[cfg(feature = "extract")]
51use serde::Serialize;
52#[cfg(feature = "validator")]
53use validator::Validate;
54
55use crate::problem::{Problem, ProblemFormat};
56#[cfg(feature = "validator")]
57use crate::ApiError;
58
59#[derive(Debug, Clone)]
90pub struct ProblemRejection {
91 pub problem: Problem,
95 pub format: ProblemFormat,
97}
98
99impl IntoResponse for ProblemRejection {
100 fn into_response(self) -> Response {
101 self.problem.into_response_with(self.format)
102 }
103}
104
105fn json_rejection_to_problem(rejection: axum::extract::rejection::JsonRejection) -> Problem {
110 Problem::from(crate::error::json_rejection_to_api_error(rejection))
111}
112
113#[cfg(feature = "extract")]
153#[derive(Debug, Clone)]
154pub struct ProblemJson<T>(pub T);
155
156#[cfg(feature = "extract")]
157impl<T, S> FromRequest<S> for ProblemJson<T>
158where
159 T: DeserializeOwned,
160 S: Send + Sync,
161{
162 type Rejection = ProblemRejection;
163
164 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
165 let format = ProblemFormat::negotiate(req.headers());
166 match Json::<T>::from_request(req, state).await {
167 Ok(Json(value)) => Ok(ProblemJson(value)),
168 Err(rejection) => Err(ProblemRejection {
169 problem: json_rejection_to_problem(rejection),
170 format,
171 }),
172 }
173 }
174}
175
176#[cfg(feature = "extract")]
177impl<T: Serialize> IntoResponse for ProblemJson<T> {
178 fn into_response(self) -> Response {
179 Json(self.0).into_response()
180 }
181}
182
183#[cfg(feature = "validator")]
240#[derive(Debug, Clone)]
241pub struct ProblemValidatedJson<T>(pub T);
242
243#[cfg(feature = "validator")]
244impl<T, S> FromRequest<S> for ProblemValidatedJson<T>
245where
246 T: DeserializeOwned + Validate,
247 S: Send + Sync,
248{
249 type Rejection = ProblemRejection;
250
251 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
252 let format = ProblemFormat::negotiate(req.headers());
253 let value = match Json::<T>::from_request(req, state).await {
254 Ok(Json(value)) => value,
255 Err(rejection) => {
256 return Err(ProblemRejection {
257 problem: json_rejection_to_problem(rejection),
258 format,
259 })
260 }
261 };
262 if let Err(errors) = value.validate() {
263 return Err(ProblemRejection {
264 problem: Problem::from((
265 axum::http::StatusCode::UNPROCESSABLE_ENTITY,
266 ApiError::from(errors),
267 )),
268 format,
269 });
270 }
271 Ok(ProblemValidatedJson(value))
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use axum::http::{header, StatusCode};
279
280 #[tokio::test]
281 async fn problem_rejection_into_response_respects_format_with_identical_bodies() {
282 let rejection = |format| ProblemRejection {
283 problem: Problem::new(StatusCode::BAD_REQUEST, "Bad Request").with_detail("nope"),
284 format,
285 };
286
287 let default = rejection(ProblemFormat::ProblemJson).into_response();
288 assert_eq!(default.status(), StatusCode::BAD_REQUEST);
289 assert_eq!(
290 default.headers().get(header::CONTENT_TYPE).unwrap(),
291 "application/problem+json"
292 );
293
294 let negotiated = rejection(ProblemFormat::Json).into_response();
295 assert_eq!(negotiated.status(), StatusCode::BAD_REQUEST);
296 assert_eq!(
297 negotiated.headers().get(header::CONTENT_TYPE).unwrap(),
298 "application/json"
299 );
300
301 let default_body = axum::body::to_bytes(default.into_body(), usize::MAX)
302 .await
303 .unwrap();
304 let negotiated_body = axum::body::to_bytes(negotiated.into_body(), usize::MAX)
305 .await
306 .unwrap();
307 assert_eq!(
308 default_body, negotiated_body,
309 "bodies must be byte-identical across formats"
310 );
311 }
312
313 #[cfg(feature = "extract")]
314 mod problem_json {
315 use super::*;
316 use axum::body::Body;
317 use axum::http::{header::ACCEPT, header::CONTENT_TYPE, Request};
318 use serde::Deserialize;
319
320 #[derive(Debug, Deserialize)]
321 struct Input {
322 name: String,
323 }
324
325 fn request(body: &str, content_type: Option<&str>, accept: Option<&str>) -> Request<Body> {
326 let mut builder = Request::builder().method("POST").uri("/");
327 if let Some(value) = content_type {
328 builder = builder.header(CONTENT_TYPE, value);
329 }
330 if let Some(value) = accept {
331 builder = builder.header(ACCEPT, value);
332 }
333 builder.body(Body::from(body.to_owned())).unwrap()
334 }
335
336 async fn extract(
337 body: &str,
338 content_type: Option<&str>,
339 ) -> Result<Input, ProblemRejection> {
340 ProblemJson::<Input>::from_request(request(body, content_type, None), &())
341 .await
342 .map(|ProblemJson(v)| v)
343 }
344
345 #[tokio::test]
346 async fn valid_body_extracts() {
347 let input = extract(r#"{"name":"abc"}"#, Some("application/json"))
348 .await
349 .unwrap();
350 assert_eq!(input.name, "abc");
351 }
352
353 #[tokio::test]
354 async fn malformed_json_is_invalid_json_problem() {
355 let rejection = extract("{not json", Some("application/json"))
356 .await
357 .unwrap_err();
358 assert_eq!(rejection.problem.status, 400);
359 assert_eq!(rejection.problem.title, "Bad Request");
360 assert_eq!(rejection.problem.extensions["code"], "INVALID_JSON");
361 assert!(rejection.problem.detail.is_some());
362 assert_eq!(rejection.format, ProblemFormat::ProblemJson);
363 }
364
365 #[tokio::test]
366 async fn wrong_shape_is_invalid_body_problem() {
367 let rejection = extract(r#"{"name":123}"#, Some("application/json"))
368 .await
369 .unwrap_err();
370 assert_eq!(rejection.problem.status, 422);
371 assert_eq!(rejection.problem.title, "Unprocessable Entity");
372 assert_eq!(rejection.problem.extensions["code"], "INVALID_BODY");
373 }
374
375 #[tokio::test]
376 async fn missing_content_type_is_unsupported_media_type_problem() {
377 let rejection = extract(r#"{"name":"abc"}"#, None).await.unwrap_err();
378 assert_eq!(rejection.problem.status, 415);
379 assert_eq!(rejection.problem.title, "Unsupported Media Type");
380 assert_eq!(
381 rejection.problem.extensions["code"],
382 "UNSUPPORTED_MEDIA_TYPE"
383 );
384 }
385
386 #[tokio::test]
387 async fn wrong_content_type_is_unsupported_media_type_problem() {
388 let rejection = extract(r#"{"name":"abc"}"#, Some("text/plain"))
389 .await
390 .unwrap_err();
391 assert_eq!(rejection.problem.status, 415);
392 assert_eq!(
393 rejection.problem.extensions["code"],
394 "UNSUPPORTED_MEDIA_TYPE"
395 );
396 }
397
398 #[tokio::test]
399 async fn json_with_charset_is_accepted() {
400 let input = extract(r#"{"name":"abc"}"#, Some("application/json; charset=utf-8"))
401 .await
402 .unwrap();
403 assert_eq!(input.name, "abc");
404 }
405
406 #[tokio::test]
407 async fn vendor_plus_json_is_accepted() {
408 let input = extract(r#"{"name":"abc"}"#, Some("application/vnd.api+json"))
409 .await
410 .unwrap();
411 assert_eq!(input.name, "abc");
412 }
413
414 #[tokio::test]
415 async fn rejection_carries_same_information_as_apijson() {
416 use crate::ApiJson;
417
418 for (body, content_type) in [
419 ("{not json", Some("application/json")),
420 (r#"{"name":123}"#, Some("application/json")),
421 (r#"{"name":"abc"}"#, None),
422 ] {
423 let problem_rejection =
424 ProblemJson::<Input>::from_request(request(body, content_type, None), &())
425 .await
426 .unwrap_err();
427 let (status, Json(api_error)) =
428 ApiJson::<Input>::from_request(request(body, content_type, None), &())
429 .await
430 .unwrap_err();
431
432 let problem = &problem_rejection.problem;
433 assert_eq!(problem.status, status.as_u16(), "body {body:?}");
434 assert_eq!(problem.extensions["code"], api_error.code, "body {body:?}");
435 assert_eq!(
436 problem.detail.as_deref(),
437 Some(api_error.message.as_str()),
438 "body {body:?}"
439 );
440 assert_eq!(
441 problem.extensions.get("details"),
442 api_error.details.as_ref(),
443 "body {body:?}"
444 );
445 }
446 }
447
448 #[tokio::test]
449 async fn rejection_negotiates_format_from_accept_header() {
450 let rejection = ProblemJson::<Input>::from_request(
451 request(
452 "{not json",
453 Some("application/json"),
454 Some("application/json"),
455 ),
456 &(),
457 )
458 .await
459 .unwrap_err();
460 assert_eq!(rejection.format, ProblemFormat::Json);
461 let res = rejection.into_response();
462 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
463 assert_eq!(
464 res.headers().get(header::CONTENT_TYPE).unwrap(),
465 "application/json"
466 );
467
468 for accept in [None, Some("*/*"), Some("text/html")] {
469 let rejection = ProblemJson::<Input>::from_request(
470 request("{not json", Some("application/json"), accept),
471 &(),
472 )
473 .await
474 .unwrap_err();
475 assert_eq!(
476 rejection.format,
477 ProblemFormat::ProblemJson,
478 "Accept: {accept:?}"
479 );
480 }
481 }
482
483 #[tokio::test]
484 async fn rejection_response_serves_problem_wire_shape() {
485 let res = extract("{not json", Some("application/json"))
486 .await
487 .unwrap_err()
488 .into_response();
489 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
490 assert_eq!(
491 res.headers().get(header::CONTENT_TYPE).unwrap(),
492 "application/problem+json"
493 );
494 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
495 .await
496 .unwrap();
497 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
498 assert_eq!(body["title"], "Bad Request");
499 assert_eq!(body["status"], 400);
500 assert_eq!(body["code"], "INVALID_JSON");
501 assert!(body["detail"].is_string());
502 }
503
504 #[tokio::test]
505 async fn serializes_as_response() {
506 use serde::Serialize;
507
508 #[derive(Serialize)]
509 struct Out {
510 id: u32,
511 }
512 let res = ProblemJson(Out { id: 7 }).into_response();
513 assert_eq!(res.status(), StatusCode::OK);
514 let bytes = axum::body::to_bytes(res.into_body(), usize::MAX)
515 .await
516 .unwrap();
517 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
518 assert_eq!(v["id"], 7);
519 }
520 }
521
522 #[cfg(feature = "validator")]
523 mod problem_validated_json {
524 use super::*;
525 use axum::body::Body;
526 use axum::http::{header::ACCEPT, header::CONTENT_TYPE, Request};
527 use serde::Deserialize;
528
529 #[derive(Debug, Deserialize, Validate)]
530 struct Input {
531 #[validate(length(min = 2))]
532 name: String,
533 }
534
535 fn request(body: &str, content_type: Option<&str>, accept: Option<&str>) -> Request<Body> {
536 let mut builder = Request::builder().method("POST").uri("/");
537 if let Some(value) = content_type {
538 builder = builder.header(CONTENT_TYPE, value);
539 }
540 if let Some(value) = accept {
541 builder = builder.header(ACCEPT, value);
542 }
543 builder.body(Body::from(body.to_owned())).unwrap()
544 }
545
546 async fn extract(
547 body: &str,
548 content_type: Option<&str>,
549 ) -> Result<Input, ProblemRejection> {
550 ProblemValidatedJson::<Input>::from_request(request(body, content_type, None), &())
551 .await
552 .map(|ProblemValidatedJson(v)| v)
553 }
554
555 #[tokio::test]
556 async fn valid_body_extracts() {
557 let input = extract(r#"{"name":"abcd"}"#, Some("application/json"))
558 .await
559 .unwrap();
560 assert_eq!(input.name, "abcd");
561 }
562
563 #[tokio::test]
564 async fn malformed_json_is_invalid_json_problem() {
565 let rejection = extract("{not json", Some("application/json"))
566 .await
567 .unwrap_err();
568 assert_eq!(rejection.problem.status, 400);
569 assert_eq!(rejection.problem.extensions["code"], "INVALID_JSON");
570 }
571
572 #[tokio::test]
573 async fn wrong_shape_is_invalid_body_problem() {
574 let rejection = extract(r#"{"name":123}"#, Some("application/json"))
575 .await
576 .unwrap_err();
577 assert_eq!(rejection.problem.status, 422);
578 assert_eq!(rejection.problem.extensions["code"], "INVALID_BODY");
579 }
580
581 #[tokio::test]
582 async fn missing_content_type_is_unsupported_media_type_problem() {
583 let rejection = extract(r#"{"name":"abcd"}"#, None).await.unwrap_err();
584 assert_eq!(rejection.problem.status, 415);
585 assert_eq!(
586 rejection.problem.extensions["code"],
587 "UNSUPPORTED_MEDIA_TYPE"
588 );
589 }
590
591 #[tokio::test]
592 async fn validation_failure_is_validation_error_problem_with_field_details() {
593 let rejection = extract(r#"{"name":"a"}"#, Some("application/json"))
594 .await
595 .unwrap_err();
596 assert_eq!(rejection.problem.status, 422);
597 assert_eq!(rejection.problem.title, "Unprocessable Entity");
598 assert_eq!(rejection.problem.extensions["code"], "VALIDATION_ERROR");
599 assert_eq!(
600 rejection.problem.detail.as_deref(),
601 Some("validation failed")
602 );
603 assert!(rejection.problem.extensions["details"]["fields"]["name"].is_array());
604 }
605
606 #[tokio::test]
607 async fn validation_failure_carries_same_information_as_validated_json() {
608 use crate::ValidatedJson;
609
610 let body = r#"{"name":"a"}"#;
611 let problem_rejection = ProblemValidatedJson::<Input>::from_request(
612 request(body, Some("application/json"), None),
613 &(),
614 )
615 .await
616 .unwrap_err();
617 let (status, Json(api_error)) = ValidatedJson::<Input>::from_request(
618 request(body, Some("application/json"), None),
619 &(),
620 )
621 .await
622 .unwrap_err();
623
624 let problem = &problem_rejection.problem;
625 assert_eq!(problem.status, status.as_u16());
626 assert_eq!(problem.extensions["code"], api_error.code);
627 assert_eq!(problem.detail.as_deref(), Some(api_error.message.as_str()));
628 assert_eq!(
629 problem.extensions.get("details"),
630 api_error.details.as_ref()
631 );
632 }
633
634 #[tokio::test]
635 async fn validation_rejection_negotiates_format_from_accept_header() {
636 let rejection = ProblemValidatedJson::<Input>::from_request(
637 request(
638 r#"{"name":"a"}"#,
639 Some("application/json"),
640 Some("application/json"),
641 ),
642 &(),
643 )
644 .await
645 .unwrap_err();
646 assert_eq!(rejection.format, ProblemFormat::Json);
647 let res = rejection.into_response();
648 assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
649 assert_eq!(
650 res.headers().get(header::CONTENT_TYPE).unwrap(),
651 "application/json"
652 );
653 }
654 }
655}