Style guide for GraphQL error handling proposal

TLDR;

Our current GQL back-end style guide has no topic on the subject of error handling. It is mostly handled by just passing a human-readable message to a GQL error instance just like here.

This is fine if the UI just needs to know if there was an error. However, it's limited if the UI needs to handle particular errors in a particular way. Because error messages are meant for developers to be read, not for the UI to be handled. Therefore, using error messages falls apart as soon as the UI has to display error messages conditionally. Consequently, there is a case to use a flavor of custom error handling as a purely additive and easily implementable style guide.

Why not use HTTP error codes in those cases?

  • GQL is transport agnostic. It can be implemented in any application layer, as long as it can support query and variables fields. whereas REST is tied to HTTP. You can implement it with multipart for file upload or GRPC in theory.
  • In GQL salvation is individual. Each resource queried is resolved independently from one other. As a result, some resources could resolve in errors and some could be perfectly fine, all in one query. So responding with a 400 error status is inappropriate with GQL. Consider, the query below
mutation {
  createHero(name: "Jarjar",  height: 2.3) {
    name
    height
  }
  createJedi(name: "Luke", height: 1.72) {
    name
    height
  }
}

could result in

{
  "data": {
    "hero": null,
    "jedi": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  },
    "errors": [
    {
      "message": "Can not create hero with name 'Jarjar'",
      "locations": [ {...} ],
      "path": [ "hero" ],
      "extensions": {
        "code": "invalid_argument"
      }
    }
  ]
}

Returning an HTTP status code error 401 doesn't reflect the reality of the GQL query as a whole in those cases. In the issue you can see an example with an error 500:

https://gitlab.com/gitlab-org/gitlab/uploads/e557546fd4c385197228cc6a2cc81c76/Screenshot_2022-11-23_at_10.19.35.png

And here is an example without HTTP error:

https://gitlab.com/gitlab-org/gitlab/uploads/bce0da17d932b9cf138702c2c937352c/Screenshot_2022-11-23_at_17.01.15.png

As you can see the fields project and repository responded normally. Only the sub-field paginatedTree couldn't be resolved. Therefore, it's the only one casting an error.

More on the message field

Besides in the cases, where it's decided as an application-wide convention (which is not our case). Using the message field to handle your error codes is a bad idea. Because message fields can originate from all places, which could introduce inconsistencies in your error-handling processes.

For example, you could have error messages meant as error codes like this:

{
  "errors": [
    {
      "message": "invalid_argument",
      "locations": [ ... ],
      "path": [ ... ]
    }
  ]
}

Or error message meant as a human-readable explanation such as.

{
    "errors": [
    {
      "message": "Can not create hero with name 'Jarjar'",
      "locations": [ ... ],
      "path": [ ... ]
    }
  ]
}

Now it's up to the FE engineer to push through this confusion to figure out what needs to be done with the error message. As a result team productivity could decrease.

GQL error extensions are made to output error codes

GQL has extension fields in the error objects that are conventionally used to give easily parsable error codes like "invalid_argument" or "unauthorized".

{
  "errors": [
    {
      "message": "Can not create hero with name 'Jarjar'",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "code": "invalid_argument",
        
      }
    }
  ]
}

But also to give whatever extra details about the error you want. Why not give the corresponding HTTP error status in the example below:

{
  "errors": [
    {
      "message": "Can not create hero with name 'Jarjar'",
      "locations": [ ... ],
      "path": [ ... ],
      "extensions": {
        "code": "invalid_argument",
        "status": 401
      }
    }
  ]
}

Implementation proposal

My proposal would be to add error extensions as when the FE needs to handle any errors in a particular way (see !104696 (merged)) as follows.

When raising an error argument error, for example:

raise Gitlab::Graphql::Errors::ArgumentError.new(
   "Expecting type 'string' for field 'name'",
   extensions: { code: "invalid_name", service: "web" }
)

With the extension fields:

field type required explaination
code: string true snake cased string representing what type of error it is. Meant for the UI to be parsed and easily be handled for internalization
service: string false If FE needs to know which of our services has cast the error. For the moment I would propose to have "git" if error comes from gitaly. And "web" if it comes from the web server
status: interger false In case there is any kind of number associated with the error code. Like a 404 status for a not_found error code for instance.

Conclusion

Please let me know what you think of this proposal.

Related items:

  • #343260 (closed)
  • !104696 (merged)
Edited Dec 02, 2022 by Patrick Cyiza
Assignee Loading
Time tracking Loading