|
| 1 | +// Copyright 2024 Oxide Computer Company |
| 2 | + |
| 3 | +//! An example demonstrating how to return user-defined error types from |
| 4 | +//! endpoint handlers. |
| 5 | +
|
| 6 | +use dropshot::endpoint; |
| 7 | +use dropshot::ApiDescription; |
| 8 | +use dropshot::ConfigLogging; |
| 9 | +use dropshot::ConfigLoggingLevel; |
| 10 | +use dropshot::ErrorStatusCode; |
| 11 | +use dropshot::HttpError; |
| 12 | +use dropshot::HttpResponseError; |
| 13 | +use dropshot::HttpResponseOk; |
| 14 | +use dropshot::Path; |
| 15 | +use dropshot::RequestContext; |
| 16 | +use dropshot::ServerBuilder; |
| 17 | +use schemars::JsonSchema; |
| 18 | +use serde::Deserialize; |
| 19 | +use serde::Serialize; |
| 20 | + |
| 21 | +// This is the custom error type returned by our API. A common use case for |
| 22 | +// custom error types is to return an `enum` type that represents |
| 23 | +// application-specific errors in a way that generated clients can interact with |
| 24 | +// programmatically, so the error in this example will be an enum type. |
| 25 | +// |
| 26 | +// In order to be returned from an endpoint handler, it must implement the |
| 27 | +// `HttpResponseError` trait, which requires implementations of: |
| 28 | +// |
| 29 | +// - `HttpResponseContent`, which determines how to produce a response body |
| 30 | +// from the error type, |
| 31 | +// - `std::fmt::Display`, which determines how to produce a human-readable |
| 32 | +// message for Dropshot to log when returning the error, |
| 33 | +// - `From<dropshot::HttpError>`, so that errors returned by request extractors |
| 34 | +// and resposne body serialization can be converted to the user-defined error |
| 35 | +// type. |
| 36 | +#[derive(Debug)] |
| 37 | +// Deriving `Serialize` and `JsonSchema` for our error type provides an |
| 38 | +// implementation of the `HttpResponseContent` trait, which is required to |
| 39 | +// implement `HttpResponseError`: |
| 40 | +#[derive(serde::Serialize, schemars::JsonSchema)] |
| 41 | +// `HttpResponseError` also requires a `std::fmt::Display` implementation, |
| 42 | +// which we'll generate using `thiserror`'s `Error` derive: |
| 43 | +#[derive(thiserror::Error)] |
| 44 | +enum ThingyError { |
| 45 | + // First, define some application-specific error variants that represent |
| 46 | + // structured error responses from our API: |
| 47 | + /// No thingies are currently available to satisfy this request. |
| 48 | + #[error("no thingies are currently available")] |
| 49 | + NoThingies, |
| 50 | + |
| 51 | + /// The requested thingy is invalid. |
| 52 | + #[error("invalid thingy: {:?}", .name)] |
| 53 | + InvalidThingy { name: String }, |
| 54 | + |
| 55 | + // Then, we'll define a variant that can be constructed from a |
| 56 | + // `dropshot::HttpError`, so that errors returned by Dropshot can also be |
| 57 | + // represented in the error schema for our API: |
| 58 | + #[error("{internal_message}")] |
| 59 | + Other { |
| 60 | + message: String, |
| 61 | + error_code: Option<String>, |
| 62 | + |
| 63 | + // Skip serializing these fields, as they are used for the |
| 64 | + // `fmt::Display` implementation and for determining the status |
| 65 | + // code, respectively, rather than included in the response body: |
| 66 | + #[serde(skip)] |
| 67 | + internal_message: String, |
| 68 | + #[serde(skip)] |
| 69 | + status: ErrorStatusCode, |
| 70 | + }, |
| 71 | +} |
| 72 | + |
| 73 | +impl HttpResponseError for ThingyError { |
| 74 | + // Note that this method returns a `dropshot::ErrorStatusCode`, rather than |
| 75 | + // an `http::StatusCode`. This type is a refinement of `http::StatusCode` |
| 76 | + // that can only be constructed from status codes in 4xx (client error) or |
| 77 | + // 5xx (server error) ranges. |
| 78 | + fn status_code(&self) -> dropshot::ErrorStatusCode { |
| 79 | + match self { |
| 80 | + ThingyError::NoThingies => { |
| 81 | + // The `dropshot::ErrorStatusCode` type provides constants for |
| 82 | + // all well-known 4xx and 5xx status codes, such as 503 Service |
| 83 | + // Unavailable. |
| 84 | + dropshot::ErrorStatusCode::SERVICE_UNAVAILABLE |
| 85 | + } |
| 86 | + ThingyError::InvalidThingy { .. } => { |
| 87 | + // Alternatively, an `ErrorStatusCode` can be constructed from a |
| 88 | + // u16, but the `ErrorStatusCode::from_u16` constructor |
| 89 | + // validates that the status code is a 4xx or 5xx. |
| 90 | + // |
| 91 | + // This allows using extended status codes, while still |
| 92 | + // validating that they are errors. |
| 93 | + dropshot::ErrorStatusCode::from_u16(442) |
| 94 | + .expect("442 is a 4xx status code") |
| 95 | + } |
| 96 | + ThingyError::Other { status, .. } => *status, |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +impl From<HttpError> for ThingyError { |
| 102 | + fn from(error: HttpError) -> Self { |
| 103 | + ThingyError::Other { |
| 104 | + message: error.external_message, |
| 105 | + internal_message: error.internal_message, |
| 106 | + status: error.status_code, |
| 107 | + error_code: error.error_code, |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +/// Just some kind of thingy returned by the API. This doesn't actually matter. |
| 113 | +#[derive(Deserialize, Serialize, JsonSchema)] |
| 114 | +struct Thingy { |
| 115 | + magic_number: u64, |
| 116 | +} |
| 117 | + |
| 118 | +#[derive(Deserialize, JsonSchema)] |
| 119 | +struct ThingyPathParams { |
| 120 | + name: ThingyName, |
| 121 | +} |
| 122 | + |
| 123 | +// Using an enum as a path parameter allows the API to also return extractor |
| 124 | +// errors. Try sending a `GET` request for `/thingy/baz` or similar to see how |
| 125 | +// the extractor error is converted into our custom error representation. |
| 126 | +#[derive(Debug, Deserialize, Serialize, JsonSchema)] |
| 127 | +#[serde(rename_all = "lowercase")] |
| 128 | +enum ThingyName { |
| 129 | + Foo, |
| 130 | + Bar, |
| 131 | +} |
| 132 | + |
| 133 | +/// Fetch the thingy with the provided name. |
| 134 | +#[endpoint { |
| 135 | + method = GET, |
| 136 | + path = "/thingy/{name}", |
| 137 | +}] |
| 138 | +async fn get_thingy( |
| 139 | + _rqctx: RequestContext<()>, |
| 140 | + path_params: Path<ThingyPathParams>, |
| 141 | +) -> Result<HttpResponseOk<Thingy>, ThingyError> { |
| 142 | + let ThingyPathParams { name } = path_params.into_inner(); |
| 143 | + Err(ThingyError::InvalidThingy { name: format!("{name:?}") }) |
| 144 | +} |
| 145 | + |
| 146 | +#[endpoint { |
| 147 | + method = GET, |
| 148 | + path = "/nothing", |
| 149 | +}] |
| 150 | +async fn get_nothing( |
| 151 | + _rqctx: RequestContext<()>, |
| 152 | +) -> Result<HttpResponseOk<Thingy>, ThingyError> { |
| 153 | + Err(ThingyError::NoThingies) |
| 154 | +} |
| 155 | + |
| 156 | +/// Endpoints which return `Result<_, HttpError>` may be part of the same |
| 157 | +/// API as endpoints which return user-defined error types. |
| 158 | +#[endpoint { |
| 159 | + method = GET, |
| 160 | + path = "/something", |
| 161 | +}] |
| 162 | +async fn get_something( |
| 163 | + _rqctx: RequestContext<()>, |
| 164 | +) -> Result<HttpResponseOk<Thingy>, dropshot::HttpError> { |
| 165 | + Ok(HttpResponseOk(Thingy { magic_number: 42 })) |
| 166 | +} |
| 167 | + |
| 168 | +#[tokio::main] |
| 169 | +async fn main() -> Result<(), String> { |
| 170 | + // See dropshot/examples/basic.rs for more details on most of these pieces. |
| 171 | + let config_logging = |
| 172 | + ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Info }; |
| 173 | + let log = config_logging |
| 174 | + .to_logger("example-custom-error") |
| 175 | + .map_err(|error| format!("failed to create logger: {}", error))?; |
| 176 | + |
| 177 | + let mut api = ApiDescription::new(); |
| 178 | + api.register(get_thingy).unwrap(); |
| 179 | + api.register(get_nothing).unwrap(); |
| 180 | + api.register(get_something).unwrap(); |
| 181 | + |
| 182 | + api.openapi("Custom Error Example", semver::Version::new(0, 0, 0)) |
| 183 | + .write(&mut std::io::stdout()) |
| 184 | + .map_err(|e| e.to_string())?; |
| 185 | + |
| 186 | + let server = ServerBuilder::new(api, (), log) |
| 187 | + .start() |
| 188 | + .map_err(|error| format!("failed to create server: {}", error))?; |
| 189 | + |
| 190 | + server.await |
| 191 | +} |
0 commit comments