urql core client with MSW: auth headers, document cache and errors: working versions (tested 2026-10-15)
Test an @urql/core client offline against MSW graphql handlers: auth via fetchOptions, cache-first hits vs network-only, mutation invalidation in the document cache, and CombinedError for GraphQL and HTTP failures.
passed5 passed · 0 failed2026-10-15 20:10 UTC- Status
- passed
- Last passed
- 2026-10-15 20:10 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("urql-core-msw-cache-exchange") (install), or see them on GitHub.
{
"name": "urql-core-msw-cache-exchange",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"@urql/core": "6.0.3"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15",
"msw": "3.0.3",
"graphql": "17.0.2"
}
}
import { Client, cacheExchange, fetchExchange, gql, mapExchange, type CombinedError } from "@urql/core";
export const CoursesQuery = gql`
query Courses { courses { __typename id title } }
`;
export const RenameMutation = gql`
mutation Rename($id: ID!, $title: String!) { renameCourse(id: $id, title: $title) { __typename id title } }
`;
export function makeClient(url: string, getToken: () => string, onError: (e: CombinedError) => void = () => {}) {
return new Client({
url,
preferGetMethod: false,
exchanges: [mapExchange({ onError }), cacheExchange, fetchExchange],
fetchOptions: () => ({ headers: { authorization: `Bearer ${getToken()}` } }),
});
}
import { test, before, after, afterEach } from "node:test";
import assert from "node:assert/strict";
import { HttpResponse } from "msw";
import { graphql } from "msw/graphql"; // msw 3 moved graphql handlers to a subpath
import { setupServer } from "msw/node";
import type { CombinedError } from "@urql/core";
import { makeClient, CoursesQuery, RenameMutation } from "../src/client.ts";
const URL = "https://api.example.com/graphql";
const api = graphql.link(URL);
let hits = 0;
let lastAuth: string | null = null;
const courses = [{ __typename: "Course", id: "c1", title: "Rigging" }];
const server = setupServer(
api.query("Courses", ({ request }) => {
hits++;
lastAuth = request.headers.get("authorization");
return HttpResponse.json({ data: { courses } });
}),
api.mutation("Rename", ({ variables }) =>
HttpResponse.json({ data: { renameCourse: { __typename: "Course", id: variables.id, title: variables.title } } }),
),
);
before(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); hits = 0; });
after(() => server.close());
test("query sends auth header and returns data", async () => {
const client = makeClient(URL, () => "tok-1");
const r = await client.query(CoursesQuery, {}).toPromise();
assert.equal(r.error, undefined);
assert.deepEqual(r.data.courses, courses);
assert.equal(lastAuth, "Bearer tok-1");
});
test("cache-first serves the second query from cache, network-only refetches", async () => {
const client = makeClient(URL, () => "t");
await client.query(CoursesQuery, {}).toPromise();
const cached = await client.query(CoursesQuery, {}).toPromise();
assert.equal(hits, 1);
assert.equal(cached.operation.context.meta?.cacheOutcome, "hit");
await client.query(CoursesQuery, {}, { requestPolicy: "network-only" }).toPromise();
assert.equal(hits, 2);
});
test("mutation on a typename invalidates cached queries (document cache)", async () => {
const client = makeClient(URL, () => "t");
await client.query(CoursesQuery, {}).toPromise();
const m = await client.mutation(RenameMutation, { id: "c1", title: "Rigging II" }).toPromise();
assert.equal(m.data.renameCourse.title, "Rigging II");
await client.query(CoursesQuery, {}).toPromise();
assert.equal(hits, 2);
});
test("GraphQL errors become CombinedError and hit the error hook", async () => {
server.use(api.query("Courses", () => HttpResponse.json({ errors: [{ message: "Not authorized" }] })));
const seen: CombinedError[] = [];
const r = await makeClient(URL, () => "bad", (e) => seen.push(e)).query(CoursesQuery, {}).toPromise();
assert.equal(r.error?.graphQLErrors[0].message, "Not authorized");
assert.equal(r.error?.message, "[GraphQL] Not authorized");
assert.equal(seen.length, 1);
});
test("HTTP failures surface as networkError", async () => {
server.use(api.query("Courses", () => new HttpResponse("upstream down", { status: 502 })));
const r = await makeClient(URL, () => "t").query(CoursesQuery, {}).toPromise();
assert.ok(r.error?.networkError);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
graphql-request client with typed operations, mocked by MSW graphql handlers
Call a GraphQL API with graphql-request (variables, headers, error handling) and test offline with MSW's graphql.link(url).query/mutation handlers (msw 3 API). graphql stays on 16.x: graphql-request 7 peers on graphql 14 - 16.
Apollo Server 5 resolvers with per-request DataLoader batching
An Apollo Server 5 schema whose nested resolvers use per-request DataLoader instances to batch and dedupe lookups (no N+1), with GraphQLError extension codes and cache invalidation after a mutation, tested in-process with executeOperation.
GraphQL schema mocking with @graphql-tools/schema and @graphql-tools/mock
Build an executable schema with makeExecutableSchema and layer deterministic mocks, a MockStore, MockList and partial real resolvers on top with addMocksToSchema, including mutations that write to the store.
GraphQL Yoga 5 schema, context and errors tested via fetch
Build a GraphQL Yoga server with SDL resolvers, per-request context auth and GraphQLError codes, tested in-process with yoga.fetch.