feat: Add maxRedirects to options of apiRequest.newContext (#35160)

This commit is contained in:
Chris 2025-03-17 14:47:52 +01:00 committed by GitHub
parent accd7c6c9e
commit beea7c30c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 5 deletions

View File

@ -37,6 +37,13 @@ for all status codes.
### option: APIRequest.newContext.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%% ### option: APIRequest.newContext.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%%
* since: v1.16 * since: v1.16
### option: APIRequest.newContext.maxRedirects
* since: v1.52
- `maxRedirects` <[int]>
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request individually.
### option: APIRequest.newContext.timeout ### option: APIRequest.newContext.timeout
* since: v1.16 * since: v1.16
- `timeout` <[float]> - `timeout` <[float]>

View File

@ -17573,6 +17573,13 @@ export interface APIRequest {
*/ */
ignoreHTTPSErrors?: boolean; ignoreHTTPSErrors?: boolean;
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request
* individually.
*/
maxRedirects?: number;
/** /**
* Network proxy settings. * Network proxy settings.
*/ */

View File

@ -386,6 +386,7 @@ scheme.PlaywrightNewRequestParams = tObject({
passphrase: tOptional(tString), passphrase: tOptional(tString),
pfx: tOptional(tBinary), pfx: tOptional(tBinary),
}))), }))),
maxRedirects: tOptional(tNumber),
httpCredentials: tOptional(tObject({ httpCredentials: tOptional(tObject({
username: tString, username: tString,
password: tString, password: tString,

View File

@ -56,6 +56,7 @@ type FetchRequestOptions = {
proxy?: ProxySettings; proxy?: ProxySettings;
timeoutSettings: TimeoutSettings; timeoutSettings: TimeoutSettings;
ignoreHTTPSErrors?: boolean; ignoreHTTPSErrors?: boolean;
maxRedirects?: number;
baseURL?: string; baseURL?: string;
clientCertificates?: types.BrowserContextOptions['clientCertificates']; clientCertificates?: types.BrowserContextOptions['clientCertificates'];
}; };
@ -185,6 +186,8 @@ export abstract class APIRequestContext extends SdkObject {
if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass))
agent = createProxyAgent(proxy); agent = createProxyAgent(proxy);
let maxRedirects = params.maxRedirects ?? (defaults.maxRedirects ?? 20);
maxRedirects = maxRedirects === 0 ? -1 : maxRedirects;
const timeout = defaults.timeoutSettings.timeout(params); const timeout = defaults.timeoutSettings.timeout(params);
const deadline = timeout && (monotonicTime() + timeout); const deadline = timeout && (monotonicTime() + timeout);
@ -193,7 +196,7 @@ export abstract class APIRequestContext extends SdkObject {
method, method,
headers, headers,
agent, agent,
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects, maxRedirects,
timeout, timeout,
deadline, deadline,
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin), ...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
@ -371,7 +374,7 @@ export abstract class APIRequestContext extends SdkObject {
} }
if (redirectStatus.includes(response.statusCode!) && options.maxRedirects >= 0) { if (redirectStatus.includes(response.statusCode!) && options.maxRedirects >= 0) {
if (!options.maxRedirects) { if (options.maxRedirects === 0) {
reject(new Error('Max redirect count exceeded')); reject(new Error('Max redirect count exceeded'));
request.destroy(); request.destroy();
return; return;
@ -662,6 +665,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
extraHTTPHeaders: options.extraHTTPHeaders, extraHTTPHeaders: options.extraHTTPHeaders,
failOnStatusCode: !!options.failOnStatusCode, failOnStatusCode: !!options.failOnStatusCode,
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors, ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
maxRedirects: options.maxRedirects,
httpCredentials: options.httpCredentials, httpCredentials: options.httpCredentials,
clientCertificates: options.clientCertificates, clientCertificates: options.clientCertificates,
proxy, proxy,

View File

@ -17573,6 +17573,13 @@ export interface APIRequest {
*/ */
ignoreHTTPSErrors?: boolean; ignoreHTTPSErrors?: boolean;
/**
* Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is
* exceeded. Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request
* individually.
*/
maxRedirects?: number;
/** /**
* Network proxy settings. * Network proxy settings.
*/ */

View File

@ -644,6 +644,7 @@ export type PlaywrightNewRequestParams = {
passphrase?: string, passphrase?: string,
pfx?: Binary, pfx?: Binary,
}[], }[],
maxRedirects?: number,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
password: string, password: string,
@ -676,6 +677,7 @@ export type PlaywrightNewRequestOptions = {
passphrase?: string, passphrase?: string,
pfx?: Binary, pfx?: Binary,
}[], }[],
maxRedirects?: number,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
password: string, password: string,

View File

@ -770,6 +770,7 @@ Playwright:
key: binary? key: binary?
passphrase: string? passphrase: string?
pfx: binary? pfx: binary?
maxRedirects: number?
httpCredentials: httpCredentials:
type: object? type: object?
properties: properties:

View File

@ -437,6 +437,8 @@ it('should return body for failing requests', async ({ playwright, server }) =>
await request.dispose(); await request.dispose();
}); });
const HTTP_METHODS = ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH'] as const;
it('should throw an error when maxRedirects is exceeded', async ({ playwright, server }) => { it('should throw an error when maxRedirects is exceeded', async ({ playwright, server }) => {
server.setRedirect('/a/redirect1', '/b/c/redirect2'); server.setRedirect('/a/redirect1', '/b/c/redirect2');
server.setRedirect('/b/c/redirect2', '/b/c/redirect3'); server.setRedirect('/b/c/redirect2', '/b/c/redirect3');
@ -444,7 +446,7 @@ it('should throw an error when maxRedirects is exceeded', async ({ playwright, s
server.setRedirect('/b/c/redirect4', '/simple.json'); server.setRedirect('/b/c/redirect4', '/simple.json');
const request = await playwright.request.newContext(); const request = await playwright.request.newContext();
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH']) { for (const method of HTTP_METHODS) {
for (const maxRedirects of [1, 2, 3]) for (const maxRedirects of [1, 2, 3])
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method: method, maxRedirects: maxRedirects })).rejects.toThrow('Max redirect count exceeded'); await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method: method, maxRedirects: maxRedirects })).rejects.toThrow('Max redirect count exceeded');
} }
@ -456,7 +458,7 @@ it('should not follow redirects when maxRedirects is set to 0', async ({ playwri
server.setRedirect('/b/c/redirect2', '/simple.json'); server.setRedirect('/b/c/redirect2', '/simple.json');
const request = await playwright.request.newContext(); const request = await playwright.request.newContext();
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH']){ for (const method of HTTP_METHODS){
const response = await request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: 0 }); const response = await request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: 0 });
expect(response.headers()['location']).toBe('/b/c/redirect2'); expect(response.headers()['location']).toBe('/b/c/redirect2');
expect(response.status()).toBe(302); expect(response.status()).toBe(302);
@ -469,11 +471,59 @@ it('should throw an error when maxRedirects is less than 0', async ({ playwright
server.setRedirect('/b/c/redirect2', '/simple.json'); server.setRedirect('/b/c/redirect2', '/simple.json');
const request = await playwright.request.newContext(); const request = await playwright.request.newContext();
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH']) for (const method of HTTP_METHODS)
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' must be greater than or equal to '0'`); await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: -1 })).rejects.toThrow(`'maxRedirects' must be greater than or equal to '0'`);
await request.dispose(); await request.dispose();
}); });
it('should not follow redirects when maxRedirects is set to 0 in newContext', async ({ playwright, server }) => {
server.setRedirect('/a/redirect1', '/b/c/redirect2');
server.setRedirect('/b/c/redirect2', '/simple.json');
const request = await playwright.request.newContext({ maxRedirects: 0 });
for (const method of HTTP_METHODS) {
const response = await request.fetch(`${server.PREFIX}/a/redirect1`, { method });
expect(response.headers()['location']).toBe('/b/c/redirect2');
expect(response.status()).toBe(302);
}
await request.dispose();
});
it('should follow redirects up to maxRedirects limit set in newContext', async ({ playwright, server }) => {
server.setRedirect('/a/redirect1', '/b/c/redirect2');
server.setRedirect('/b/c/redirect2', '/b/c/redirect3');
server.setRedirect('/b/c/redirect3', '/b/c/redirect4');
server.setRedirect('/b/c/redirect4', '/simple.json');
for (const maxRedirects of [1, 2, 3, 4]) {
const request = await playwright.request.newContext({ maxRedirects });
for (const method of HTTP_METHODS) {
if (maxRedirects < 4) {
await expect(async () => request.fetch(`${server.PREFIX}/a/redirect1`, { method }))
.rejects.toThrow('Max redirect count exceeded');
} else {
const response = await request.fetch(`${server.PREFIX}/a/redirect1`, { method });
expect(response.status()).toBe(200);
}
}
await request.dispose();
}
});
it('should use maxRedirects from fetch when provided, overriding newContext', async ({ playwright, server }) => {
server.setRedirect('/a/redirect1', '/b/c/redirect2');
server.setRedirect('/b/c/redirect2', '/b/c/redirect3');
server.setRedirect('/b/c/redirect3', '/b/c/redirect4');
server.setRedirect('/b/c/redirect4', '/simple.json');
const request = await playwright.request.newContext({ maxRedirects: 1 });
for (const method of HTTP_METHODS) {
const response = await request.fetch(`${server.PREFIX}/a/redirect1`, { method, maxRedirects: 4 });
expect(response.status()).toBe(200);
}
await request.dispose();
});
it('should keep headers capitalization', async ({ playwright, server }) => { it('should keep headers capitalization', async ({ playwright, server }) => {
const request = await playwright.request.newContext(); const request = await playwright.request.newContext();
const [serverRequest, response] = await Promise.all([ const [serverRequest, response] = await Promise.all([