feat(fetch): introduce failOnStatusCode (#8896)

This commit is contained in:
Yury Semikhatsky 2021-09-13 15:38:27 -07:00 committed by GitHub
parent bb33b8923e
commit b79be5d98d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 73 additions and 18 deletions

View file

@ -41,6 +41,12 @@ Allows to set post data of the fetch.
Request timeout in milliseconds. Request timeout in milliseconds.
### option: FetchRequest.fetch.failOnStatusCode
- `failOnStatusCode` <[boolean]>
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
for all status codes.
## async method: FetchRequest.get ## async method: FetchRequest.get
- returns: <[FetchResponse]> - returns: <[FetchResponse]>
@ -67,6 +73,12 @@ Allows to set HTTP headers.
Request timeout in milliseconds. Request timeout in milliseconds.
### option: FetchRequest.get.failOnStatusCode
- `failOnStatusCode` <[boolean]>
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
for all status codes.
## async method: FetchRequest.post ## async method: FetchRequest.post
- returns: <[FetchResponse]> - returns: <[FetchResponse]>
@ -97,3 +109,9 @@ Allows to set post data of the fetch.
- `timeout` <[float]> - `timeout` <[float]>
Request timeout in milliseconds. Request timeout in milliseconds.
### option: FetchRequest.post.failOnStatusCode
- `failOnStatusCode` <[boolean]>
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
for all status codes.

View file

@ -28,7 +28,8 @@ export type FetchOptions = {
method?: string, method?: string,
headers?: Headers, headers?: Headers,
data?: string | Buffer, data?: string | Buffer,
timeout?: number timeout?: number,
failOnStatusCode?: boolean,
}; };
export class FetchRequest implements api.FetchRequest { export class FetchRequest implements api.FetchRequest {
@ -44,6 +45,7 @@ export class FetchRequest implements api.FetchRequest {
params?: { [key: string]: string; }; params?: { [key: string]: string; };
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
timeout?: number; timeout?: number;
failOnStatusCode?: boolean;
}): Promise<FetchResponse> { }): Promise<FetchResponse> {
return this.fetch(urlOrRequest, { return this.fetch(urlOrRequest, {
...options, ...options,
@ -58,6 +60,7 @@ export class FetchRequest implements api.FetchRequest {
headers?: { [key: string]: string; }; headers?: { [key: string]: string; };
data?: string | Buffer; data?: string | Buffer;
timeout?: number; timeout?: number;
failOnStatusCode?: boolean;
}): Promise<FetchResponse> { }): Promise<FetchResponse> {
return this.fetch(urlOrRequest, { return this.fetch(urlOrRequest, {
...options, ...options,
@ -86,6 +89,7 @@ export class FetchRequest implements api.FetchRequest {
headers, headers,
postData, postData,
timeout: options.timeout, timeout: options.timeout,
failOnStatusCode: options.failOnStatusCode,
}); });
if (result.error) if (result.error)
throw new Error(`Request failed: ${result.error}`); throw new Error(`Request failed: ${result.error}`);

View file

@ -115,6 +115,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
headers: params.headers ? headersArrayToObject(params.headers, false) : undefined, headers: params.headers ? headersArrayToObject(params.headers, false) : undefined,
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined, postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
timeout: params.timeout, timeout: params.timeout,
failOnStatusCode: params.failOnStatusCode,
}); });
let response; let response;
if (fetchResponse) { if (fetchResponse) {

View file

@ -862,6 +862,7 @@ export type BrowserContextFetchParams = {
headers?: NameValue[], headers?: NameValue[],
postData?: Binary, postData?: Binary,
timeout?: number, timeout?: number,
failOnStatusCode?: boolean,
}; };
export type BrowserContextFetchOptions = { export type BrowserContextFetchOptions = {
params?: NameValue[], params?: NameValue[],
@ -869,6 +870,7 @@ export type BrowserContextFetchOptions = {
headers?: NameValue[], headers?: NameValue[],
postData?: Binary, postData?: Binary,
timeout?: number, timeout?: number,
failOnStatusCode?: boolean,
}; };
export type BrowserContextFetchResult = { export type BrowserContextFetchResult = {
response?: FetchResponse, response?: FetchResponse,

View file

@ -625,6 +625,7 @@ BrowserContext:
items: NameValue items: NameValue
postData: binary? postData: binary?
timeout: number? timeout: number?
failOnStatusCode: boolean?
returns: returns:
response: FetchResponse? response: FetchResponse?
error: string? error: string?

View file

@ -399,6 +399,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
headers: tOptional(tArray(tType('NameValue'))), headers: tOptional(tArray(tType('NameValue'))),
postData: tOptional(tBinary), postData: tOptional(tBinary),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
failOnStatusCode: tOptional(tBoolean),
}); });
scheme.BrowserContextFetchResponseBodyParams = tObject({ scheme.BrowserContextFetchResponseBodyParams = tObject({
fetchUid: tString, fetchUid: tString,

View file

@ -74,6 +74,8 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet
const fetchResponse = await sendRequest(context, requestUrl, options, params.postData); const fetchResponse = await sendRequest(context, requestUrl, options, params.postData);
const fetchUid = context.storeFetchResponseBody(fetchResponse.body); const fetchUid = context.storeFetchResponseBody(fetchResponse.body);
if (params.failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400))
return { error: `${fetchResponse.status} ${fetchResponse.statusText}` };
return { fetchResponse: { ...fetchResponse, fetchUid } }; return { fetchResponse: { ...fetchResponse, fetchUid } };
} catch (e) { } catch (e) {
return { error: String(e) }; return { error: String(e) };

View file

@ -379,6 +379,7 @@ export type FetchOptions = {
headers?: { [name: string]: string }, headers?: { [name: string]: string },
postData?: Buffer, postData?: Buffer,
timeout?: number, timeout?: number,
failOnStatusCode?: boolean,
}; };
export type FetchResponse = { export type FetchResponse = {

View file

@ -128,13 +128,16 @@ it('should add session cookies to request', async ({context, server}) => {
expect(req.headers.cookie).toEqual('username=John Doe'); expect(req.headers.cookie).toEqual('username=John Doe');
}); });
it('should support queryParams', async ({context, server}) => { for (const method of ['get', 'post', 'fetch']) {
let request; it(`${method} should support queryParams`, async ({context, server}) => {
server.setRoute('/empty.html', (req, res) => { let request;
request = req; const url = new URL(server.EMPTY_PAGE);
server.serveFile(req, res); url.searchParams.set('p1', 'v1');
}); url.searchParams.set('парам2', 'знач2');
for (const method of ['get', 'post', 'fetch']) { server.setRoute(url.pathname + url.search, (req, res) => {
request = req;
server.serveFile(req, res);
});
await context.request[method](server.EMPTY_PAGE + '?p1=foo', { await context.request[method](server.EMPTY_PAGE + '?p1=foo', {
params: { params: {
'p1': 'v1', 'p1': 'v1',
@ -144,8 +147,15 @@ it('should support queryParams', async ({context, server}) => {
const params = new URLSearchParams(request.url.substr(request.url.indexOf('?'))); const params = new URLSearchParams(request.url.substr(request.url.indexOf('?')));
expect(params.get('p1')).toEqual('v1'); expect(params.get('p1')).toEqual('v1');
expect(params.get('парам2')).toEqual('знач2'); expect(params.get('парам2')).toEqual('знач2');
} });
});
it(`${method} should support failOnStatusCode`, async ({context, server}) => {
const error = await context.request[method](server.PREFIX + '/does-not-exist.html', {
failOnStatusCode: true
}).catch(e => e);
expect(error.message).toContain('Request failed: 404 Not Found');
});
}
it('should not add context cookie if cookie header passed as a parameter', async ({context, server}) => { it('should not add context cookie if cookie header passed as a parameter', async ({context, server}) => {
await context.addCookies([{ await context.addCookies([{

15
types/types.d.ts vendored
View file

@ -12636,6 +12636,11 @@ export interface FetchRequest {
*/ */
data?: string|Buffer; data?: string|Buffer;
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
failOnStatusCode?: boolean;
/** /**
* Allows to set HTTP headers. * Allows to set HTTP headers.
*/ */
@ -12664,6 +12669,11 @@ export interface FetchRequest {
* @param options * @param options
*/ */
get(urlOrRequest: string|Request, options?: { get(urlOrRequest: string|Request, options?: {
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
failOnStatusCode?: boolean;
/** /**
* Allows to set HTTP headers. * Allows to set HTTP headers.
*/ */
@ -12692,6 +12702,11 @@ export interface FetchRequest {
*/ */
data?: string|Buffer; data?: string|Buffer;
/**
* Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
*/
failOnStatusCode?: boolean;
/** /**
* Allows to set HTTP headers. * Allows to set HTTP headers.
*/ */

View file

@ -238,10 +238,10 @@ class TestServer {
request.on('data', chunk => body = Buffer.concat([body, chunk])); request.on('data', chunk => body = Buffer.concat([body, chunk]));
request.on('end', () => resolve(body)); request.on('end', () => resolve(body));
}); });
const pathName = url.parse(request.url).pathname; const path = url.parse(request.url).path;
this.debugServer(`request ${request.method} ${pathName}`); this.debugServer(`request ${request.method} ${path}`);
if (this._auths.has(pathName)) { if (this._auths.has(path)) {
const auth = this._auths.get(pathName); const auth = this._auths.get(path);
const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString();
this.debugServer(`request credentials ${credentials}`); this.debugServer(`request credentials ${credentials}`);
this.debugServer(`actual credentials ${auth.username}:${auth.password}`); this.debugServer(`actual credentials ${auth.username}:${auth.password}`);
@ -253,11 +253,11 @@ class TestServer {
} }
} }
// Notify request subscriber. // Notify request subscriber.
if (this._requestSubscribers.has(pathName)) { if (this._requestSubscribers.has(path)) {
this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request); this._requestSubscribers.get(path)[fulfillSymbol].call(null, request);
this._requestSubscribers.delete(pathName); this._requestSubscribers.delete(path);
} }
const handler = this._routes.get(pathName); const handler = this._routes.get(path);
if (handler) { if (handler) {
handler.call(null, request, response); handler.call(null, request, response);
} else { } else {