diff --git a/docs/en/src/error_extensions.md b/docs/en/src/error_extensions.md index f1051a32..25103b81 100644 --- a/docs/en/src/error_extensions.md +++ b/docs/en/src/error_extensions.md @@ -1 +1,196 @@ # Error extensions +To quote the [graphql-spec](https://spec.graphql.org/June2018/#example-fce18): +> GraphQL services may provide an additional entry to errors with key extensions. +> This entry, if set, must have a map as its value. This entry is reserved for implementors to add +> additional information to errors however they see fit, and there are no additional restrictions on +> its contents. + +## Example +I would recommend on checking out this [async-graphql example](https://github.com/async-graphql/examples/blob/master/actix-web/error-extensions/src/main.rs) as a quickstart. + +## General Concept +In async-graphql all user-facing errors are cast to the `FieldError` type which by default provides +the error message exposed by `std::fmt::Display`. However `FieldError` actually provides an additional +field `Option` which - if some valid `serde_json::Map` - will be exposed as the extensions key to any error. + +A resolver like this: + +```rust +async fn parse_with_extensions(&self) -> Result { + let my_extension = json!({ "details": "CAN_NOT_FETCH" }); + Err(FieldError("MyMessage", Some(my_extension))) + } +``` + +may then return a response like this: + +```json +{ + "errors": [ + { + "message": "MyMessage", + "locations": [ ... ], + "path": [ ... ], + "extensions": { + "details": "CAN_NOT_FETCH", + } + } + ] +} +``` + + +## ErrorExtensions +Constructing new `FieldError`s by hand quickly becomes tedious. That is why async_graphql provides +two convenience traits for casting your errors to the appropriate `FieldError` with +extensions. + +The easies way to provide extensions to any error is by calling `extend_with` on the error. +This will on the fly convert any error into a `FieldError` with the given extension. + +```rust +use std::num::ParseIntError; +async fn parse_with_extensions(&self) -> FieldResult { + Ok("234a" + .parse() + .map_err(|err: ParseIntError| err.extend_with(|_err| json!({"code": 404})))?) + } +``` + +### Implementing ErrorExtensions for custom errors. +If you find yourself attaching extensions to your errors all over the place you might want to consider +implementing the trait on your custom error type directly. + +```rust +#[macro_use] +extern crate thiserror; + +#[derive(Debug, Error)] +pub enum MyError { + #[error("Could not find resource")] + NotFound, + + #[error("ServerError")] + ServerError(String), + + #[error("No Extensions")] + ErrorWithoutExtensions, +} + +impl ErrorExtensions for MyError { + // lets define our base extensions + fn extend(&self) -> FieldError { + let extensions = match self { + MyError::NotFound => json!({"code": "NOT_FOUND"}), + MyError::ServerError(reason) => json!({ "reason": reason }), + MyError::ErrorWithoutExtensions => { + json!("This will be ignored since it does not represent an object.") + } + }; + + FieldError(format!("{}", self), Some(extensions)) + } +} +``` + +This way you only need to call `extend` on your error to deliver the error message alongside the provided extensions. +Or further extend your error through `extend_with`. + +```rust +async fn parse_with_extensions_result(&self) -> FieldResult { + // Err(MyError::NotFound.extend()) + // OR + Err(MyError::NotFound.extend_with(|_| json!({ "on_the_fly": "some_more_info" }))) +} + +``` + +```json +{ + "errors": [ + { + "message": "NotFound", + "locations": [ ... ], + "path": [ ... ], + "extensions": { + "code": "NOT_FOUND", + "on_the_fly": "some_more_info" + } + } + ] +} + +``` + +### Pitfalls +Rust does not provide stable trait specialization yet. +That is why `ErrorExtensions` is actually implemented for `&E where E: std::fmt::Display` +instead of `E: std::fmt::Display` to provide some specialization through +[Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md). +The disadvantage is that the below code does **NOT** compile: + +```rust,ignore,does_not_compile +async fn parse_with_extensions_result(&self) -> FieldResult { + // does not work because ErrorExtensions is not implemented for ParseIntError + Ok("234a".parse().extend_err(|_| json!({"code": 404}))?) +} +``` + +however this does: + +```rust,ignore,does_not_compile +async fn parse_with_extensions_result(&self) -> FieldResult { + // does work because ErrorExtensions is implemented for &ParseIntError + Ok("234a".parse().map_err(|ref e| e.extend_with(|_| json!({"code": 404})))?) +} +``` + + + +## ResultExt +This trait enables you to call `extend_err` directly on results. So the above code becomes less verbose. + +```rust +use async_graphql::*; +async fn parse_with_extensions(&self) -> FieldResult { + Ok("234a" + .parse() + .extend_err(|_| json!({"code": 404}))?) + } + +``` +### Chained extensions +Since `ErrorExtensions` and `ResultExt` are implemented for any type `&E where E: std::fmt::Display` +we can chain the extension together. + + +```rust +use async_graphql::*; +async fn parse_with_extensions(&self) -> FieldResult { + Ok("234a" + .parse() + .extend_with(|_| json!({"code": 404})) + .extend_with(|_| json!("details": "some more info..")) + + // keys may also overwrite previous keys... + .extend_with(|_| json!({"code": 500}))?) + } +``` +Expected response: + +```json +{ + "errors": [ + { + "message": "MyMessage", + "locations": [ ... ], + "path": [ ... ], + "extensions": { + "details": "some more info...", + "code": 500, + } + } + ] +} +``` + diff --git a/docs/en/src/error_handling.md b/docs/en/src/error_handling.md index 5b28b5fb..cd250324 100644 --- a/docs/en/src/error_handling.md +++ b/docs/en/src/error_handling.md @@ -6,9 +6,10 @@ Resolve can return a `FieldResult`, following is the definition: type FieldResult = std::result::Result; ``` -Any `Error` can be converted to `FieldError` and you can extend error message. +Any `Error` that implements `std::fmt::Display` can be converted to `FieldError` and you can extend error message. Following example shows how to parse an input string to integer. When parsing failed, it would return error and attach error message. +See [ErrorExtensions](error_extensions.md) sections of this book for more details. ```rust use async_graphql::*; @@ -24,4 +25,4 @@ impl Query { .map_err(|err| err.extend_with(|_| json!({"code": 400})))?) } } -``` \ No newline at end of file +```