From 8c4dc7f29c50874e0125b5f167d182bac875ee81 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 8 Oct 2026 17:48:48 +0800 Subject: [PATCH] fix(node): send Node requests through the shared Fetch transport NodeAdapter overrode request() with (method, url) while the inherited get() calls request(path, 'GET', ...), so every GET under Node went to '//GET' with the path as its method. It also read the constructor-time config, so headers set later with setHeaders (Customer-Token, Accept-Language) and host changes were dropped. NodeAdapter now only adds its User-Agent and inherits the browser transport. --- src/adapters/node.ts | 29 ++++++----------------------- tests/core.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/adapters/node.ts b/src/adapters/node.ts index 4dc7796d..1cf1f35c 100644 --- a/src/adapters/node.ts +++ b/src/adapters/node.ts @@ -1,7 +1,11 @@ import BrowserAdapter from './browser.js'; -import { requestWithFetch } from './fetch.js'; -import type { AdapterConfig, RequestOptions } from '../types.js'; +import type { AdapterConfig } from '../types.js'; +/** + * The Fetch transport for Node.js. It is the browser transport plus a User-Agent, so + * every verb, the current host and namespace, and headers set later with `setHeaders` + * work the same way in both. + */ export default class NodeAdapter extends BrowserAdapter { constructor(config: AdapterConfig = {}) { const headers = new Headers(config.headers); @@ -10,25 +14,4 @@ export default class NodeAdapter extends BrowserAdapter { } super({ ...config, headers }); } - - override request(method: string, url: string, options: RequestOptions & { data?: unknown } = {}): Promise { - const { data, ...requestOptions } = options; - return requestWithFetch(this.fetchConfig, { path: url, method, data, options: requestOptions }); - } - - override post(path: string, data: unknown = {}, options: RequestOptions = {}): Promise { - return this.request('POST', path, { ...options, data }); - } - - override put(path: string, data: unknown = {}, options: RequestOptions = {}): Promise { - return this.request('PUT', path, { ...options, data }); - } - - override patch(path: string, data: unknown = {}, options: RequestOptions = {}): Promise { - return this.request('PATCH', path, { ...options, data }); - } - - override delete(path: string, options: RequestOptions = {}, legacyOptions?: RequestOptions): Promise { - return this.request('DELETE', path, legacyOptions ?? options); - } } diff --git a/tests/core.test.ts b/tests/core.test.ts index c2c85611..5299b431 100644 --- a/tests/core.test.ts +++ b/tests/core.test.ts @@ -863,6 +863,33 @@ describe('edge and failure contracts', () => { expect(new Headers(presetNode.headers).get('User-Agent')).toBe('custom'); }); + it('sends Node requests to the right URL with the method, body and current headers', async () => { + const fetchMock = vi.fn(() => Promise.resolve(new Response('{"id":"order_1"}', { status: 200 }))); + const node = new NodeAdapter({ host: 'https://api.test', namespace: 'v1', publicKey: 'pk', fetch: fetchMock }); + node.setHeaders({ 'Customer-Token': 'token_1' }); + + await expect(node.get('orders', { status: 'active' })).resolves.toEqual({ id: 'order_1' }); + await node.post('orders', { id: 1 }); + await node.delete('orders/1', {}, { headers: { 'X-Request': 'yes' } }); + + const [getUrl, getInit] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + expect(getUrl).toBe('https://api.test/v1/orders?status=active'); + expect(getInit.method).toBe('GET'); + const getHeaders = new Headers(getInit.headers); + expect(getHeaders.get('Authorization')).toBe('Bearer pk'); + expect(getHeaders.get('Customer-Token')).toBe('token_1'); + expect(getHeaders.get('User-Agent')).toBe('@fleetbase/sdk;node'); + + const [postUrl, postInit] = fetchMock.mock.calls[1] as unknown as [string, RequestInit]; + expect(postUrl).toBe('https://api.test/v1/orders'); + expect(postInit).toMatchObject({ method: 'POST', body: '{"id":1}' }); + + const [deleteUrl, deleteInit] = fetchMock.mock.calls[2] as unknown as [string, RequestInit]; + expect(deleteUrl).toBe('https://api.test/v1/orders/1'); + expect(deleteInit.method).toBe('DELETE'); + expect(new Headers(deleteInit.headers).get('X-Request')).toBe('yes'); + }); + it('covers UUID fallback and sparse address data', () => { const originalCrypto = globalThis.crypto; vi.stubGlobal('crypto', {});