Skip to content

Commit 6be69a5

Browse files
committed
Fix CI
1 parent 309e36d commit 6be69a5

File tree

9 files changed

+853
-669
lines changed

9 files changed

+853
-669
lines changed

maintainer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Notes
2+
3+
## Testing
4+
5+
Never use `nock` in tests. nock v14 uses `@mswjs/interceptors` under the hood and auto-activates MSW interceptors for the entire process on import — affecting all HTTP requests, even in tests that don't use nock at all. Use real local test servers via `withServer` / `withServer.exec` instead.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@
8888
"expect-type": "^1.3.0",
8989
"express": "^5.2.1",
9090
"get-stream": "^9.0.1",
91-
"nock": "^14.0.11",
9291
"node-fetch": "^3.3.2",
9392
"np": "^11.0.2",
9493
"p-event": "^7.1.0",

source/core/index.ts

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,12 @@ export default class Request extends Duplex implements RequestEvents<Request> {
947947
}
948948
});
949949

950+
const noPipeCookieJarRawBodyPromise = this._noPipe
951+
&& is.object(options.cookieJar)
952+
&& !(response.headers.location && redirectCodes.has(statusCode))
953+
? this._setRawBody(response)
954+
: undefined;
955+
950956
const rawCookies = response.headers['set-cookie'];
951957
if (is.object(options.cookieJar) && rawCookies) {
952958
let promises: Array<Promise<unknown>> = rawCookies.map(async (rawCookie: string) => (options.cookieJar as PromiseCookieJar).setCookie(rawCookie, url!.toString()));
@@ -1109,7 +1115,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11091115
}
11101116

11111117
// Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
1112-
response.once('end', () => {
1118+
let responseEndHandled = false;
1119+
const handleResponseEnd = () => {
1120+
if (responseEndHandled) {
1121+
return;
1122+
}
1123+
1124+
responseEndHandled = true;
1125+
11131126
// Validate content-length if it was provided
11141127
// Per RFC 9112: "If the sender closes the connection before the indicated number
11151128
// of octets are received, the recipient MUST consider the message to be incomplete"
@@ -1130,7 +1143,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11301143
});
11311144

11321145
this.push(null);
1133-
});
1146+
};
1147+
1148+
response.once('end', handleResponseEnd);
11341149

11351150
this.emit('downloadProgress', this.downloadProgress);
11361151

@@ -1149,7 +1164,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11491164
});
11501165

11511166
if (this._noPipe) {
1152-
const success = await this._setRawBody();
1167+
const captureFromResponse = response.readableEnded || noPipeCookieJarRawBodyPromise !== undefined;
1168+
const success = noPipeCookieJarRawBodyPromise
1169+
? await noPipeCookieJarRawBodyPromise
1170+
: await this._setRawBody(captureFromResponse ? response : this);
1171+
1172+
if (captureFromResponse) {
1173+
handleResponseEnd();
1174+
}
11531175

11541176
if (success) {
11551177
this.emit('response', response);
@@ -1192,10 +1214,6 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11921214
}
11931215

11941216
private async _setRawBody(from: Readable = this): Promise<boolean> {
1195-
if (from.readableEnded) {
1196-
return false;
1197-
}
1198-
11991217
try {
12001218
// Errors are emitted via the `error` event
12011219
const fromArray = await from.toArray();
@@ -1212,6 +1230,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
12121230
&& this.response
12131231
) {
12141232
this.response.rawBody = rawBody;
1233+
if (from !== this) {
1234+
this._downloadedSize = rawBody.byteLength;
1235+
}
12151236

12161237
if (shouldUseIncrementalDecodedBody) {
12171238
try {
@@ -1305,7 +1326,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
13051326
}
13061327

13071328
/*
1308-
Transient write errors (EPIPE, ECONNRESET) often fire during redirects when the server closes the connection after sending the redirect response. Defer by one microtask to let the response event make the request stale.
1329+
Transient write errors (EPIPE, ECONNRESET) often fire during redirects when the
1330+
server closes the connection after sending the redirect response. Defer by one
1331+
microtask to let the response event make the request stale.
13091332
*/
13101333
if (isTransientWriteError(error)) {
13111334
queueMicrotask(() => {
@@ -1419,25 +1442,28 @@ export default class Request extends Duplex implements RequestEvents<Request> {
14191442
const isInitialRequest = currentRequest === this;
14201443

14211444
(async () => {
1445+
let request: ClientRequest | undefined;
1446+
14221447
try {
1423-
const request = isInitialRequest ? this._request : currentRequest as ClientRequest;
1448+
request = isInitialRequest ? this._request : currentRequest as ClientRequest;
1449+
const activeRequest = request;
14241450

1425-
if (!request) {
1451+
if (!activeRequest) {
14261452
if (isInitialRequest) {
14271453
super.end();
14281454
}
14291455

14301456
return;
14311457
}
14321458

1433-
if (request.destroyed) {
1459+
if (activeRequest.destroyed) {
14341460
return;
14351461
}
14361462

1437-
await this._writeChunksToRequest(buffer, request);
1463+
await this._writeChunksToRequest(buffer, activeRequest);
14381464

1439-
if (this._isRequestStale(request)) {
1440-
this._finalizeStaleChunkedWrite(request, isInitialRequest);
1465+
if (this._isRequestStale(activeRequest)) {
1466+
this._finalizeStaleChunkedWrite(activeRequest, isInitialRequest);
14411467
return;
14421468
}
14431469

@@ -1447,25 +1473,54 @@ export default class Request extends Duplex implements RequestEvents<Request> {
14471473
}
14481474

14491475
await new Promise<void>((resolve, reject) => {
1450-
request.end((error?: Error | null) => { // eslint-disable-line @typescript-eslint/no-restricted-types
1476+
activeRequest.end((error?: Error | null) => { // eslint-disable-line @typescript-eslint/no-restricted-types
14511477
if (error) {
14521478
reject(error);
14531479
return;
14541480
}
14551481

1456-
if (this._request === request && !request.destroyed) {
1457-
this._emitUploadComplete(request);
1482+
if (this._request === activeRequest && !activeRequest.destroyed) {
1483+
this._emitUploadComplete(activeRequest);
14581484
}
14591485

14601486
resolve();
14611487
});
14621488
});
14631489
} catch (error: unknown) {
1490+
const normalizedError = normalizeError(error);
1491+
1492+
// Transient write errors (EPIPE, ECONNRESET) are handled by the request-level
1493+
// error and close handlers. For initial redirected writes, still finalize
1494+
// writable state once the stale transition becomes observable.
1495+
if (isTransientWriteError(normalizedError)) {
1496+
if (isInitialRequest && request) {
1497+
const initialRequest = request;
1498+
let didFinalize = false;
1499+
const finalizeIfStale = () => {
1500+
if (didFinalize || !this._isRequestStale(initialRequest)) {
1501+
return;
1502+
}
1503+
1504+
didFinalize = true;
1505+
this._finalizeStaleChunkedWrite(initialRequest, true);
1506+
};
1507+
1508+
finalizeIfStale();
1509+
1510+
if (!didFinalize) {
1511+
initialRequest.once('response', finalizeIfStale);
1512+
queueMicrotask(finalizeIfStale);
1513+
}
1514+
}
1515+
1516+
return;
1517+
}
1518+
14641519
if (!isInitialRequest && this._isRequestStale(currentRequest as ClientRequest)) {
14651520
return;
14661521
}
14671522

1468-
this._beforeError(normalizeError(error));
1523+
this._beforeError(normalizedError);
14691524
}
14701525
})();
14711526
}
@@ -1716,7 +1771,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {
17161771

17171772
// Set cookies
17181773
if (cookieJar) {
1719-
const cookieString: string = await cookieJar.getCookieString(options.url!.toString());
1774+
let cookieString = '';
1775+
cookieString = await cookieJar.getCookieString(options.url!.toString());
17201776

17211777
if (is.nonEmptyString(cookieString)) {
17221778
options.setInternalHeader('cookie', cookieString);

test/cache.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import test from 'ava';
88
import {pEvent} from 'p-event';
99
import getStream from 'get-stream';
1010
import type {Handler} from 'express';
11-
import nock from 'nock';
1211
import CacheableLookup from 'cacheable-lookup';
1312
import delay from 'delay';
1413
import got, {CacheError, type Response} from '../source/index.js';
@@ -431,11 +430,19 @@ test('http-cache-semantics typings', t => {
431430
t.is(instance.defaults.options.cacheOptions.shared, false);
432431
});
433432

434-
test('allows internal modifications', async t => {
435-
nock('http://example.com').get('/test').reply(401);
436-
nock('http://example.com').get('/test').reply(200, JSON.stringify({
437-
wat: ['123'],
438-
}));
433+
test('allows internal modifications', withServer, async (t, server, got) => {
434+
let requestCount = 0;
435+
436+
server.get('/test', (_request, response) => {
437+
requestCount++;
438+
if (requestCount === 1) {
439+
response.writeHead(401);
440+
response.end();
441+
} else {
442+
response.writeHead(200);
443+
response.end(JSON.stringify({wat: ['123']}));
444+
}
445+
});
439446

440447
const client = got.extend({
441448
cache: new Map(),
@@ -452,7 +459,7 @@ test('allows internal modifications', async t => {
452459
},
453460
});
454461

455-
await t.notThrowsAsync(client.get('http://example.com/test'));
462+
await t.notThrowsAsync(client.get('test'));
456463
});
457464

458465
test('response.complete is true when using keepalive agent', withServer, async (t, server, got) => {

test/cookies.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import net from 'node:net';
22
import test from 'ava';
33
import * as toughCookie from 'tough-cookie';
44
import delay from 'delay';
5-
import nock from 'nock';
65
import got, {RequestError} from '../source/index.js';
76
import withServer from './helpers/with-server.js';
87

@@ -206,24 +205,26 @@ test('throws on invalid `options.cookieJar.getCookieString`', async t => {
206205
});
207206
});
208207

209-
test('cookies are cleared when redirecting to a different hostname (no cookieJar)', withServer, async (t, server, got) => {
210-
server.get('/', (_request, response) => {
211-
response.writeHead(302, {
212-
location: 'https://example.com/',
208+
test('cookies are cleared when redirecting to a different hostname (no cookieJar)', withServer, async (t, server1, got) => {
209+
await withServer.exec(t, async (t, server2) => {
210+
server1.get('/', (_request, response) => {
211+
response.writeHead(302, {
212+
location: `http://localhost:${server2.port}/`,
213+
});
214+
response.end();
213215
});
214-
response.end();
215-
});
216216

217-
nock('https://example.com').get('/').reply(200, function () {
218-
return JSON.stringify({headers: this.req.headers});
219-
});
217+
server2.get('/', (request, response) => {
218+
response.end(JSON.stringify({headers: request.headers}));
219+
});
220220

221-
const {headers} = await got('', {
222-
headers: {
223-
cookie: 'foo=bar',
224-
'user-agent': 'custom',
225-
},
226-
}).json<{headers: Record<string, string | undefined>}>();
227-
t.is(headers.cookie, undefined);
228-
t.is(headers['user-agent'], 'custom');
221+
const {headers} = await got('', {
222+
headers: {
223+
cookie: 'foo=bar',
224+
'user-agent': 'custom',
225+
},
226+
}).json<{headers: Record<string, string | undefined>}>();
227+
t.is(headers.cookie, undefined);
228+
t.is(headers['user-agent'], 'custom');
229+
});
229230
});

0 commit comments

Comments
 (0)