Skip to main content

aws_smithy_http_server/schema/routing/
route_errors.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! The standard routing rejection shared by every schema-mode protocol router.
7//!
8//! Routers classify a rejection into a [`RoutingErrorKind`]; how a kind looks on
9//! the wire is each protocol's decision, made in
10//! [`serialize_routing_error`](crate::schema::ServerProtocol::serialize_routing_error). The
11//! default there keeps the historical smithy-rs responses: every kind collapses into one of
12//! two member-less error shapes — `UnknownOperationException` (`404`) and
13//! `MethodNotAllowedException` (`405`) — framed by the protocol's own
14//! [`serialize_error`](crate::schema::ServerProtocol::serialize_error). Diagnostic detail
15//! never reaches the wire; it travels in the error's [`source`](std::error::Error::source)
16//! chain for logs and tests.
17
18use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
19use aws_smithy_schema::{shape_id, Schema, ShapeType, StringTrait, TraitMap};
20
21use crate::schema::HttpModeledError;
22
23static ERROR_CLIENT_TRAITS: std::sync::LazyLock<TraitMap> = std::sync::LazyLock::new(|| {
24    let mut map = TraitMap::new();
25    map.insert(Box::new(StringTrait::new(shape_id!("smithy.api", "error"), "client")));
26    map
27});
28
29/// The wire shape of a "no such operation" routing rejection: `404`, member-less.
30pub static UNKNOWN_OPERATION: Schema<'static> = Schema::new_struct(
31    shape_id!("smithy.framework", "UnknownOperationException"),
32    ShapeType::Structure,
33    &[],
34)
35.with_traits(&ERROR_CLIENT_TRAITS);
36
37/// The wire shape of a "method not allowed" routing rejection: `405`, member-less.
38pub static METHOD_NOT_ALLOWED: Schema<'static> = Schema::new_struct(
39    shape_id!("smithy.framework", "MethodNotAllowedException"),
40    ShapeType::Structure,
41    &[],
42)
43.with_traits(&ERROR_CLIENT_TRAITS);
44
45/// The routing-semantic classification of a rejection.
46///
47/// Keep this classification coarse; finer diagnostic detail belongs in
48/// [`RoutingError`]'s source chain.
49/// `#[non_exhaustive]` so a protocol that someday needs a new wire distinction can get a
50/// kind instead of downcasting sources.
51#[non_exhaustive]
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum RoutingErrorKind {
54    /// The request is framed as this protocol but names no operation the service serves —
55    /// or is missing what would name one (e.g. `X-Amz-Target`).
56    UnknownOperation,
57    /// The URI matched an operation but the HTTP method did not.
58    ///
59    /// Only REST routing can produce this: RPC protocols don't route on the method, so a
60    /// wrong verb there is indistinguishable from an unknown operation. Protocols that
61    /// do not distinguish method failures can map this kind to their unknown-operation response.
62    MethodNotAllowed,
63    /// The request claims the protocol but violates its framing rules — forbidden headers,
64    /// a malformed rpcv2 path, an invalid header value.
65    MalformedRequest,
66    /// A known operation uses a feature unsupported by the selected protocol.
67    UnsupportedOperation,
68    /// A matched REST input has a required body member without a default, but the
69    /// request omits Content-Type. REST protocols frame this as unsupported media type.
70    MissingContentType,
71}
72
73/// A schema-mode routing rejection: a [`RoutingErrorKind`] plus an optional diagnostic
74/// source that never reaches the wire.
75#[derive(Debug)]
76pub struct RoutingError {
77    kind: RoutingErrorKind,
78    source: Option<Box<dyn std::error::Error + Send + Sync>>,
79    // The codegen'd static string, not a parsed `mime::Mime`: deferred rejections ride the
80    // routing future, and an inline `Mime` would widen it by ~80 bytes for a value that is
81    // only parsed when a rejection actually serializes.
82    expected_content_type: Option<&'static str>,
83}
84
85impl RoutingError {
86    /// A rejection naming no operation the service serves.
87    pub fn unknown_operation() -> Self {
88        Self {
89            kind: RoutingErrorKind::UnknownOperation,
90            source: None,
91            expected_content_type: None,
92        }
93    }
94
95    /// A rejection for a URI whose matched operations accept a different method.
96    pub fn method_not_allowed() -> Self {
97        Self {
98            kind: RoutingErrorKind::MethodNotAllowed,
99            source: None,
100            expected_content_type: None,
101        }
102    }
103
104    /// A rejection for a request that violates the protocol's framing rules; `source` names
105    /// the violation for logs.
106    pub fn malformed(source: impl std::error::Error + Send + Sync + 'static) -> Self {
107        Self {
108            kind: RoutingErrorKind::MalformedRequest,
109            source: Some(Box::new(source)),
110            expected_content_type: None,
111        }
112    }
113
114    /// A rejection for a known operation whose features the protocol does not support.
115    pub fn unsupported_operation() -> Self {
116        Self {
117            kind: RoutingErrorKind::UnsupportedOperation,
118            source: None,
119            expected_content_type: None,
120        }
121    }
122
123    /// A missing Content-Type for an input that requires body data.
124    pub fn missing_content_type(expected: &'static str) -> Self {
125        Self {
126            kind: RoutingErrorKind::MissingContentType,
127            source: Some(Box::new(
128                crate::rejection::MissingContentTypeReason::UnexpectedMimeType {
129                    expected_mime: expected.parse().ok(),
130                    found_mime: None,
131                },
132            )),
133            expected_content_type: Some(expected),
134        }
135    }
136
137    /// The expected media type for a missing-content-type rejection.
138    pub fn expected_content_type(&self) -> Option<&'static str> {
139        self.expected_content_type
140    }
141
142    /// Attaches a diagnostic source.
143    pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
144        self.source = Some(Box::new(source));
145        self
146    }
147
148    /// Returns the routing-semantic classification.
149    pub fn kind(&self) -> RoutingErrorKind {
150        self.kind
151    }
152}
153
154impl std::fmt::Display for RoutingError {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self.kind {
157            RoutingErrorKind::UnknownOperation => f.write_str("unknown operation"),
158            RoutingErrorKind::MethodNotAllowed => f.write_str("method not allowed"),
159            RoutingErrorKind::MalformedRequest => f.write_str("malformed request"),
160            RoutingErrorKind::MissingContentType => f.write_str("missing content type"),
161            RoutingErrorKind::UnsupportedOperation => f.write_str("unsupported operation"),
162        }
163    }
164}
165
166impl std::error::Error for RoutingError {
167    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168        self.source.as_deref().map(|err| err as _)
169    }
170}
171
172// The smithy-rs default wire form: `MethodNotAllowed` is the `405` shape, every other kind
173// the `404`, exactly as routing rejections have always answered. A protocol wanting a
174// different wire form overrides `serialize_routing_error` rather than these impls.
175impl SerializableStruct for RoutingError {
176    fn schema(&self) -> &Schema<'_> {
177        match self.kind {
178            RoutingErrorKind::MethodNotAllowed => &METHOD_NOT_ALLOWED,
179            _ => &UNKNOWN_OPERATION,
180        }
181    }
182
183    fn serialize_members(&self, _: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
184        Ok(())
185    }
186}
187
188impl HttpModeledError for RoutingError {
189    fn status_code(&self) -> u16 {
190        match self.kind {
191            RoutingErrorKind::MethodNotAllowed => 405,
192            _ => 404,
193        }
194    }
195}
196
197impl From<crate::protocol::rest::router::Error> for RoutingError {
198    fn from(err: crate::protocol::rest::router::Error) -> Self {
199        use crate::protocol::rest::router::Error;
200        match err {
201            Error::NotFound => Self::unknown_operation(),
202            Error::MethodNotAllowed => Self::method_not_allowed(),
203        }
204    }
205}
206
207impl From<crate::protocol::aws_json::router::Error> for RoutingError {
208    fn from(err: crate::protocol::aws_json::router::Error) -> Self {
209        use crate::protocol::aws_json::router::Error;
210        match err {
211            // A missing `X-Amz-Target` (or a non-root URI) is indistinguishable from an
212            // unknown operation for routing-error classification.
213            Error::NotFound | Error::NotRootUrl | Error::MissingHeader => Self::unknown_operation(),
214            Error::MethodNotAllowed => Self::method_not_allowed(),
215            Error::InvalidHeader(_) => Self::malformed(err),
216        }
217    }
218}
219
220impl From<crate::protocol::rpc_v2_cbor::router::Error> for RoutingError {
221    fn from(err: crate::protocol::rpc_v2_cbor::router::Error) -> Self {
222        use crate::protocol::rpc_v2_cbor::router::{Error, WireFormatError};
223        match err {
224            Error::NotFound => Self::unknown_operation(),
225            Error::MethodNotAllowed => Self::method_not_allowed(),
226            Error::ForbiddenHeaders => Self::malformed(err),
227            // A missing or unrecognized `smithy-protocol` header means the request never
228            // identified the protocol, so classify it as an unknown operation.
229            Error::InvalidWireFormatHeader(
230                WireFormatError::HeaderNotFound | WireFormatError::WireFormatNotSupported(_),
231            ) => Self::unknown_operation().with_source(err),
232            // The header is present and rpcv2-shaped but its value is invalid: the request
233            // claims the protocol and violates it.
234            Error::InvalidWireFormatHeader(_) => Self::malformed(err),
235        }
236    }
237}