feat: add failOnStatusCode option to API request context (#34346)

This commit is contained in:
JacksonLei123 2025-02-11 16:23:11 -05:00 committed by GitHub
parent 46b048f018
commit 5a76b17c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 152 additions and 1 deletions

View File

@ -21,6 +21,9 @@ Creates new instances of [APIRequestContext].
### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%% ### option: APIRequest.newContext.extraHTTPHeaders = %%-context-option-extrahttpheaders-%%
* since: v1.16 * since: v1.16
### option: APIRequest.newContext.apiRequestFailsOnErrorStatus = %%-context-option-apiRequestFailsOnErrorStatus-%%
* since: v1.51
### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%% ### option: APIRequest.newContext.httpCredentials = %%-context-option-httpcredentials-%%
* since: v1.16 * since: v1.16

View File

@ -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. 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 ## context-option-offline
- `offline` <[boolean]> - `offline` <[boolean]>
@ -996,6 +1001,7 @@ between the same pixel in compared images, between zero (strict) and one (lax),
- %%-context-option-locale-%% - %%-context-option-locale-%%
- %%-context-option-permissions-%% - %%-context-option-permissions-%%
- %%-context-option-extrahttpheaders-%% - %%-context-option-extrahttpheaders-%%
- %%-context-option-apiRequestFailsOnErrorStatus-%%
- %%-context-option-offline-%% - %%-context-option-offline-%%
- %%-context-option-httpcredentials-%% - %%-context-option-httpcredentials-%%
- %%-context-option-colorscheme-%% - %%-context-option-colorscheme-%%

View File

@ -371,6 +371,7 @@ scheme.PlaywrightNewRequestParams = tObject({
userAgent: tOptional(tString), userAgent: tOptional(tString),
ignoreHTTPSErrors: tOptional(tBoolean), ignoreHTTPSErrors: tOptional(tBoolean),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
clientCertificates: tOptional(tArray(tObject({ clientCertificates: tOptional(tArray(tObject({
origin: tString, origin: tString,
cert: tOptional(tBinary), cert: tOptional(tBinary),
@ -600,6 +601,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
})), })),
permissions: tOptional(tArray(tString)), permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean), offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({ httpCredentials: tOptional(tObject({
username: tString, username: tString,
@ -687,6 +689,7 @@ scheme.BrowserNewContextParams = tObject({
})), })),
permissions: tOptional(tArray(tString)), permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean), offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({ httpCredentials: tOptional(tObject({
username: tString, username: tString,
@ -757,6 +760,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
})), })),
permissions: tOptional(tArray(tString)), permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean), offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({ httpCredentials: tOptional(tObject({
username: tString, username: tString,
@ -2664,6 +2668,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
})), })),
permissions: tOptional(tArray(tString)), permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean), offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({ httpCredentials: tOptional(tObject({
username: tString, username: tString,

View File

@ -50,6 +50,7 @@ import type { Readable, TransformCallback } from 'stream';
type FetchRequestOptions = { type FetchRequestOptions = {
userAgent: string; userAgent: string;
extraHTTPHeaders?: HeadersArray; extraHTTPHeaders?: HeadersArray;
apiRequestFailsOnErrorStatus?: boolean;
httpCredentials?: HTTPCredentials; httpCredentials?: HTTPCredentials;
proxy?: ProxySettings; proxy?: ProxySettings;
timeoutSettings: TimeoutSettings; timeoutSettings: TimeoutSettings;
@ -210,7 +211,8 @@ export abstract class APIRequestContext extends SdkObject {
}); });
const fetchUid = this._storeResponseBody(fetchResponse.body); const fetchUid = this._storeResponseBody(fetchResponse.body);
this.fetchLog.set(fetchUid, controller.metadata.log); 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 = ''; let responseText = '';
if (fetchResponse.body.byteLength) { if (fetchResponse.body.byteLength) {
let text = fetchResponse.body.toString('utf8'); let text = fetchResponse.body.toString('utf8');
@ -605,6 +607,7 @@ export class BrowserContextAPIRequestContext extends APIRequestContext {
return { return {
userAgent: this._context._options.userAgent || this._context._browser.userAgent(), userAgent: this._context._options.userAgent || this._context._browser.userAgent(),
extraHTTPHeaders: this._context._options.extraHTTPHeaders, extraHTTPHeaders: this._context._options.extraHTTPHeaders,
apiRequestFailsOnErrorStatus: this._context._options.apiRequestFailsOnErrorStatus,
httpCredentials: this._context._options.httpCredentials, httpCredentials: this._context._options.httpCredentials,
proxy: this._context._options.proxy || this._context._browser.options.proxy, proxy: this._context._options.proxy || this._context._browser.options.proxy,
timeoutSettings: this._context._timeoutSettings, timeoutSettings: this._context._timeoutSettings,
@ -656,6 +659,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
baseURL: options.baseURL, baseURL: options.baseURL,
userAgent: options.userAgent || getUserAgent(), userAgent: options.userAgent || getUserAgent(),
extraHTTPHeaders: options.extraHTTPHeaders, extraHTTPHeaders: options.extraHTTPHeaders,
apiRequestFailsOnErrorStatus: !!options.apiRequestFailsOnErrorStatus,
ignoreHTTPSErrors: !!options.ignoreHTTPSErrors, ignoreHTTPSErrors: !!options.ignoreHTTPSErrors,
httpCredentials: options.httpCredentials, httpCredentials: options.httpCredentials,
clientCertificates: options.clientCertificates, clientCertificates: options.clientCertificates,

View File

@ -9742,6 +9742,12 @@ export interface Browser {
*/ */
acceptDownloads?: boolean; 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), * 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), * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),
@ -14806,6 +14812,12 @@ export interface BrowserType<Unused = {}> {
*/ */
acceptDownloads?: boolean; 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. * **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; 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. * **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 * @param options
*/ */
newContext(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 * Methods like
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get) * [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
@ -22117,6 +22141,12 @@ export interface BrowserContextOptions {
*/ */
acceptDownloads?: boolean; 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), * 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), * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route),

View File

@ -623,6 +623,7 @@ export type PlaywrightNewRequestParams = {
userAgent?: string, userAgent?: string,
ignoreHTTPSErrors?: boolean, ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
clientCertificates?: { clientCertificates?: {
origin: string, origin: string,
cert?: Binary, cert?: Binary,
@ -654,6 +655,7 @@ export type PlaywrightNewRequestOptions = {
userAgent?: string, userAgent?: string,
ignoreHTTPSErrors?: boolean, ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
clientCertificates?: { clientCertificates?: {
origin: string, origin: string,
cert?: Binary, cert?: Binary,
@ -1027,6 +1029,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -1108,6 +1111,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -1224,6 +1228,7 @@ export type BrowserNewContextParams = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -1291,6 +1296,7 @@ export type BrowserNewContextOptions = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -1361,6 +1367,7 @@ export type BrowserNewContextForReuseParams = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -1428,6 +1435,7 @@ export type BrowserNewContextForReuseOptions = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -4794,6 +4802,7 @@ export type AndroidDeviceLaunchBrowserParams = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,
@ -4859,6 +4868,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
}, },
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: NameValue[], extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean, offline?: boolean,
httpCredentials?: { httpCredentials?: {
username: string, username: string,

View File

@ -520,6 +520,7 @@ ContextOptions:
extraHTTPHeaders: extraHTTPHeaders:
type: array? type: array?
items: NameValue items: NameValue
apiRequestFailsOnErrorStatus: boolean?
offline: boolean? offline: boolean?
httpCredentials: httpCredentials:
type: object? type: object?
@ -751,6 +752,7 @@ Playwright:
extraHTTPHeaders: extraHTTPHeaders:
type: array? type: array?
items: NameValue items: NameValue
apiRequestFailsOnErrorStatus: boolean?
clientCertificates: clientCertificates:
type: array? type: array?
items: items:

View File

@ -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();
});

View File

@ -536,3 +536,27 @@ it('should retry ECONNRESET', {
expect(requestCount).toBe(4); expect(requestCount).toBe(4);
await request.dispose(); 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();
});