diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md index c984b9f121..d27e22b034 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md @@ -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]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index da5df0f346..c5c836db9f 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -17573,6 +17573,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. */ diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index da56445699..7f3f064937 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -386,6 +386,7 @@ scheme.PlaywrightNewRequestParams = tObject({ passphrase: tOptional(tString), pfx: tOptional(tBinary), }))), + maxRedirects: tOptional(tNumber), httpCredentials: tOptional(tObject({ username: tString, password: tString, diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index e7647759a2..9760ea89a1 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -56,6 +56,7 @@ type FetchRequestOptions = { proxy?: ProxySettings; timeoutSettings: TimeoutSettings; ignoreHTTPSErrors?: boolean; + maxRedirects?: number; baseURL?: string; clientCertificates?: types.BrowserContextOptions['clientCertificates']; }; @@ -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); @@ -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), @@ -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; @@ -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, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index da5df0f346..c5c836db9f 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17573,6 +17573,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. */ diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 80aca67578..c6767029da 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -644,6 +644,7 @@ export type PlaywrightNewRequestParams = { passphrase?: string, pfx?: Binary, }[], + maxRedirects?: number, httpCredentials?: { username: string, password: string, @@ -676,6 +677,7 @@ export type PlaywrightNewRequestOptions = { passphrase?: string, pfx?: Binary, }[], + maxRedirects?: number, httpCredentials?: { username: string, password: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 3c2366f166..ce0c3f800f 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -770,6 +770,7 @@ Playwright: key: binary? passphrase: string? pfx: binary? + maxRedirects: number? httpCredentials: type: object? properties: diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index 53db88e325..95b0555db3 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -437,6 +437,8 @@ 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'); @@ -444,7 +446,7 @@ it('should throw an error when maxRedirects is exceeded', async ({ playwright, s 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'); } @@ -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); @@ -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([