Skip to main content

GraphQL over HTTP Gets a First Edition, and 200 OK Stops Meaning Everything

· 12 min read
GraphQL Guy

GraphQL over HTTP first edition

Send a GraphQL server a document with a typo in it, and for most of the last decade the answer came back as 200 OK. Ask for a field that does not exist, and you got 200 OK. Run a query that half worked, and you got 200 OK again, with the bad news inside the body. Load balancers, CDNs and dashboards do not read response bodies, so to them all three requests looked like successes. The GraphQL over HTTP specification is getting its first edition, named September 2026, and its main job is to let the status code tell the truth again without breaking the clients that learned to ignore it.

Release status on 10 October 2026

The September 2026 edition has five approvals in PR #444, and its release is on the agenda of the 29 October working group meeting. The draft changelog of the GraphQL specification's September 2026 edition says the two will launch together. Until the release, the text lives at http-spec.graphql.org/draft, and every rule in this post comes from that draft.

How We Got to 200 for Everything​

For years, the reference for serving GraphQL over HTTP was a short page on graphql.org, first published in 2016. It described GET and POST requests and a JSON response, and it left status codes mostly open.

The habit that grew from it had a good reason behind it. A 503 might come from the GraphQL server, or from a proxy in front of it that did not forward the request, and the two bodies look completely different. A client could not tell them apart, so servers sent 200 for anything GraphQL produced, and clients learned to read the body for the real answer. The editor's notes in the first edition's changelog describe the cost:

This led to issues with monitoring tools and related infrastructure, which could not determine the status of GraphQL requests without reading the response body.

The same notes call the first edition "the result of 8 years of collaboration between over 20 contributors", and the GraphQL specification's own changelog describes the draft as "already widely adopted". The edition gives those rules a stable name and a stable URL.

The Fix Is a Media Type​

The central idea of the edition fits in one header. A response labeled application/graphql-response+json is a GraphQL response, whatever its status code. When a client sees that label, it reads the body. Any other label tells the client that the response may have come from something else in the path, such as a proxy, a load balancer or an error page. The Status Codes section puts it this way:

Clients should process a response using the application/graphql-response+json media type as a well-formed GraphQL-over-HTTP response independent of the HTTP status code.

Because the client reads the body anyway, the status code becomes available for everything in the middle that cannot read GraphQL.

Clients opt in through the Accept header. The Accept section requires every client to include the new media type, and it suggests a fallback for servers that may not know it yet:

A request from a client that follows the first edition
POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/graphql-response+json, application/json;q=0.9

{"query":"query MovieTitle($id: ID!) { movie(id: $id) { title } }","variables":{"id":"1"}}

The q=0.9 gives application/json a lower weight, so a server that knows the new type picks it, and an older server falls back to the type it has always sent.

A server MUST support the new media type. The edition calls a server without it a legacy server, and a client that accepts only application/json a legacy client. Legacy clients are the reason the change can roll out gradually. When a request accepts only application/json, the server follows all the new rules anyway and relabels every 2xx response as application/json. Old clients keep receiving what they received before on success. On failure they receive a 4xx with the new label, and most HTTP clients already treat a 4xx as a failed request.

The Status Codes, Written Down at Last​

The Status Codes section maps each outcome to a code:

What happenedStatus code
The operation executed, with data and no errors200
The operation executed, with data (even null) and errors294 Partial Success
The request body is not valid JSON400
The GraphQL document cannot be parsed400
The request is not a well-formed GraphQL-over-HTTP request, such as a qeury typo422
The document fails validation422
The operation to run cannot be determined422
The variables cannot be coerced to their types422
A mutation arrives over GET, or the method is unsupported405, with an Allow header
The request Content-Type is unsupported415
The Accept header lists neither GraphQL media type406
The URI, the headers or the body are too large414, 431 or 413
The client is not allowed to make the request401 or 403
The server is down for maintenance or shedding load503

Two of the status code rules are MUSTs: a result with a data entry that is not null gets a 2xx code, and a result without a data entry gets a 4xx or 5xx code. The specific codes in the table are SHOULDs. A partial result means the operation executed successfully, so its status code stays in the 2xx range.

The validation row changed recently. Until PR #354 in April 2026, the draft recommended 400 for a document that fails validation. It now separates a request the server cannot read (400) from a request it can read but will not run (422 Unprocessable Content).

The list of 4xx cases also holds a joke, which deserves a quote:

If the server cannot process the request due to being a short and stout ceramic vessel, status code 418 SHOULD be used.

294, a Status Code You Have Probably Never Seen​

Every registered HTTP status code means something more specific than "this worked, but only partly". The Partial success notes go through the candidates: 203 means a proxy transformed the response, 206 needs a Range request, and WebDAV's 207 reports several independent operations. The notes conclude:

None of those quite fit GraphQL's needs, so we recommend using the custom code 294 Partial Success.

The code exists for the infrastructure in the middle. Clients that receive the new media type read the body and ignore the status code. An access log, a CDN rule or a dashboard can now count partial successes without opening a single response.

Because 294 sits in the 2xx range, RFC 9110 requires HTTP clients that do not recognize it to treat it as 200 OK. The same notes add a warning about software that keeps its own lists:

Infrastructure may apply specific behaviors based on a fixed list of status codes; because 294 is unlikely to appear in such lists by default, 294 might be handled differently from 200.

Issue #435 has a real example. The nginx add_header directive applies only to a fixed list of status codes unless it has the always parameter, so CORS and security headers silently disappear from 294 responses.

294 is also not registered with yet. Registering it, and registering the application/graphql-response+json media type (issue #31), are both on the 29 October agenda.

What a Spring Server Does Today​

We wanted to know how close a current Java stack is to the edition, so we ran a small Spring Boot 4.2.0-M2 application with Spring for GraphQL 2.1.0-M1 and graphql-java 26.1. The schema has one movie query, and the data fetcher for its boxOffice field throws an exception, so that one query can produce a partial result. Here is what came back:

RequestSpring for GraphQL 2.1.0-M1First edition
A query that succeeds200200
A query where boxOffice fails200294
A document that does not parse400400
A field that does not exist400422
A required variable that is missing400422
A field that does not exist, from a legacy client200, application/json422
Accept: text/html415406

Here is the validation failure from the fourth row, unedited apart from the headers we left out:

Request
POST /graphql HTTP/1.1
Content-Type: application/json
Accept: application/graphql-response+json

{"query":"query MovieBudget { movie(id: \"1\") { budget } }"}
Response
HTTP/1.1 400
Content-Type: application/graphql-response+json

{"errors":[{"message":"Validation error (FieldUndefined@[movie/budget]) : Field 'budget' in type 'Movie' is undefined","locations":[{"line":1,"column":38}],"extensions":{"classification":"ValidationError"}}]}

Spring is close to the edition. The new media type is "the content type of choice" according to the transports documentation, and parse and validation failures already get 4xx codes. The validation code follows the draft from before April. The partial result gets 200, and issue #1314 tracks 294 for the 2.1.x line.

One response surprised us. A request body with broken JSON came back as Spring Boot's standard error response (timestamp, status, error, path) with status 400, which is the right code. Its label was application/graphql-response+json. The Status Codes section says a server "MUST NOT use" that label when it cannot produce a well-formed GraphQL response. Whatever framework you run, check what your error handling sends under the new label, because clients will trust it.

What the First Edition Leaves for Later​

Subscriptions are outside the scope of this edition. The editor's notes list the ideas for future editions:

  • support for the HTTP QUERY method
  • persisted documents
  • dedicated operation URLs
  • variable batching
  • request media types
  • feature discovery, so that clients can configure themselves
  • an endpoint that returns the schema SDL

The first item already has news of its own. The IETF published the QUERY method as RFC 10008 in June 2026, and PR #411, which adds it to GraphQL over HTTP, is open.

What to Do This Week​

None of this needs a big migration. The edition was designed so that each side can move on its own:

  • Clients: send Accept: application/graphql-response+json, application/json;q=0.9 on every request. The edition requires it, and servers that do not know the first type fall back to the second.
  • Servers: support application/graphql-response+json and make it the default. A server without it is a legacy server in the edition's words.
  • Status codes: move validation and variable errors to 422, and check what your error pages send under the new label.
  • 294: before you send it, test it against your proxies, CDN rules, log parsers and uptime checks. In nginx, add always to every add_header you depend on.
  • Testing: the graphql-http project ships an audit suite that checks a server against the specification.

Class 3 of our error handling course covers the same rules in more depth, with diagrams of the content negotiation and of every status code decision.


We proofread this post twice and it probably still has one typo, so by the new rules it should be served as 294 Partial Success. Your browser treats that as 200 OK, as RFC 9110 requires, so please read it anyway.

Sources​