-
-
Notifications
You must be signed in to change notification settings - Fork 536
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add extension to support partial errors, response with data and…
… errors
- Loading branch information
1 parent
6d86d1c
commit 2edc713
Showing
4 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
--- | ||
title: PartialResultsExtension | ||
summary: Hide error messages from the client. | ||
tags: errors | ||
--- | ||
|
||
# `PartialResultsExtension` | ||
|
||
This extension enables populating an array of exceptions on the `info.context` object | ||
that get mapped to the `errors` array after execution, | ||
enabling responses to contain both `data` and `errors` values. | ||
This also enables returning multiple errors in the response. | ||
|
||
1. Add `PartialResultsExtension` to the `extensions` array in `Schema` initialization | ||
1. In execution, add any exceptions to the `info.context.partial_errors` array | ||
|
||
## Usage example: | ||
|
||
```python | ||
import strawberry | ||
from strawberry.extensions import PartialResultsExtension | ||
|
||
schema = strawberry.Schema(Query, extensions=[PartialResultsExtension]) | ||
|
||
# ... | ||
|
||
|
||
@strawberry.field | ||
def query(self, info) -> bool: | ||
info.context.partial_errors.append(Exception("Partial failure")) | ||
return True | ||
``` | ||
|
||
Results: | ||
|
||
```json | ||
{ | ||
"data": { | ||
"query": true | ||
}, | ||
"errors": [ | ||
{ | ||
"message": "Partial failure" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
## API reference: | ||
|
||
_No arguments_ | ||
|
||
## More examples: | ||
|
||
<details> | ||
<summary>Add multiple exceptions</summary> | ||
|
||
```python | ||
import strawberry | ||
from strawberry.extensions import PartialResultsExtension | ||
|
||
schema = strawberry.Schema(Query, extensions=[PartialResultsExtension]) | ||
|
||
# ... | ||
|
||
|
||
@strawberry.field | ||
def query(self, info) -> bool: | ||
info.context.partial_errors.extend( | ||
[ | ||
Exception("Failure 0"), | ||
Exception("Failure 1"), | ||
Exception("Failure 2"), | ||
] | ||
) | ||
return True | ||
``` | ||
|
||
Results: | ||
|
||
```json | ||
{ | ||
"data": { | ||
"query": true | ||
}, | ||
"errors": [ | ||
{ | ||
"message": "Failure 0" | ||
}, | ||
{ | ||
"message": "Failure 1" | ||
}, | ||
{ | ||
"message": "Failure 2" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
</details> | ||
|
||
<details> | ||
<summary>Add GraphQLError with location and path detail</summary> | ||
|
||
```python | ||
import strawberry | ||
from strawberry.extensions import PartialResultsExtension | ||
|
||
schema = strawberry.Schema(Query, extensions=[PartialResultsExtension]) | ||
|
||
# ... | ||
|
||
|
||
@strawberry.field | ||
def query(self, info) -> bool: | ||
nodes = [next(n for n in info.field_nodes if n.name.value == "query")] | ||
info.context.partial_errors.append( | ||
located_error( | ||
Exception("Error with location and path information"), | ||
nodes=nodes, | ||
path=info.path.as_list(), | ||
), | ||
) | ||
return True | ||
``` | ||
|
||
Results: | ||
|
||
```json | ||
{ | ||
"data": { | ||
"query": true | ||
}, | ||
"errors": [ | ||
{ | ||
"message": "Error with location and path information", | ||
"location": { | ||
"line": 1, | ||
"column": 9 | ||
}, | ||
"path": ["query"] | ||
} | ||
] | ||
} | ||
``` | ||
|
||
</details> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from graphql import GraphQLError, located_error | ||
|
||
from strawberry.extensions import SchemaExtension | ||
from strawberry.utils.await_maybe import AsyncIteratorOrIterator | ||
|
||
|
||
class PartialResultsExtension(SchemaExtension): | ||
""" | ||
Allow returning partial results, with both: | ||
- non-null `data` field | ||
- non-empty `errors` array | ||
One powerful feature of GraphQL is returning both data AND errors | ||
The typical way to return an error in python graphql is to simply raise it, | ||
and let the library catch it and plumb through to a singleton `errors` array | ||
But this means we can't return data AND errors | ||
To work around this issue, this extension adds errors to the result after execution | ||
Note: | ||
Because these errors are not caught and formatted by the library, | ||
we lose some extra error metadata like locations and path, which can sometimes | ||
be auto-added for us in the errors array | ||
Users of this extension can optionally wrap their exceptions in a | ||
`GraphQLError` to add those fields themselves | ||
Usage: | ||
``` | ||
@strawberry.field | ||
def query(self, info: Info) -> bool: | ||
info.context.partial_errors.append(Exception("Partial failure")) | ||
return True | ||
``` | ||
""" | ||
|
||
def on_execute( | ||
self, | ||
) -> AsyncIteratorOrIterator[None]: | ||
""" | ||
Before execution: | ||
- Initialize `partial_errors` to an empty list | ||
After execution: | ||
- Pull any errors off `partial_errors` and add them to | ||
the `result` as `GraphQLErrors` | ||
- To mirror existing library code, add to `execution_context` and process them | ||
since partial errors would otherwise get skipped | ||
""" | ||
initial: list[Exception] = [] | ||
if self.execution_context.context: | ||
self.execution_context.context.partial_errors = initial | ||
|
||
yield | ||
|
||
result = self.execution_context.result | ||
context = self.execution_context.context | ||
if result and context and context.partial_errors: | ||
# map all partial errors to `GraphQLError` if not one already | ||
partial_errors: list[GraphQLError] = [ | ||
error if isinstance(error, GraphQLError) else located_error(error) | ||
for error in context.partial_errors | ||
] | ||
|
||
# add partial errors to result's errors | ||
if not result.errors: | ||
result.errors = [] | ||
result.errors.extend(partial_errors) | ||
|
||
# below block copied from `strawberry.schema.execute.execute(_sync)` | ||
# to mirror what was skipped in the library's execution | ||
# note that we set the execution context to ALL errors, | ||
# but only process the partial errors, | ||
# because all others were already processed | ||
|
||
# === begin block === | ||
# Also set errors on the execution_context so that it's easier | ||
# to access in extensions | ||
self.execution_context.errors = result.errors | ||
# Run the `Schema.process_errors` function here before | ||
# extensions have a chance to modify them (see the MaskErrors | ||
# extension). That way we can log the original errors but | ||
# only return a sanitised version to the client. | ||
self.execution_context.schema.process_errors( | ||
partial_errors, self.execution_context | ||
) | ||
# === end block === |
Oops, something went wrong.