chore: fix headers api again (#8854)

This commit is contained in:
Pavel Feldman 2021-09-11 13:27:00 -07:00 committed by GitHub
parent 737b155869
commit 798d0bfa9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 232 additions and 81 deletions

View file

@ -17,14 +17,6 @@ Disposes the body of this response. If not called then the body will stay in mem
An object with all the response HTTP headers associated with this response.
## method: FetchResponse.headersArray
* langs: js, csharp, python
- returns: <[Array]<[Array]<[string]>>>
An array with all the request HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## method: FetchResponse.headersArray
* langs: java
- returns: <[Array]<[Object]>>
- `name` <[string]> Name of the header.
- `value` <[string]> Value of the header.

View file

@ -62,21 +62,24 @@ Returns the [Frame] that initiated this request.
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Request.allHeaders`] instead.
## async method: Request.headersArray
* langs: js, csharp, python
- 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.
## async method: Request.headersArray
* langs: java
- returns: <[Array]<[Object]>>
- `name` <[string]> Name of the header.
- `value` <[string]> Value of the header.
An array with all the request HTTP headers associated with this request. Unlike [`method: Request.allHeaders`], header names are not lower-cased.
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.
## async method: Request.headerValue
- returns: <[null]|[string]>
Returns the value of the header matching the name. The name is case insensitive.
### param: Request.headerValue.name
- `name` <[string]>
Name of the header.
## method: Request.isNavigationRequest
- returns: <[boolean]>

View file

@ -28,21 +28,34 @@ Returns the [Frame] that initiated this response.
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use [`method: Response.allHeaders`] instead.
## async method: Response.headersArray
* langs: js, csharp, python
- 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.headersArray
* langs: java
- returns: <[Array]<[Object]>>
- `name` <[string]> Name of the header.
- `value` <[string]> Value of the header.
An array with all the request HTTP headers associated with this response. Unlike [`method: Response.allHeaders`], header names are not lower-cased.
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.headerValue
- returns: <[null]|[string]>
Returns the value of the header matching the name. The name is case insensitive. If multiple headers have
the same name (except `set-cookie`), they are returned as a list separated by `, `. For `set-cookie`, the `\n` separator is used. If no headers are found, `null` is returned.
### param: Response.headerValue.name
- `name` <[string]>
Name of the header.
## async method: Response.headerValues
- returns: <[Array]<[string]>>
Returns all values of the headers matching the name, for example `set-cookie`. The name is case insensitive.
### param: Response.headerValues.name
- `name` <[string]>
Name of the header.
## async method: Response.json
* langs: js, python
- returns: <[Serializable]>

View file

@ -21,15 +21,16 @@ import { Frame } from './frame';
import { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
import fs from 'fs';
import * as mime from 'mime';
import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils';
import { isString, headersObjectToArray } from '../utils/utils';
import { ManualPromise } from '../utils/async';
import { Events } from './events';
import { Page } from './page';
import { Waiter } from './waiter';
import * as api from '../../types/types';
import { URLMatch } from '../common/types';
import { HeadersArray, URLMatch } from '../common/types';
import { urlMatches } from './clientHelper';
import { BrowserContext } from './browserContext';
import { MultiMap } from '../utils/multimap';
export type NetworkCookie = {
name: string,
@ -58,8 +59,8 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
private _redirectedFrom: Request | null = null;
private _redirectedTo: Request | null = null;
_failureText: string | null = null;
_headers: channels.NameValue[];
private _allHeadersPromise: Promise<channels.NameValue[]> | undefined;
private _provisionalHeaders: RawHeaders;
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
private _postData: Buffer | null;
_timing: ResourceTiming;
@ -76,7 +77,7 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom)
this._redirectedFrom._redirectedTo = this;
this._headers = initializer.headers;
this._provisionalHeaders = new RawHeaders(initializer.headers);
this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null;
this._timing = {
startTime: 0,
@ -136,29 +137,33 @@ export class Request extends ChannelOwner<channels.RequestChannel, channels.Requ
* @deprecated
*/
headers(): Headers {
return headersArrayToObject(this._headers, true /* lowerCase */);
return this._provisionalHeaders.headers();
}
_getHeadersIfNeeded() {
if (!this._allHeadersPromise) {
this._allHeadersPromise = this.response().then(response => {
_actualHeaders(): Promise<RawHeaders> {
if (!this._actualHeadersPromise) {
this._actualHeadersPromise = this.response().then(response => {
// there is no response, so should we return the headers we have now?
if (!response)
return this._headers;
return this._provisionalHeaders;
return response._wrapApiCall(async (channel: channels.ResponseChannel) => {
return (await channel.rawRequestHeaders()).headers;
return new RawHeaders((await channel.rawRequestHeaders()).headers);
});
});
}
return this._allHeadersPromise;
return this._actualHeadersPromise;
}
async allHeaders(): Promise<Headers> {
return headersArrayToObject(await this._getHeadersIfNeeded(), true);
return (await this._actualHeaders()).headers();
}
async headersArray(): Promise<string[][]> {
return (await this._getHeadersIfNeeded()).map(header => [header.name, header.value]);
async headersArray(): Promise<HeadersArray> {
return (await this._actualHeaders()).headersArray();
}
async headerValue(name: string): Promise<string | null> {
return (await this._actualHeaders()).get(name);
}
async response(): Promise<Response | null> {
@ -213,10 +218,12 @@ export class InterceptedResponse implements api.Response {
private readonly _route: Route;
private readonly _initializer: channels.InterceptedResponse;
private readonly _request: Request;
private readonly _headers: RawHeaders;
constructor(route: Route, initializer: channels.InterceptedResponse) {
this._route = route;
this._initializer = initializer;
this._headers = new RawHeaders(initializer.headers);
this._request = Request.from(initializer.request);
}
@ -256,15 +263,23 @@ export class InterceptedResponse implements api.Response {
}
headers(): Headers {
return headersArrayToObject(this._initializer.headers, true /* lowerCase */);
return this._headers.headers();
}
async allHeaders(): Promise<Headers> {
return headersArrayToObject(this._initializer.headers, true /* lowerCase */);
return this.headers();
}
async headersArray(): Promise<string[][]> {
return this._initializer.headers.map(header => [header.name, header.value]);
async headersArray(): Promise<HeadersArray> {
return this._headers.headersArray();
}
async headerValue(name: string): Promise<string | null> {
return this._headers.get(name);
}
async headerValues(name: string): Promise<string[]> {
return this._headers.getAll(name);
}
async body(): Promise<Buffer> {
@ -423,10 +438,10 @@ export type RequestSizes = {
};
export class Response extends ChannelOwner<channels.ResponseChannel, channels.ResponseInitializer> implements api.Response {
_headers: Headers;
private _provisionalHeaders: RawHeaders;
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
private _request: Request;
readonly _finishedPromise = new ManualPromise<void>();
private _rawHeadersPromise: Promise<channels.ResponseRawResponseHeadersResult> | undefined;
static from(response: channels.ResponseChannel): Response {
return (response as any)._object;
@ -438,7 +453,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
super(parent, type, guid, initializer);
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
this._provisionalHeaders = new RawHeaders(initializer.headers);
this._request = Request.from(this._initializer.request);
Object.assign(this._request._timing, this._initializer.timing);
}
@ -463,24 +478,32 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
* @deprecated
*/
headers(): Headers {
return { ...this._headers };
return this._provisionalHeaders.headers();
}
async _getHeadersIfNeeded() {
if (!this._rawHeadersPromise) {
this._rawHeadersPromise = this._wrapApiCall(async (channel: channels.ResponseChannel) => {
return await channel.rawResponseHeaders();
async _actualHeaders(): Promise<RawHeaders> {
if (!this._actualHeadersPromise) {
this._actualHeadersPromise = this._wrapApiCall(async (channel: channels.ResponseChannel) => {
return new RawHeaders((await channel.rawResponseHeaders()).headers);
});
}
return this._rawHeadersPromise;
return this._actualHeadersPromise;
}
async allHeaders(): Promise<Headers> {
return headersArrayToObject((await this._getHeadersIfNeeded()).headers, true /* lowerCase */);
return (await this._actualHeaders()).headers();
}
async headersArray(): Promise<string[][]> {
return (await this._getHeadersIfNeeded()).headers.map(header => [header.name, header.value]);
async headersArray(): Promise<HeadersArray> {
return (await this._actualHeaders()).headersArray().slice();
}
async headerValue(name: string): Promise<string | null> {
return (await this._actualHeaders()).get(name);
}
async headerValues(name: string): Promise<string[]> {
return (await this._actualHeaders()).getAll(name);
}
async finished(): Promise<null> {
@ -526,13 +549,13 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
export class FetchResponse implements api.FetchResponse {
private readonly _initializer: channels.FetchResponse;
private readonly _headers: Headers;
private readonly _headers: RawHeaders;
private readonly _context: BrowserContext;
constructor(context: BrowserContext, initializer: channels.FetchResponse) {
this._context = context;
this._initializer = initializer;
this._headers = headersArrayToObject(this._initializer.headers, true /* lowerCase */);
this._headers = new RawHeaders(this._initializer.headers);
}
ok(): boolean {
@ -552,11 +575,11 @@ export class FetchResponse implements api.FetchResponse {
}
headers(): Headers {
return { ...this._headers };
return this._headers.headers();
}
headersArray(): string[][] {
return this._initializer.headers.map(({name, value}) => [name, value]);
headersArray(): HeadersArray {
return this._headers.headersArray();
}
async body(): Promise<Buffer> {
@ -679,3 +702,36 @@ export class RouteHandler {
this.handledCount++;
}
}
export class RawHeaders {
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 || !values.length)
return null;
return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', ');
}
getAll(name: string): string[] {
return [...this._headersMap.get(name.toLowerCase())];
}
headers(): Headers {
const result: Headers = {};
for (const name of this._headersMap.keys())
result[name] = this.get(name)!;
return result;
}
headersArray(): HeadersArray {
return this._headersArray;
}
}

View file

@ -48,7 +48,7 @@ it('should work', async ({context, server}) => {
expect(response.ok()).toBeTruthy();
expect(response.url()).toBe(server.PREFIX + '/simple.json');
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
expect(response.headersArray()).toContainEqual(['Content-Type', 'application/json; charset=utf-8']);
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
expect(await response.text()).toBe('{"foo": "bar"}\n');
});
@ -268,10 +268,10 @@ it('should return raw headers', async ({context, page, server}) => {
});
const response = await context.fetch(`${server.PREFIX}/headers`);
expect(response.status()).toBe(200);
const headers = response.headersArray().filter(([name, value]) => name.toLowerCase().includes('name-'));
expect(headers).toEqual([['Name-A', 'v1'], ['name-b', 'v4'], ['Name-a', 'v2'], ['name-A', 'v3']]);
// Last value wins, this matches Response.headers()
expect(response.headers()['name-a']).toBe('v3');
const headers = response.headersArray().filter(({ name }) => name.toLowerCase().includes('name-'));
expect(headers).toEqual([{ name: 'Name-A', value: 'v1' }, { name: 'name-b', value: 'v4' }, { name: 'Name-a', value: 'v2' }, { name: 'name-A', value: 'v3' }]);
// Comma separated values, this matches Response.headers()
expect(response.headers()['name-a']).toBe('v1, v2, v3');
expect(response.headers()['name-b']).toBe('v4');
});

View file

@ -267,13 +267,13 @@ it('should return navigation bit when navigating to image', async ({page, server
});
it('should report raw headers', async ({ page, server, browserName, platform }) => {
let expectedHeaders: string[][];
let expectedHeaders: { name: string, value: string }[];
server.setRoute('/headers', (req, res) => {
expectedHeaders = [];
for (let i = 0; i < req.rawHeaders.length; i += 2)
expectedHeaders.push([req.rawHeaders[i], req.rawHeaders[i + 1]]);
expectedHeaders.push({ name: req.rawHeaders[i], value: req.rawHeaders[i + 1] });
if (browserName === 'webkit' && platform === 'win32')
expectedHeaders = expectedHeaders.filter(([name, value]) => name.toLowerCase() !== 'accept-encoding' && name.toLowerCase() !== 'accept-language');
expectedHeaders = expectedHeaders.filter(({ name }) => name.toLowerCase() !== 'accept-encoding' && name.toLowerCase() !== 'accept-language');
res.end();
});
await page.goto(server.EMPTY_PAGE);
@ -289,7 +289,9 @@ it('should report raw headers', async ({ page, server, browserName, platform })
}))
]);
const headers = await request.headersArray();
expect(headers.sort()).toEqual(expectedHeaders.sort());
expect(headers.sort((a, b) => a.name.localeCompare(b.name))).toEqual(expectedHeaders.sort((a, b) => a.name.localeCompare(b.name)));
expect(await request.headerValue('header-a')).toEqual('value-a, value-a-1, value-a-2');
expect(await request.headerValue('not-there')).toEqual(null);
});
it('should report raw response headers in redirects', async ({ page, server, browserName }) => {

View file

@ -30,8 +30,7 @@ it('should work', async ({page, server}) => {
expect((await response.allHeaders())['BaZ']).toBe(undefined);
});
it('should return last header value for duplicates', async ({page, server}) => {
it.fixme();
it('should return multiple header value', async ({page, server}) => {
server.setRoute('/headers', (req, res) => {
// Headers array is only supported since Node v14.14.0 so we write directly to the socket.
// res.writeHead(200, ['name-a', 'v1','name-b', 'v4','Name-a', 'v2', 'name-A', 'v3']);
@ -196,7 +195,7 @@ it('should report all headers', async ({ page, server, browserName, platform })
]);
const headers = await response.headersArray();
const actualHeaders = {};
for (const [name, value] of headers) {
for (const { name, value } of headers) {
if (!actualHeaders[name])
actualHeaders[name] = [];
actualHeaders[name].push(value);
@ -227,6 +226,42 @@ it('should report multiple set-cookie headers', async ({ page, server }) => {
page.evaluate(() => fetch('/headers'))
]);
const headers = await response.headersArray();
const cookies = headers.filter(([name, value]) => name.toLowerCase() === 'set-cookie').map(([, value]) => value);
const cookies = headers.filter(({ name }) => name.toLowerCase() === 'set-cookie').map(({ value }) => value);
expect(cookies).toEqual(['a=b', 'c=d']);
expect(await response.headerValue('not-there')).toEqual(null);
expect(await response.headerValue('set-cookie')).toEqual('a=b\nc=d');
expect(await response.headerValues('set-cookie')).toEqual(['a=b', 'c=d']);
});
it('should behave the same way for headers and allHeaders', async ({ page, server, browserName, channel }) => {
it.skip(!!channel, 'Stable chrome uses \n as a header separator in non-raw headers');
server.setRoute('/headers', (req, res) => {
const headers = {
'Set-Cookie': ['a=b', 'c=d'],
'header-a': ['a=b', 'c=d'],
'Name-A': 'v1',
'name-b': 'v4',
'Name-a': 'v2',
'name-A': 'v3',
};
// Chromium does not report set-cookie headers immediately, so they are missing from .headers()
if (browserName === 'chromium')
delete headers['Set-Cookie'];
res.writeHead(200, headers);
res.write('\r\n');
res.end();
});
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse('**/*'),
page.evaluate(() => fetch('/headers'))
]);
const allHeaders = await response.allHeaders();
expect(response.headers()).toEqual(allHeaders);
expect(allHeaders['header-a']).toEqual('a=b, c=d');
expect(allHeaders['name-a']).toEqual('v1, v2, v3');
expect(allHeaders['name-b']).toEqual('v4');
});

View file

@ -198,7 +198,7 @@ it('should give access to the intercepted response', async ({page, server}) => {
expect(response.url()).toBe(server.PREFIX + '/title.html');
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']]);
expect(await (await response.headersArray()).filter(({ name }) => name.toLowerCase() === 'content-type')).toEqual([{ name: 'Content-Type', value: 'text/html; charset=utf-8' }]);
// @ts-expect-error
await Promise.all([route.fulfill({ response }), evalPromise]);

60
types/types.d.ts vendored
View file

@ -12688,7 +12688,17 @@ export interface FetchResponse {
* An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers with
* multiple entries, such as `Set-Cookie`, appear in the array multiple times.
*/
headersArray(): Array<Array<string>>;
headersArray(): Array<{
/**
* Name of the header.
*/
name: string;
/**
* Value of the header.
*/
value: string;
}>;
/**
* Returns the JSON representation of response body.
@ -13174,10 +13184,26 @@ export interface Request {
/**
* 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
* [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>>>;
headersArray(): Promise<Array<{
/**
* Name of the header.
*/
name: string;
/**
* Value of the header.
*/
value: string;
}>>;
/**
* Returns the value of the header matching the name. The name is case insensitive.
* @param name Name of the header.
*/
headerValue(name: string): Promise<null|string>;
/**
* Whether this request is driving frame's navigation.
@ -13389,10 +13415,34 @@ export interface Response {
/**
* 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
* [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>>>;
headersArray(): Promise<Array<{
/**
* Name of the header.
*/
name: string;
/**
* Value of the header.
*/
value: string;
}>>;
/**
* Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name
* (except `set-cookie`), they are returned as a list separated by `, `. For `set-cookie`, the `\n` separator is used. If
* no headers are found, `null` is returned.
* @param name Name of the header.
*/
headerValue(name: string): Promise<null|string>;
/**
* Returns all values of the headers matching the name, for example `set-cookie`. The name is case insensitive.
* @param name Name of the header.
*/
headerValues(name: string): Promise<Array<string>>;
/**
* Returns the JSON representation of response body.