Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/src/api/class-apirequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ for all status codes.
### option: APIRequest.newContext.ignoreHTTPSErrors = %%-context-option-ignorehttpserrors-%%
* 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
* since: v1.16
- `timeout` <[float]>
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17569,6 +17569,13 @@ export interface APIRequest {
*/
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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ scheme.PlaywrightNewRequestParams = tObject({
passphrase: tOptional(tString),
pfx: tOptional(tBinary),
}))),
maxRedirects: tOptional(tNumber),
httpCredentials: tOptional(tObject({
username: tString,
password: tString,
Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type FetchRequestOptions = {
proxy?: ProxySettings;
timeoutSettings: TimeoutSettings;
ignoreHTTPSErrors?: boolean;
maxRedirects?: number;
baseURL?: string;
clientCertificates?: types.BrowserContextOptions['clientCertificates'];
};
Expand Down Expand Up @@ -185,6 +186,8 @@ export abstract class APIRequestContext extends SdkObject {
if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass))
agent = createProxyAgent(proxy);

let maxRedirects = params.maxRedirects ?? (defaults.maxRedirects ?? 20);
maxRedirects = maxRedirects === 0 ? -1 : maxRedirects;

const timeout = defaults.timeoutSettings.timeout(params);
const deadline = timeout && (monotonicTime() + timeout);
Expand All @@ -193,7 +196,7 @@ export abstract class APIRequestContext extends SdkObject {
method,
headers,
agent,
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
maxRedirects,
timeout,
deadline,
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
Expand Down Expand Up @@ -371,7 +374,7 @@ export abstract class APIRequestContext extends SdkObject {
}

if (redirectStatus.includes(response.statusCode!) && options.maxRedirects >= 0) {
if (!options.maxRedirects) {
if (options.maxRedirects === 0) {
reject(new Error('Max redirect count exceeded'));
request.destroy();
return;
Expand Down Expand Up @@ -662,6 +665,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
extraHTTPHeaders: options.extraHTTPHeaders,
failOnStatusCode: !!options.failOnStatusCode,
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
maxRedirects: options.maxRedirects,
httpCredentials: options.httpCredentials,
clientCertificates: options.clientCertificates,
proxy,
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17569,6 +17569,13 @@ export interface APIRequest {
*/
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.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ export type PlaywrightNewRequestParams = {
passphrase?: string,
pfx?: Binary,
}[],
maxRedirects?: number,
httpCredentials?: {
username: string,
password: string,
Expand Down Expand Up @@ -663,6 +664,7 @@ export type PlaywrightNewRequestOptions = {
passphrase?: string,
pfx?: Binary,
}[],
maxRedirects?: number,
httpCredentials?: {
username: string,
password: string,
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ Playwright:
key: binary?
passphrase: string?
pfx: binary?
maxRedirects: number?
httpCredentials:
type: object?
properties:
Expand Down
56 changes: 53 additions & 3 deletions tests/library/global-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,14 +437,16 @@ it('should return body for failing requests', async ({ playwright, server }) =>
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 }) => {
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();
for (const method of ['GET', 'PUT', 'POST', 'OPTIONS', 'HEAD', 'PATCH']) {
for (const method of HTTP_METHODS) {
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');
}
Expand All @@ -456,7 +458,7 @@ it('should not follow redirects when maxRedirects is set to 0', async ({ playwri
server.setRedirect('/b/c/redirect2', '/simple.json');

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 });
expect(response.headers()['location']).toBe('/b/c/redirect2');
expect(response.status()).toBe(302);
Expand All @@ -469,11 +471,59 @@ it('should throw an error when maxRedirects is less than 0', async ({ playwright
server.setRedirect('/b/c/redirect2', '/simple.json');

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 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 }) => {
const request = await playwright.request.newContext();
const [serverRequest, response] = await Promise.all([
Expand Down
Loading