-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Service capabilities / error behaviors #1163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
31c90e7
f4fab96
692d811
94446ab
3c63355
7056690
0fa7a33
1f975e4
8c40086
641a786
026982b
a7c6ad5
fe559ea
b5f64ae
1c3f0cd
7ab36b8
cc50991
df977eb
b1f039c
b28ef2f
144e854
dc9315c
6fd7239
c44a7ae
aef7069
955acd4
2fc8b0c
aef2e2d
4d2a94b
bbe2512
55458b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,13 +85,14 @@ operation. | |
|
|
||
| ## Schema Introspection | ||
|
|
||
| The schema introspection system is accessible from the meta-fields `__schema` | ||
| and `__type` which are accessible from the type of the root of a query | ||
| operation. | ||
| The schema introspection system is accessible from the meta-fields `__schema`, | ||
| `__type` and `__service` which are accessible from the type of the root of a | ||
| query operation. | ||
|
|
||
| ```graphql | ||
| __schema: __Schema! | ||
| __type(name: String!): __Type | ||
| __service: __Service! | ||
|
benjie marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| Like all meta-fields, these are implicit and do not appear in the fields list in | ||
|
|
@@ -228,6 +229,16 @@ enum __DirectiveLocation { | |
| INPUT_OBJECT | ||
| INPUT_FIELD_DEFINITION | ||
| } | ||
|
|
||
| type __Service { | ||
| capabilities: [__Capability!]! | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering if this should be a more generic "metadata" rather than "capabilities"? It would be essentially the same thing, just a different name and different expectation of what to expect in the list. For example, if some service would like to advertise some form of versioning they could do
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've thought about this quite a bit, and I think capabilities is better - it's more indicative of intent/purpose. The aim is that clients can use the "capabilities" of the service to inform their own behaviors in an automated way, such that end users can just point to a GraphQL endpoint and the client will take care of the rest - they don't need to configure it extensively (and the configuration can be automatically updated on each release without frontend engineers needing to do anything). For example:
The capabilities system can be used for metadata, but that's not its purpose; and calling it metadata significantly changes what is being proposed (and also encourages it to be used for feeding a huge amount more "metadata" into the system). Capabilities inform automated client decisions and actions; metadata is ancillary and often intended for human consumption. I will try and ensure this is correctly reflected in the spec text. |
||
| } | ||
|
|
||
| type __Capability { | ||
| identifier: String! | ||
| description: String | ||
| value: String | ||
| } | ||
| ``` | ||
|
|
||
| ### The \_\_Schema Type | ||
|
|
@@ -512,3 +523,29 @@ Fields\: | |
| {true}, deprecated arguments are also returned. | ||
| - `isRepeatable` must return a Boolean that indicates if the directive may be | ||
| used repeatedly at a single location. | ||
|
|
||
| ### The \_\_Service Type | ||
|
|
||
| The `__Service` type is returned from the `__service` meta-field and provides | ||
| information about the GraphQL service, most notably about its capabilities. | ||
|
|
||
| Note: Services implementing an older version of this specification may not | ||
| support the `__service` meta-field or `__Service` type. Support may be probed | ||
| using the introspection query: `{ __type(name: "__Service") { name } }`, a | ||
| {null} result indicates lack of support. | ||
|
|
||
| Fields\: | ||
|
|
||
| - `capabilities` must return a list of `__Capability` detailing each _service | ||
| capability_ supported by the service. | ||
|
|
||
| ### The \_\_Capability Type | ||
|
|
||
| A `__Capability` object describes a specific _service capability_, and has the | ||
| following fields\: | ||
|
|
||
| - `identifier` must return the string _capability identifier_ uniquely | ||
| identifying this service capability. | ||
| - `description` may return a String or {null}. | ||
| - `value` the String value of the service capability, or {null} if there is no | ||
| associated value. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,13 +15,24 @@ A GraphQL service generates a response from a request via execution. | |
| being executed. Conceptually, an initial value represents the "universe" of | ||
| data available via a GraphQL Service. It is common for a GraphQL Service to | ||
| always use the same initial value for every request. | ||
| - {onError} (optional): The _error behavior_ to apply to the request; see | ||
|
martinbonnin marked this conversation as resolved.
|
||
| [Handling Execution Errors](#sec-Handling-Execution-Errors). If {onError} is | ||
| provided and its value is not one of {"NULL"}, {"PROPAGATE"}, or {"HALT"}, | ||
| then a _request error_ must be raised. | ||
| - {extensions} (optional): A map reserved for implementation-specific additional | ||
| information. | ||
|
|
||
| Given this information, the result of {ExecuteRequest(schema, document, | ||
| operationName, variableValues, initialValue)} produces the response, to be | ||
| formatted according to the Response section below. | ||
|
|
||
| Note: Previous versions of this specification did not define the {onError} | ||
| request attribute. Clients can detect support for {onError} by checking for the | ||
| {"graphql.onError"} capability. If this capability is not present, or if | ||
| capabilities themselves are not supported by introspection, then clients should | ||
| not include {onError} in the request and must assume the _error behavior_ is | ||
| {"PROPAGATE"}. | ||
|
|
||
| Implementations should not add additional properties to a _request_, which may | ||
| conflict with future editions of the GraphQL specification. Instead, | ||
| {extensions} provides a reserved location for implementation-specific additional | ||
|
|
@@ -600,13 +611,26 @@ section. | |
| </a> | ||
|
|
||
| If during {ExecuteCollectedFields()} a _response position_ with a non-null type | ||
| raises an _execution error_ then that error must propagate to the parent | ||
| response position (the entire selection set in the case of a field, or the | ||
| entire list in the case of a list position), either resolving to {null} if | ||
| allowed or being further propagated to a parent response position. | ||
|
|
||
| If this occurs, any sibling response positions which have not yet executed or | ||
| have not yet yielded a value may be cancelled to avoid unnecessary work. | ||
| raises an _execution error_, the error must be added to the {"errors"} list in | ||
| the _execution result_ and then handled according to the _error behavior_ of the | ||
| request: | ||
|
|
||
| - {"NULL"}: The _response position_ must be set to {null}, even if such position | ||
| is indicated by the schema to be non-nullable. (The client is responsible for | ||
| interpreting this {null} in conjunction with the {"errors"} list to | ||
| distinguish error results from intentional {null} values.) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of mutation resolution, does eg if I have a mutation: mutation {
doThing1
doThing2
doThing3
}and all fields were not nullable, which of these responses would be correct in the presence of an error on resolution of A: response fields deviate from graphql request ask {
"data": {
"doThing1": true,
"doThing2": null
/* doThing3 never resolved */
},
"errors": [
{ /* error from doThing2 */ }
]
}B: synthetic field production - propagate null to down-operation resolution {
"data": {
"doThing1": true,
"doThing2": null,
"doThing3": null /* never resolved but server injects null because field is requested */
},
"errors": [
{ /* error path doThing2 */ }
]
}C: continue resolving {
"data": {
"doThing1": true,
"doThing2": null,
"doThing3": null /* server application's responsibility for stopping execution after error */
},
"errors": [
{ /* error path doThing2 */ },
{ /* error path doThing3 thrown due to response errored flag set, does not indicate a problem with doThing3 */ }
]
}or perhaps more confusingly {
"data": {
"doThing1": true,
"doThing2": null,
"doThing3": true
},
"errors": [
{ /* error from doThing2 */ }
]
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The final “perhaps more confusingly” is the correct behavior according to the current semantics. It’s an interesting question, because if the client changes the onError without the knowledge of the user, the resulting side-effects will differ. Option C is interesting, but also wrong in its own way. In my own schemas, root level fields (even mutations) are nullable so it wouldn’t make a difference to me, I wonder if making the fields non-nullable is done specifically to block follow-up mutations currently…? Though it does mean you wouldn’t see the result of previously completed mutations so it seems like a weird choice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hot Chocolate's mutation convention makes mutations not nullable and it is quite annoying to change: https://chillicream.com/docs/hotchocolate/v16/building-a-schema/mutations/#mutation-conventions and I think semantically it is correct (the mutation always has a result or there is an error). I agree the current wording suggests option C as well (both of the last 2 samples are option C). The difference between them is if the server implementer (or framework they use) flag that an error occurred via some mechanism such that if inside I suppose my point is that either application authors (both clients and servers) will need to beware of of the possibility that this could happen (and frameworks could take an opinionated stance or provide a default) or the spec force A or B and clients (and potentially client libraries) would need to be able to handle such responses. I don't think there is a satisfying answer here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bbarry can you ellaborate why C. is not satisfying?
Edit: found it! It's "batched mutations" article: https://medium.com/@xuorig/graphql-mutation-design-batch-updates-ca2452f92833 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is a very hard problem and every solution I've seen so far is unsatisfying in some way. In each case there is room for interpretation and miscommunication between client and server. If clients could just send valid data and servers could avoid flaky systems the world would be a much simpler place. What I find unsatisfying about C is that a client a client sending such a mutation operation could intend that the 3 mutation fields are causality related. If the second doesn't happen and the 3rd does, there could be a logic bug in the client. If the server author chooses to avoid this potential by raising down-field errors the server is adding unnecessary response content and falsely stating there is a problem with a field when in fact the problem is outside the field. |
||
| - {"PROPAGATE"}: The _execution error_ must propagate to the parent _response | ||
| position_ (the entire selection set in the case of a field, or the entire list | ||
| in the case of a list position). The parent position resolves to {null} if | ||
| allowed, or else the error is further propagated to a parent response | ||
| position. Any sibling response positions that have not yet executed or have | ||
| not yet yielded a value may be cancelled to avoid unnecessary work. | ||
| - {"HALT"}: The current {ExecuteRootSelectionSet()} must be aborted immediately | ||
| and must yield an execution result with an {"errors"} list consisting of this | ||
| _execution error_ only and the {"data"} entry set to {null}. Any _response | ||
| position_ that has not yet executed or has not yet yielded a value may be | ||
| cancelled to avoid unnecessary work. (Note: For a subscription operation the | ||
| underlying stream is not terminated.) | ||
|
|
||
| Note: See [Handling Execution Errors](#sec-Handling-Execution-Errors) for more | ||
| about this behavior. | ||
|
|
@@ -902,29 +926,60 @@ ResolveAbstractType(abstractType, objectValue): | |
| </a> | ||
|
|
||
| An _execution error_ is an error raised during field execution, value resolution | ||
| or coercion, at a specific _response position_. While these errors must be | ||
| reported in the response, they are "handled" by producing partial {"data"} in | ||
| the _response_. | ||
| or coercion, at a specific _response position_. These errors must be added to | ||
| the {"errors"} list in the _response_, and are "handled" according to the _error | ||
| behavior_ of the request. | ||
|
|
||
| Note: An _execution error_ is distinct from a _request error_ which results in a | ||
| response with no {"data"}. | ||
|
|
||
| If a _response position_ resolves to {null} because of an execution error which | ||
| has already been added to the {"errors"} list in the _execution result_, the | ||
| {"errors"} list must not be further affected. That is, only one error should be | ||
| added to the errors list per _response position_. | ||
|
|
||
| :: The _error behavior_ of a request indicates how an _execution error_ is | ||
| handled. It may be specified using the optional {onError} attribute of the | ||
| _request_. If omitted, the _default error behavior_ of the service applies. | ||
| Valid values for _error behavior_ are {"NULL"}, {"PROPAGATE"} and {"HALT"}. | ||
|
|
||
| :: The _default error behavior_ of a service is implementation-defined. For | ||
| compatibility with existing clients, services should default to {"PROPAGATE"} | ||
| which reflects prior behavior. <!-- For new services, {"NULL"} is | ||
| recommended. --> The default error behavior is indicated via the {"graphql.defaultErrorBehavior"} | ||
| _service capability_. | ||
|
|
||
| Note: {"HALT"} is not recommended as the _default error behavior_ because it | ||
| prevents generating partial responses which may still contain useful data. | ||
|
|
||
| Regardless of error behavior, if a _response position_ with a non-null type | ||
| results in {null} due to the result of {ResolveFieldValue()} then an execution | ||
| error must be raised at that position as specified in {CompleteValue()}. | ||
|
|
||
| Note: This is distinct from a _request error_ which results in a _request error | ||
| result_ with no data. | ||
| The _error behavior_ of a request applies to every _execution error_ raised | ||
| during execution. The following sections describe the behavior of each valid | ||
| value: | ||
|
|
||
| If an execution error is raised while resolving a field (either directly or | ||
| nested inside any lists), it is handled as though the _response position_ at | ||
| which the error occurred resolved to {null}, and the error must be added to the | ||
| {"errors"} list in the _execution result_. | ||
| **{"NULL"}** | ||
|
|
||
| With {"NULL"}, a `Non-Null` _response position_ will have the value {null} if | ||
| and only if an error occurred at that position. | ||
|
|
||
| Note: Clients must inspect the {"errors"} list and use the {"path"} of each | ||
| error result to distinguish between intentional {null} values and those | ||
| resulting from an _execution error_. | ||
|
|
||
| **{"PROPAGATE"}** | ||
|
|
||
| With {"PROPAGATE"}, a `Non-Null` _response position_ must not contain {null} in | ||
| the _response_. | ||
|
|
||
| If the result of resolving a _response position_ is {null} (either due to the | ||
| result of {ResolveFieldValue()} or because an execution error was raised), and | ||
| that position is of a `Non-Null` type, then an execution error is raised at that | ||
| position. The error must be added to the {"errors"} list in the _execution | ||
| result_. | ||
|
|
||
| If a _response position_ resolves to {null} because of an execution error which | ||
| has already been added to the {"errors"} list in the _execution result_, the | ||
| {"errors"} list must not be further affected. That is, only one error should be | ||
| added to the errors list per _response position_. | ||
|
|
||
| Since `Non-Null` response positions cannot be {null}, execution errors are | ||
| propagated to be handled by the parent _response position_. If the parent | ||
| response position may be {null} then it resolves to {null}, otherwise if it is a | ||
|
|
@@ -939,3 +994,13 @@ position_ must resolve to {null}. If the `List` type is also wrapped in a | |
| If every _response position_ from the root of the request to the source of the | ||
| execution error has a `Non-Null` type, then the {"data"} entry in the _execution | ||
| result_ should be {null}. | ||
|
|
||
| **{"HALT"}** | ||
|
|
||
| With {"HALT"}, {ExecuteRootSelectionSet()} must cease immediately that the first | ||
| _execution error_ is raised. That error must be added to the {"errors"} list, | ||
| and {"data"} must be {null}. | ||
|
|
||
| Note: For subscription operations, processing of the current event is ceased, | ||
| but the subscription still remains in place and future events will be processed | ||
| as normal. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this expecting to grammatically require that at least one dot appears, or should
Nameon its own should be a qualified Name as well, and we just use validation to ensure it's the shape that we expect?That might help build IDE tooling, if the first Name is a valid grammar, so you get better typeahead completion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requiring 2+ names was intentional; we could do it with a validation rule but the "qualified" in "qualified name" is intended to indicate that at least one period is required.