diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md index f1465cb8c6..37bb2cb999 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md @@ -21,6 +21,9 @@ Creates new instances of [APIRequestContext]. ### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%% * since: v1.16 +### option: APIRequest.newContext.apiRequestFailsOnErrorStatus = %%-context-option-apiRequestFailsOnErrorStatus-%% +* since: v1.51 + ### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%% * since: v1.16 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 920ba10943..e3968f622d 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -639,6 +639,11 @@ A list of permissions to grant to all pages in this context. See An object containing additional HTTP headers to be sent with every request. Defaults to none. +## context-option-apiRequestFailsOnErrorStatus +- `apiRequestFailsOnErrorStatus` <[boolean]> + +An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By default, response object is returned for all status codes. + ## context-option-offline - `offline` <[boolean]> @@ -996,6 +1001,7 @@ between the same pixel in compared images, between zero (strict) and one (lax), - %%-context-option-locale-%% - %%-context-option-permissions-%% - %%-context-option-extrahttpheaders-%% +- %%-context-option-apiRequestFailsOnErrorStatus-%% - %%-context-option-offline-%% - %%-context-option-httpcredentials-%% - %%-context-option-colorscheme-%% diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 7072464089..b9f8e9c405 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -371,6 +371,7 @@ scheme.PlaywrightNewRequestParams = tObject({ userAgent: tOptional(tString), ignoreHTTPSErrors: tOptional(tBoolean), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + apiRequestFailsOnErrorStatus: tOptional(tBoolean), clientCertificates: tOptional(tArray(tObject({ origin: tString, cert: tOptional(tBinary), @@ -600,6 +601,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({ })), permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + apiRequestFailsOnErrorStatus: tOptional(tBoolean), offline: tOptional(tBoolean), httpCredentials: tOptional(tObject({ username: tString, @@ -687,6 +689,7 @@ scheme.BrowserNewContextParams = tObject({ })), permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + apiRequestFailsOnErrorStatus: tOptional(tBoolean), offline: tOptional(tBoolean), httpCredentials: tOptional(tObject({ username: tString, @@ -757,6 +760,7 @@ scheme.BrowserNewContextForReuseParams = tObject({ })), permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + apiRequestFailsOnErrorStatus: tOptional(tBoolean), offline: tOptional(tBoolean), httpCredentials: tOptional(tObject({ username: tString, @@ -2664,6 +2668,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({ })), permissions: tOptional(tArray(tString)), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + apiRequestFailsOnErrorStatus: tOptional(tBoolean), offline: tOptional(tBoolean), httpCredentials: tOptional(tObject({ username: tString, diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 9210340c8c..efd67f66b1 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -50,6 +50,7 @@ import type { Readable, TransformCallback } from 'stream'; type FetchRequestOptions = { userAgent: string; extraHTTPHeaders?: HeadersArray; + apiRequestFailsOnErrorStatus?: boolean; httpCredentials?: HTTPCredentials; proxy?: ProxySettings; timeoutSettings: TimeoutSettings; @@ -210,7 +211,8 @@ export abstract class APIRequestContext extends SdkObject { }); const fetchUid = this._storeResponseBody(fetchResponse.body); this.fetchLog.set(fetchUid, controller.metadata.log); - if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) { + const failOnStatusCode = params.failOnStatusCode !== undefined ? params.failOnStatusCode : !!defaults.apiRequestFailsOnErrorStatus; + if (failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) { let responseText = ''; if (fetchResponse.body.byteLength) { let text = fetchResponse.body.toString('utf8'); @@ -605,6 +607,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext { return { userAgent: this._context._options.userAgent || this._context._browser.userAgent(), extraHTTPHeaders: this._context._options.extraHTTPHeaders, + apiRequestFailsOnErrorStatus: this._context._options.apiRequestFailsOnErrorStatus, httpCredentials: this._context._options.httpCredentials, proxy: this._context._options.proxy || this._context._browser.options.proxy, timeoutSettings: this._context._timeoutSettings, @@ -656,6 +659,7 @@ export class GlobalAPIRequestContext extends APIRequestContext { baseURL: options.baseURL, userAgent: options.userAgent || getUserAgent(), extraHTTPHeaders: options.extraHTTPHeaders, + apiRequestFailsOnErrorStatus: !!options.apiRequestFailsOnErrorStatus, ignoreHTTPSErrors: !!options.ignoreHTTPSErrors, httpCredentials: options.httpCredentials, clientCertificates: options.clientCertificates, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index cdd4dcb930..9ba88071f4 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9742,6 +9742,12 @@ export interface Browser { */ acceptDownloads?: boolean; + /** + * An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By + * default, response object is returned for all status codes. + */ + apiRequestFailsOnErrorStatus?: boolean; + /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), @@ -14806,6 +14812,12 @@ export interface BrowserType { */ acceptDownloads?: boolean; + /** + * An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By + * default, response object is returned for all status codes. + */ + apiRequestFailsOnErrorStatus?: boolean; + /** * **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality. * @@ -16696,6 +16708,12 @@ export interface AndroidDevice { */ acceptDownloads?: boolean; + /** + * An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By + * default, response object is returned for all status codes. + */ + apiRequestFailsOnErrorStatus?: boolean; + /** * **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality. * @@ -17542,6 +17560,12 @@ export interface APIRequest { * @param options */ newContext(options?: { + /** + * An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By + * default, response object is returned for all status codes. + */ + apiRequestFailsOnErrorStatus?: boolean; + /** * Methods like * [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get) @@ -22117,6 +22141,12 @@ export interface BrowserContextOptions { */ acceptDownloads?: boolean; + /** + * An object containing an option to throw an error when API request returns status codes other than 2xx and 3xx. By + * default, response object is returned for all status codes. + */ + apiRequestFailsOnErrorStatus?: boolean; + /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 522bbb779e..117f0e287e 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -623,6 +623,7 @@ export type PlaywrightNewRequestParams = { userAgent?: string, ignoreHTTPSErrors?: boolean, extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, clientCertificates?: { origin: string, cert?: Binary, @@ -654,6 +655,7 @@ export type PlaywrightNewRequestOptions = { userAgent?: string, ignoreHTTPSErrors?: boolean, extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, clientCertificates?: { origin: string, cert?: Binary, @@ -1027,6 +1029,7 @@ export type BrowserTypeLaunchPersistentContextParams = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -1108,6 +1111,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -1224,6 +1228,7 @@ export type BrowserNewContextParams = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -1291,6 +1296,7 @@ export type BrowserNewContextOptions = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -1361,6 +1367,7 @@ export type BrowserNewContextForReuseParams = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -1428,6 +1435,7 @@ export type BrowserNewContextForReuseOptions = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -4794,6 +4802,7 @@ export type AndroidDeviceLaunchBrowserParams = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, @@ -4859,6 +4868,7 @@ export type AndroidDeviceLaunchBrowserOptions = { }, permissions?: string[], extraHTTPHeaders?: NameValue[], + apiRequestFailsOnErrorStatus?: boolean, offline?: boolean, httpCredentials?: { username: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 316ac8c2a8..1044f8ccb2 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -520,6 +520,7 @@ ContextOptions: extraHTTPHeaders: type: array? items: NameValue + apiRequestFailsOnErrorStatus: boolean? offline: boolean? httpCredentials: type: object? @@ -751,6 +752,7 @@ Playwright: extraHTTPHeaders: type: array? items: NameValue + apiRequestFailsOnErrorStatus: boolean? clientCertificates: type: array? items: diff --git a/tests/library/browsercontext-fetchFailOnStatusCode.spec.ts b/tests/library/browsercontext-fetchFailOnStatusCode.spec.ts new file mode 100644 index 0000000000..e922218d68 --- /dev/null +++ b/tests/library/browsercontext-fetchFailOnStatusCode.spec.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browserTest as it, expect } from '../config/browserTest'; + +it('should throw when apiRequestFailsOnErrorStatus is set to true inside BrowserContext options', async ({ browser, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const context = await browser.newContext({ apiRequestFailsOnErrorStatus: true }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e); + expect(error.message).toContain('404 Not Found'); + await context.close(); +}); + +it('should not throw when failOnStatusCode is set to false inside BrowserContext options', async ({ browser, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const context = await browser.newContext({ apiRequestFailsOnErrorStatus: false }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e); + expect(error.message).toBeUndefined(); + await context.close(); +}); + +it('should throw when apiRequestFailsOnErrorStatus is set to true inside browserType.launchPersistentContext options', async ({ browserType, server, createUserDataDir }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const userDataDir = await createUserDataDir(); + const context = await browserType.launchPersistentContext(userDataDir, { apiRequestFailsOnErrorStatus: true }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e); + expect(error.message).toContain('404 Not Found'); + await context.close(); +}); + +it('should not throw when apiRequestFailsOnErrorStatus is set to false inside browserType.launchPersistentContext options', async ({ browserType, server, createUserDataDir }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const userDataDir = await createUserDataDir(); + const context = await browserType.launchPersistentContext(userDataDir, { apiRequestFailsOnErrorStatus: false }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const error = await context.request.fetch(server.EMPTY_PAGE).catch(e => e); + expect(error.message).toBeUndefined(); + await context.close(); +}); diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts index 2e146874c1..0ad73bb9b6 100644 --- a/tests/library/global-fetch.spec.ts +++ b/tests/library/global-fetch.spec.ts @@ -536,3 +536,27 @@ it('should retry ECONNRESET', { expect(requestCount).toBe(4); await request.dispose(); }); + +it('should throw when apiRequestFailsOnErrorStatus is set to true inside APIRequest context options', async ({ playwright, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: true }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const error = await request.fetch(server.EMPTY_PAGE).catch(e => e); + expect(error.message).toContain('404 Not Found'); + await request.dispose(); +}); + +it('should not throw when apiRequestFailsOnErrorStatus is set to false inside APIRequest context options', async ({ playwright, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/34204' }); + const request = await playwright.request.newContext({ apiRequestFailsOnErrorStatus: false }); + server.setRoute('/empty.html', (req, res) => { + res.writeHead(404, { 'Content-Length': 10, 'Content-Type': 'text/plain' }); + res.end('Not found.'); + }); + const response = await request.fetch(server.EMPTY_PAGE); + expect(response.status()).toBe(404); + await request.dispose(); +});