UNPKG

@whatwg-node/node-fetch

Version:

Fetch API implementation for Node

188 lines (187 loc) • 7.94 kB
import { PassThrough, Readable } from 'node:stream'; import { isUint8Array } from 'node:util/types'; import { createBrotliDecompress, createGunzip, createInflate, createInflateRaw } from 'node:zlib'; import { fetchNodeHttp } from './fetchNodeHttp.js'; import { PonyfillRequest } from './Request.js'; import { PonyfillResponse } from './Response.js'; import { PonyfillURL } from './URL.js'; import { createDeferredPromise, getHeadersObj, isNodeReadable } from './utils.js'; export function createFetchUndici(getGlobalDispatcher) { return function fetchUndici(fetchRequest) { const dispatcher = fetchRequest.dispatcher || getGlobalDispatcher(); if (!dispatcher) { if (process.env.DEBUG) { console.debug('[@whatwg-node/node-fetch] - native undici dispatcher not available, falling back to node:http'); } return fetchNodeHttp(fetchRequest); } const deferred = createDeferredPromise(); let abortListener; let passthrough; let response; let body = null; const bodyInit = fetchRequest['bodyInit']; if (bodyInit != null) { if (typeof bodyInit === 'string' || Buffer.isBuffer(bodyInit) || isUint8Array(bodyInit) || bodyInit instanceof Readable) { body = bodyInit; } else if (fetchRequest.body != null) { if (isNodeReadable(fetchRequest.body?.readable)) { body = fetchRequest.body.readable; } else { body = Readable.from(fetchRequest.body); } } } function setPassthrough(contentEncoding) { switch (contentEncoding) { case 'x-gzip': case 'gzip': passthrough = createGunzip(); break; case 'x-deflate': case 'deflate': passthrough = createInflate(); break; case 'x-deflate-raw': case 'deflate-raw': passthrough = createInflateRaw(); break; case 'br': passthrough = createBrotliDecompress(); break; default: passthrough = new PassThrough(); } return passthrough; } function onAbort(e) { fetchRequest['_signal']?.removeEventListener('abort', abortListener); passthrough?.destroy(e); deferred.reject(e); } const headersSerializer = fetchRequest.headersSerializer || getHeadersObj; const nodeHeaders = headersSerializer(fetchRequest.headers); if (nodeHeaders['accept-encoding'] == null) { nodeHeaders['accept-encoding'] = 'gzip, deflate, br'; } const dispatcherReturn = dispatcher.dispatch({ origin: fetchRequest.parsedUrl.origin, path: fetchRequest.parsedUrl.pathname, query: Object.fromEntries(fetchRequest.parsedUrl.searchParams), method: fetchRequest.method, headers: nodeHeaders, body, }, { onRequestStart(controller) { abortListener = function abortListener() { onAbort(fetchRequest['_signal']?.reason); controller.abort(fetchRequest['_signal']?.reason); }; fetchRequest['_signal']?.addEventListener('abort', abortListener, { once: true }); }, onRequestUpgrade(_controller, statusCode, headers, socket) { response = new PonyfillResponse(socket, { status: statusCode, headers: headers, url: fetchRequest.url, }); deferred.resolve(response); fetchRequest['_signal']?.removeEventListener('abort', abortListener); }, onResponseStart(controller, statusCode, headers, statusMessage) { if (headers.location) { if (fetchRequest.redirect === 'error') { const redirectError = new Error('Redirects are not allowed'); deferred.reject(redirectError); controller.resume(); return; } if (fetchRequest.redirect === 'follow') { const redirectedUrl = new PonyfillURL(headers.location, fetchRequest.parsedUrl || fetchRequest.url); const redirectResponse$ = fetchUndici(new PonyfillRequest(redirectedUrl, fetchRequest)); deferred.resolve(redirectResponse$.then(redirectResponse => { redirectResponse.redirected = true; return redirectResponse; })); controller.resume(); return; } } passthrough = setPassthrough(headers['content-encoding']); deferred.resolve(new PonyfillResponse(passthrough, { status: statusCode, statusText: statusMessage, headers: headers, url: fetchRequest.url, })); }, onResponseData(controller, chunk) { passthrough.write(chunk); if (controller.reason) { onAbort(controller.reason); } }, onResponseEnd(controller, _trailers) { if (controller.reason) { onAbort(controller.reason); } else { passthrough.end(); fetchRequest['_signal']?.removeEventListener('abort', abortListener); } }, onResponseError(controller, error) { onAbort(error || controller.reason); }, // Old Undici support onConnect(abort) { abortListener = function abortListener() { abort(fetchRequest['_signal']?.reason); onAbort(fetchRequest['_signal']?.reason); }; fetchRequest['_signal']?.addEventListener('abort', abortListener, { once: true }); }, onError(error) { onAbort(error); }, // TODO: onUpgrade onHeaders(statusCode, headersBuf, _resume, statusText) { const headers = headersBuf.map(headerBuf => { const header = headerBuf .toString('utf-8') .split(/:\s(.+)/) .slice(0, 2); if (header[0] === 'content-encoding') { const contentEncoding = header[1]; setPassthrough(contentEncoding); } return header; }); passthrough ||= new PassThrough(); deferred.resolve(new PonyfillResponse(passthrough, { status: statusCode, statusText, headers, url: fetchRequest.url, })); return true; }, onData(chunk) { return passthrough.write(chunk); }, onComplete() { passthrough.end(); fetchRequest['_signal']?.removeEventListener('abort', abortListener); }, }); if (!dispatcherReturn) { console.warn('Undici dispatcher returned false'); } return deferred.promise; }; }