diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index ba34e4320973..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'); +} 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); + }); +});