HTTP 200. Resolver failed.
This interactive lab shows how transport-only telemetry can report perfect availability while GraphQL returns field errors, null subtrees, and partial data.
The model is not a hand-authored JSON fixture. A dependency-free Node.js service loads an executable GraphQL schema, runs the checked-in operation through the GraphQL reference implementation, compares four observation strategies, renders the real response envelope, and exposes correlated OTLP-shaped metrics, logs, and traces.
GraphQL handles field execution errors by adding them to the response errors entry and replacing data at the affected nullability boundary. Execution may continue and produce partial data. See the GraphQL specification sections on handling execution errors and response format.
That creates several independent notions of success:
- the HTTP exchange completed;
- the response contains a non-null top-level
dataentry; - the operation contains no execution errors;
- every field needed by the user was fulfilled.
The default scenario executes 20 real CheckoutPage queries. Eight inventory resolvers time out:
| Observer | Reported result | Missed failures |
|---|---|---|
| HTTP 2xx monitor | 100% success | 8 |
| Top-level data check | 100% data present | 8 |
| Errors-envelope monitor | 60% full success | 0 |
| Field-path telemetry | 94.3% fields fulfilled | 0 |
The user still receives viewer, cart, recommendations, and loyalty data. inventory is null and the envelope includes errors[0].path = ["storefront", "inventory"].
The lab has two response profiles:
- legacy JSON uses
application/jsonand HTTP 200 for field execution errors, matching common GraphQL server behavior; - draft partial uses
application/graphql-response+jsonand status 294 for partial success, following the current GraphQL-over-HTTP working draft.
Status 294 is still in the 2xx class. A dashboard that groups all 2xx responses as success remains blind under either profile. The draft is prerelease; the lab labels this option explicitly rather than presenting it as a finalized HTTP standard.
Transport metrics are necessary, but they cannot describe a GraphQL execution result by themselves. The lab emits:
graphql.observed.success, split by observation strategy;graphql.response.error.count, includinggraphql.error.pathanderror.type;graphql.field.fulfillment, measuring delivered fields against requested fields;- error logs containing operation, path, service, status, and missing-field count;
- GraphQL server spans named
query, with operation attributes and error status; - the transport status on the same span that records the execution failure.
This follows the developing OpenTelemetry GraphQL span conventions: use a low-cardinality operation-type span name, record the operation attributes separately, and set error status from execution truth.
Requirements:
- Node.js 24 or newer
- GNU Make
npm ci
make check
make runOpen http://127.0.0.1:3000.
Useful endpoints:
GET /api/simulate
GET /api/telemetry
GET /healthz
POST /graphql
Every UI control is also a query parameter:
/api/simulate?requests=30&failPercent=60&failureTarget=recommendations&transportProfile=draft-partial&resolverLatencyMs=420
The repository also exposes the actual GraphQL endpoint:
curl --request POST \
--header 'content-type: application/json' \
--data '{"query":"query { viewer { id name } storefront(session: \"demo\") { inventory { status } } }"}' \
'http://127.0.0.1:3000/graphql?failureTarget=inventory'docker compose up --buildThen open http://127.0.0.1:3000.
schema/storefront.graphql defines the failure boundaries:
type Query {
viewer: Viewer!
storefront(session: ID!): Storefront
}
type Storefront {
cart: Cart!
inventory: Inventory
recommendations: [Product!]!
loyalty: Loyalty
}An inventory error stops at its nullable field:
storefront.inventory → null
A recommendations error starts on a non-null field and bubbles farther:
storefront.recommendations → storefront → null
The independent non-null viewer root field survives, so the top-level data entry remains non-null in both cases.
browser controls
│
▼
Node.js scenario runner
│
├──▶ storefront.graphql ──┐
└──▶ checkout-page.graphql│
▼
GraphQL reference execution
│
real { data, errors } envelope
│
┌───────────────┴────────────────┐
▼ ▼
strategy comparison OTLP metrics / logs / spans
The browser does not implement resolver behavior or calculate GraphQL null propagation. It renders the server-side execution results.
- Set the failure target to recommendations. Its non-null contract causes the whole storefront branch to become null while viewer data survives.
- Switch to draft partial. Error responses become HTTP 294, but a 2xx-class monitor still reports 100% success.
- Raise resolver failure to 100%. Transport success remains green while full-operation success reaches zero.
- Compare operation success with field fulfillment. Partial data can retain substantial value without being a fully successful operation.
This is a deterministic educational simulation. It uses no credentials, makes no upstream requests, stores no data, and exports no telemetry.