feat(headers): add Headers.headers that would mimic the behavior of the deprecated getters (#8665)

This commit is contained in:
Pavel Feldman 2021-09-02 20:48:23 -07:00 committed by GitHub
parent 962a33993f
commit 0d5b41ce7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 166 additions and 62 deletions

View file

@ -25,6 +25,7 @@ Header name, case-insensitive.
Returns all header names in this headers collection. Returns all header names in this headers collection.
## method: Headers.headers ## method: Headers.headers
- returns: <[Array]<{ name: string, value: string }>> - returns: <[Object]<[string], [string]>>
Returns all raw headers. Returns all headers as a dictionary. Header names are normalized to lower case, multi-value headers are concatenated
using comma.

View file

@ -652,7 +652,10 @@ export class RawHeaders implements api.Headers {
} }
get(name: string): string | null { get(name: string): string | null {
return this.getAll(name)[0] || null; const values = this.getAll(name);
if (!values)
return null;
return values.join(', ');
} }
getAll(name: string): string[] { getAll(name: string): string[] {
@ -660,10 +663,13 @@ export class RawHeaders implements api.Headers {
} }
headerNames(): string[] { headerNames(): string[] {
return [...new Set(this._headersArray.map(h => h.name))]; return [...this._headersMap.keys()];
} }
headers(): HeadersArray { headers(): Headers {
return this._headersArray; const result: Headers = {};
for (const name of this._headersMap.keys())
result[name] = this.get(name)!;
return result;
} }
} }

View file

@ -16,9 +16,9 @@
*/ */
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import type { NameValue, Size } from '../common/types'; import type { Size } from '../common/types';
import type { ParsedStackTrace } from '../utils/stackTrace'; import type { ParsedStackTrace } from '../utils/stackTrace';
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions } from '../common/types'; export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error'; type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export interface Logger { export interface Logger {
@ -32,7 +32,6 @@ export interface ClientSideInstrumentation {
export type StrictOptions = { strict?: boolean }; export type StrictOptions = { strict?: boolean };
export type Headers = { [key: string]: string }; export type Headers = { [key: string]: string };
export type HeadersArray = NameValue[];
export type Env = { [key: string]: string | number | boolean | undefined }; export type Env = { [key: string]: string | number | boolean | undefined };
export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number }; export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number };

View file

@ -21,3 +21,4 @@ export type Quad = [ Point, Point, Point, Point ];
export type URLMatch = string | RegExp | ((url: URL) => boolean); export type URLMatch = string | RegExp | ((url: URL) => boolean);
export type TimeoutOptions = { timeout?: number }; export type TimeoutOptions = { timeout?: number };
export type NameValue = { name: string, value: string }; export type NameValue = { name: string, value: string };
export type HeadersArray = NameValue[];

View file

@ -675,10 +675,10 @@ class ResponseExtraInfoTracker {
const response = info.responses[index]; const response = info.responses[index];
const requestExtraInfo = info.requestWillBeSentExtraInfo[index]; const requestExtraInfo = info.requestWillBeSentExtraInfo[index];
if (response && requestExtraInfo) if (response && requestExtraInfo)
response.setRawRequestHeaders(headersObjectToArray(requestExtraInfo.headers)); response.setRawRequestHeaders(headersObjectToArray(requestExtraInfo.headers, '\n'));
const responseExtraInfo = info.responseReceivedExtraInfo[index]; const responseExtraInfo = info.responseReceivedExtraInfo[index];
if (response && responseExtraInfo) if (response && responseExtraInfo)
response.setRawResponseHeaders(headersObjectToArray(responseExtraInfo.headers)); response.setRawResponseHeaders(headersObjectToArray(responseExtraInfo.headers, '\n'));
} }
private _checkFinished(info: RequestInfo) { private _checkFinished(info: RequestInfo) {

View file

@ -23,6 +23,7 @@ import * as frames from '../frames';
import * as types from '../types'; import * as types from '../types';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { InterceptedResponse } from '../network'; import { InterceptedResponse } from '../network';
import { HeadersArray } from '../../server/types';
export class FFNetworkManager { export class FFNetworkManager {
private _session: FFSession; private _session: FFSession;
@ -96,7 +97,7 @@ export class FFNetworkManager {
requestStart: relativeToStart(event.timing.requestStart), requestStart: relativeToStart(event.timing.requestStart),
responseStart: relativeToStart(event.timing.responseStart), responseStart: relativeToStart(event.timing.responseStart),
}; };
const response = new network.Response(request.request, event.status, event.statusText, event.headers, timing, getResponseBody); const response = new network.Response(request.request, event.status, event.statusText, parseMultivalueHeaders(event.headers), timing, getResponseBody);
if (event?.remoteIPAddress && typeof event?.remotePort === 'number') { if (event?.remoteIPAddress && typeof event?.remotePort === 'number') {
response._serverAddrFinished({ response._serverAddrFinished({
ipAddress: event.remoteIPAddress, ipAddress: event.remoteIPAddress,
@ -252,3 +253,14 @@ class FFRouteImpl implements network.RouteDelegate {
}); });
} }
} }
function parseMultivalueHeaders(headers: HeadersArray) {
const result: HeadersArray = [];
for (const header of headers) {
const separator = header.name.toLowerCase() === 'set-cookie' ? '\n' : ',';
const tokens = header.value.split(separator).map(s => s.trim());
for (const token of tokens)
result.push({ name: header.name, value: token });
}
return result;
}

View file

@ -446,21 +446,31 @@ export class Response extends SdkObject {
headersSize += 8; // httpVersion; headersSize += 8; // httpVersion;
headersSize += 3; // statusCode; headersSize += 3; // statusCode;
headersSize += this.statusText().length; headersSize += this.statusText().length;
const headers = this._rawResponseHeadersPromise ? await this._rawResponseHeadersPromise : this._headers; const headers = await this._bestEffortResponseHeaders();
for (const header of headers) for (const header of headers)
headersSize += header.name.length + header.value.length + 4; // 4 = ': ' + '\r\n' headersSize += header.name.length + header.value.length + 4; // 4 = ': ' + '\r\n'
headersSize += 2; // '\r\n' headersSize += 2; // '\r\n'
return headersSize; return headersSize;
} }
private async _bestEffortResponseHeaders(): Promise<types.HeadersArray> {
return this._rawResponseHeadersPromise ? await this._rawResponseHeadersPromise : this._headers;
}
async sizes(): Promise<ResourceSizes> { async sizes(): Promise<ResourceSizes> {
await this._finishedPromise; await this._finishedPromise;
const requestHeadersSize = await this._requestHeadersSize(); const requestHeadersSize = await this._requestHeadersSize();
const responseHeadersSize = await this._responseHeadersSize(); const responseHeadersSize = await this._responseHeadersSize();
let { bodySize, encodedBodySize, transferSize } = this._request.responseSize; let { bodySize, encodedBodySize, transferSize } = this._request.responseSize;
if (!bodySize) {
const headers = await this._bestEffortResponseHeaders();
const contentLength = headers.find(h => h.name.toLowerCase() === 'content-length')?.value;
bodySize = contentLength ? +contentLength : 0;
}
if (!encodedBodySize && transferSize) { if (!encodedBodySize && transferSize) {
// Chromium only populates transferSize // Chromium only populates transferSize
encodedBodySize = transferSize - responseHeadersSize; // Firefox can return 0 transferSize
encodedBodySize = Math.max(0, transferSize - responseHeadersSize);
// Firefox only populate transferSize. // Firefox only populate transferSize.
if (!bodySize) if (!bodySize)
bodySize = encodedBodySize; bodySize = encodedBodySize;

View file

@ -252,8 +252,9 @@ export class HarTracer {
status: response.status(), status: response.status(),
statusText: response.statusText(), statusText: response.statusText(),
httpVersion: response.httpVersion(), httpVersion: response.httpVersion(),
cookies: cookiesForHar(response.headerValue('set-cookie'), '\n'), // These are bad values that will be overwritten bellow.
headers: response.headers().map(header => ({ name: header.name, value: header.value })), cookies: [],
headers: [],
content: { content: {
size: -1, size: -1,
mimeType: 'x-unknown', mimeType: 'x-unknown',
@ -292,12 +293,12 @@ export class HarTracer {
})); }));
this._addBarrier(page, response.rawRequestHeaders().then(headers => { this._addBarrier(page, response.rawRequestHeaders().then(headers => {
for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie')) for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie'))
harEntry.request.cookies.push(...cookiesForHar(header.value, ';')); harEntry.request.cookies.push(...header.value.split(';').map(parseCookie));
harEntry.request.headers = headers; harEntry.request.headers = headers;
})); }));
this._addBarrier(page, response.rawResponseHeaders().then(headers => { this._addBarrier(page, response.rawResponseHeaders().then(headers => {
for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie')) for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie'))
harEntry.response.cookies.push(...cookiesForHar(header.value, '\n')); harEntry.response.cookies.push(parseCookie(header.value));
harEntry.response.headers = headers; harEntry.response.headers = headers;
const contentType = headers.find(header => header.name.toLowerCase() === 'content-type'); const contentType = headers.find(header => header.name.toLowerCase() === 'content-type');
if (contentType) if (contentType)
@ -365,12 +366,6 @@ function postDataForHar(request: network.Request, content: 'omit' | 'sha1' | 'em
return result; return result;
} }
function cookiesForHar(header: string | undefined, separator: string): har.Cookie[] {
if (!header)
return [];
return header.split(separator).map(c => parseCookie(c));
}
function parseCookie(c: string): har.Cookie { function parseCookie(c: string): har.Cookie {
const cookie: har.Cookie = { const cookie: har.Cookie = {
name: '', name: '',

View file

@ -89,7 +89,8 @@ export class WKInterceptableRequest {
requestStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.requestStart) : -1, requestStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.requestStart) : -1,
responseStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.responseStart) : -1, responseStart: timingPayload ? wkMillisToRoundishMillis(timingPayload.responseStart) : -1,
}; };
return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody); const setCookieSeparator = process.platform === 'linux' ? '\n' : ',';
return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers, ',', setCookieSeparator), timing, getResponseBody);
} }
} }

View file

@ -246,11 +246,19 @@ export async function mkdirIfNeeded(filePath: string) {
type HeadersArray = { name: string, value: string }[]; type HeadersArray = { name: string, value: string }[];
type HeadersObject = { [key: string]: string }; type HeadersObject = { [key: string]: string };
export function headersObjectToArray(headers: HeadersObject): HeadersArray { export function headersObjectToArray(headers: HeadersObject, separator?: string, setCookieSeparator?: string): HeadersArray {
if (!setCookieSeparator)
setCookieSeparator = separator;
const result: HeadersArray = []; const result: HeadersArray = [];
for (const name in headers) { for (const name in headers) {
if (!Object.is(headers[name], undefined)) const values = headers[name];
result.push({ name, value: headers[name] }); if (separator) {
const sep = name.toLowerCase() === 'set-cookie' ? setCookieSeparator : separator;
for (const value of values.split(sep!))
result.push({ name, value: value.trim() });
} else {
result.push({ name, value: values });
}
} }
return result; return result;
} }

View file

@ -194,9 +194,7 @@ it('should include cookies', async ({ contextFactory, server }, testInfo) => {
]); ]);
}); });
it('should include set-cookies', async ({ contextFactory, server, browserName, platform }, testInfo) => { it('should include set-cookies', async ({ contextFactory, server }, testInfo) => {
it.fail(browserName === 'webkit' && platform === 'darwin', 'Does not work yet');
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', [ res.setHeader('Set-Cookie', [
@ -214,18 +212,20 @@ it('should include set-cookies', async ({ contextFactory, server, browserName, p
expect(new Date(cookies[2].expires).valueOf()).toBeGreaterThan(Date.now()); expect(new Date(cookies[2].expires).valueOf()).toBeGreaterThan(Date.now());
}); });
it('should include set-cookies with comma', async ({ contextFactory, server }, testInfo) => { it('should include set-cookies with comma', async ({ contextFactory, server, browserName }, testInfo) => {
it.fixme(browserName === 'webkit', 'We get "name1=val, ue1, name2=val, ue2" as a header value');
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.setHeader('Set-Cookie', [ res.setHeader('Set-Cookie', [
'name1=val,ue1', 'name1=val, ue1', 'name2=val, ue2',
]); ]);
res.end(); res.end();
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const log = await getLog(); const log = await getLog();
const cookies = log.entries[0].response.cookies; const cookies = log.entries[0].response.cookies;
expect(cookies[0]).toEqual({ name: 'name1', value: 'val,ue1' }); expect(cookies[0]).toEqual({ name: 'name1', value: 'val, ue1' });
expect(cookies[1]).toEqual({ name: 'name2', value: 'val, ue2' });
}); });
it('should include secure set-cookies', async ({ contextFactory, httpsServer }, testInfo) => { it('should include secure set-cookies', async ({ contextFactory, httpsServer }, testInfo) => {

View file

@ -90,10 +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();
const result = {}; expect(headers.headers()).toEqual(serverRequest.headers);
for (const header of headers.headers())
result[header.name.toLowerCase()] = header.value;
expect(result).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}) => {
@ -114,11 +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();
const result = {}; expect(headers.headers()).toEqual(serverRequest.headers);
for (const header of headers.headers())
result[header.name.toLowerCase()] = header.value;
expect(result).toEqual(serverRequest.headers);
}); });
it('should return postData', async ({page, server, isAndroid}) => { it('should return postData', async ({page, server, isAndroid}) => {
@ -273,17 +266,28 @@ 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 raw headers', async ({ page, server, browserName }) => { it('should report all headers', async ({ page, server, browserName, platform }) => {
const response = await page.goto(server.EMPTY_PAGE); const expectedHeaders = {};
const requestHeaders = await response.request().allHeaders(); server.setRoute('/headers', (req, res) => {
expect(requestHeaders.headerNames().map(h => h.toLowerCase())).toContain('accept'); for (let i = 0; i < req.rawHeaders.length; i += 2)
expect(requestHeaders.getAll('host')).toHaveLength(1); expectedHeaders[req.rawHeaders[i].toLowerCase()] = req.rawHeaders[i + 1];
expect(requestHeaders.get('host')).toBe(`localhost:${server.PORT}`); res.end();
});
const responseHeaders = await response.allHeaders(); await page.goto(server.EMPTY_PAGE);
expect(responseHeaders.headerNames().map(h => h.toLowerCase())).toContain('content-type'); const [request] = await Promise.all([
expect(responseHeaders.getAll('content-type')).toHaveLength(1); page.waitForRequest('**/*'),
expect(responseHeaders.get('content-type')).toBe('text/html; charset=utf-8'); page.evaluate(() => fetch('/headers', {
headers: [
['header-a', 'value-a'],
['header-b', 'value-b'],
['header-a', 'value-a-1'],
['header-a', 'value-a-2'],
]
}))
]);
const headers = await request.allHeaders();
expect(headers.headers()).toEqual(expectedHeaders);
}); });
it('should report raw response headers in redirects', async ({ page, server, browserName }) => { it('should report raw response headers in redirects', async ({ page, server, browserName }) => {
@ -310,3 +314,22 @@ it('should report raw response headers in redirects', async ({ page, server, bro
expect(redirectChain).toEqual(expectedUrls); expect(redirectChain).toEqual(expectedUrls);
expect(headersChain).toEqual(expectedHeaders); expect(headersChain).toEqual(expectedHeaders);
}); });
it('should report all cookies in one header', async ({ page, server }) => {
const expectedHeaders = {};
server.setRoute('/headers', (req, res) => {
for (let i = 0; i < req.rawHeaders.length; i += 2)
expectedHeaders[req.rawHeaders[i]] = req.rawHeaders[i + 1];
res.end();
});
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
document.cookie = 'myCookie=myValue';
document.cookie = 'myOtherCookie=myOtherValue';
});
const response = await page.goto(server.EMPTY_PAGE);
const headers = await response.request().allHeaders();
const cookie = headers.get('cookie');
expect(cookie).toBe('myCookie=myValue; myOtherCookie=myOtherValue');
});

View file

@ -117,3 +117,52 @@ it('should return status text', async ({page, server}) => {
const response = await page.goto(server.PREFIX + '/cool'); const response = await page.goto(server.PREFIX + '/cool');
expect(response.statusText()).toBe('cool!'); expect(response.statusText()).toBe('cool!');
}); });
it('should report all headers', async ({ page, server }) => {
const expectedHeaders = {
'header-a': ['value-a', 'value-a-1', 'value-a-2'],
'header-b': ['value-b'],
};
server.setRoute('/headers', (req, res) => {
res.writeHead(200, expectedHeaders);
res.end();
});
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse('**/*'),
page.evaluate(() => fetch('/headers'))
]);
const headers = await response.allHeaders();
const actualHeaders = {};
for (const name of headers.headerNames())
actualHeaders[name] = headers.getAll(name);
delete actualHeaders['Keep-Alive'];
delete actualHeaders['keep-alive'];
delete actualHeaders['Connection'];
delete actualHeaders['connection'];
delete actualHeaders['Date'];
delete actualHeaders['date'];
delete actualHeaders['Transfer-Encoding'];
delete actualHeaders['transfer-encoding'];
expect(actualHeaders).toEqual(expectedHeaders);
});
it('should report multiple set-cookie headers', async ({ page, server }) => {
server.setRoute('/headers', (req, res) => {
res.writeHead(200, {
'Set-Cookie': ['a=b', 'c=d']
});
res.write('\r\n');
res.end();
});
await page.goto(server.EMPTY_PAGE);
const [response] = await Promise.all([
page.waitForResponse('**/*'),
page.evaluate(() => fetch('/headers'))
]);
const headers = await response.allHeaders();
const cookies = headers.getAll('set-cookie');
expect(cookies).toEqual(['a=b', 'c=d']);
});

View file

@ -55,8 +55,8 @@ it('should set bodySize, headersSize, and transferSize', async ({page, server, b
]); ]);
const sizes = await response.request().sizes(); const sizes = await response.request().sizes();
expect(sizes.responseBodySize).toBe(6); expect(sizes.responseBodySize).toBe(6);
expect(sizes.responseHeadersSize).toBeGreaterThanOrEqual(150); expect(sizes.responseHeadersSize).toBeGreaterThanOrEqual(100);
expect(sizes.responseTransferSize).toBeGreaterThanOrEqual(160); expect(sizes.responseTransferSize).toBeGreaterThanOrEqual(100);
}); });
it('should set bodySize to 0 when there was no response body', async ({page, server, browserName, platform}) => { it('should set bodySize to 0 when there was no response body', async ({page, server, browserName, platform}) => {

View file

@ -98,10 +98,9 @@ it('should work when header manipulation headers with redirect', async ({page, s
// @see https://github.com/GoogleChrome/puppeteer/issues/4743 // @see https://github.com/GoogleChrome/puppeteer/issues/4743
it('should be able to remove headers', async ({page, server}) => { it('should be able to remove headers', async ({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.route('**/*', route => { await page.route('**/*', async route => {
const headers = Object.assign({}, route.request().headers(), { const headers = { ...route.request().headers() };
foo: undefined, // remove "foo" header delete headers['foo'];
});
route.continue({ headers }); route.continue({ headers });
}); });

View file

@ -20,7 +20,6 @@ import { test as it, expect } from './pageTest';
it('should work', async ({page, server}) => { it('should work', async ({page, server}) => {
await page.setExtraHTTPHeaders({ await page.setExtraHTTPHeaders({
foo: 'bar', foo: 'bar',
baz: undefined,
}); });
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),

5
types/types.d.ts vendored
View file

@ -12682,9 +12682,10 @@ export interface Headers {
headerNames(): Array<string>; headerNames(): Array<string>;
/** /**
* Returns all raw headers. * Returns all headers as a dictionary. Header names are normalized to lower case, multi-value headers are concatenated
* using comma.
*/ */
headers(): Array<{ name: string, value: string }>; headers(): { [key: string]: string; };
} }
/** /**