Skip to content

feat: add failOnStatusCode option to API request context #34346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 11, 2025
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
3 changes: 3 additions & 0 deletions docs/src/api/class-apirequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,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]>

Expand Down Expand Up @@ -965,6 +970,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-%%
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,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),
Expand Down Expand Up @@ -568,6 +569,7 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
})),
permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({
username: tString,
Expand Down Expand Up @@ -654,6 +656,7 @@ scheme.BrowserNewContextParams = tObject({
})),
permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({
username: tString,
Expand Down Expand Up @@ -723,6 +726,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
})),
permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({
username: tString,
Expand Down Expand Up @@ -2620,6 +2624,7 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
})),
permissions: tOptional(tArray(tString)),
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
apiRequestFailsOnErrorStatus: tOptional(tBoolean),
offline: tOptional(tBoolean),
httpCredentials: tOptional(tObject({
username: tString,
Expand Down
6 changes: 5 additions & 1 deletion packages/playwright-core/src/server/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { TLSSocket } from 'tls';
type FetchRequestOptions = {
userAgent: string;
extraHTTPHeaders?: HeadersArray;
apiRequestFailsOnErrorStatus?: boolean;
httpCredentials?: HTTPCredentials;
proxy?: ProxySettings;
timeoutSettings: TimeoutSettings;
Expand Down Expand Up @@ -205,7 +206,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');
Expand Down Expand Up @@ -610,6 +612,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,
Expand Down Expand Up @@ -661,6 +664,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,
Expand Down
30 changes: 30 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9683,6 +9683,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),
Expand Down Expand Up @@ -14685,6 +14691,12 @@ export interface BrowserType<Unused = {}> {
*/
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.
*
Expand Down Expand Up @@ -16568,6 +16580,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.
*
Expand Down Expand Up @@ -17407,6 +17425,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)
Expand Down Expand Up @@ -21877,6 +21901,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),
Expand Down
10 changes: 10 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ export type PlaywrightNewRequestParams = {
userAgent?: string,
ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
clientCertificates?: {
origin: string,
cert?: Binary,
Expand Down Expand Up @@ -619,6 +620,7 @@ export type PlaywrightNewRequestOptions = {
userAgent?: string,
ignoreHTTPSErrors?: boolean,
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
clientCertificates?: {
origin: string,
cert?: Binary,
Expand Down Expand Up @@ -992,6 +994,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -1072,6 +1075,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -1187,6 +1191,7 @@ export type BrowserNewContextParams = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -1253,6 +1258,7 @@ export type BrowserNewContextOptions = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -1322,6 +1328,7 @@ export type BrowserNewContextForReuseParams = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -1388,6 +1395,7 @@ export type BrowserNewContextForReuseOptions = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -4737,6 +4745,7 @@ export type AndroidDeviceLaunchBrowserParams = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down Expand Up @@ -4801,6 +4810,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
},
permissions?: string[],
extraHTTPHeaders?: NameValue[],
apiRequestFailsOnErrorStatus?: boolean,
offline?: boolean,
httpCredentials?: {
username: string,
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ ContextOptions:
extraHTTPHeaders:
type: array?
items: NameValue
apiRequestFailsOnErrorStatus: boolean?
offline: boolean?
httpCredentials:
type: object?
Expand Down Expand Up @@ -693,6 +694,7 @@ Playwright:
extraHTTPHeaders:
type: array?
items: NameValue
apiRequestFailsOnErrorStatus: boolean?
clientCertificates:
type: array?
items:
Expand Down
67 changes: 67 additions & 0 deletions tests/library/browsercontext-fetchFailOnStatusCode.spec.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should probably be renamed in accord with the new name of the property.

Original file line number Diff line number Diff line change
@@ -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();
});
24 changes: 24 additions & 0 deletions tests/library/global-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Loading