Skip to content

Commit f54bca9

Browse files
committed
add recommended_status_code() to dropshot errors
As I proposed in [this comment][1]. This is intended to be used by user code that constructs its own error type from Dropshot's errors, to make it easier to get the same status codes that are used by `HttpError` when the custom user error type just structures the response differently, or only wishes to override a small subset of Dropshot errors. Perhaps this should be a trait eventually --- I'm kinda on the fence about this. We may also want to do a similar "recommended headers" thing, since some error conditions are supposed to return specific headers, like which methods are allowed for a Method Not Allowed error code, or the desired protocol to upgrade to for a 426 Upgrade Required error. While I was here, I also changed some error variants to return more correct status codes --- a bunch of stuff currently just returns 400 Bad Request when it should really return a more specific error like 426 Upgrade Required etc. [1]: #1164 (comment)
1 parent 3c6bcb7 commit f54bca9

File tree

7 files changed

+233
-36
lines changed

7 files changed

+233
-36
lines changed

dropshot/src/error.rs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,31 @@ pub enum ServerError {
6868
Response(ResponseError),
6969
}
7070

71+
impl From<ServerError> for HttpError {
72+
fn from(error: ServerError) -> Self {
73+
match error {
74+
ServerError::Route(e) => e.into(),
75+
ServerError::Extractor(e) => e.into(),
76+
ServerError::Response(e) => e.into(),
77+
}
78+
}
79+
}
80+
81+
impl ServerError {
82+
/// Returns the recommended status code for this error.
83+
///
84+
/// This can be used when constructing a HTTP response for this error. These
85+
/// are the status codes used by the `From<ServerError>`
86+
/// implementation for [`HttpError`].
87+
pub fn recommended_status_code(&self) -> http::StatusCode {
88+
match self {
89+
Self::Route(e) => e.recommended_status_code(),
90+
Self::Extractor(e) => e.recommended_status_code(),
91+
Self::Response(e) => e.recommended_status_code(),
92+
}
93+
}
94+
}
95+
7196
#[derive(Debug, thiserror::Error)]
7297
pub enum ResponseError {
7398
#[error(transparent)]
@@ -88,22 +113,23 @@ pub enum ResponseError {
88113
},
89114
}
90115

91-
impl From<ServerError> for HttpError {
92-
fn from(error: ServerError) -> Self {
93-
match error {
94-
ServerError::Route(e) => e.into(),
95-
ServerError::Extractor(e) => e.into(),
96-
ServerError::Response(e) => e.into(),
97-
}
98-
}
99-
}
100-
101116
impl From<ResponseError> for HttpError {
102117
fn from(error: ResponseError) -> Self {
103118
HttpError::for_internal_error(error.to_string())
104119
}
105120
}
106121

122+
impl ResponseError {
123+
/// Returns the recommended status code for this error.
124+
///
125+
/// This can be used when constructing a HTTP response for this error. These
126+
/// are the status codes used by the `From<ResponseError>`
127+
/// implementation for [`HttpError`].
128+
pub fn recommended_status_code(&self) -> http::StatusCode {
129+
http::StatusCode::INTERNAL_SERVER_ERROR
130+
}
131+
}
132+
107133
/// Trait implemented by errors which can be converted into an HTTP response.
108134
///
109135
/// In order to be returned as an error from a Dropshot endpoint handler, a type

dropshot/src/extractor/body.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,31 @@ pub enum MultipartBodyError {
6868
}
6969

7070
impl From<MultipartBodyError> for HttpError {
71-
fn from(value: MultipartBodyError) -> Self {
72-
HttpError::for_bad_request(None, value.to_string())
71+
fn from(error: MultipartBodyError) -> Self {
72+
HttpError::for_client_error(
73+
None,
74+
error.recommended_status_code(),
75+
error.to_string(),
76+
)
77+
}
78+
}
79+
80+
impl MultipartBodyError {
81+
/// Returns the recommended status code for this error.
82+
///
83+
/// This can be used when constructing a HTTP response for this error. These
84+
/// are the status codes used by the `From<MultipartBodyError>` implementation
85+
/// for [`HttpError`].
86+
pub fn recommended_status_code(&self) -> http::StatusCode {
87+
match self {
88+
// Invalid or unsupported content-type headers should return 415
89+
// Unsupported Media Type
90+
Self::MissingContentType | Self::InvalidContentType(_) => {
91+
http::StatusCode::UNSUPPORTED_MEDIA_TYPE
92+
}
93+
// Everything else gets a generic `400 Bad Request`.
94+
_ => http::StatusCode::BAD_REQUEST,
95+
}
7396
}
7497
}
7598

@@ -150,7 +173,36 @@ pub enum TypedBodyError {
150173

151174
impl From<TypedBodyError> for HttpError {
152175
fn from(error: TypedBodyError) -> Self {
153-
HttpError::for_bad_request(None, error.to_string())
176+
match error {
177+
TypedBodyError::StreamingBody(e) => e.into(),
178+
_ => HttpError::for_client_error(
179+
None,
180+
error.recommended_status_code(),
181+
error.to_string(),
182+
),
183+
}
184+
}
185+
}
186+
187+
impl TypedBodyError {
188+
/// Returns the recommended status code for this error.
189+
///
190+
/// This can be used when constructing a HTTP response for this error. These
191+
/// are the status codes used by the `From<TypedBodyError>` implementation
192+
/// for [`HttpError`].
193+
pub fn recommended_status_code(&self) -> http::StatusCode {
194+
match self {
195+
// Invalid or unsupported content-type headers should return 415
196+
// Unsupported Media Type
197+
Self::InvalidContentType(_)
198+
| Self::UnsupportedMimeType(_)
199+
| Self::UnexpectedMimeType { .. } => {
200+
http::StatusCode::UNSUPPORTED_MEDIA_TYPE
201+
}
202+
Self::StreamingBody(e) => e.recommended_status_code(),
203+
// Everything else gets a generic `400 Bad Request`.
204+
_ => http::StatusCode::BAD_REQUEST,
205+
}
154206
}
155207
}
156208

@@ -312,7 +364,28 @@ pub enum StreamingBodyError {
312364

313365
impl From<StreamingBodyError> for HttpError {
314366
fn from(error: StreamingBodyError) -> Self {
315-
HttpError::for_bad_request(None, error.to_string())
367+
HttpError::for_client_error(
368+
None,
369+
error.recommended_status_code(),
370+
error.to_string(),
371+
)
372+
}
373+
}
374+
375+
impl StreamingBodyError {
376+
/// Returns the recommended status code for this error.
377+
///
378+
/// This can be used when constructing a HTTP response for this error. These
379+
/// are the status codes used by the `From<StreamingBodyError>` implementation
380+
/// for [`HttpError`].
381+
pub fn recommended_status_code(&self) -> http::StatusCode {
382+
match self {
383+
// If the max body size was exceeded, return 413 Payload Too Large
384+
// (nee Content Too Large).
385+
Self::MaxSizeExceeded(_) => http::StatusCode::PAYLOAD_TOO_LARGE,
386+
// Everything else gets a generic `400 Bad Request`.
387+
_ => http::StatusCode::BAD_REQUEST,
388+
}
316389
}
317390
}
318391

dropshot/src/extractor/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,21 @@ impl From<ExtractorError> for HttpError {
7575
}
7676
}
7777
}
78+
79+
impl ExtractorError {
80+
/// Returns the recommended status code for this error.
81+
///
82+
/// This can be used when constructing a HTTP response for this error. These
83+
/// are the status codes used by the `From<ExtractorError>`
84+
/// implementation for [`HttpError`].
85+
pub fn recommended_status_code(&self) -> http::StatusCode {
86+
match self {
87+
Self::MultipartBody(e) => e.recommended_status_code(),
88+
Self::StreamingBody(e) => e.recommended_status_code(),
89+
Self::TypedBody(e) => e.recommended_status_code(),
90+
Self::PathParams(e) => e.recommended_status_code(),
91+
Self::QueryParams(e) => e.recommended_status_code(),
92+
Self::Websocket(e) => e.recommended_status_code(),
93+
}
94+
}
95+
}

dropshot/src/extractor/query.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,22 @@ pub struct QueryError(#[from] serde_urlencoded::de::Error);
4040

4141
impl From<QueryError> for HttpError {
4242
fn from(error: QueryError) -> Self {
43-
HttpError::for_bad_request(None, error.to_string())
43+
HttpError::for_client_error(
44+
None,
45+
error.recommended_status_code(),
46+
error.to_string(),
47+
)
48+
}
49+
}
50+
51+
impl QueryError {
52+
/// Returns the recommended status code for this error.
53+
///
54+
/// This can be used when constructing a HTTP response for this error. These
55+
/// are the status codes used by the `From<QueryError>` implementation
56+
/// for [`HttpError`].
57+
pub fn recommended_status_code(&self) -> http::StatusCode {
58+
http::StatusCode::BAD_REQUEST
4459
}
4560
}
4661

dropshot/src/http_util.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,22 @@ pub struct PathError(String);
5555

5656
impl From<PathError> for HttpError {
5757
fn from(error: PathError) -> Self {
58-
HttpError::for_bad_request(None, error.to_string())
58+
HttpError::for_client_error(
59+
None,
60+
error.recommended_status_code(),
61+
error.to_string(),
62+
)
63+
}
64+
}
65+
66+
impl PathError {
67+
/// Returns the recommended status code for this error.
68+
///
69+
/// This can be used when constructing a HTTP response for this error. These
70+
/// are the status codes used by the `From<PathError>` implementation
71+
/// for [`HttpError`].
72+
pub fn recommended_status_code(&self) -> http::StatusCode {
73+
http::StatusCode::BAD_REQUEST
5974
}
6075
}
6176

dropshot/src/router.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,25 +236,30 @@ impl RouterError {
236236
pub fn is_not_found(&self) -> bool {
237237
matches!(self, Self::NotFound(_))
238238
}
239+
240+
/// Returns the recommended status code for this error.
241+
///
242+
/// This can be used when constructing a HTTP response for this error. These
243+
/// are the status codes used by the `From<RouterError>`
244+
/// implementation for [`HttpError`].
245+
pub fn recommended_status_code(&self) -> http::StatusCode {
246+
match self {
247+
Self::InvalidPath(_) => http::StatusCode::BAD_REQUEST,
248+
Self::NotFound(_) => http::StatusCode::NOT_FOUND,
249+
Self::MethodNotAllowed => http::StatusCode::METHOD_NOT_ALLOWED,
250+
}
251+
}
239252
}
240253

241254
impl From<RouterError> for HttpError {
242255
fn from(error: RouterError) -> Self {
243-
match error {
244-
RouterError::InvalidPath(_) => {
245-
HttpError::for_bad_request(None, error.to_string())
246-
}
247-
RouterError::NotFound(s) => {
248-
HttpError::for_not_found(None, s.to_string())
249-
}
250-
RouterError::MethodNotAllowed => HttpError::for_status(
251-
None,
252-
http::StatusCode::METHOD_NOT_ALLOWED,
253-
),
254-
}
256+
HttpError::for_client_error(
257+
None,
258+
error.recommended_status_code(),
259+
error.to_string(),
260+
)
255261
}
256262
}
257-
258263
impl<Context: ServerContext> HttpRouter<Context> {
259264
/// Returns a new `HttpRouter` with no routes configured.
260265
pub fn new() -> Self {

dropshot/src/websocket.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,59 @@ pub enum WebsocketUpgradeError {
9393
NoUpgradeHeader,
9494
#[error("unexpected protocol for upgrade")]
9595
NotWebsocket,
96-
#[error("missing or invalid websocket version")]
97-
BadVersion,
96+
#[error("missing websocket version header")]
97+
NoVersion,
98+
#[error("unsupported websocket version")]
99+
WrongVersion,
98100
#[error("missing websocket key")]
99101
NoKey,
100102
}
101103

102104
impl From<WebsocketUpgradeError> for HttpError {
103-
fn from(e: WebsocketUpgradeError) -> Self {
104-
HttpError::for_bad_request(None, e.to_string())
105+
fn from(error: WebsocketUpgradeError) -> Self {
106+
HttpError::for_client_error(
107+
None,
108+
error.recommended_status_code(),
109+
error.to_string(),
110+
)
111+
}
112+
}
113+
114+
impl WebsocketUpgradeError {
115+
/// Returns the recommended status code for this error.
116+
///
117+
/// This can be used when constructing a HTTP response for this error. These
118+
/// are the status codes used by the `From<WebsocketUpgradeError>`
119+
/// implementation for [`HttpError`].
120+
pub fn recommended_status_code(&self) -> http::StatusCode {
121+
match self {
122+
// TODO(eliza): in this case, the response should also include an
123+
// `Upgrade: websocket` header to indicate what protocol the client
124+
// must upgrade to. We should eventually figure out an API for
125+
// errors to indicate "recommended headers" as well as recommended
126+
// statuses...
127+
Self::NoUpgradeHeader | Self::NotWebsocket => {
128+
http::StatusCode::UPGRADE_REQUIRED
129+
}
130+
// Per RFC 6455 § 4.2.2, if the client has requested an unsupported
131+
// websocket version, the server should respond with a 426 Upgrade
132+
// Required status code:
133+
// https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.2
134+
//
135+
// Again, we should also include a header (Sec-WebSocket-Version)
136+
// indicating the supported cversions, but we gotta figure that out
137+
// later...
138+
Self::WrongVersion => http::StatusCode::UPGRADE_REQUIRED,
139+
// Note that we differentiate between "missing version header" and
140+
// "wrong version" because RFC 6455 § 4.2.1 kind of vaguely implies
141+
// that if any of the expected websocket headers are not present,
142+
// the server responds with a 400, but if the version header is
143+
// present but has the wrong value, that's an 426 Upgrade Required:
144+
// https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.1
145+
Self::NoVersion => http::StatusCode::BAD_REQUEST,
146+
// Similarly, a missing `Sec-Websocket-Key` also gets Bad Request'd.
147+
Self::NoKey => http::StatusCode::BAD_REQUEST,
148+
}
105149
}
106150
}
107151

@@ -143,10 +187,11 @@ impl ExclusiveExtractor for WebsocketUpgrade {
143187
if request
144188
.headers()
145189
.get(header::SEC_WEBSOCKET_VERSION)
146-
.map(|v| v.as_bytes())
147-
!= Some(b"13")
190+
.ok_or(WebsocketUpgradeError::NoVersion)?
191+
.as_bytes()
192+
!= b"13"
148193
{
149-
return Err(WebsocketUpgradeError::BadVersion.into());
194+
return Err(WebsocketUpgradeError::WrongVersion.into());
150195
}
151196

152197
let accept_key = request

0 commit comments

Comments
 (0)