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

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.
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+jsonmedia 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:
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 happened | Status code |
|---|---|
The operation executed, with data and no errors | 200 |
The operation executed, with data (even null) and errors | 294 Partial Success |
| The request body is not valid JSON | 400 |
| The GraphQL document cannot be parsed | 400 |
The request is not a well-formed GraphQL-over-HTTP request, such as a qeury typo | 422 |
| The document fails validation | 422 |
| The operation to run cannot be determined | 422 |
| The variables cannot be coerced to their types | 422 |
A mutation arrives over GET, or the method is unsupported | 405, with an Allow header |
The request Content-Type is unsupported | 415 |
The Accept header lists neither GraphQL media type | 406 |
| The URI, the headers or the body are too large | 414, 431 or 413 |
| The client is not allowed to make the request | 401 or 403 |
| The server is down for maintenance or shedding load | 503 |
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
418SHOULD 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
294is unlikely to appear in such lists by default,294might be handled differently from200.
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 IANA 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:
| Request | Spring for GraphQL 2.1.0-M1 | First edition |
|---|---|---|
| A query that succeeds | 200 | 200 |
A query where boxOffice fails | 200 | 294 |
| A document that does not parse | 400 | 400 |
| A field that does not exist | 400 | 422 |
| A required variable that is missing | 400 | 422 |
| A field that does not exist, from a legacy client | 200, application/json | 422 |
Accept: text/html | 415 | 406 |
Here is the validation failure from the fourth row, unedited apart from the headers we left out:
POST /graphql HTTP/1.1
Content-Type: application/json
Accept: application/graphql-response+json
{"query":"query MovieBudget { movie(id: \"1\") { budget } }"}
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
QUERYmethod - 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.9on 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+jsonand 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
alwaysto everyadd_headeryou 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
- GraphQL over HTTP, current working draft - the text of the first edition until the release, with every rule quoted in this post
- graphql-over-http PR #444 - the September 2026 edition and its changelog with the editor's notes
- graphql-over-http PR #354 - the change that introduced
422and the detailed4xxlist - graphql-over-http issue #435 - how proxies and CDNs handle
294, including the nginxalwaysparameter - RFC 9110, HTTP Semantics - the rule that unknown status codes are treated like the
x00code of their class - Spring for GraphQL, Server Transports - the media types and status codes Spring sends today
- spring-graphql issue #1314 -
294support for partial responses - graphql-http - the reference implementation and audit suite for the specification