feat(network): remove Headers class and add headersArray method (#8749)

This commit is contained in:
Joel Einbinder 2021-09-07 13:27:53 -04:00 committed by GitHub
parent 4f4bc72828
commit e914f6bbc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 109 additions and 155 deletions

View file

@ -1,31 +0,0 @@
# class: Headers
HTTP request and response all headers collection.
## method: Headers.get
- returns: <[string|null]>
Returns header value for the given name.
### param: Headers.get.name
- `name` <[string]>
Header name, case-insensitive.
## method: Headers.getAll
- returns: <[Array]<[string]>>
Returns all header values for the given header name.
### param: Headers.getAll.name
- `name` <[string]>
Header name, case-insensitive.
## method: Headers.headerNames
- returns: <[Array]<[string]>>
Returns all header names in this headers collection.
## method: Headers.headers
- returns: <[Object]<[string], [string]>>
Returns all headers as a dictionary. Header names are normalized to lower case, multi-value headers are concatenated
using comma.

View file

@ -17,9 +17,9 @@ If request gets a 'redirect' response, the request is successfully finished with
request is issued to a redirected url. request is issued to a redirected url.
## async method: Request.allHeaders ## async method: Request.allHeaders
- returns: <[Headers]> - returns: <[Object]<[string], [string]>>
An object with all the request HTTP headers associated with this request. An object with all the request HTTP headers associated with this request. The header names are lower-cased.
## method: Request.failure ## method: Request.failure
- returns: <[null]|[string]> - returns: <[null]|[string]>
@ -61,6 +61,12 @@ Returns the [Frame] that initiated this request.
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Request.allHeaders`] instead. **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Request.allHeaders`] instead.
## async method: Request.headersArray
- returns: <[Array]<[Array]<[string]>>>
An array with all the request HTTP headers associated with this request. Unlike [`method: Request.allHeaders`], header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## method: Request.isNavigationRequest ## method: Request.isNavigationRequest
- returns: <[boolean]> - returns: <[boolean]>

View file

@ -3,7 +3,7 @@
[Response] class represents responses which are received by page. [Response] class represents responses which are received by page.
## async method: Response.allHeaders ## async method: Response.allHeaders
- returns: <[Headers]> - returns: <[Object]<[string], [string]>>
An object with all the response HTTP headers associated with this response. An object with all the response HTTP headers associated with this response.
@ -27,6 +27,12 @@ Returns the [Frame] that initiated this response.
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Response.allHeaders`] instead. **DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Response.allHeaders`] instead.
## async method: Response.headersArray
- returns: <[Array]<[Array]<[string]>>>
An array with all the request HTTP headers associated with this response. Unlike [`method: Response.allHeaders`], header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## async method: Response.json ## async method: Response.json
* langs: js, python * langs: js, python
- returns: <[Serializable]> - returns: <[Serializable]>

View file

@ -41,4 +41,3 @@ export { Video } from './video';
export { Worker } from './worker'; export { Worker } from './worker';
export { CDPSession } from './cdpSession'; export { CDPSession } from './cdpSession';
export { Playwright } from './playwright'; export { Playwright } from './playwright';
export { RawHeaders as Headers } from './network';

View file

@ -18,7 +18,7 @@ import { URLSearchParams } from 'url';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { Frame } from './frame'; import { Frame } from './frame';
import { Headers, HeadersArray, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types'; import { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
import fs from 'fs'; import fs from 'fs';
import * as mime from 'mime'; import * as mime from 'mime';
import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils'; import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils';
@ -29,7 +29,6 @@ import { Waiter } from './waiter';
import * as api from '../../types/types'; import * as api from '../../types/types';
import { URLMatch } from '../common/types'; import { URLMatch } from '../common/types';
import { urlMatches } from './clientHelper'; import { urlMatches } from './clientHelper';
import { MultiMap } from '../utils/multimap';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -58,8 +57,8 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
private _redirectedFrom: Request | null = null; private _redirectedFrom: Request | null = null;
private _redirectedTo: Request | null = null; private _redirectedTo: Request | null = null;
_failureText: string | null = null; _failureText: string | null = null;
_headers: Headers; _headers: channels.NameValue[];
private _rawHeadersPromise: Promise<RawHeaders> | undefined; private _allHeadersPromise: Promise<channels.NameValue[]> | undefined;
private _postData: Buffer | null; private _postData: Buffer | null;
_timing: ResourceTiming; _timing: ResourceTiming;
@ -76,7 +75,7 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom); this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom) if (this._redirectedFrom)
this._redirectedFrom._redirectedTo = this; this._redirectedFrom._redirectedTo = this;
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */); this._headers = initializer.headers;
this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null; this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null;
this._timing = { this._timing = {
startTime: 0, startTime: 0,
@ -136,20 +135,29 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
* @deprecated * @deprecated
*/ */
headers(): Headers { headers(): Headers {
return { ...this._headers }; return headersArrayToObject(this._headers, true /* lowerCase */);
} }
async allHeaders(): Promise<RawHeaders> { _getHeadersIfNeeded() {
if (this._rawHeadersPromise) if (!this._allHeadersPromise) {
return this._rawHeadersPromise; this._allHeadersPromise = this.response().then(response => {
this._rawHeadersPromise = this.response().then(response => { // there is no response, so should we return the headers we have now?
if (!response) if (!response)
return new RawHeaders([]); return this._headers;
return response._wrapApiCall(async (channel: channels.ResponseChannel) => { return response._wrapApiCall(async (channel: channels.ResponseChannel) => {
return new RawHeaders((await channel.rawRequestHeaders()).headers); return await (await channel.rawRequestHeaders()).headers;
});
}); });
}); }
return this._rawHeadersPromise; return this._allHeadersPromise;
}
async allHeaders(): Promise<Headers> {
return headersArrayToObject(await this._getHeadersIfNeeded(), true);
}
async headersArray(): Promise<string[][]> {
return (await this._getHeadersIfNeeded()).map(header => [header.name, header.value]);
} }
async response(): Promise<Response | null> { async response(): Promise<Response | null> {
@ -204,14 +212,10 @@ export class InterceptedResponse implements api.Response {
private readonly _route: Route; private readonly _route: Route;
private readonly _initializer: channels.InterceptedResponse; private readonly _initializer: channels.InterceptedResponse;
private readonly _request: Request; private readonly _request: Request;
private readonly _headers: Headers;
private readonly _rawHeaders: RawHeaders;
constructor(route: Route, initializer: channels.InterceptedResponse) { constructor(route: Route, initializer: channels.InterceptedResponse) {
this._route = route; this._route = route;
this._initializer = initializer; this._initializer = initializer;
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
this._rawHeaders = new RawHeaders(initializer.headers);
this._request = Request.from(initializer.request); this._request = Request.from(initializer.request);
} }
@ -251,11 +255,15 @@ export class InterceptedResponse implements api.Response {
} }
headers(): Headers { headers(): Headers {
return { ...this._headers }; return headersArrayToObject(this._initializer.headers, true /* lowerCase */);
} }
async allHeaders(): Promise<RawHeaders> { async allHeaders(): Promise<Headers> {
return this._rawHeaders; return headersArrayToObject(this._initializer.headers, true /* lowerCase */);
}
async headersArray(): Promise<string[][]> {
return this._initializer.headers.map(header => [header.name, header.value]);
} }
async body(): Promise<Buffer> { async body(): Promise<Buffer> {
@ -413,7 +421,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
_headers: Headers; _headers: Headers;
private _request: Request; private _request: Request;
readonly _finishedPromise = new ManualPromise<void>(); readonly _finishedPromise = new ManualPromise<void>();
private _rawHeadersPromise: Promise<RawHeaders> | undefined; private _rawHeadersPromise: Promise<channels.ResponseRawResponseHeadersResult> | undefined;
static from(response: channels.ResponseChannel): Response { static from(response: channels.ResponseChannel): Response {
return (response as any)._object; return (response as any)._object;
@ -453,15 +461,23 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
return { ...this._headers }; return { ...this._headers };
} }
async allHeaders(): Promise<RawHeaders> { async _getHeadersIfNeeded() {
if (this._rawHeadersPromise) if (!this._rawHeadersPromise) {
return this._rawHeadersPromise; this._rawHeadersPromise = this._wrapApiCall(async (channel: channels.ResponseChannel) => {
this._rawHeadersPromise = this._wrapApiCall(async (channel: channels.ResponseChannel) => { return await channel.rawResponseHeaders();
return new RawHeaders((await channel.rawResponseHeaders()).headers); });
}); }
return this._rawHeadersPromise; return this._rawHeadersPromise;
} }
async allHeaders(): Promise<Headers> {
return headersArrayToObject((await this._getHeadersIfNeeded()).headers, true /* lowerCase */);
}
async headersArray(): Promise<string[][]> {
return (await this._getHeadersIfNeeded()).headers.map(header => [header.name, header.value]);
}
async finished(): Promise<null> { async finished(): Promise<null> {
return this._finishedPromise.then(() => null); return this._finishedPromise.then(() => null);
} }
@ -639,36 +655,3 @@ export class RouteHandler {
this.handledCount++; this.handledCount++;
} }
} }
export class RawHeaders implements api.Headers {
private _headersArray: HeadersArray;
private _headersMap = new MultiMap<string, string>();
constructor(headers: HeadersArray) {
this._headersArray = headers;
for (const header of headers)
this._headersMap.set(header.name.toLowerCase(), header.value);
}
get(name: string): string | null {
const values = this.getAll(name);
if (!values)
return null;
return values.join(', ');
}
getAll(name: string): string[] {
return [...this._headersMap.get(name.toLowerCase())];
}
headerNames(): string[] {
return [...this._headersMap.keys()];
}
headers(): Headers {
const result: Headers = {};
for (const name of this._headersMap.keys())
result[name] = this.get(name)!;
return result;
}
}

View file

@ -18,6 +18,7 @@ import http from 'http';
import zlib from 'zlib'; import zlib from 'zlib';
import { pipeline } from 'stream'; import { pipeline } from 'stream';
import { contextTest as it, expect } from './config/browserTest'; import { contextTest as it, expect } from './config/browserTest';
import type { Response } from '..';
it.skip(({ mode }) => mode !== 'default'); it.skip(({ mode }) => mode !== 'default');
@ -41,7 +42,7 @@ it.afterAll(() => {
it('should work', async ({context, server}) => { it('should work', async ({context, server}) => {
// @ts-expect-error // @ts-expect-error
const response = await context._fetch(server.PREFIX + '/simple.json'); const response: Response = await context._fetch(server.PREFIX + '/simple.json');
expect(response.url()).toBe(server.PREFIX + '/simple.json'); expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(response.statusText()).toBe('OK'); expect(response.statusText()).toBe('OK');

View file

@ -90,7 +90,7 @@ it('should get the same headers as the server', async ({ page, server, browserNa
}); });
const response = await page.goto(server.PREFIX + '/empty.html'); const response = await page.goto(server.PREFIX + '/empty.html');
const headers = await response.request().allHeaders(); const headers = await response.request().allHeaders();
expect(headers.headers()).toEqual(serverRequest.headers); expect(headers).toEqual(serverRequest.headers);
}); });
it('should get the same headers as the server CORS', async ({page, server, browserName, platform}) => { it('should get the same headers as the server CORS', async ({page, server, browserName, platform}) => {
@ -111,7 +111,7 @@ it('should get the same headers as the server CORS', async ({page, server, brows
expect(text).toBe('done'); expect(text).toBe('done');
const response = await responsePromise; const response = await responsePromise;
const headers = await response.request().allHeaders(); const headers = await response.request().allHeaders();
expect(headers.headers()).toEqual(serverRequest.headers); expect(headers).toEqual(serverRequest.headers);
}); });
it('should return postData', async ({page, server, isAndroid}) => { it('should return postData', async ({page, server, isAndroid}) => {
@ -266,15 +266,14 @@ it('should return navigation bit when navigating to image', async ({page, server
expect(requests[0].isNavigationRequest()).toBe(true); expect(requests[0].isNavigationRequest()).toBe(true);
}); });
it('should report all headers', async ({ page, server, browserName, platform }) => { it('should report raw headers', async ({ page, server, browserName, platform }) => {
const expectedHeaders = {}; let expectedHeaders: string[][];
server.setRoute('/headers', (req, res) => { server.setRoute('/headers', (req, res) => {
expectedHeaders = [];
for (let i = 0; i < req.rawHeaders.length; i += 2) for (let i = 0; i < req.rawHeaders.length; i += 2)
expectedHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1]; expectedHeaders.push([req.rawHeaders[i], req.rawHeaders[i + 1]]);
if (browserName === 'webkit' && platform === 'win32') { if (browserName === 'webkit' && platform === 'win32')
delete expectedHeaders['accept-encoding']; expectedHeaders = expectedHeaders.filter(([name, value]) => name.toLowerCase() !== 'accept-encoding' && name.toLowerCase() !== 'accept-language');
delete expectedHeaders['accept-language'];
}
res.end(); res.end();
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -289,8 +288,8 @@ it('should report all headers', async ({ page, server, browserName, platform })
] ]
})) }))
]); ]);
const headers = await request.allHeaders(); const headers = await request.headersArray();
expect(headers.headers()).toEqual(expectedHeaders); expect(headers.sort()).toEqual(expectedHeaders.sort());
}); });
it('should report raw response headers in redirects', async ({ page, server, browserName }) => { it('should report raw response headers in redirects', async ({ page, server, browserName }) => {
@ -311,7 +310,7 @@ it('should report raw response headers in redirects', async ({ page, server, bro
redirectChain.unshift(req.url()); redirectChain.unshift(req.url());
const res = await req.response(); const res = await req.response();
const headers = await res.allHeaders(); const headers = await res.allHeaders();
headersChain.unshift(headers.get('sec-test-header')); headersChain.unshift(headers['sec-test-header']);
} }
expect(redirectChain).toEqual(expectedUrls); expect(redirectChain).toEqual(expectedUrls);
@ -332,7 +331,6 @@ it('should report all cookies in one header', async ({ page, server }) => {
document.cookie = 'myOtherCookie=myOtherValue'; document.cookie = 'myOtherCookie=myOtherValue';
}); });
const response = await page.goto(server.EMPTY_PAGE); const response = await page.goto(server.EMPTY_PAGE);
const headers = await response.request().allHeaders(); const cookie = (await response.request().allHeaders())['cookie'];
const cookie = headers.get('cookie');
expect(cookie).toBe('myCookie=myValue; myOtherCookie=myOtherValue'); expect(cookie).toBe('myCookie=myValue; myOtherCookie=myOtherValue');
}); });

View file

@ -25,9 +25,9 @@ it('should work', async ({page, server}) => {
res.end(); res.end();
}); });
const response = await page.goto(server.EMPTY_PAGE); const response = await page.goto(server.EMPTY_PAGE);
expect(response.headers()['foo']).toBe('bar'); expect((await response.allHeaders())['foo']).toBe('bar');
expect(response.headers()['baz']).toBe('bAz'); expect((await response.allHeaders())['baz']).toBe('bAz');
expect(response.headers()['BaZ']).toBe(undefined); expect((await response.allHeaders())['BaZ']).toBe(undefined);
}); });
@ -134,10 +134,13 @@ it('should report all headers', async ({ page, server, browserName, platform })
page.waitForResponse('**/*'), page.waitForResponse('**/*'),
page.evaluate(() => fetch('/headers')) page.evaluate(() => fetch('/headers'))
]); ]);
const headers = await response.allHeaders(); const headers = await response.headersArray();
const actualHeaders = {}; const actualHeaders = {};
for (const name of headers.headerNames()) for (const [name, value] of headers) {
actualHeaders[name] = headers.getAll(name); if (!actualHeaders[name])
actualHeaders[name] = [];
actualHeaders[name].push(value);
}
delete actualHeaders['Keep-Alive']; delete actualHeaders['Keep-Alive'];
delete actualHeaders['keep-alive']; delete actualHeaders['keep-alive'];
delete actualHeaders['Connection']; delete actualHeaders['Connection'];
@ -163,7 +166,7 @@ it('should report multiple set-cookie headers', async ({ page, server }) => {
page.waitForResponse('**/*'), page.waitForResponse('**/*'),
page.evaluate(() => fetch('/headers')) page.evaluate(() => fetch('/headers'))
]); ]);
const headers = await response.allHeaders(); const headers = await response.headersArray();
const cookies = headers.getAll('set-cookie'); const cookies = headers.filter(([name, value]) => name.toLowerCase() === 'set-cookie').map(([, value]) => value);
expect(cookies).toEqual(['a=b', 'c=d']); expect(cookies).toEqual(['a=b', 'c=d']);
}); });

View file

@ -17,7 +17,7 @@
import { fail } from 'assert'; import { fail } from 'assert';
import os from 'os'; import os from 'os';
import type { Route } from '../../index'; import type { Route, Response } from '../../index';
import { expect, test as it } from './pageTest'; import { expect, test as it } from './pageTest';
it('should fulfill intercepted response', async ({page, server, browserName}) => { it('should fulfill intercepted response', async ({page, server, browserName}) => {
@ -195,12 +195,14 @@ it('should give access to the intercepted response', async ({page, server}) => {
const route = await routePromise; const route = await routePromise;
// @ts-expect-error // @ts-expect-error
const response = await route._continueToResponse(); const response: Response = await route._continueToResponse();
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); expect(response.ok()).toBeTruthy();
expect(response.url()).toBe(server.PREFIX + '/title.html'); expect(response.url()).toBe(server.PREFIX + '/title.html');
expect(response.headers()['content-type']).toBe('text/html; charset=utf-8'); expect(response.headers()['content-type']).toBe('text/html; charset=utf-8');
expect((await response.allHeaders())['content-type']).toBe('text/html; charset=utf-8');
expect(await (await response.headersArray()).filter(([name, value]) => name.toLowerCase() === 'content-type')).toEqual([['Content-Type', 'text/html; charset=utf-8']]);
// @ts-expect-error // @ts-expect-error
await Promise.all([route.fulfill({ response }), evalPromise]); await Promise.all([route.fulfill({ response }), evalPromise]);

47
types/types.d.ts vendored
View file

@ -12690,33 +12690,6 @@ export interface FileChooser {
}): Promise<void>; }): Promise<void>;
} }
/**
* HTTP request and response all headers collection.
*/
export interface Headers {
/**
* @param name
*/
get(name: string): string|null;
/**
* Returns all header values for the given header name.
* @param name
*/
getAll(name: string): Array<string>;
/**
* Returns all header names in this headers collection.
*/
headerNames(): Array<string>;
/**
* Returns all headers as a dictionary. Header names are normalized to lower case, multi-value headers are concatenated
* using comma.
*/
headers(): { [key: string]: string; };
}
/** /**
* Keyboard provides an api for managing a virtual keyboard. The high level api is * Keyboard provides an api for managing a virtual keyboard. The high level api is
* [keyboard.type(text[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-type), which takes raw * [keyboard.type(text[, options])](https://playwright.dev/docs/api/class-keyboard#keyboard-type), which takes raw
@ -13048,9 +13021,9 @@ export interface Mouse {
*/ */
export interface Request { export interface Request {
/** /**
* An object with all the request HTTP headers associated with this request. * An object with all the request HTTP headers associated with this request. The header names are lower-cased.
*/ */
allHeaders(): Promise<Headers>; allHeaders(): Promise<{ [key: string]: string; }>;
/** /**
* The method returns `null` unless this request has failed, as reported by `requestfailed` event. * The method returns `null` unless this request has failed, as reported by `requestfailed` event.
@ -13083,6 +13056,13 @@ export interface Request {
*/ */
headers(): { [key: string]: string; }; headers(): { [key: string]: string; };
/**
* An array with all the request HTTP headers associated with this request. Unlike
* [request.allHeaders()](https://playwright.dev/docs/api/class-request#request-all-headers), header names are not
* lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
*/
headersArray(): Promise<Array<Array<string>>>;
/** /**
* Whether this request is driving frame's navigation. * Whether this request is driving frame's navigation.
*/ */
@ -13268,7 +13248,7 @@ export interface Response {
/** /**
* An object with all the response HTTP headers associated with this response. * An object with all the response HTTP headers associated with this response.
*/ */
allHeaders(): Promise<Headers>; allHeaders(): Promise<{ [key: string]: string; }>;
/** /**
* Returns the buffer with response body. * Returns the buffer with response body.
@ -13292,6 +13272,13 @@ export interface Response {
*/ */
headers(): { [key: string]: string; }; headers(): { [key: string]: string; };
/**
* An array with all the request HTTP headers associated with this response. Unlike
* [response.allHeaders()](https://playwright.dev/docs/api/class-response#response-all-headers), header names are not
* lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
*/
headersArray(): Promise<Array<Array<string>>>;
/** /**
* Returns the JSON representation of response body. * Returns the JSON representation of response body.
* *

View file

@ -56,13 +56,13 @@ module.exports = function lint(documentation, jsSources, apiFileName) {
continue; continue;
const params = methods.get(member.alias); const params = methods.get(member.alias);
if (!params) { if (!params) {
errors.push(`Documented "${cls.name}.${member.alias}" not found is sources`); errors.push(`Documented "${cls.name}.${member.alias}" not found in sources`);
continue; continue;
} }
const memberParams = paramsForMember(member); const memberParams = paramsForMember(member);
for (const paramName of memberParams) { for (const paramName of memberParams) {
if (!params.has(paramName) && paramName !== 'options') if (!params.has(paramName) && paramName !== 'options')
errors.push(`Documented "${cls.name}.${member.alias}.${paramName}" not found is sources`); errors.push(`Documented "${cls.name}.${member.alias}.${paramName}" not found in sources`);
} }
} }
} }