UNPKG

12.9 kBJavaScriptView Raw
1/**
2 * Index.js
3 *
4 * a request API compatible with window.fetch
5 *
6 * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
7 */
8
9import http from 'node:http';
10import https from 'node:https';
11import zlib from 'node:zlib';
12import Stream, {PassThrough, pipeline as pump} from 'node:stream';
13import {Buffer} from 'node:buffer';
14
15import dataUriToBuffer from 'data-uri-to-buffer';
16
17import {writeToStream, clone} from './body.js';
18import Response from './response.js';
19import Headers, {fromRawHeaders} from './headers.js';
20import Request, {getNodeRequestOptions} from './request.js';
21import {FetchError} from './errors/fetch-error.js';
22import {AbortError} from './errors/abort-error.js';
23import {isRedirect} from './utils/is-redirect.js';
24import {FormData} from 'formdata-polyfill/esm.min.js';
25import {isDomainOrSubdomain, isSameProtocol} from './utils/is.js';
26import {parseReferrerPolicyFromHeader} from './utils/referrer.js';
27import {
28 Blob,
29 File,
30 fileFromSync,
31 fileFrom,
32 blobFromSync,
33 blobFrom
34} from 'fetch-blob/from.js';
35
36export {FormData, Headers, Request, Response, FetchError, AbortError, isRedirect};
37export {Blob, File, fileFromSync, fileFrom, blobFromSync, blobFrom};
38
39const supportedSchemas = new Set(['data:', 'http:', 'https:']);
40
41/**
42 * Fetch function
43 *
44 * @param {string | URL | import('./request').default} url - Absolute url or Request instance
45 * @param {*} [options_] - Fetch options
46 * @return {Promise<import('./response').default>}
47 */
48export default async function fetch(url, options_) {
49 return new Promise((resolve, reject) => {
50 // Build request object
51 const request = new Request(url, options_);
52 const {parsedURL, options} = getNodeRequestOptions(request);
53 if (!supportedSchemas.has(parsedURL.protocol)) {
54 throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`);
55 }
56
57 if (parsedURL.protocol === 'data:') {
58 const data = dataUriToBuffer(request.url);
59 const response = new Response(data, {headers: {'Content-Type': data.typeFull}});
60 resolve(response);
61 return;
62 }
63
64 // Wrap http.request into fetch
65 const send = (parsedURL.protocol === 'https:' ? https : http).request;
66 const {signal} = request;
67 let response = null;
68
69 const abort = () => {
70 const error = new AbortError('The operation was aborted.');
71 reject(error);
72 if (request.body && request.body instanceof Stream.Readable) {
73 request.body.destroy(error);
74 }
75
76 if (!response || !response.body) {
77 return;
78 }
79
80 response.body.emit('error', error);
81 };
82
83 if (signal && signal.aborted) {
84 abort();
85 return;
86 }
87
88 const abortAndFinalize = () => {
89 abort();
90 finalize();
91 };
92
93 // Send request
94 const request_ = send(parsedURL.toString(), options);
95
96 if (signal) {
97 signal.addEventListener('abort', abortAndFinalize);
98 }
99
100 const finalize = () => {
101 request_.abort();
102 if (signal) {
103 signal.removeEventListener('abort', abortAndFinalize);
104 }
105 };
106
107 request_.on('error', error => {
108 reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, 'system', error));
109 finalize();
110 });
111
112 fixResponseChunkedTransferBadEnding(request_, error => {
113 if (response && response.body) {
114 response.body.destroy(error);
115 }
116 });
117
118 /* c8 ignore next 18 */
119 if (process.version < 'v14') {
120 // Before Node.js 14, pipeline() does not fully support async iterators and does not always
121 // properly handle when the socket close/end events are out of order.
122 request_.on('socket', s => {
123 let endedWithEventsCount;
124 s.prependListener('end', () => {
125 endedWithEventsCount = s._eventsCount;
126 });
127 s.prependListener('close', hadError => {
128 // if end happened before close but the socket didn't emit an error, do it now
129 if (response && endedWithEventsCount < s._eventsCount && !hadError) {
130 const error = new Error('Premature close');
131 error.code = 'ERR_STREAM_PREMATURE_CLOSE';
132 response.body.emit('error', error);
133 }
134 });
135 });
136 }
137
138 request_.on('response', response_ => {
139 request_.setTimeout(0);
140 const headers = fromRawHeaders(response_.rawHeaders);
141
142 // HTTP fetch step 5
143 if (isRedirect(response_.statusCode)) {
144 // HTTP fetch step 5.2
145 const location = headers.get('Location');
146
147 // HTTP fetch step 5.3
148 let locationURL = null;
149 try {
150 locationURL = location === null ? null : new URL(location, request.url);
151 } catch {
152 // error here can only be invalid URL in Location: header
153 // do not throw when options.redirect == manual
154 // let the user extract the errorneous redirect URL
155 if (request.redirect !== 'manual') {
156 reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect'));
157 finalize();
158 return;
159 }
160 }
161
162 // HTTP fetch step 5.5
163 switch (request.redirect) {
164 case 'error':
165 reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect'));
166 finalize();
167 return;
168 case 'manual':
169 // Nothing to do
170 break;
171 case 'follow': {
172 // HTTP-redirect fetch step 2
173 if (locationURL === null) {
174 break;
175 }
176
177 // HTTP-redirect fetch step 5
178 if (request.counter >= request.follow) {
179 reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
180 finalize();
181 return;
182 }
183
184 // HTTP-redirect fetch step 6 (counter increment)
185 // Create a new Request object.
186 const requestOptions = {
187 headers: new Headers(request.headers),
188 follow: request.follow,
189 counter: request.counter + 1,
190 agent: request.agent,
191 compress: request.compress,
192 method: request.method,
193 body: clone(request),
194 signal: request.signal,
195 size: request.size,
196 referrer: request.referrer,
197 referrerPolicy: request.referrerPolicy
198 };
199
200 // when forwarding sensitive headers like "Authorization",
201 // "WWW-Authenticate", and "Cookie" to untrusted targets,
202 // headers will be ignored when following a redirect to a domain
203 // that is not a subdomain match or exact match of the initial domain.
204 // For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com"
205 // will forward the sensitive headers, but a redirect to "bar.com" will not.
206 // headers will also be ignored when following a redirect to a domain using
207 // a different protocol. For example, a redirect from "https://foo.com" to "http://foo.com"
208 // will not forward the sensitive headers
209 if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
210 for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
211 requestOptions.headers.delete(name);
212 }
213 }
214
215 // HTTP-redirect fetch step 9
216 if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) {
217 reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
218 finalize();
219 return;
220 }
221
222 // HTTP-redirect fetch step 11
223 if (response_.statusCode === 303 || ((response_.statusCode === 301 || response_.statusCode === 302) && request.method === 'POST')) {
224 requestOptions.method = 'GET';
225 requestOptions.body = undefined;
226 requestOptions.headers.delete('content-length');
227 }
228
229 // HTTP-redirect fetch step 14
230 const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
231 if (responseReferrerPolicy) {
232 requestOptions.referrerPolicy = responseReferrerPolicy;
233 }
234
235 // HTTP-redirect fetch step 15
236 resolve(fetch(new Request(locationURL, requestOptions)));
237 finalize();
238 return;
239 }
240
241 default:
242 return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`));
243 }
244 }
245
246 // Prepare response
247 if (signal) {
248 response_.once('end', () => {
249 signal.removeEventListener('abort', abortAndFinalize);
250 });
251 }
252
253 let body = pump(response_, new PassThrough(), error => {
254 if (error) {
255 reject(error);
256 }
257 });
258 // see https://github.com/nodejs/node/pull/29376
259 /* c8 ignore next 3 */
260 if (process.version < 'v12.10') {
261 response_.on('aborted', abortAndFinalize);
262 }
263
264 const responseOptions = {
265 url: request.url,
266 status: response_.statusCode,
267 statusText: response_.statusMessage,
268 headers,
269 size: request.size,
270 counter: request.counter,
271 highWaterMark: request.highWaterMark
272 };
273
274 // HTTP-network fetch step 12.1.1.3
275 const codings = headers.get('Content-Encoding');
276
277 // HTTP-network fetch step 12.1.1.4: handle content codings
278
279 // in following scenarios we ignore compression support
280 // 1. compression support is disabled
281 // 2. HEAD request
282 // 3. no Content-Encoding header
283 // 4. no content response (204)
284 // 5. content not modified response (304)
285 if (!request.compress || request.method === 'HEAD' || codings === null || response_.statusCode === 204 || response_.statusCode === 304) {
286 response = new Response(body, responseOptions);
287 resolve(response);
288 return;
289 }
290
291 // For Node v6+
292 // Be less strict when decoding compressed responses, since sometimes
293 // servers send slightly invalid responses that are still accepted
294 // by common browsers.
295 // Always using Z_SYNC_FLUSH is what cURL does.
296 const zlibOptions = {
297 flush: zlib.Z_SYNC_FLUSH,
298 finishFlush: zlib.Z_SYNC_FLUSH
299 };
300
301 // For gzip
302 if (codings === 'gzip' || codings === 'x-gzip') {
303 body = pump(body, zlib.createGunzip(zlibOptions), error => {
304 if (error) {
305 reject(error);
306 }
307 });
308 response = new Response(body, responseOptions);
309 resolve(response);
310 return;
311 }
312
313 // For deflate
314 if (codings === 'deflate' || codings === 'x-deflate') {
315 // Handle the infamous raw deflate response from old servers
316 // a hack for old IIS and Apache servers
317 const raw = pump(response_, new PassThrough(), error => {
318 if (error) {
319 reject(error);
320 }
321 });
322 raw.once('data', chunk => {
323 // See http://stackoverflow.com/questions/37519828
324 if ((chunk[0] & 0x0F) === 0x08) {
325 body = pump(body, zlib.createInflate(), error => {
326 if (error) {
327 reject(error);
328 }
329 });
330 } else {
331 body = pump(body, zlib.createInflateRaw(), error => {
332 if (error) {
333 reject(error);
334 }
335 });
336 }
337
338 response = new Response(body, responseOptions);
339 resolve(response);
340 });
341 raw.once('end', () => {
342 // Some old IIS servers return zero-length OK deflate responses, so
343 // 'data' is never emitted. See https://github.com/node-fetch/node-fetch/pull/903
344 if (!response) {
345 response = new Response(body, responseOptions);
346 resolve(response);
347 }
348 });
349 return;
350 }
351
352 // For br
353 if (codings === 'br') {
354 body = pump(body, zlib.createBrotliDecompress(), error => {
355 if (error) {
356 reject(error);
357 }
358 });
359 response = new Response(body, responseOptions);
360 resolve(response);
361 return;
362 }
363
364 // Otherwise, use response as-is
365 response = new Response(body, responseOptions);
366 resolve(response);
367 });
368
369 // eslint-disable-next-line promise/prefer-await-to-then
370 writeToStream(request_, request).catch(reject);
371 });
372}
373
374function fixResponseChunkedTransferBadEnding(request, errorCallback) {
375 const LAST_CHUNK = Buffer.from('0\r\n\r\n');
376
377 let isChunkedTransfer = false;
378 let properLastChunkReceived = false;
379 let previousChunk;
380
381 request.on('response', response => {
382 const {headers} = response;
383 isChunkedTransfer = headers['transfer-encoding'] === 'chunked' && !headers['content-length'];
384 });
385
386 request.on('socket', socket => {
387 const onSocketClose = () => {
388 if (isChunkedTransfer && !properLastChunkReceived) {
389 const error = new Error('Premature close');
390 error.code = 'ERR_STREAM_PREMATURE_CLOSE';
391 errorCallback(error);
392 }
393 };
394
395 const onData = buf => {
396 properLastChunkReceived = Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0;
397
398 // Sometimes final 0-length chunk and end of message code are in separate packets
399 if (!properLastChunkReceived && previousChunk) {
400 properLastChunkReceived = (
401 Buffer.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 &&
402 Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0
403 );
404 }
405
406 previousChunk = buf;
407 };
408
409 socket.prependListener('close', onSocketClose);
410 socket.on('data', onData);
411
412 request.on('close', () => {
413 socket.removeListener('close', onSocketClose);
414 socket.removeListener('data', onData);
415 });
416 });
417}