diff --git a/src/layers/transit/source.js b/src/layers/transit/source.js index 185516102..6ccdb9e55 100644 --- a/src/layers/transit/source.js +++ b/src/layers/transit/source.js @@ -13,9 +13,23 @@ export function createTransitSource({ signal?.throwIfAborted(); if (typeof feedId !== 'string' || !feedId || feedId.length > 160) throw new TypeError('A transit feed identifier is required'); - return fetchImpl(`/api/transit/vehicles/${encodeURIComponent(feedId)}`, { - signal, - headers: { Accept: 'application/json' }, + return Promise.resolve( + fetchImpl(`/api/transit/vehicles/${encodeURIComponent(feedId)}`, { + signal, + headers: { Accept: 'application/json' }, + }), + ).then((response) => { + signal?.throwIfAborted(); + return { + ok: response.ok, + status: response.status, + headers: response.headers, + async json() { + const body = await response.json(); + signal?.throwIfAborted(); + return body; + }, + }; }); }, }; diff --git a/src/tooling/transitService.test.mjs b/src/tooling/transitService.test.mjs index 009054b93..c36af9aef 100644 --- a/src/tooling/transitService.test.mjs +++ b/src/tooling/transitService.test.mjs @@ -110,3 +110,32 @@ test('source retains history validation and never retries through another transp ); assert.equal(calls, 1); }); + +test('snapshot cancellation covers headers and body parsing', async () => { + for (const phase of ['headers', 'body']) { + const controller = new AbortController(); + const source = createTransitSource({ + fetchImpl: async () => { + if (phase === 'headers') controller.abort(); + return { + ok: true, + status: 200, + headers: new Headers(), + json: async () => { + controller.abort(); + return { vehicles: [] }; + }, + }; + }, + }); + await assert.rejects( + async () => { + const response = await source.requestSnapshot('mbta', { + signal: controller.signal, + }); + await response.json(); + }, + { name: 'AbortError' }, + ); + } +});