Skip to main content

GraphQL over HTTP

A reference for the GraphQL Foundation's GraphQL-over-HTTP specification: request parameters, GET and POST rules, the application/graphql-response+json media type, Accept negotiation for legacy clients, and the status code rules, including 294 for partial success.

Current versionStage 2 (Draft), working draft
Updated2026-10-17

Why GraphQL needed an HTTP specification

The GraphQL specification deliberately says nothing about transport. For years every server picked its own answers to the questions that matter at the HTTP layer: whether GET is allowed, what the request body looks like, which media type the response has, and above all which status code a failed query gets. The commonest answer to the last one was “always 200”, which left gateways, logs and monitoring unable to tell a broken query from a working one without parsing every body.

GraphQL over HTTP, maintained by the GraphQL Foundation’s working group, standardizes those answers. It is at Stage 2 (Draft) in the GraphQL Foundation’s process, which means it can still change before reaching the Accepted stage. It does not override the GraphQL specification; where they disagree, GraphQL wins.

The headline change is a new response media type, application/graphql-response+json. Because a response with that type is known to come from a GraphQL server, the server is free to use meaningful status codes without confusing clients, which read the outcome from the body. If you run a GraphQL server, adopt it: the rules are cheap to follow and fix real operational blind spots.


1. Request parameters

Every GraphQL-over-HTTP request carries the same four parameters, whatever the method:

Parameter Required Type Meaning
query Yes string The GraphQL document source text
operationName No string Which operation in the document to run
variables No map Values for the operation’s variables
extensions No map Reserved for implementers, for example persisted query hashes

query is a misleading name: the document can contain mutations. A request with the right names but wrong types, or missing query, is not well-formed. A query that does not parse is still well-formed at this layer; it fails later, at parsing.


2. POST and GET

A server must accept POST with a UTF-8 JSON body using Content-Type: application/json, and may accept other methods, such as GET.

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($id: ID!) { user(id: $id) { name } }", "variables": {"id": "QVBJcy5ndXJ1"}}

Clients must send Content-Type, and a server should reject a POST without one with a 4xx.

For GET, the parameters go in the query string, URL-encoded, with variables and extensions serialized as JSON strings:

GET /graphql?query=query(%24id%3A%20ID!)%7Buser(id%3A%24id)%7Bname%7D%7D&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D HTTP/1.1
Host: api.example.com
Accept: application/graphql-response+json

GET must never execute a mutation. If the document and operationName select a mutation, the server must halt with a 4xx; 405 Method Not Allowed is recommended, and RFC 9110 then requires an Allow header. This is what keeps GET safe, and so what makes it cacheable.

Support GET if you want CDN or browser caching of queries, ideally with persisted queries so the URL stays short. Otherwise POST-only is fully conformant and simpler.


3. Media types and Accept

Media type Used for
application/json Request bodies, and responses to legacy clients
application/graphql-response+json Responses; every conforming server must support it

A client must list application/graphql-response+json in Accept. When it does not know whether the server supports it, it should send:

Accept: application/graphql-response+json, application/json;q=0.9

If a client accepts only application/json, which is how a legacy client looks, the server processes the request exactly as if it had asked for the new type, but labels any 2xx response Content-Type: application/json. Error responses keep the meaningful status code either way. If Accept names nothing the server supports and not application/json either, the server should respond 406 Not Acceptable, though it may ignore Accept and pick a type. See content negotiation for the general mechanics.

A server that does not support application/graphql-response+json at all is a legacy server and does not conform.


4. Status codes

With application/graphql-response+json, clients must treat the response as a well-formed GraphQL response regardless of status, and read success or failure from the body. The status code is for everything else on the path: gateways, logs, metrics and anomaly detection.

GraphQL result Status
data present and not null, no errors 200 (must be 2xx)
data present (even null) and errors present 294 recommended
No data entry: a request error An appropriate 4xx or 5xx

For request errors the specification recommends specific codes:

Failure Status
Request body is not valid JSON 400
GraphQL document does not parse 400
Not a well-formed GraphQL-over-HTTP request 422
Document fails validation 422
Operation cannot be determined unambiguously 422
Variables cannot be coerced 422
Mutation via GET, or unsupported method 405
Unsupported request Content-Type 415
Request body too large 413
URI too long 414, and the client should switch to POST
Not permitted 401 or 403
Server unavailable, maintenance, load shedding 503

294 Partial Success

294 is a status code the specification defines itself, not one registered with IANA. A result with both data and errors is neither a success nor a request failure, and none of 203, 206 or 207 fits. RFC 9110 requires clients that do not recognize a code to treat it as the x00 of its class, so an unaware client sees 200. Use 294 only with application/graphql-response+json, and check that your CDN, gateway and caches handle it the way you intend; 294 is not on the list of heuristically cacheable codes and fixed lists in infrastructure will not know it.

If an error prevents producing a GraphQL response at all, the server uses an ordinary 4xx or 5xx and must not label the body application/graphql-response+json.


5. Security notes

The specification calls out one cross-site risk specifically. A POST with application/json triggers a CORS preflight when sent cross-origin; multipart/form-data or application/x-www-form-urlencoded do not, which opens the endpoint to cross-site request forgery. If you accept those media types (for file uploads, typically), require a custom header that forces a preflight. See CORS.


Best Practices

  • Return application/graphql-response+json and real status codes. “Always 200” hides failures from every tool that is not a GraphQL client.
  • Keep POST with application/json as the baseline, and add GET only for queries you want cached.
  • Reject mutations over GET with 405 and an Allow: POST header.
  • Do not turn partial results into 4xx or 5xx. Some data came back; that is 294.
On this page (7 sections)