Skip to content

Commit 0018f66

Browse files
authored
Error responses in OpenAPI output (#286)
1 parent 784399a commit 0018f66

File tree

7 files changed

+370
-9
lines changed

7 files changed

+370
-9
lines changed

CHANGELOG.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ https://github.com/oxidecomputer/dropshot/compare/v0.6.0\...HEAD[Full list of co
2626
* https://github.com/oxidecomputer/dropshot/pull/252[#252] Endpoints specified with the `#[endpoint ..]` attribute macro now use the first line of a doc comment as the OpenAPI `summary` and subsequent lines as the `description`. Previously all lines were used as the `description`.
2727
* https://github.com/oxidecomputer/dropshot/pull/260[#260] Pulls in a newer serde that changes error messages around parsing NonZeroU32.
2828
* https://github.com/oxidecomputer/dropshot/pull/283[#283] Add support for response headers with the `HttpResponseHeaders` type. Headers may either be defined by a struct type parameter (in which case they appear in the OpenAPI output) or *ad-hoc* added via `HttpResponseHeaders::headers_mut()`.
29+
* https://github.com/oxidecomputer/dropshot/pull/286[#286] OpenAPI output includes descriptions of 4xx and 5xx error responses.
2930

3031
== 0.6.0 (released 2021-11-18)
3132

dropshot/examples/pagination-multiple-sorts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ enum ProjectSort {
186186
* selector back, you find the object having the next value after the one stored
187187
* in the token and start returning results from there.
188188
*/
189-
#[derive(Deserialize, JsonSchema, Serialize)]
189+
#[derive(Deserialize, Serialize)]
190190
#[serde(rename_all = "kebab-case")]
191191
enum ProjectScanPageSelector {
192192
Name(PaginationOrder, String),

dropshot/src/api_description.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::server::ServerContext;
1414
use crate::type_util::type_is_scalar;
1515
use crate::type_util::type_is_string_enum;
1616
use crate::Extractor;
17+
use crate::HttpErrorResponseBody;
1718
use crate::CONTENT_TYPE_JSON;
1819
use crate::CONTENT_TYPE_OCTET_STREAM;
1920

@@ -668,8 +669,12 @@ impl<Context: ServerContext> ApiDescription<Context> {
668669

669670
match &endpoint.response.success {
670671
None => {
671-
operation.responses.default =
672-
Some(openapiv3::ReferenceOr::Item(response))
672+
// Without knowing the specific status code we use the
673+
// 2xx range.
674+
operation.responses.responses.insert(
675+
openapiv3::StatusCode::Range(2),
676+
openapiv3::ReferenceOr::Item(response),
677+
);
673678
}
674679
Some(code) => {
675680
operation.responses.responses.insert(
@@ -678,9 +683,23 @@ impl<Context: ServerContext> ApiDescription<Context> {
678683
);
679684
}
680685
}
686+
687+
// 4xx and 5xx responses all use the same error information
688+
let err_ref = openapiv3::ReferenceOr::ref_(
689+
"#/components/responses/Error",
690+
);
691+
operation
692+
.responses
693+
.responses
694+
.insert(openapiv3::StatusCode::Range(4), err_ref.clone());
695+
operation
696+
.responses
697+
.responses
698+
.insert(openapiv3::StatusCode::Range(5), err_ref);
681699
} else {
682700
// If no schema was specified, the response is hand-rolled. In
683-
// this case we'll fall back to the default response type.
701+
// this case we'll fall back to the default response type which
702+
// we assume to be inclusive of errors.
684703
operation.responses.default =
685704
Some(openapiv3::ReferenceOr::Item(openapiv3::Response {
686705
// TODO: perhaps we should require even free-form
@@ -695,11 +714,35 @@ impl<Context: ServerContext> ApiDescription<Context> {
695714
method_ref.replace(operation);
696715
}
697716

698-
// Add the schemas for which we generated references.
699-
let schemas = &mut openapi
717+
let components = &mut openapi
700718
.components
701-
.get_or_insert_with(openapiv3::Components::default)
702-
.schemas;
719+
.get_or_insert_with(openapiv3::Components::default);
720+
721+
// All endpoints share an error response
722+
let responses = &mut components.responses;
723+
let mut content = indexmap::IndexMap::new();
724+
content.insert(
725+
CONTENT_TYPE_JSON.to_string(),
726+
openapiv3::MediaType {
727+
schema: Some(j2oas_schema(
728+
None,
729+
&generator.subschema_for::<HttpErrorResponseBody>(),
730+
)),
731+
..Default::default()
732+
},
733+
);
734+
735+
responses.insert(
736+
"Error".to_string(),
737+
openapiv3::ReferenceOr::Item(openapiv3::Response {
738+
description: "Error".to_string(),
739+
content: content,
740+
..Default::default()
741+
}),
742+
);
743+
744+
// Add the schemas for which we generated references.
745+
let schemas = &mut components.schemas;
703746

704747
let root_schema = generator.into_root_schema_for::<()>();
705748
root_schema.definitions.iter().for_each(|(key, schema)| {

dropshot/src/error.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
*/
4646

4747
use hyper::Error as HyperError;
48+
use schemars::JsonSchema;
4849
use serde::Deserialize;
4950
use serde::Serialize;
5051
use std::error::Error;
@@ -107,9 +108,19 @@ pub struct HttpError {
107108
* deserialize an HTTP response corresponding to an error in order to access the
108109
* error code, message, etc.
109110
*/
110-
#[derive(Debug, Deserialize, Serialize)]
111+
/*
112+
* TODO: does this need to be pub if it's going to be expressed in the OpenAPI
113+
* output?
114+
*/
115+
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
116+
#[schemars(rename = "Error")]
117+
#[schemars(description = "Error information from a response.")]
111118
pub struct HttpErrorResponseBody {
112119
pub request_id: String,
120+
// The combination of default and required removes "nullable" from the
121+
// OpenAPI-flavored JSON Schema output.
122+
#[schemars(default, required)]
123+
#[serde(skip_serializing_if = "Option::is_none")]
113124
pub error_code: Option<String>,
114125
pub message: String,
115126
}
@@ -298,3 +309,30 @@ impl Error for HttpError {
298309
None
299310
}
300311
}
312+
313+
#[cfg(test)]
314+
mod test {
315+
use crate::HttpErrorResponseBody;
316+
317+
#[test]
318+
fn test_serialize_error_response_body() {
319+
let err = HttpErrorResponseBody {
320+
request_id: "123".to_string(),
321+
error_code: None,
322+
message: "oy!".to_string(),
323+
};
324+
let out = serde_json::to_string(&err).unwrap();
325+
assert_eq!(out, r#"{"request_id":"123","message":"oy!"}"#);
326+
327+
let err = HttpErrorResponseBody {
328+
request_id: "123".to_string(),
329+
error_code: Some("err".to_string()),
330+
message: "oy!".to_string(),
331+
};
332+
let out = serde_json::to_string(&err).unwrap();
333+
assert_eq!(
334+
out,
335+
r#"{"request_id":"123","error_code":"err","message":"oy!"}"#
336+
);
337+
}
338+
}

0 commit comments

Comments
 (0)