159 lines
6.8 KiB
JavaScript
159 lines
6.8 KiB
JavaScript
import { EventEmitter } from 'node:events';
|
|
import is from '@sindresorhus/is';
|
|
import PCancelable from 'p-cancelable';
|
|
import { HTTPError, RetryError, } from '../core/errors.js';
|
|
import Request from '../core/index.js';
|
|
import { parseBody, isResponseOk } from '../core/response.js';
|
|
import proxyEvents from '../core/utils/proxy-events.js';
|
|
import { CancelError } from './types.js';
|
|
const proxiedRequestEvents = [
|
|
'request',
|
|
'response',
|
|
'redirect',
|
|
'uploadProgress',
|
|
'downloadProgress',
|
|
];
|
|
export default function asPromise(firstRequest) {
|
|
let globalRequest;
|
|
let globalResponse;
|
|
let normalizedOptions;
|
|
const emitter = new EventEmitter();
|
|
const promise = new PCancelable((resolve, reject, onCancel) => {
|
|
onCancel(() => {
|
|
globalRequest.destroy();
|
|
});
|
|
onCancel.shouldReject = false;
|
|
onCancel(() => {
|
|
reject(new CancelError(globalRequest));
|
|
});
|
|
const makeRequest = (retryCount) => {
|
|
// Errors when a new request is made after the promise settles.
|
|
// Used to detect a race condition.
|
|
// See https://github.com/sindresorhus/got/issues/1489
|
|
onCancel(() => { });
|
|
const request = firstRequest ?? new Request(undefined, undefined, normalizedOptions);
|
|
request.retryCount = retryCount;
|
|
request._noPipe = true;
|
|
globalRequest = request;
|
|
request.once('response', async (response) => {
|
|
// Parse body
|
|
const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase();
|
|
const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br';
|
|
const { options } = request;
|
|
if (isCompressed && !options.decompress) {
|
|
response.body = response.rawBody;
|
|
}
|
|
else {
|
|
try {
|
|
response.body = parseBody(response, options.responseType, options.parseJson, options.encoding);
|
|
}
|
|
catch (error) {
|
|
// Fall back to `utf8`
|
|
response.body = response.rawBody.toString();
|
|
if (isResponseOk(response)) {
|
|
request._beforeError(error);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
const hooks = options.hooks.afterResponse;
|
|
for (const [index, hook] of hooks.entries()) {
|
|
// @ts-expect-error TS doesn't notice that CancelableRequest is a Promise
|
|
// eslint-disable-next-line no-await-in-loop
|
|
response = await hook(response, async (updatedOptions) => {
|
|
options.merge(updatedOptions);
|
|
options.prefixUrl = '';
|
|
if (updatedOptions.url) {
|
|
options.url = updatedOptions.url;
|
|
}
|
|
// Remove any further hooks for that request, because we'll call them anyway.
|
|
// The loop continues. We don't want duplicates (asPromise recursion).
|
|
options.hooks.afterResponse = options.hooks.afterResponse.slice(0, index);
|
|
throw new RetryError(request);
|
|
});
|
|
if (!(is.object(response) && is.number(response.statusCode) && !is.nullOrUndefined(response.body))) {
|
|
throw new TypeError('The `afterResponse` hook returned an invalid value');
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
request._beforeError(error);
|
|
return;
|
|
}
|
|
globalResponse = response;
|
|
if (!isResponseOk(response)) {
|
|
request._beforeError(new HTTPError(response));
|
|
return;
|
|
}
|
|
request.destroy();
|
|
resolve(request.options.resolveBodyOnly ? response.body : response);
|
|
});
|
|
const onError = (error) => {
|
|
if (promise.isCanceled) {
|
|
return;
|
|
}
|
|
const { options } = request;
|
|
if (error instanceof HTTPError && !options.throwHttpErrors) {
|
|
const { response } = error;
|
|
request.destroy();
|
|
resolve(request.options.resolveBodyOnly ? response.body : response);
|
|
return;
|
|
}
|
|
reject(error);
|
|
};
|
|
request.once('error', onError);
|
|
const previousBody = request.options?.body;
|
|
request.once('retry', (newRetryCount, error) => {
|
|
firstRequest = undefined;
|
|
const newBody = request.options.body;
|
|
if (previousBody === newBody && is.nodeStream(newBody)) {
|
|
error.message = 'Cannot retry with consumed body stream';
|
|
onError(error);
|
|
return;
|
|
}
|
|
// This is needed! We need to reuse `request.options` because they can get modified!
|
|
// For example, by calling `promise.json()`.
|
|
normalizedOptions = request.options;
|
|
makeRequest(newRetryCount);
|
|
});
|
|
proxyEvents(request, emitter, proxiedRequestEvents);
|
|
if (is.undefined(firstRequest)) {
|
|
void request.flush();
|
|
}
|
|
};
|
|
makeRequest(0);
|
|
});
|
|
promise.on = (event, fn) => {
|
|
emitter.on(event, fn);
|
|
return promise;
|
|
};
|
|
promise.off = (event, fn) => {
|
|
emitter.off(event, fn);
|
|
return promise;
|
|
};
|
|
const shortcut = (responseType) => {
|
|
const newPromise = (async () => {
|
|
// Wait until downloading has ended
|
|
await promise;
|
|
const { options } = globalResponse.request;
|
|
return parseBody(globalResponse, responseType, options.parseJson, options.encoding);
|
|
})();
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));
|
|
return newPromise;
|
|
};
|
|
promise.json = () => {
|
|
if (globalRequest.options) {
|
|
const { headers } = globalRequest.options;
|
|
if (!globalRequest.writableFinished && !('accept' in headers)) {
|
|
headers.accept = 'application/json';
|
|
}
|
|
}
|
|
return shortcut('json');
|
|
};
|
|
promise.buffer = () => shortcut('buffer');
|
|
promise.text = () => shortcut('text');
|
|
return promise;
|
|
}
|