What does a GraphQL server return when a GET request's document contains a mutation?
answer
- The carrier answers, not the executor
- Nothing ran, so nothing to report
- Not every failure is GraphQL-shaped
- A status code answers, after parsing
basics
~20 sAn HTTP-level refusal, not a GraphQL result. The GraphQL over HTTP working draft has the server answer 405 Method Not Allowed and execute nothing, so there is no data/errors envelope for the client to parse.
solid answer
~50 sA GET carries query operations only, so a server that parses the URL, selects the operation and finds a mutation must refuse before executing anything. The GraphQL over HTTP working draft names the answer: **405 Method Not Allowed**. The part worth saying out loud is what the client receives. This is a *transport* refusal, not a request error and not a field error, so there is no `data`/`errors` body to read — client code that assumes every response from the endpoint parses as a GraphQL envelope fails on the parse rather than surfacing a useful message. Note also when the server can know: it needs the document text and a selected operation first. If the GET carries a persisted identifier instead of text, the refusal can only come after that identifier is resolved back to a document.
code
graphql · 6 linesmutation ApproveSegment($unitId: ID!, $target: String!) {
approveSegment(unitId: $unitId, target: $target) {
unitId
approvedAt
}
}go deeper
Recall that a mutation sent over GET is turned away at the HTTP layer rather than executed, and that the response is a status code — 405 — not a GraphQL result. Knowing there is no data/errors body to read is the useful half.
Explain the ordering: parameters read, document parsed, operation selected, type inspected, then refused — all before execution. Be ready to say what changes when the GET carries a persisted identifier rather than document text.
Show you would catch this in client and monitoring design: a per-operation method choice so the condition never arises, and awareness that these requests appear in edge HTTP metrics while being invisible to GraphQL operation metrics.
Own the policy that keeps write paths off safe methods across every client team, and the contract that clients must handle non-GraphQL bodies behind non-2xx statuses rather than assuming one endpoint always answers in one shape.
## The refusal is a transport answer, not an operation result GraphQL responses have a familiar shape: a JSON object carrying some combination of `data`, `errors` and `extensions`. Almost everything that can go wrong with an operation arrives inside that shape. A document that fails validation comes back as a **request error** — `errors` present, no `data` key at all. A resolver that throws comes back as a **field error** — `errors` present alongside a `data` tree with a hole in it. A mutation sent over GET is neither. It is refused by the layer that carries the request, before execution begins, and the answer is an HTTP status line: **405 Method Not Allowed**, which is what the GraphQL over HTTP working draft directs a server to send. Nothing executed, so there is no operation result to report, and the body is whatever that server writes for an HTTP-level refusal — commonly a short text or JSON diagnostic that is *not* a GraphQL envelope. That distinction is the reason this gets asked at all. Client code written on the assumption "every response from the GraphQL endpoint parses as a GraphQL response" fails here in an unhelpful way: instead of surfacing "this operation may not use GET", it surfaces a JSON parse failure, or an `errors` array that is undefined, or a silent empty render. The useful answer in an interview names the status *and* names what the client actually has to handle, which is a non-GraphQL body behind a non-2xx status. The rules HTTP itself imposes on a 405 response — which headers it must carry, how it differs from other client-error statuses — are HTTP's business, not GraphQL's, and the GraphQL layer simply reuses the status. ## What the server must do before it can refuse The method is on the first line of the request, plainly visible to every hop. The operation type is not: it lives inside the document, which on a GET is a URL parameter. So the server's sequence is fixed — read the parameters, parse the document, select the operation (using `operationName` when the document defines more than one), read that selected operation's type, and only then decide whether to proceed. Two consequences fall out of that ordering, and both make good follow-up material. First, the check has to sit *before* execution rather than after it. A server that ran the resolvers and then noticed the operation was a mutation would have already done the write, which is exactly the outcome the restriction exists to prevent: every intermediary that treats a GET as repeatable — a prefetcher, a retrying client, a crawler following a link — could then trigger writes. Second, a document that *contains* a mutation definition is not automatically refused. What matters is the operation actually selected. A document defining both `SegmentMatches` and `ApproveSegment`, sent with `operationName=SegmentMatches`, is a query request and runs normally. ## The persisted-identifier wrinkle On a caching-oriented endpoint the GET usually carries a short identifier rather than document text. The rule does not change, but the timing shifts: the server cannot know the operation type until it has resolved that identifier back to a document. If the identifier is unknown, there is no operation type to judge at all, so the server answers the not-found condition rather than 405. Candidates who assume the identifier somehow pre-clears the check have the sequence backwards — the identifier is looked up first, and only the recovered document reveals whether a write was requested. ## Why you rarely see this in a healthy system A client library that supports GET normally decides per operation: it inspects the parsed operation type locally and sends anything that is not a query over POST, so the 405 is never provoked. When 405s do appear in production it usually means something bypassed that choice — a hand-built URL, a saved link, a smoke test, a bookmark. An invented example on a translation-memory graph makes the diagnosis concrete. An internal integration had a saved GET URL for `SegmentMatches`, and somebody edited the operation inside that URL from `segmentMatches` to `approveSegment` to "reuse the plumbing". Every call afterwards returned 405, the integration logged an unparseable body, and — the telling part — the server's GraphQL operation metrics showed nothing whatsoever, because no operation was ever executed. Traffic that is visible at the HTTP layer and absent from operation metrics is the fingerprint of a request refused before execution. ## Traps Saying the response is a 200 with an `errors` entry is the common wrong answer; so is describing it as a validation error, when the document may be perfectly valid against the schema and the objection is purely about the method carrying it. The other trap is arguing that a server may simply choose to allow it. A server *can* be configured that way, but then any layer that treats GET as safe can cause a write, and the working draft's answer to that request is a refusal.
- How does the server know it is a mutation, given the method is visible but the operation type is not?It has to read the parameters, parse the document, and select the operation — using `operationName` if the document defines more than one — before the operation's type is known. Only then can it refuse. That whole sequence sits before execution, which is the point: checking afterwards would mean the write had already happened, which is exactly what restricting the method prevents.
- A document defines both a query and a mutation and is sent over GET. Is it automatically refused?No. What matters is the operation actually selected for execution. If `operationName` names the query, that is a query request and it runs normally; the unselected mutation definition is just text in the document. It is refused only when the selected operation is the mutation, or when an ambiguous selection resolves to one.
- Why do healthy production systems almost never see this status?Because a client that supports GET decides per operation: it inspects the parsed operation type locally and sends anything that is not a query over POST, so the condition never arises. A 405 in the logs usually means something bypassed that choice — a hand-built URL, a saved link, a bookmark or a smoke test — and it will show up in HTTP metrics while being completely absent from operation metrics.
saying these in an interview costs you the question
- Expects HTTP 200 with an errors array
- Assumes the body is a parseable GraphQL envelope
- Calls it a document validation error
- Says the server may just execute it if configured to
- Thinks a persisted identifier skips the operation-type check
- Believes the check happens after the resolvers run