ProvenPin Install

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
pinned versions5
@urql/core@6.0.3@types/node@26.6.5tsx@4.23.15msw@3.0.3graphql@17.0.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
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.

package.json
{
  "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"
  }
}
src/client.ts
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()}` } }),
  });
}
test/client.test.ts
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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.