Add documentation for error_extensions

This commit is contained in:
Nicolai Unrein 2020-05-11 14:22:20 +02:00
parent 1499b67a78
commit fd9a7553ae
2 changed files with 198 additions and 2 deletions

View File

@ -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<serde_json::Value>` 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<i32, FieldError> {
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<i32> {
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<i32> {
// 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<i32> {
// 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<i32> {
// 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<i32> {
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<i32> {
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,
}
}
]
}
```

View File

@ -6,9 +6,10 @@ Resolve can return a `FieldResult`, following is the definition:
type FieldResult<T> = std::result::Result<T, FieldError>;
```
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})))?)
}
}
```
```