Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTTP status code handling in GraphQL APIs

A lot of resources say, that GraphQL should always respond with a 200 status code, even when an error occurred:

  • https://www.graph.cool/docs/faq/api-eep0ugh1wa/#how-does-error-handling-work-with-graphcool
  • https://github.com/rmosolgo/graphql-ruby/issues/1130#issuecomment-347373937
  • https://blog.hasura.io/handling-graphql-hasura-errors-with-react/

Because GraphQL can return multiple responses in one response, this makes sense. When a user requests two resources in one request, and only has access to the first resource, you can send back the first resource and return a forbidden error for the second resource.

However, this is just something I figured out along the way reading docs of multiple GraphQL libraries and blog posts. I didn't find anything about HTTP status codes in the offical specs, here https://spec.graphql.org/ or here https://graphql.org/

So I still have a few questions left:

  1. Is it ok to return a HTTP 500 status code if I have an unexpected server error?
  2. Is it ok to return a HTTP 401 status code, if credentials are wrong?
  3. Should I include the potential HTTP status code inside the errors key of the GraphQL response like this
{
  "errors" => [{
    "message" => "Graphql::Forbidden",
    "locations" => [],
    "extensions" => {
      "error_class" => "Graphql::Forbidden", "status" => 403
    }
  }]
}
  1. Should I match common errors like a wrong field name to the HTTP status code 400 Bad Request?
{
  "errors" => [{
    "message" => "Field 'foobar' doesn't exist on type 'UserConnection'",
    "locations" => [{
      "line" => 1,
      "column" => 11
    }],
    "path" => ["query", "users", "foobar"],
    "extensions" => {
      "status" => 400, "code" => "undefinedField", "typeName" => "UserConnection", "fieldName" => "foobar"
    }
  }]
}

I'd be great if you could share your experiences / resources / best practises when handling HTTP status codes in GraphQL.

like image 677
23tux Avatar asked Jan 14 '20 08:01

23tux


People also ask

Does GraphQL use HTTP status codes?

HTTP response code​By default, when you throw a GraphQLException , the HTTP status code will be 500. If your exception code is in the 4xx - 5xx range, the exception code will be used as an HTTP status code. throw new GraphQLException("Not found", 404); GraphQL allows to have several errors for one request.

What HTTP method does GraphQL use?

Your GraphQL HTTP server should handle the HTTP GET and POST methods.

How do you handle errors in GraphQL?

To add errors to your data, you need to use the Union type (a.k.a. Result ) in your GraphQL schema. Also, the logic in the resolver is rewritten so that next to the result or error for the operation you also need to return a type for it.

Can GraphQL return 500?

This is about graphQl APIs which is returning HTTP 500 error when invalid or non-existing ID is passed in the query. For E.g. Here item_id is non-existing and it returns me below 500 error. This is not same for all APIs.


1 Answers

GraphQL is transport-agnostic. While GraphQL services are commonly web services that accept requests over HTTP, they can and do accept requests over other transports as well. In fact, a GraphQL service can execute queries with no network requests at all -- all it needs is a query, and, optionally, a variables object and operation name.

Because of this, the GraphQL spec isn't concerned with methods, status codes or anything else specific to HTTP (it only mentions HTTP when discussing serialization). Any practices with regard to these things are at best conventions that have either evolved over time or are simply artifacts from some of the original libraries that were written for GraphQL. As such, any kind of answer to your question is going to be mostly based on opinion.

That said, because your GraphQL service shouldn't care about how its queries are received, arguably there should be a separation between its code and whatever code is handling receiving the requests and sending back the responses (like an Express app in Node.js). In other words, we could say it's never ok for your resolver code to mutate things like the response's status code. This is the current thinking in the community and most libraries only return one of two codes -- 400 if the request itself is somehow invalid and 200 otherwise.

If your entire GraphQL endpoint is guarded by some authentication logic (say your server checks for some header value), then a GraphQL request might come back with a 401 status. But this is something we handle at the web server level, not as part of your schema. It's no different if something went terribly wrong with your web server code and it had to return a 500 status, or the nginx server sitting in front of your returned a 494 (Request header too large), etc.

Traditionally, errors encountered during execution should be thrown and that's it. GraphQL extensions can be used to provide additional context when the errors are collected and serialized -- the name of the error, the stack trace, etc. However, it makes little sense to include HTTP status codes with these errors when, again, the errors have nothing to do with HTTP. Doing so unnecessarily mixes unrelated concepts -- if you want to identify the type of error, you're better off using descriptive codes like GENERIC_SERVER, INVALID_INPUT, etc.

However, conventions around error handling are also changing. Some services want to better distinguish client errors from other execution errors. It's becoming more common to see validation errors or other errors that would be shown to the end user to be returned as part of the data instead of being treated like an execution error.

type Mutation {
  login(username: String!, password: String!): LoginPayload!
}

type LoginPayload {
  user: User
  error: Error
}

You can see payload types like these in action with public APIs like Shopify's. A variant on this approach is to utilize unions to represent a number of possible responses.

type Mutation {
  login(username: String!, password: String!): LoginPayload!
}

union LoginPayload = User | InvalidCredentialsError | ExceededLoginAttemptsError

The end result is that the client errors are strongly typed and easily distinguishable from other errors that the end user doesn't care about. There's a lot of benefits to adopting these sort of conventions, but whether they are the right fit for your server is ultimately up to you.

like image 150
Daniel Rearden Avatar answered Oct 18 '22 21:10

Daniel Rearden