Skip to content

Commit d4c0d75

Browse files
authored
chore: make fetch progress "strict" (#36318)
1 parent ab7b18e commit d4c0d75

File tree

1 file changed

+17
-32
lines changed
  • packages/playwright-core/src/server

1 file changed

+17
-32
lines changed

packages/playwright-core/src/server/fetch.ts

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { BrowserContext, verifyClientCertificates } from './browserContext';
2727
import { Cookie, CookieStore, domainMatches, parseRawCookie } from './cookieStore';
2828
import { MultipartFormData } from './formData';
2929
import { SdkObject } from './instrumentation';
30-
import { ProgressController } from './progress';
30+
import { isAbortError, ProgressController } from './progress';
3131
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
3232
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent, timingForSocket } from './utils/happyEyeballs';
3333
import { Tracing } from './trace/recorder/tracing';
@@ -84,11 +84,12 @@ export type APIRequestFinishedEvent = {
8484

8585
type SendRequestOptions = https.RequestOptions & {
8686
maxRedirects: number,
87-
deadline: number,
8887
headers: HeadersObject,
8988
__testHookLookup?: (hostname: string) => LookupAddress[]
9089
};
9190

91+
type SendRequestResult = Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer };
92+
9293
export abstract class APIRequestContext extends SdkObject {
9394
static Events = {
9495
Dispose: 'dispose',
@@ -185,16 +186,11 @@ export abstract class APIRequestContext extends SdkObject {
185186
let maxRedirects = params.maxRedirects ?? (defaults.maxRedirects ?? 20);
186187
maxRedirects = maxRedirects === 0 ? -1 : maxRedirects;
187188

188-
const timeout = params.timeout;
189-
const deadline = timeout && (monotonicTime() + timeout);
190-
191189
const options: SendRequestOptions = {
192190
method,
193191
headers,
194192
agent,
195193
maxRedirects,
196-
timeout,
197-
deadline,
198194
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
199195
__testHookLookup: (params as any).__testHookLookup,
200196
};
@@ -205,10 +201,10 @@ export abstract class APIRequestContext extends SdkObject {
205201
const postData = serializePostData(params, headers);
206202
if (postData)
207203
setHeader(headers, 'content-length', String(postData.byteLength));
208-
const controller = new ProgressController(metadata, this);
204+
const controller = new ProgressController(metadata, this, 'strict');
209205
const fetchResponse = await controller.run(progress => {
210206
return this._sendRequestWithRetries(progress, requestUrl, options, postData, params.maxRetries);
211-
}, timeout);
207+
}, params.timeout);
212208
const fetchUid = this._storeResponseBody(fetchResponse.body);
213209
this.fetchLog.set(fetchUid, controller.metadata.log);
214210
const failOnStatusCode = params.failOnStatusCode !== undefined ? params.failOnStatusCode : !!defaults.failOnStatusCode;
@@ -252,10 +248,10 @@ export abstract class APIRequestContext extends SdkObject {
252248
return cookies;
253249
}
254250

255-
private async _updateRequestCookieHeader(url: URL, headers: HeadersObject) {
251+
private async _updateRequestCookieHeader(progress: Progress, url: URL, headers: HeadersObject) {
256252
if (getHeader(headers, 'cookie') !== undefined)
257253
return;
258-
const contextCookies = await this._cookies(url);
254+
const contextCookies = await progress.race(this._cookies(url));
259255
// Browser context returns cookies with domain matching both .example.com and
260256
// example.com. Those without leading dot are only sent when domain is strictly
261257
// matching example.com, but not for sub.example.com.
@@ -266,31 +262,33 @@ export abstract class APIRequestContext extends SdkObject {
266262
}
267263
}
268264

269-
private async _sendRequestWithRetries(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer, maxRetries?: number): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
265+
private async _sendRequestWithRetries(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer, maxRetries?: number): Promise<SendRequestResult>{
270266
maxRetries ??= 0;
271267
let backoff = 250;
272268
for (let i = 0; i <= maxRetries; i++) {
273269
try {
274270
return await this._sendRequest(progress, url, options, postData);
275271
} catch (e) {
272+
if (isAbortError(e))
273+
throw e;
276274
e = rewriteOpenSSLErrorIfNeeded(e);
277275
if (maxRetries === 0)
278276
throw e;
279-
if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline))
277+
if (i === maxRetries)
280278
throw new Error(`Failed after ${i + 1} attempt(s): ${e}`);
281279
// Retry on connection reset only.
282280
if (e.code !== 'ECONNRESET')
283281
throw e;
284282
progress.log(` Received ECONNRESET, will retry after ${backoff}ms.`);
285-
await new Promise(f => setTimeout(f, backoff));
283+
await progress.wait(backoff);
286284
backoff *= 2;
287285
}
288286
}
289287
throw new Error('Unreachable');
290288
}
291289

292-
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<Omit<channels.APIResponse, 'fetchUid'> & { body: Buffer }>{
293-
await this._updateRequestCookieHeader(url, options.headers);
290+
private async _sendRequest(progress: Progress, url: URL, options: SendRequestOptions, postData?: Buffer): Promise<SendRequestResult>{
291+
await this._updateRequestCookieHeader(progress, url, options.headers);
294292

295293
const requestCookies = getHeader(options.headers, 'cookie')?.split(';').map(p => {
296294
const [name, value] = p.split('=').map(v => v.trim());
@@ -305,7 +303,7 @@ export abstract class APIRequestContext extends SdkObject {
305303
};
306304
this.emit(APIRequestContext.Events.Request, requestEvent);
307305

308-
return new Promise((fulfill, reject) => {
306+
const resultPromise = new Promise<SendRequestResult>((fulfill, reject) => {
309307
const requestConstructor: ((url: URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest)
310308
= (url.protocol === 'https:' ? https : http).request;
311309
// If we have a proxy agent already, do not override it.
@@ -402,8 +400,6 @@ export abstract class APIRequestContext extends SdkObject {
402400
headers,
403401
agent: options.agent,
404402
maxRedirects: options.maxRedirects - 1,
405-
timeout: options.timeout,
406-
deadline: options.deadline,
407403
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, url.origin),
408404
__testHookLookup: options.__testHookLookup,
409405
};
@@ -492,6 +488,7 @@ export abstract class APIRequestContext extends SdkObject {
492488
body.on('end', notifyBodyFinished);
493489
});
494490
request.on('error', reject);
491+
progress.cleanupWhenAborted(() => request.destroy());
495492

496493
listeners.push(
497494
eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => {
@@ -543,23 +540,11 @@ export abstract class APIRequestContext extends SdkObject {
543540
progress.log(` ${name}: ${value}`);
544541
}
545542

546-
if (options.deadline) {
547-
const rejectOnTimeout = () => {
548-
reject(new Error(`Request timed out after ${options.timeout}ms`));
549-
request.destroy();
550-
};
551-
const remaining = options.deadline - monotonicTime();
552-
if (remaining <= 0) {
553-
rejectOnTimeout();
554-
return;
555-
}
556-
request.setTimeout(remaining, rejectOnTimeout);
557-
}
558-
559543
if (postData)
560544
request.write(postData);
561545
request.end();
562546
});
547+
return progress.race(resultPromise);
563548
}
564549

565550
private _getHttpCredentials(url: URL) {

0 commit comments

Comments
 (0)