pub struct Problem {
pub type_uri: Option<String>,
pub title: String,
pub status: u16,
pub detail: Option<String>,
pub instance: Option<String>,
pub extensions: Map<String, Value>,
pub retry_after: Option<Duration>,
}Expand description
An RFC 9457 problem details response body.
Serializes as:
{ "title": "Not Found", "status": 404 }
{ "type": "https://example.com/probs/out-of-credit", "title": "Insufficient credit",
"status": 403, "detail": "Balance is 30, item costs 50",
"instance": "/account/12345/msgs/abc", "balance": 30 }Implements IntoResponse with Content-Type: application/problem+json
and an optional delay-seconds Retry-After header.
§Example
use axum::{http::StatusCode, response::IntoResponse};
use axum_api_kit::Problem;
async fn handler() -> impl IntoResponse {
Problem::new(StatusCode::FORBIDDEN, "Insufficient credit")
.with_type("https://example.com/probs/out-of-credit")
.with_detail("Balance is 30, item costs 50")
.with_instance("/account/12345/msgs/abc")
.with_extension("balance", 30)
}Fields§
§type_uri: Option<String>A URI reference identifying the problem type. Absent means “about:blank” per RFC 9457.
title: StringA short, human-readable summary of the problem type. Stable per problem type.
status: u16The HTTP status code, duplicated in the body per RFC 9457.
detail: Option<String>A human-readable explanation specific to this occurrence.
instance: Option<String>A URI reference identifying this specific occurrence.
extensions: Map<String, Value>RFC 9457 extension members, flattened to top-level JSON keys.
retry_after: Option<Duration>Optional Retry-After header delay. Header-only; never serialized in the body.
Implementations§
Source§impl Problem
impl Problem
Sourcepub fn new(status: StatusCode, title: impl Into<String>) -> Self
pub fn new(status: StatusCode, title: impl Into<String>) -> Self
Builds a minimal Problem with the given status and title.
type, detail, and instance start absent, extensions start empty,
and no Retry-After header is emitted.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::NOT_FOUND, "Not Found");
assert_eq!(
serde_json::to_value(&problem).unwrap(),
serde_json::json!({ "title": "Not Found", "status": 404 })
);Sourcepub fn with_type(self, type_uri: impl Into<String>) -> Self
pub fn with_type(self, type_uri: impl Into<String>) -> Self
Sets the type member: a URI reference identifying the problem type.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::FORBIDDEN, "Insufficient credit")
.with_type("https://example.com/probs/out-of-credit");
let v = serde_json::to_value(&problem).unwrap();
assert_eq!(v["type"], "https://example.com/probs/out-of-credit");Sourcepub fn with_detail(self, detail: impl Into<String>) -> Self
pub fn with_detail(self, detail: impl Into<String>) -> Self
Sets the detail member: a human-readable explanation specific to this
occurrence.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::FORBIDDEN, "Insufficient credit")
.with_detail("Balance is 30, item costs 50");
let v = serde_json::to_value(&problem).unwrap();
assert_eq!(v["detail"], "Balance is 30, item costs 50");Sourcepub fn with_instance(self, instance: impl Into<String>) -> Self
pub fn with_instance(self, instance: impl Into<String>) -> Self
Sets the instance member: a URI reference identifying this specific
occurrence.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::FORBIDDEN, "Insufficient credit")
.with_instance("/account/12345/msgs/abc");
let v = serde_json::to_value(&problem).unwrap();
assert_eq!(v["instance"], "/account/12345/msgs/abc");Sourcepub fn with_extension(
self,
key: impl Into<String>,
value: impl Into<Value>,
) -> Self
pub fn with_extension( self, key: impl Into<String>, value: impl Into<Value>, ) -> Self
Adds an RFC 9457 extension member, serialized as a top-level JSON key.
If key is one of the reserved members ("type", "title",
"status", "detail", "instance"), the call is a silent no-op so
the flattened extensions can never emit duplicate JSON keys. This
mirrors the ApiError::with_source silent-no-op precedent.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::FORBIDDEN, "Insufficient credit")
.with_extension("balance", 30)
.with_extension("status", 999); // reserved key: ignored
let v = serde_json::to_value(&problem).unwrap();
assert_eq!(v["balance"], 30);
assert_eq!(v["status"], 403);Sourcepub fn with_retry_after(self, delay: Duration) -> Self
pub fn with_retry_after(self, delay: Duration) -> Self
Emits a delay-seconds Retry-After header on the response, rounded up
to whole seconds (1500ms becomes "2").
The delay never appears in the JSON body; add it via
with_extension if body presence is wanted.
§Example
use axum::{http::StatusCode, response::IntoResponse};
use axum_api_kit::Problem;
use std::time::Duration;
let res = Problem::new(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")
.with_retry_after(Duration::from_secs(30))
.into_response();
assert_eq!(res.headers().get("retry-after").unwrap(), "30");Sourcepub fn status_code(&self) -> StatusCode
pub fn status_code(&self) -> StatusCode
Returns the status field as a StatusCode, falling back to
500 Internal Server Error when it is not a valid status.
StatusCode::from_u16 accepts 100..=999, so the fallback only
triggers for a hand-set pub status outside that range.
§Example
use axum::http::StatusCode;
use axum_api_kit::Problem;
let problem = Problem::new(StatusCode::NOT_FOUND, "Not Found");
assert_eq!(problem.status_code(), StatusCode::NOT_FOUND);Sourcepub fn into_response_for(self, headers: &HeaderMap) -> Response
pub fn into_response_for(self, headers: &HeaderMap) -> Response
Converts into a response, choosing the Content-Type by negotiating
against the request’s Accept headers.
Shorthand for self.into_response_with(ProblemFormat::negotiate(headers));
see ProblemFormat::negotiate for the exact rules. Plain
application/json is served only when the client strictly prefers it;
every ambiguous case (no Accept header, */*, ties, unparseable
values) serves application/problem+json. The body bytes and any
Retry-After header are identical either way, and the plain
IntoResponse behavior is untouched.
§Example
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use axum_api_kit::Problem;
let mut headers = HeaderMap::new();
headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
let res = Problem::new(StatusCode::NOT_FOUND, "Not Found").into_response_for(&headers);
assert_eq!(res.headers().get(header::CONTENT_TYPE).unwrap(), "application/json");
let res = Problem::new(StatusCode::NOT_FOUND, "Not Found").into_response_for(&HeaderMap::new());
assert_eq!(
res.headers().get(header::CONTENT_TYPE).unwrap(),
"application/problem+json"
);Sourcepub fn into_response_with(self, format: ProblemFormat) -> Response
pub fn into_response_with(self, format: ProblemFormat) -> Response
Converts into a response served as the given ProblemFormat.
Pairs with the ProblemFormat extractor, which negotiates the format
from the request’s Accept headers before the handler runs. The JSON
body bytes and any Retry-After header are identical for both formats;
only the Content-Type header differs.
§Example
use axum::http::{header, StatusCode};
use axum_api_kit::{Problem, ProblemFormat};
let res = Problem::new(StatusCode::NOT_FOUND, "Not Found")
.into_response_with(ProblemFormat::Json);
assert_eq!(res.headers().get(header::CONTENT_TYPE).unwrap(), "application/json");Trait Implementations§
Source§impl From<(StatusCode, ApiError)> for Problem
Convert a (StatusCode, ApiError) pair into a Problem, losslessly.
impl From<(StatusCode, ApiError)> for Problem
Convert a (StatusCode, ApiError) pair into a Problem, losslessly.
| source | Problem member |
|---|---|
| status | status |
| status canonical reason | title (falls back to code for nonstandard statuses; cosmetic) |
message | detail |
code | "code" extension member |
details (when present) | "details" extension member, verbatim |
details is kept under the single "details" key, never flattened, so
validation field maps cannot collide with reserved members. type and
instance are left absent, and no Retry-After header is set.
Source§fn from((status, err): (StatusCode, ApiError)) -> Self
fn from((status, err): (StatusCode, ApiError)) -> Self
Source§impl From<(StatusCode, Json<ApiError>)> for Problem
Convert a (StatusCode, Json<ApiError>) factory tuple into a Problem.
impl From<(StatusCode, Json<ApiError>)> for Problem
Convert a (StatusCode, Json<ApiError>) factory tuple into a Problem.
Unwraps the Json and delegates to From<(StatusCode, ApiError)>, so
existing factory results convert directly:
Problem::from(ApiError::not_found("nope")).
Source§impl IntoResponse for Problem
impl IntoResponse for Problem
Source§fn into_response(self) -> Response
fn into_response(self) -> Response
Auto Trait Implementations§
impl Freeze for Problem
impl RefUnwindSafe for Problem
impl Send for Problem
impl Sync for Problem
impl Unpin for Problem
impl UnsafeUnpin for Problem
impl UnwindSafe for Problem
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T, S> Handler<IntoResponseHandler, S> for T
impl<T, S> Handler<IntoResponseHandler, S> for T
Source§fn call(
self,
_req: Request<Body>,
_state: S,
) -> <T as Handler<IntoResponseHandler, S>>::Future
fn call( self, _req: Request<Body>, _state: S, ) -> <T as Handler<IntoResponseHandler, S>>::Future
Source§fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>where
L: Layer<HandlerService<Self, T, S>> + Clone,
<L as Layer<HandlerService<Self, T, S>>>::Service: Service<Request<Body>>,
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>where
L: Layer<HandlerService<Self, T, S>> + Clone,
<L as Layer<HandlerService<Self, T, S>>>::Service: Service<Request<Body>>,
tower::Layer to the handler. Read moreSource§fn with_state(self, state: S) -> HandlerService<Self, T, S>
fn with_state(self, state: S) -> HandlerService<Self, T, S>
Service by providing the stateSource§impl<H, T> HandlerWithoutStateExt<T> for H
impl<H, T> HandlerWithoutStateExt<T> for H
Source§fn into_service(self) -> HandlerService<H, T, ()>
fn into_service(self) -> HandlerService<H, T, ()>
Service and no state.Source§fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, ()>>
fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, ()>>
MakeService and no state. Read moreSource§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more