From fd6edde077cd54e1f132278e645cf59baa768b3d Mon Sep 17 00:00:00 2001 From: Mark Faga Date: Wed, 24 Jun 2026 16:33:29 -0400 Subject: [PATCH] feat: surface HTTP status code in onNext metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onNext already receives a metadata arg ({ responseHeaders } on the fetch transport), but not the HTTP status. A non-2xx JSON response (e.g. a 401) parses successfully, so it is delivered through onNext — not onError — with the status discarded. Consumers therefore cannot distinguish a 401 from a normal GraphQL response and cannot react to auth failures. Add `status` to the onNext metadata on both transports: - fetch.js: response.status (multipart onResponse + json branches) - xhr.js: xhr.status (multipart onResponse + json branch) - typings.d.ts: type the previously-untyped onNext metadata arg ({ responseHeaders?: Headers; status?: number }). --- src/fetch.js | 4 ++-- src/xhr.js | 4 ++-- typings.d.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fetch.js b/src/fetch.js index 98a96db..9224875 100644 --- a/src/fetch.js +++ b/src/fetch.js @@ -13,7 +13,7 @@ export function fetchImpl(url, { onNext, onComplete, onError, ...fetchOptions }) const reader = response.body.getReader(); const textDecoder = new TextDecoder(); const patchResolver = new PatchResolver({ - onResponse: (r) => onNext(r, { responseHeaders: response.headers }), + onResponse: (r) => onNext(r, { responseHeaders: response.headers, status: response.status }), boundary, }); return reader.read().then(function sendNext({ value, done }) { @@ -37,7 +37,7 @@ export function fetchImpl(url, { onNext, onComplete, onError, ...fetchOptions }) }); } else { return response.json().then((json) => { - onNext([json], { responseHeaders: response.headers }); + onNext([json], { responseHeaders: response.headers, status: response.status }); onComplete(); }, (err) => { const parseError = err; diff --git a/src/xhr.js b/src/xhr.js index 90c6e58..edc5573 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -31,7 +31,7 @@ export function xhrImpl(url, { method, headers, credentials, body, onNext, onErr if (contentType.indexOf('multipart/mixed') >= 0) { isDeferred = true; boundary = getBoundary(contentType); - patchResolver = new PatchResolver({ onResponse: (r) => onNext(r, {}), boundary }); + patchResolver = new PatchResolver({ onResponse: (r) => onNext(r, { status: xhr.status }), boundary }); } } else if ( (this.readyState === this.LOADING || this.readyState === this.DONE) && @@ -41,7 +41,7 @@ export function xhrImpl(url, { method, headers, credentials, body, onNext, onErr patchResolver.handleChunk(chunk); index = xhr.responseText.length; } else if (this.readyState === this.DONE && !isDeferred) { - onNext([JSON.parse(xhr.response)], {}); + onNext([JSON.parse(xhr.response)], { status: xhr.status }); onComplete(); } } diff --git a/typings.d.ts b/typings.d.ts index c5caa3e..da448ba 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -12,7 +12,7 @@ declare function MultipartFetchFunction(url: string, params: { headers?: Record; credentials?: string; body?: string; - onNext: (result: T[]) => void; + onNext: (result: T[], meta?: { responseHeaders?: Headers; status?: number }) => void; onError: (error: unknown) => void; onComplete : () => void }): void