Error Response Shapes in Ash Graphql
What response shapes will different error conditions return in GraphQL. There have seemed to be a lot.
These are the ones I checked and catalogued.
- Bad query syntax
- Querying unknown fields
- Filter/arg not found
- Bad arguments
- Bad/unreachable host
- Unset variables in query
- Unset variables in mutation
- Invalid variable value
- Authz error
- Runtime server error
I don't know if there's a graphql standard for error reporting. I think anyone who implements a resolver can make it return whatever he designs for error handling. So, surely this doesn't cover all cases. But these are my findings for ash_graphql 1.3.1 on ash 3.3.3.
Error Cases
Bad Query Syntax
Query:
query fetchMe {
badSyntax
}
Responds:
{
"errors": [
{
"message": "Cannot query field \"badSyntax\" on type \"RootQueryType\".",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
Query Unknown Fields
Query:
query fetchMe {
listProjects {
results {
badField
}
}
}
Responds:
{
"errors": [
{
"message": "Cannot query field \"badField\" on type \"Project\".",
"locations": [
{
"line": 4,
"column": 7
}
]
}
]
}
Filter/Argument Not Found
Query:
query fetchMe {
getProject(id: "notFound") {
id
}
}
Logs in the Backend:
[warning] `2267ce9a-bcf3-488a-abfa-095b2a6a0780`: AshGraphql.Error not implemented for error:
** (Ash.Error.Query.InvalidFilterValue) Invalid filter value `"notFound"` supplied in `#Ecto.Query`
(elixir 1.16.2) lib/enum.ex:2528: Enum."-reduce/3-lists^foldl/2-0-"/3
(elixir 1.16.2) lib/enum.ex:1826: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
(elixir 1.16.2) lib/enum.ex:2528: Enum."-reduce/3-lists^foldl/2-0-"/3
(ecto 3.11.2) lib/ecto/repo/queryable.ex:214: Ecto.Repo.Queryable.execute/4
(ecto 3.11.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
(ash_postgres 2.1.18) lib/data_layer.ex:767: anonymous fn/3 in AshPostgres.DataLayer.run_query/2
(ash_postgres 2.1.18) lib/data_layer.ex:765: AshPostgres.DataLayer.run_query/2
(ash 3.3.3) lib/ash/actions/read/read.ex:2456: Ash.Actions.Read.run_query/4
(ash 3.3.3) lib/ash/actions/read/read.ex:448: anonymous fn/5 in Ash.Actions.Read.do_read/4
(ash 3.3.3) lib/ash/actions/read/read.ex:786: Ash.Actions.Read.maybe_in_transaction/3
Custom error handling looks possible here. Though I don't know if it changes the error serialization shape.
Responds generically:
{
"data": {
"getProject": null
},
"errors": [
{
"message": "Something went wrong. Unique error id: `2267ce9a-bcf3-488a-abfa-095b2a6a0780`",
"path": [
"getProject"
],
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
Bad Argument
Query:
query fetchMe {
getProject(badArg: 123) {
id
}
}
Responds:
{
"errors": [
{
"message": "In argument \"id\": Expected type \"ID!\", found null.",
"locations": [
{
"line": 2,
"column": 3
}
]
},
{
"message": "Unknown argument \"badArg\" on field \"getProject\" of type \"RootQueryType\".",
"locations": [
{
"line": 2,
"column": 14
}
]
}
]
}
Bad/unreachable host
It's a more basic problem for the client, which can't make a network request happen.
Failed to fetch
We will not further categorize this case.
Unset Variables in Mutation
Mutation:
mutation upsertThing($projectId: ID!, $slug: String!, $value: String!) {
upsertThing(input:{ projectId: $projectId, slug: $slug, value: $value }) {
result {
id
}
errors {
code
fields
message
}
}
}
Variables:
{}
Response:
{
"errors": [
{
"message": "Argument \"input\" has invalid value {projectId: $projectId, slug: $slug, value: $value}.\nIn field \"slug\": Expected type \"String!\", found $slug.\nIn field \"value\": Expected type \"String!\", found $value.",
"locations": [
{
"line": 2,
"column": 14
}
]
},
{
"message": "Variable \"slug\": Expected non-null, found null.",
"locations": [
{
"line": 1,
"column": 38
}
]
},
{
"message": "Variable \"value\": Expected non-null, found null.",
"locations": [
{
"line": 1,
"column": 54
}
]
}
]
}
Invalid Variable Value
Only for a mutation are the root errors
(MutationError[]
) available to query. Without specifying that query, the validation errors do not get returned.
Mutation:
mutation upsertThing($projectId: ID!, $slug: String!, $value: String!) {
upsertThing(input:{ projectId: $projectId, slug: $slug, value: $value }) {
result {
id
}
errors {
code
fields
message
}
}
}
Variables:
{
"projectId": "0bf878f9-ea2b-408d-a1cb-b48b7ce28f78",
"slug": "badEnum",
"value": "testVal"
}
Response:
{
"data": {
"upsertThing": {
"errors": [
{
"code": "invalid_attribute",
"fields": [
"slug"
],
"message": "is invalid"
}
],
"result": null
}
}
}
Unauthorized on Resource Policy
In this case, the user, which will be the actor, doesn't have permissions to fulfill the action it's attempting.
I've yet to determine if the same behavior is true for a query.
Only for a mutation are the root errors
(MutationError[]
) available to query. Without specifying that query, the validation errors do not get returned.
Here's an example mutation:
mutation createTrack(
$regionId: ID!,
$start: DateTime!,
) {
createTrack(input: {
regionId: $regionId,
start: $start,
}) {
result {
id
}
errors {
code
fields
message
shortMessage
vars
}
}
}
Variables:
{
"regionId": "120db220-2034-4421-bbb9-afe949be5ba4",
"start": "2024-11-11T11:11:11Z"
}
Response:
{
"data": {
"createTrack": {
"errors": [
{
"code": "forbidden",
"fields": [],
"message": "MyProject.Track.create\n\n\nPolicy Breakdown\n Policy | 🔎:\n condition: action.type in [:create, :update, :destroy] \n forbid if: user access token has expired | ✘ | 🔎 \n authorize if: user has permissions to edit tracks | ✘ | 🔎",
"shortMessage": "forbidden",
"vars": {}
}
],
"result": null
}
}
}
Runtime Server Error
(Result is the same whether query or mutation.)
Query:
query listStatuses {
listStatuses {
id
}
}
Logs in the Backend:
[error] 6be8423a-075b-45b2-a277-57117412a3a4: Exception raised while resolving query.
** (RuntimeError) Query fake error
(myproject 0.1.0) lib/myproject/resources/status.ex:64: MyProject.Status.manual_0_generated_2C94E746D22616EC412CE65B99247076/3
(ash 3.3.3) lib/ash/actions/read/read.ex:2440: Ash.Actions.Read.run_query/4
(ash 3.3.3) lib/ash/actions/read/read.ex:448: anonymous fn/5 in Ash.Actions.Read.do_read/4
(ash 3.3.3) lib/ash/actions/read/read.ex:786: Ash.Actions.Read.maybe_in_transaction/3
(ash 3.3.3) lib/ash/actions/read/read.ex:249: Ash.Actions.Read.do_run/3
(ash 3.3.3) lib/ash/actions/read/read.ex:66: anonymous fn/3 in Ash.Actions.Read.run/3
(ash 3.3.3) lib/ash/actions/read/read.ex:65: Ash.Actions.Read.run/3
(ash 3.3.3) lib/ash.ex:1863: Ash.read/2
(ash_graphql 1.3.1) lib/graphql/resolver.ex:474: AshGraphql.Graphql.Resolver.resolve/2
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:234: Absinthe.Status.Document.Execution.Resolution.reduce_resolution/1
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:189: Absinthe.Status.Document.Execution.Resolution.do_resolve_field/3
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:174: Absinthe.Status.Document.Execution.Resolution.do_resolve_fields/6
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:145: Absinthe.Status.Document.Execution.Resolution.resolve_fields/4
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:88: Absinthe.Status.Document.Execution.Resolution.walk_result/5
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:67: Absinthe.Status.Document.Execution.Resolution.perform_resolution/3
(absinthe 1.7.8) lib/absinthe/status/document/execution/resolution.ex:24: Absinthe.Status.Document.Execution.Resolution.resolve_current/3
(absinthe 1.7.8) lib/absinthe/pipeline.ex:408: Absinthe.Pipeline.run_status/3
(absinthe_plug 1.5.8) lib/absinthe/plug.ex:536: Absinthe.Plug.run_query/4
(absinthe_plug 1.5.8) lib/absinthe/plug.ex:290: Absinthe.Plug.call/2
(phoenix 1.7.14) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2
Responds generically:
{
"data": null,
"errors": [
{
"code": "something_went_wrong",
"message": "Something went wrong. Unique error id: `6be8423a-075b-45b2-a277-57117412a3a4`",
"path": [
"listStatuses"
],
"fields": [],
"vars": {},
"locations": [
{
"line": 2,
"column": 3
}
],
"short_message": "Something went wrong."
}
]
}
Grouped Possible Shapes
I have grouped these specific error shapes into these categories: programmer, not found, data validation and server errors.
"Programmer Errors"
These should be known at dev time. May or may not be worth creating error handling for.
The types of queries that yield this error shape include:
- Bad query syntax
- Querying unknown fields
- Bad arguments
- Unset variables in query
The shape is:
{
errors {
message
locations {
line
column
}
}
}
This shape is distinguished by having no data
key.
"Not Found Error"
There is only one kind of error that will return this shape:
- Filter/arg not found
This means that the id
or other filter you use to find a resource 404ed on that resource. The shape:
{
data {
myQueryName: null
}
errors {
message
path
locations {
line
column
}
}
}
This shape is distinguished by having a data
object with the myQueryName
with the value of null
.
"Data Validation Error"
This category of errors means that you have a data problem -- with the resource or with the requesting actor.
- Invalid variable value
- Authz error
{
data {
myQueryName {
errors {
code
fields
message
}
result: null
}
}
}
This shape is distinguished by having a data.myQueryName
object that includes an errors
array and a null result
. That query-internal errors
element shape is also unique and exposes field-level validation messages.
"Server Error"
This category represents all the other runtime exceptions that happen somewhere, usually outside of graphql proper, that could create a 500 error that returns through a graphql response. Namely:
- Runtime server error
And the shape:
{
data: null
errors {
code
message
path
fields
vars {
}
locations {
line
column
}
short_message
}
}
This shape is distinguished by having a null data
value. It also has a unique errors
element shape.
Have you seen any other shapes for error responses? Call before you dig.