Skip to content

Commit b4b51c2

Browse files
authored
fix(remix): Add nativeFetch support for accessing request headers (#12479)
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.
1 parent a98ad0f commit b4b51c2

File tree

2 files changed

+133
-2
lines changed

2 files changed

+133
-2
lines changed

packages/remix/src/utils/web-fetch.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,7 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
150150
query: parsedURL.query,
151151
href: parsedURL.href,
152152
method: request.method,
153-
// @ts-expect-error - not sure what this supposed to do
154-
headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
153+
headers: objectFromHeaders(headers),
155154
insecureHTTPParser: request.insecureHTTPParser,
156155
agent,
157156

@@ -164,3 +163,35 @@ export const normalizeRemixRequest = (request: RemixRequest): Record<string, any
164163

165164
return requestOptions;
166165
};
166+
167+
// This function is a `polyfill` for Object.fromEntries()
168+
function objectFromHeaders(headers: Headers): Record<string, string> {
169+
const result: Record<string, string> = {};
170+
let iterator: IterableIterator<[string, string]>;
171+
172+
if (hasIterator(headers)) {
173+
iterator = getIterator(headers) as IterableIterator<[string, string]>;
174+
} else {
175+
return {};
176+
}
177+
178+
for (const [key, value] of iterator) {
179+
result[key] = value;
180+
}
181+
return result;
182+
}
183+
184+
type IterableType<T> = {
185+
[Symbol.iterator]: () => Iterator<T>;
186+
};
187+
188+
function hasIterator<T>(obj: T): obj is T & IterableType<unknown> {
189+
return obj !== null && typeof (obj as IterableType<unknown>)[Symbol.iterator] === 'function';
190+
}
191+
192+
function getIterator<T>(obj: T): Iterator<unknown> {
193+
if (hasIterator(obj)) {
194+
return (obj as IterableType<unknown>)[Symbol.iterator]();
195+
}
196+
throw new Error('Object does not have an iterator');
197+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { RemixRequest } from '../../src/utils/vendor/types';
2+
import { normalizeRemixRequest } from '../../src/utils/web-fetch';
3+
4+
class Headers {
5+
private _headers: Record<string, string> = {};
6+
7+
constructor(headers?: Iterable<[string, string]>) {
8+
if (headers) {
9+
for (const [key, value] of headers) {
10+
this.set(key, value);
11+
}
12+
}
13+
}
14+
static fromEntries(entries: Iterable<[string, string]>): Headers {
15+
return new Headers(entries);
16+
}
17+
entries(): IterableIterator<[string, string]> {
18+
return Object.entries(this._headers)[Symbol.iterator]();
19+
}
20+
21+
[Symbol.iterator](): IterableIterator<[string, string]> {
22+
return this.entries();
23+
}
24+
25+
get(key: string): string | null {
26+
return this._headers[key] ?? null;
27+
}
28+
29+
has(key: string): boolean {
30+
return this._headers[key] !== undefined;
31+
}
32+
33+
set(key: string, value: string): void {
34+
this._headers[key] = value;
35+
}
36+
}
37+
38+
class Request {
39+
private _url: string;
40+
private _options: { method: string; body?: any; headers: Headers };
41+
42+
constructor(url: string, options: { method: string; body?: any; headers: Headers }) {
43+
this._url = url;
44+
this._options = options;
45+
}
46+
47+
get method() {
48+
return this._options.method;
49+
}
50+
51+
get url() {
52+
return this._url;
53+
}
54+
55+
get headers() {
56+
return this._options.headers;
57+
}
58+
59+
get body() {
60+
return this._options.body;
61+
}
62+
}
63+
64+
describe('normalizeRemixRequest', () => {
65+
it('should normalize remix web-fetch request', () => {
66+
const headers = new Headers();
67+
headers.set('Accept', 'text/html,application/json');
68+
headers.set('Cookie', 'name=value');
69+
const request = new Request('https://example.com/api/json?id=123', {
70+
method: 'GET',
71+
headers: headers as any,
72+
});
73+
74+
const expected = {
75+
agent: undefined,
76+
hash: '',
77+
headers: {
78+
Accept: 'text/html,application/json',
79+
Connection: 'close',
80+
Cookie: 'name=value',
81+
'User-Agent': 'node-fetch',
82+
},
83+
hostname: 'example.com',
84+
href: 'https://example.com/api/json?id=123',
85+
insecureHTTPParser: undefined,
86+
ip: null,
87+
method: 'GET',
88+
originalUrl: 'https://example.com/api/json?id=123',
89+
path: '/api/json?id=123',
90+
pathname: '/api/json',
91+
port: '',
92+
protocol: 'https:',
93+
query: undefined,
94+
search: '?id=123',
95+
};
96+
97+
const normalizedRequest = normalizeRemixRequest(request as unknown as RemixRequest);
98+
expect(normalizedRequest).toEqual(expected);
99+
});
100+
});

0 commit comments

Comments
 (0)