From bfd49266c85f7ae49bd081e5bbfa81feea67db51 Mon Sep 17 00:00:00 2001 From: Michael Carter Date: Wed, 12 Jun 2024 12:42:05 -0500 Subject: [PATCH 1/3] fix: invalid headers access when using nativeFetch Remix v2.9+ now supports `undici` native fetch. `normalizeRemixRequest()` would throw an error when processing `headers`. This PR uses the standard `Object.fromEntries()` to convert `Headers` into an object. --- packages/remix/src/utils/web-fetch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index ba34e4320973..9425bc6ef4f3 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -150,8 +150,8 @@ export const normalizeRemixRequest = (request: RemixRequest): Record Date: Fri, 14 Jun 2024 08:20:23 -0500 Subject: [PATCH 2/3] Replace object.fromEntries() with version that works with older JS --- packages/remix/src/utils/web-fetch.ts | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 9425bc6ef4f3..8450a12eb05d 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -150,8 +150,7 @@ export const normalizeRemixRequest = (request: RemixRequest): Record { + const result: Record = {}; + let iterator: IterableIterator<[string, string]>; + + if (hasIterator(headers)) { + iterator = getIterator(headers) as IterableIterator<[string, string]>; + } else { + return {}; + } + + for (const [key, value] of iterator) { + result[key] = value; + } + return result; +} + +type IterableType = { + [Symbol.iterator]: () => Iterator; +}; + +function hasIterator(obj: T): obj is T & IterableType { + return obj !== null && typeof (obj as IterableType)[Symbol.iterator] === 'function'; +} + +function getIterator(obj: T): Iterator { + if (hasIterator(obj)) { + return (obj as IterableType)[Symbol.iterator](); + } + throw new Error('Object does not have an iterator'); +} From f5d0ec91fe177bd2fd38059d03d4c4f09520d292 Mon Sep 17 00:00:00 2001 From: kiliman Date: Fri, 14 Jun 2024 08:20:39 -0500 Subject: [PATCH 3/3] Add unit test for normalizeRemixRequest() --- .../test/utils/normalizeRemixRequest.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/remix/test/utils/normalizeRemixRequest.test.ts diff --git a/packages/remix/test/utils/normalizeRemixRequest.test.ts b/packages/remix/test/utils/normalizeRemixRequest.test.ts new file mode 100644 index 000000000000..b627a34e4f12 --- /dev/null +++ b/packages/remix/test/utils/normalizeRemixRequest.test.ts @@ -0,0 +1,100 @@ +import type { RemixRequest } from '../../src/utils/vendor/types'; +import { normalizeRemixRequest } from '../../src/utils/web-fetch'; + +class Headers { + private _headers: Record = {}; + + constructor(headers?: Iterable<[string, string]>) { + if (headers) { + for (const [key, value] of headers) { + this.set(key, value); + } + } + } + static fromEntries(entries: Iterable<[string, string]>): Headers { + return new Headers(entries); + } + entries(): IterableIterator<[string, string]> { + return Object.entries(this._headers)[Symbol.iterator](); + } + + [Symbol.iterator](): IterableIterator<[string, string]> { + return this.entries(); + } + + get(key: string): string | null { + return this._headers[key] ?? null; + } + + has(key: string): boolean { + return this._headers[key] !== undefined; + } + + set(key: string, value: string): void { + this._headers[key] = value; + } +} + +class Request { + private _url: string; + private _options: { method: string; body?: any; headers: Headers }; + + constructor(url: string, options: { method: string; body?: any; headers: Headers }) { + this._url = url; + this._options = options; + } + + get method() { + return this._options.method; + } + + get url() { + return this._url; + } + + get headers() { + return this._options.headers; + } + + get body() { + return this._options.body; + } +} + +describe('normalizeRemixRequest', () => { + it('should normalize remix web-fetch request', () => { + const headers = new Headers(); + headers.set('Accept', 'text/html,application/json'); + headers.set('Cookie', 'name=value'); + const request = new Request('https://example.com/api/json?id=123', { + method: 'GET', + headers: headers as any, + }); + + const expected = { + agent: undefined, + hash: '', + headers: { + Accept: 'text/html,application/json', + Connection: 'close', + Cookie: 'name=value', + 'User-Agent': 'node-fetch', + }, + hostname: 'example.com', + href: 'https://example.com/api/json?id=123', + insecureHTTPParser: undefined, + ip: null, + method: 'GET', + originalUrl: 'https://example.com/api/json?id=123', + path: '/api/json?id=123', + pathname: '/api/json', + port: '', + protocol: 'https:', + query: undefined, + search: '?id=123', + }; + + const normalizedRequest = normalizeRemixRequest(request as unknown as RemixRequest); + expect(normalizedRequest).toEqual(expected); + }); +});