graphql / graphql/graphql-over-http

294 and unregistered status codes: two observable consequences in deployed infrastructure

Closed Beginner friendly
#435 10 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
427
Forks
70
Avg merge
2d 4h
Merged PRs (30d)
9

Description

**tl;dr** — `294` is not in the [IANA registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml). All four proxies measured forward it and its body intact, and it is *less* likely to be cached than `200`, not more. The residual risks:

1. nginx's `add_header` only fires for an allowlist of status codes unless `always` is set, so CORS and HSTS headers silently vanish on a `294`.
2. Azure Front Door publishes no status-code list; with caching enabled and no `Cache-Control`, it assigns a random 1–3 day TTL.
3. Whatever else follows from the code being unregistered.

The caching argument under "Issue 2" below overstates the risk and is [corrected in this comment](https://github.com/graphql/graphql-over-http/issues/435#issuecomment-5636237118). Registering `294` with IANA is now on the [working group agenda](https://github.com/graphql/graphql-over-http/pull/436).

---

## Summary

The spec recommends status code `294` for partial success, and the [Partial success](https://graphql.github.io/graphql-over-http/draft/#sec-Partial-success) note explains why an existing code was not suitable. What neither section mentions is that `294` is not in the [IANA HTTP Status Code Registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml), and that this has observable consequences in deployed infrastructure beyond "clients should ignore the status code".

I measured four widely deployed proxies and read the documentation of three CDNs. Every one of them forwards `294` and its body intact, so the core recommendation holds. But two classes of behaviour change silently, and I think they are worth a short non-normative note so implementers and operators are not surprised.

## What was measured

An origin returning `294` with an `application/graphql-response+json` body, behind each proxy in turn, requested with `curl`.

| Proxy | Status forwarded | Reason phrase | Body |
| --- | --- | --- | --- |
| nginx 1.31.5 | `294` | preserved | intact |
| HAProxy | `294` | preserved | intact |
| Caddy | `294` | rewritten to "status code 294" | intact |
| Envoy 1.31 | `294` | rewritten to "Unknown" | intact |

Reason-phrase rewriting is harmless, since reason phrases are advisory in HTTP/1.1 and absent in HTTP/2.

## Issue 1: status-code allowlists silently drop behaviour

nginx's `add_header` directive applies only to a fixed allowlist of status codes — `200`, `201`, `204`, `206`, `301`, `302`, `303`, `304`, `307`, `308` — unless the `always` flag is given.

The effect is that headers an operator has configured this way are **not emitted on a `294` response**. I confirmed this directly: a plain `add_header` was absent from the `294` response, while the same header with `always` was present.

This matters because CORS headers are very commonly added this way. An operator who adds `Access-Control-Allow-Origin` via `add_header` will find it present on `200` responses and missing on `294` responses, which breaks browser clients on exactly the responses that carry errors. The same pattern applies to HSTS and other security headers.

This is not a defect in the spec, and it is not nginx behaving incorrectly. It is a predictable consequence of choosing a code outside the registry, and it is the kind of thing a note could save each implementer from rediscovering.

## Issue 2: caching behaviour for an unregistered 2xx is divergent

`294` is not heuristically cacheable under [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111), so by default it is not stored. But explicit configuration overrides that, and the defaults vary:

- **nginx** with `proxy_cache_valid any 1m` caches the `294` and serves it from cache. Measured: cache hits on subsequent requests, one entry on disk.
- **Cloudflare** documents caching only `200`, `206`, `301`, `302`, `303`, `404`, and `410` by default, and states that "all other status codes are not cached by default". `294` is therefore not cached.
- **Azure Front Door** publishes no status-code list. With caching enabled and no `Cache-Control` on the origin response, it [assigns a random TTL between one and three days](https://learn.microsoft.com/en-us/azure/frontdoor/front-door-caching).
- **CloudFront** documents its `3xx`, `4xx`, and `5xx` caching behaviour but publishes no authoritative list of cacheable `2xx` codes.

A cached partial-success response is a more interesting object than a cached `200`, because it carries an `errors` entry and possibly request-specific partial data. An operator who reasons "GraphQL responses are not cacheable, so I need not think about this" may be wrong in the presence of a catch-all cache rule.

## Suggestion

Both of these belong in the existing [Partial success](https://graphql.github.io/graphql-over-http/draft/#sec-Partial-success) note rather than in normative text — the `SHOULD` in [Status Codes](https://graphql.github.io/graphql-over-http/draft/#sec-Status-Codes) seems right as it stands. Something to the effect of:

> Note: `294` is not registered with IANA. Conforming intermediaries will forward it, since [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110) requires an unrecognized status code to be treated as the `x00` code of its class. However, infrastructure that acts on a fixed list of status codes rather than on the status class may behave differently than it does for `200` — for example by omitting headers it would otherwise add, or by applying different cache rules. Servers emitting `294` should verify that response headers and caching behave as intended through their own infrastructure.

Happy to open a PR with wording along those lines if that would be useful.

## Context

Found while auditing an existing GraphQL server implementation against the current draft, ahead of adopting `294`. Raising it here because every implementation that adopts the recommendation will meet the same two behaviours, and the measurements were cheap to take once rather than repeatedly.

_Drafted by Claude (Anthropic AI assistant)._

Contributor guide

No contributing guide indexed for this repository

Research direction

Read the existing Partial success note and the Status Codes section in the GraphQL over HTTP draft, then compare them with the IANA registry and the measured proxy and CDN behavior described here. Done means adding a concise non-normative warning about unregistered 294 responses, fixed status-code allowlists, and differing cache behavior, without changing the normative SHOULD.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, nginx
Domain
cloud, documentation, infrastructure
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.