Skip to content

Commit e5e645a

Browse files
committed
Various improvements
1 parent dc2656b commit e5e645a

28 files changed

+5400
-868
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"keyv": "^5.6.0",
6262
"lowercase-keys": "^4.0.1",
6363
"responselike": "^4.0.2",
64-
"type-fest": "^5.4.4"
64+
"type-fest": "^5.4.4",
65+
"uint8array-extras": "^1.5.0"
6566
},
6667
"devDependencies": {
6768
"@hapi/bourne": "^3.0.0",

source/as-promise/index.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
import {Buffer} from 'node:buffer';
21
import {EventEmitter} from 'node:events';
32
import is from '@sindresorhus/is';
43
import {
54
HTTPError,
65
RetryError,
76
type RequestError,
87
} from '../core/errors.js';
9-
import Request from '../core/index.js';
8+
import Request, {normalizeError} from '../core/index.js';
109
import {
10+
decodeUint8Array,
11+
isUtf8Encoding,
1112
parseBody,
1213
isResponseOk,
1314
type Response, ParseError,
1415
} from '../core/response.js';
1516
import proxyEvents from '../core/utils/proxy-events.js';
17+
import {
18+
applyUrlOverride,
19+
isSameOrigin,
20+
snapshotCrossOriginState,
21+
} from '../core/options.js';
1622
import type Options from '../core/options.js';
1723
import {type RequestPromise} from './types.js';
1824

25+
const compressedEncodings = new Set(['gzip', 'deflate', 'br', 'zstd']);
26+
1927
const proxiedRequestEvents = [
2028
'request',
2129
'response',
@@ -24,34 +32,6 @@ const proxiedRequestEvents = [
2432
'downloadProgress',
2533
];
2634

27-
const normalizeError = (error: unknown): Error => {
28-
if (error instanceof Error) {
29-
return error;
30-
}
31-
32-
if (is.object(error)) {
33-
const errorLike = error as Partial<Error & {code?: string; input?: string}>;
34-
const message = typeof errorLike.message === 'string' ? errorLike.message : 'Non-error object thrown';
35-
const normalizedError = new Error(message, {cause: error}) as Error & {code?: string; input?: string};
36-
37-
if (typeof errorLike.stack === 'string') {
38-
normalizedError.stack = errorLike.stack;
39-
}
40-
41-
if (typeof errorLike.code === 'string') {
42-
normalizedError.code = errorLike.code;
43-
}
44-
45-
if (typeof errorLike.input === 'string') {
46-
normalizedError.input = errorLike.input;
47-
}
48-
49-
return normalizedError;
50-
}
51-
52-
return new Error(String(error));
53-
};
54-
5535
export default function asPromise<T>(firstRequest?: Request): RequestPromise<T> {
5636
let globalRequest: Request;
5737
let globalResponse: Response;
@@ -69,7 +49,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
6949
request.once('response', async (response: Response) => {
7050
// Parse body
7151
const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase();
72-
const isCompressed = contentEncoding === 'gzip' || contentEncoding === 'deflate' || contentEncoding === 'br' || contentEncoding === 'zstd';
52+
const isCompressed = compressedEncodings.has(contentEncoding);
7353

7454
const {options} = request;
7555

@@ -81,7 +61,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
8161
} catch (error: unknown) {
8262
// Fall back to `utf8`
8363
try {
84-
response.body = Buffer.from(response.rawBody).toString();
64+
response.body = decodeUint8Array(response.rawBody);
8565
} catch (error) {
8666
request._beforeError(new ParseError(normalizeError(error), response));
8767
return;
@@ -98,16 +78,46 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
9878
const hooks = options.hooks.afterResponse;
9979

10080
for (const [index, hook] of hooks.entries()) {
81+
const previousUrl = options.url ? new URL(options.url) : undefined;
82+
const previousState = previousUrl ? snapshotCrossOriginState(options) : undefined;
83+
const requestOptions = response.request.options;
84+
const responseSnapshot = response;
85+
10186
// @ts-expect-error TS doesn't notice that RequestPromise is a Promise
10287
// eslint-disable-next-line no-await-in-loop
103-
response = await hook(response, async (updatedOptions): RequestPromise<Response> => {
88+
response = await requestOptions.trackStateMutations(async changedState => hook(responseSnapshot, async (updatedOptions): RequestPromise<Response> => {
10489
const preserveHooks = updatedOptions.preserveHooks ?? false;
90+
const reusesRequestOptions = updatedOptions === requestOptions;
91+
const hasExplicitBody = reusesRequestOptions
92+
? changedState.has('body') || changedState.has('json') || changedState.has('form')
93+
: (Object.hasOwn(updatedOptions, 'body') && updatedOptions.body !== undefined)
94+
|| (Object.hasOwn(updatedOptions, 'json') && updatedOptions.json !== undefined)
95+
|| (Object.hasOwn(updatedOptions, 'form') && updatedOptions.form !== undefined);
96+
97+
if (hasExplicitBody && !reusesRequestOptions) {
98+
options.clearBody();
99+
}
105100

106-
options.merge(updatedOptions);
107-
options.prefixUrl = '';
101+
if (!reusesRequestOptions) {
102+
options.merge(updatedOptions);
103+
}
108104

109105
if (updatedOptions.url) {
110-
options.url = updatedOptions.url;
106+
const nextUrl = reusesRequestOptions
107+
? options.url as URL
108+
: applyUrlOverride(options, updatedOptions.url, updatedOptions);
109+
110+
if (previousUrl) {
111+
if (reusesRequestOptions && !isSameOrigin(previousUrl, nextUrl)) {
112+
options.stripUnchangedCrossOriginState(previousState!, changedState, {clearBody: !hasExplicitBody});
113+
} else {
114+
options.stripSensitiveHeaders(previousUrl, nextUrl, updatedOptions);
115+
116+
if (!isSameOrigin(previousUrl, nextUrl) && !hasExplicitBody) {
117+
options.clearBody();
118+
}
119+
}
120+
}
111121
}
112122

113123
// Remove any further hooks for that request, because we'll call them anyway.
@@ -118,7 +128,7 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
118128
}
119129

120130
throw new RetryError(request);
121-
});
131+
}));
122132

123133
if (!(is.object(response) && is.number(response.statusCode) && 'body' in response)) {
124134
throw new TypeError('The `afterResponse` hook returned an invalid value');
@@ -238,6 +248,11 @@ export default function asPromise<T>(firstRequest?: Request): RequestPromise<T>
238248

239249
const {options} = globalResponse.request;
240250

251+
if (responseType === 'text') {
252+
const text = decodeUint8Array(globalResponse.rawBody, options.encoding);
253+
return (isUtf8Encoding(options.encoding) ? text.replace(/^\uFEFF/u, '') : text) as T;
254+
}
255+
241256
return parseBody(globalResponse, responseType, options.parseJson, options.encoding);
242257
})();
243258

source/as-promise/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface RequestPromiseShape<T extends Response | Response['body'] = Response['
1515
1616
It is semantically the same as setting `options.resolveBodyOnly` to `true` and `options.responseType` to `'buffer'`.
1717
*/
18-
buffer: () => RequestPromise<Uint8Array>;
18+
buffer: () => RequestPromise<Uint8Array<ArrayBuffer>>;
1919

2020
/**
2121
A shortcut method that gives a Promise returning a string.

source/core/calculate-retry-delay.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ const calculateRetryDelay: Returns<RetryFunction, number> = ({
2727
if (error.response) {
2828
if (retryAfter) {
2929
// In this case `computedValue` is `options.request.timeout`
30-
if (retryAfter > computedValue) {
31-
return 0;
32-
}
33-
34-
return retryAfter;
30+
return retryAfter > computedValue ? 0 : retryAfter;
3531
}
3632

3733
if (error.response.statusCode === 413) {

source/core/diagnostics-channel.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,44 +104,36 @@ export function generateRequestId(): RequestId {
104104
return randomUUID();
105105
}
106106

107-
export function publishRequestCreate(message: DiagnosticRequestCreate): void {
108-
if (channels.requestCreate.hasSubscribers) {
109-
channels.requestCreate.publish(message);
107+
const publishToChannel = (channel: diagnosticsChannel.Channel, message: unknown): void => {
108+
if (channel.hasSubscribers) {
109+
channel.publish(message);
110110
}
111+
};
112+
113+
export function publishRequestCreate(message: DiagnosticRequestCreate): void {
114+
publishToChannel(channels.requestCreate, message);
111115
}
112116

113117
export function publishRequestStart(message: DiagnosticRequestStart): void {
114-
if (channels.requestStart.hasSubscribers) {
115-
channels.requestStart.publish(message);
116-
}
118+
publishToChannel(channels.requestStart, message);
117119
}
118120

119121
export function publishResponseStart(message: DiagnosticResponseStart): void {
120-
if (channels.responseStart.hasSubscribers) {
121-
channels.responseStart.publish(message);
122-
}
122+
publishToChannel(channels.responseStart, message);
123123
}
124124

125125
export function publishResponseEnd(message: DiagnosticResponseEnd): void {
126-
if (channels.responseEnd.hasSubscribers) {
127-
channels.responseEnd.publish(message);
128-
}
126+
publishToChannel(channels.responseEnd, message);
129127
}
130128

131129
export function publishRetry(message: DiagnosticRequestRetry): void {
132-
if (channels.retry.hasSubscribers) {
133-
channels.retry.publish(message);
134-
}
130+
publishToChannel(channels.retry, message);
135131
}
136132

137133
export function publishError(message: DiagnosticRequestError): void {
138-
if (channels.error.hasSubscribers) {
139-
channels.error.publish(message);
140-
}
134+
publishToChannel(channels.error, message);
141135
}
142136

143137
export function publishRedirect(message: DiagnosticResponseRedirect): void {
144-
if (channels.redirect.hasSubscribers) {
145-
channels.redirect.publish(message);
146-
}
138+
publishToChannel(channels.redirect, message);
147139
}

source/core/errors.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ export class CacheError extends RequestError {
114114

115115
constructor(error: Error, request: Request) {
116116
super(error.message, error, request);
117-
if (this.code === 'ERR_GOT_REQUEST_ERROR') {
118-
this.code = 'ERR_CACHE_ACCESS';
119-
}
117+
this.code = 'ERR_CACHE_ACCESS';
120118
}
121119
}
122120

@@ -129,9 +127,7 @@ export class UploadError extends RequestError {
129127

130128
constructor(error: Error, request: Request) {
131129
super(error.message, error, request);
132-
if (this.code === 'ERR_GOT_REQUEST_ERROR') {
133-
this.code = 'ERR_UPLOAD';
134-
}
130+
this.code = 'ERR_UPLOAD';
135131
}
136132
}
137133

@@ -157,14 +153,16 @@ An error to be thrown when reading from response stream fails.
157153
*/
158154
export class ReadError extends RequestError {
159155
override name = 'ReadError';
156+
override code = 'ERR_READING_RESPONSE_STREAM';
160157
declare readonly request: Request;
161158
declare readonly response: Response;
162159
declare readonly timings: Timings;
163160

164161
constructor(error: Error, request: Request) {
165162
super(error.message, error, request);
166-
if (this.code === 'ERR_GOT_REQUEST_ERROR') {
167-
this.code = 'ERR_READING_RESPONSE_STREAM';
163+
164+
if (error.code === 'ECONNRESET' || error.code === 'ERR_HTTP_CONTENT_LENGTH_MISMATCH') {
165+
this.code = error.code;
168166
}
169167
}
170168
}

0 commit comments

Comments
 (0)