Your GraphQL server switches every response to application/graphql-response+json. What breaks, and how does Accept let you stage it?
answer
- Only one class of response moves
- Client bugs were the invisible class
- The status line has many readers
- Let the caller ask for it
- No Accept header means the legacy type
basics
~20 sFailures that used to answer 200 start answering 4xx, so client and infrastructure code keyed on the status takes a different branch. The GraphQL over HTTP draft makes the choice Accept-driven, so a server answers the newer type only to clients that asked.
solid answer
~50 sFlipping unconditionally is a breaking change delivered to clients you cannot redeploy. Documents rejected before execution move from 200 to 4xx, and every layer that reads the status takes its failure branch: generic client network handlers surface "request failed" instead of the message in the body, retry policies change behaviour, a CDN or reverse proxy may substitute its own error page, and alerting lights up on requests that were already broken but invisible. The draft's answer is negotiation. The client states what it can handle in `Accept`; a server supports both types and answers the legacy `application/json` to anything that did not ask for the newer one, including requests with no `Accept` header, which the draft says to treat as the legacy type. If a request's `Accept` names nothing the server can produce, the answer is 406.
code
pseudocode · 20 lineson request:
accepted = parse_accept(request.headers["Accept"])
if accepted is empty:
respond_as = "application/json" # draft: absent Accept = legacy
else if accepted allows "application/graphql-response+json":
respond_as = "application/graphql-response+json"
else if accepted allows "application/json":
respond_as = "application/json"
else:
return 406 Not Acceptable
result = run_graphql(request)
if respond_as == "application/json":
return 200 with result
else if result has a data entry:
return 200 with result
else:
return 400 with result # or 5xx when the server was at faultgo deeper
Know that a client says which response media type it can handle and the server answers accordingly, so the newer status behaviour is opt-in rather than something a server imposes on everyone at once.
Be able to explain which responses actually change — only the ones where execution never began — and that a request with no Accept header is treated as asking for the legacy type.
Demonstrate that you would stage this. Name the readers of the status line beyond the client, pair the Accept change with the client's error handling in one release, and rebaseline status-derived alerts before the first build ships.
Own whether the migration is worth its risk at all. Weigh who reads your status codes — third-party callers, an edge layer you do not control, a limiter shedding client-fault traffic — against a client release and an alerting rebaseline for every consumer you have.
## What actually changes on the wire Only one class of response moves: the ones where execution never began. Under `application/json` a document that failed to parse, failed validation, or carried a variable that could not be coerced came back `200` with the failure in the body. Under `application/graphql-response+json` the same response, unchanged in content, arrives with a 4xx status. Resolver failures do not move — they were 200 before and stay 200. That sounds narrow. It is not, because the class that moved is exactly the class caused by client bugs, and client bugs are what a shipped fleet has. ## The concrete break A fleet telematics platform runs depot tablets on a long release cycle. The schema replaced a coarse `VehicleStatus` enum with finer-grained values, and one tablet build still sends the retired value in a variable — an enum value nobody handled on either side. Today that request answers 200 with a coercion failure in the body, the app's GraphQL layer reads it, and the screen shows "this filter is no longer available". The day the server flips media types, the same request answers 400. The app's HTTP layer sits underneath its GraphQL layer and short-circuits on non-2xx, so the screen now shows a generic connectivity error, support tickets say "the app is offline in the depot", and the actual message never reaches anyone. Nothing about the failure changed; only the layer that reports it did. ## Everything else that reads the status The client is only the most visible casualty. Anything between the two ends is keyed on the status line: * **Retry and circuit-breaking.** Policies that retried nothing at 200 may now retry a request that can never succeed, or may open a breaker on a burst of client-fault 4xx responses. * **Edge infrastructure.** A CDN or reverse proxy may cache, log or transform non-2xx responses differently — in the worst case replacing the body with its own error document, which destroys the failure detail the client needed. * **Alerting and SLOs.** An error-rate objective computed from statuses jumps the moment the flip lands, not because reliability changed but because previously invisible failures became countable. That is an improvement, and it will still page someone at 3am if nobody was warned. * **Server-side logging.** Log pipelines that sample or route on status suddenly reclassify a whole traffic class. ## Why Accept is the mechanism, not a version header The draft deliberately makes this a content-negotiation problem rather than a versioning one, because the response format is genuinely the same document under two names with two status regimes. The client advertises what it can handle in `Accept`; the server answers with a `Content-Type` naming what it produced. Three rules matter for a rollout: 1. A client that wants the status-carrying behaviour must ask for `application/graphql-response+json` in `Accept`. Old builds do not, so they keep the legacy behaviour untouched. 2. A request with **no** `Accept` header is treated as if it asked for `application/json`. The default is the compatible one, which is what makes an incremental rollout safe by construction. 3. If a request's `Accept` names nothing the server can produce, the server answers **406 Not Acceptable** rather than guessing. That third rule is the one that bites during a rollout in the other direction: a server that decided to support *only* the newer type now answers 406 to every legacy client — a failure mode that looks nothing like a media-type problem from the client side, since the request never gets a GraphQL response at all. ## How to stage it * **Support both types indefinitely.** There is no cost to the server in doing so; the response document is identical and only the status differs. * **Move the ask into the client, not the server.** The new build sends the new `Accept` value and its network layer is updated in the same change to read a GraphQL response body on 4xx instead of short-circuiting. That pairing is the whole migration, and it ships as one client release. * **Instrument the split.** Count requests by the `Accept` value they sent, so you can see what share of live traffic is still on the legacy behaviour and whether an old fleet is actually retiring. * **Warn the infrastructure owners first.** Rebaseline error-rate alerts before the first client build ships, not after the first page. * **Check the path, not just the endpoint.** Verify that whatever sits in front of the service passes a 4xx body through untouched; if it substitutes an error page, the newer media type is strictly worse for your clients than the legacy one. ## The judgement call underneath A candidate should be willing to say when this migration is not worth doing. The benefit is real but bounded: one class of failure becomes visible to status-keyed infrastructure. If your clients already read every body and your observability is server-side, you are spending a client release and a rollout risk to make dashboards marginally more honest. The case is much stronger when third parties call the endpoint, when an edge layer you do not own is making decisions on status, or when a rate limiter needs to shed client-fault traffic cheaply.
- A client build sends the newer Accept value but its network layer is unchanged. What do users see?Generic failures where they used to see specific ones. The layer short-circuits on the 4xx and never hands the body to the GraphQL layer, so a coercion or validation failure that used to render a precise message now renders a connectivity error. That is why the Accept change and the error-handling change have to ship in the same client release rather than being staged separately.
- A server drops support for application/json entirely. What happens to a legacy client that asks only for it?It gets 406 Not Acceptable and no GraphQL response at all — every operation fails identically, including ones that would have worked, and the failure looks like a broken endpoint rather than a media-type disagreement. Supporting both types costs the server nothing, since the response document is the same and only the status rule differs, so there is rarely a reason to drop the legacy one.
- How would you measure whether the old client fleet has actually retired?Instrument requests by the `Accept` value they carried and, where you can, by client build. That gives a direct share of live traffic still on the legacy behaviour rather than an inference from release adoption. Only when that share reaches zero for a sustained period is dropping the legacy type even a question worth asking.
saying these in an interview costs you the question
- Flips the media type server-wide as a config change
- Thinks Accept negotiation needs a custom version header
- Forgets a missing Accept header defaults to the legacy type
- Ignores proxies and caches that read the status line
- Ships the new Accept value without changing client error handling