/
web-fetch.ts
122 lines (102 loc) · 3.78 KB
/
web-fetch.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Based on Remix's implementation of Fetch API
// https://github.com/remix-run/web-std-io/tree/main/packages/fetch
import { RemixRequest } from './types';
/*
* Symbol extractor utility to be able to access internal fields of Remix requests.
*/
const getInternalSymbols = (
request: Record<string, unknown>,
): {
bodyInternalsSymbol: string;
requestInternalsSymbol: string;
} => {
const symbols = Object.getOwnPropertySymbols(request);
return {
bodyInternalsSymbol: symbols.find(symbol => symbol.toString().includes('Body internals')) as any,
requestInternalsSymbol: symbols.find(symbol => symbol.toString().includes('Request internals')) as any,
};
};
/**
* Vendored from:
* https://github.com/remix-run/web-std-io/blob/f715b354c8c5b8edc550c5442dec5712705e25e7/packages/fetch/src/utils/get-search.js#L5
*/
export const getSearch = (parsedURL: URL): string => {
if (parsedURL.search) {
return parsedURL.search;
}
const lastOffset = parsedURL.href.length - 1;
const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : '');
return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : '';
};
/**
* Convert a Request to Node.js http request options.
* The options object to be passed to http.request
* Vendored / modified from:
* https://github.com/remix-run/web-std-io/blob/f715b354c8c5b8edc550c5442dec5712705e25e7/packages/fetch/src/request.js#L259
*/
export const normalizeRemixRequest = (request: RemixRequest): Record<string, any> => {
const { requestInternalsSymbol, bodyInternalsSymbol } = getInternalSymbols(request);
if (!requestInternalsSymbol) {
throw new Error('Could not find request internals symbol');
}
const { parsedURL } = request[requestInternalsSymbol];
const headers = new Headers(request[requestInternalsSymbol].headers);
// Fetch step 1.3
if (!headers.has('Accept')) {
headers.set('Accept', '*/*');
}
// HTTP-network-or-cache fetch steps 2.4-2.7
let contentLengthValue = null;
if (request.body === null && /^(post|put)$/i.test(request.method)) {
contentLengthValue = '0';
}
if (request.body !== null) {
const totalBytes = request[bodyInternalsSymbol].size;
// Set Content-Length if totalBytes is a number (that is not NaN)
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
contentLengthValue = String(totalBytes);
}
}
if (contentLengthValue) {
headers.set('Content-Length', contentLengthValue);
}
// HTTP-network-or-cache fetch step 2.11
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'node-fetch');
}
// HTTP-network-or-cache fetch step 2.15
if (request.compress && !headers.has('Accept-Encoding')) {
headers.set('Accept-Encoding', 'gzip,deflate,br');
}
let { agent } = request;
if (typeof agent === 'function') {
agent = agent(parsedURL);
}
if (!headers.has('Connection') && !agent) {
headers.set('Connection', 'close');
}
// HTTP-network fetch step 4.2
// chunked encoding is handled by Node.js
const search = getSearch(parsedURL);
// Manually spread the URL object instead of spread syntax
const requestOptions = {
path: parsedURL.pathname + search,
pathname: parsedURL.pathname,
hostname: parsedURL.hostname,
protocol: parsedURL.protocol,
port: parsedURL.port,
hash: parsedURL.hash,
search: parsedURL.search,
// @ts-ignore - it does not has a query
query: parsedURL.query,
href: parsedURL.href,
method: request.method,
// @ts-ignore - not sure what this supposed to do
headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
insecureHTTPParser: request.insecureHTTPParser,
agent,
// [SENTRY] For compatibility with Sentry SDK RequestData parser, adding `originalUrl` property.
originalUrl: parsedURL.href,
};
return requestOptions;
};