chore: use HeadersArray instead of Headers object on the server side (#3512)

This simplifies implementation and avoids multiple conversions.
Also adding some tests around lowercase and wrong types.
This commit is contained in:
Dmitry Gozman 2020-08-18 15:38:29 -07:00 committed by GitHub
parent 77cab8bed3
commit aeadf50165
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 157 additions and 229 deletions

View file

@ -39,7 +39,7 @@ export interface BrowserContext extends EventEmitter {
grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void>; grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void>;
clearPermissions(): Promise<void>; clearPermissions(): Promise<void>;
setGeolocation(geolocation?: types.Geolocation): Promise<void>; setGeolocation(geolocation?: types.Geolocation): Promise<void>;
setExtraHTTPHeaders(headers: types.Headers): Promise<void>; setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void>;
setOffline(offline: boolean): Promise<void>; setOffline(offline: boolean): Promise<void>;
setHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>; setHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>; addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>;
@ -103,7 +103,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
abstract _doClearPermissions(): Promise<void>; abstract _doClearPermissions(): Promise<void>;
abstract setGeolocation(geolocation?: types.Geolocation): Promise<void>; abstract setGeolocation(geolocation?: types.Geolocation): Promise<void>;
abstract _doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>; abstract _doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
abstract setExtraHTTPHeaders(headers: types.Headers): Promise<void>; abstract setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void>;
abstract setOffline(offline: boolean): Promise<void>; abstract setOffline(offline: boolean): Promise<void>;
abstract _doAddInitScript(expression: string): Promise<void>; abstract _doAddInitScript(expression: string): Promise<void>;
abstract _doExposeBinding(binding: PageBinding): Promise<void>; abstract _doExposeBinding(binding: PageBinding): Promise<void>;
@ -189,9 +189,11 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
const { username, password } = proxy; const { username, password } = proxy;
if (username) { if (username) {
this._options.httpCredentials = { username, password: password! }; this._options.httpCredentials = { username, password: password! };
this._options.extraHTTPHeaders = this._options.extraHTTPHeaders || {};
const token = Buffer.from(`${username}:${password}`).toString('base64'); const token = Buffer.from(`${username}:${password}`).toString('base64');
this._options.extraHTTPHeaders['Proxy-Authorization'] = `Basic ${token}`; this._options.extraHTTPHeaders = network.mergeHeaders([
this._options.extraHTTPHeaders,
network.singleHeader('Proxy-Authorization', `Basic ${token}`),
]);
} }
} }
@ -236,8 +238,6 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
if (!options.viewport && !options.noDefaultViewport) if (!options.viewport && !options.noDefaultViewport)
options.viewport = { width: 1280, height: 720 }; options.viewport = { width: 1280, height: 720 };
verifyGeolocation(options.geolocation); verifyGeolocation(options.geolocation);
if (options.extraHTTPHeaders)
options.extraHTTPHeaders = network.verifyHeaders(options.extraHTTPHeaders);
} }
export function verifyGeolocation(geolocation?: types.Geolocation) { export function verifyGeolocation(geolocation?: types.Geolocation) {

View file

@ -387,8 +387,8 @@ export class CRBrowserContext extends BrowserContextBase {
await (page._delegate as CRPage).updateGeolocation(); await (page._delegate as CRPage).updateGeolocation();
} }
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = headers;
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as CRPage).updateExtraHTTPHeaders(); await (page._delegate as CRPage).updateExtraHTTPHeaders();
} }

View file

@ -23,6 +23,7 @@ import * as network from '../network';
import * as frames from '../frames'; import * as frames from '../frames';
import * as types from '../types'; import * as types from '../types';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
import { headersObjectToArray } from '../converters';
export class CRNetworkManager { export class CRNetworkManager {
private _client: CRSession; private _client: CRSession;
@ -239,7 +240,7 @@ export class CRNetworkManager {
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
}; };
return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), getResponseBody);
} }
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) { _handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) {
@ -350,7 +351,7 @@ class InterceptableRequest implements network.RouteDelegate {
if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes) if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes)
postDataBuffer = Buffer.from(postDataEntries[0].bytes, 'base64'); postDataBuffer = Buffer.from(postDataEntries[0].bytes, 'base64');
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postDataBuffer, headersObject(headers)); this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
} }
async continue(overrides: types.NormalizedContinueOverrides) { async continue(overrides: types.NormalizedContinueOverrides) {
@ -406,11 +407,3 @@ const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = {
'timedout': 'TimedOut', 'timedout': 'TimedOut',
'failed': 'Failed', 'failed': 'Failed',
}; };
function headersObject(headers: Protocol.Network.Headers): types.Headers {
const result: types.Headers = {};
for (const key of Object.keys(headers))
result[key.toLowerCase()] = headers[key];
return result;
}

View file

@ -37,6 +37,7 @@ import * as types from '../types';
import { ConsoleMessage } from '../console'; import { ConsoleMessage } from '../console';
import * as sourceMap from '../utils/sourceMap'; import * as sourceMap from '../utils/sourceMap';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import { headersArrayToObject } from '../converters';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -729,7 +730,7 @@ class FrameSession {
this._crPage._browserContext._options.extraHTTPHeaders, this._crPage._browserContext._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders this._page._state.extraHTTPHeaders
]); ]);
await this._client.send('Network.setExtraHTTPHeaders', { headers }); await this._client.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
} }
async _updateGeolocation(): Promise<void> { async _updateGeolocation(): Promise<void> {

View file

@ -19,7 +19,6 @@ import * as mime from 'mime';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as types from './types'; import * as types from './types';
import { helper, assert } from './helper';
export async function normalizeFilePayloads(files: string | types.FilePayload | string[] | types.FilePayload[]): Promise<types.FilePayload[]> { export async function normalizeFilePayloads(files: string | types.FilePayload | string[] | types.FilePayload[]): Promise<types.FilePayload[]> {
let ff: string[] | types.FilePayload[]; let ff: string[] | types.FilePayload[];
@ -43,65 +42,18 @@ export async function normalizeFilePayloads(files: string | types.FilePayload |
return filePayloads; return filePayloads;
} }
export async function normalizeFulfillParameters(params: types.FulfillResponse & { path?: string }): Promise<types.NormalizedFulfillResponse> { export function headersObjectToArray(headers: { [key: string]: string }): types.HeadersArray {
let body = '';
let isBase64 = false;
let length = 0;
if (params.path) {
const buffer = await util.promisify(fs.readFile)(params.path);
body = buffer.toString('base64');
isBase64 = true;
length = buffer.length;
} else if (helper.isString(params.body)) {
body = params.body;
isBase64 = false;
length = Buffer.byteLength(body);
} else if (params.body) {
body = params.body.toString('base64');
isBase64 = true;
length = params.body.length;
}
const headers: types.Headers = {};
for (const header of Object.keys(params.headers || {}))
headers[header.toLowerCase()] = String(params.headers![header]);
if (params.contentType)
headers['content-type'] = String(params.contentType);
else if (params.path)
headers['content-type'] = mime.getType(params.path) || 'application/octet-stream';
if (length && !('content-length' in headers))
headers['content-length'] = String(length);
return {
status: params.status || 200,
headers: headersObjectToArray(headers),
body,
isBase64
};
}
export function normalizeContinueOverrides(overrides: types.ContinueOverrides): types.NormalizedContinueOverrides {
return {
method: overrides.method,
headers: overrides.headers ? headersObjectToArray(overrides.headers) : undefined,
postData: helper.isString(overrides.postData) ? Buffer.from(overrides.postData, 'utf8') : overrides.postData,
};
}
export function headersObjectToArray(headers: types.Headers): types.HeadersArray {
const result: types.HeadersArray = []; const result: types.HeadersArray = [];
for (const name in headers) { for (const name in headers) {
if (!Object.is(headers[name], undefined)) { if (!Object.is(headers[name], undefined))
const value = headers[name]; result.push({ name, value: headers[name] });
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
result.push({ name, value });
}
} }
return result; return result;
} }
export function headersArrayToObject(headers: types.HeadersArray): types.Headers { export function headersArrayToObject(headers: types.HeadersArray, lowerCase: boolean): { [key: string]: string } {
const result: types.Headers = {}; const result: { [key: string]: string } = {};
for (const { name, value } of headers) for (const { name, value } of headers)
result[name] = value; result[lowerCase ? name.toLowerCase() : name] = value;
return result; return result;
} }

View file

@ -24,7 +24,6 @@ import { Page, PageBinding } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types'; import * as types from '../types';
import { ConnectionEvents, FFConnection } from './ffConnection'; import { ConnectionEvents, FFConnection } from './ffConnection';
import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
@ -211,7 +210,7 @@ export class FFBrowserContext extends BrowserContextBase {
if (this._options.permissions) if (this._options.permissions)
promises.push(this.grantPermissions(this._options.permissions)); promises.push(this.grantPermissions(this._options.permissions));
if (this._options.extraHTTPHeaders || this._options.locale) if (this._options.extraHTTPHeaders || this._options.locale)
promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || {})); promises.push(this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []));
if (this._options.httpCredentials) if (this._options.httpCredentials)
promises.push(this.setHTTPCredentials(this._options.httpCredentials)); promises.push(this.setHTTPCredentials(this._options.httpCredentials));
if (this._options.geolocation) if (this._options.geolocation)
@ -294,12 +293,12 @@ export class FFBrowserContext extends BrowserContextBase {
await this._browser._connection.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId || undefined, geolocation: geolocation || null }); await this._browser._connection.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId || undefined, geolocation: geolocation || null });
} }
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = headers;
const allHeaders = { ...this._options.extraHTTPHeaders }; let allHeaders = this._options.extraHTTPHeaders;
if (this._options.locale) if (this._options.locale)
allHeaders['Accept-Language'] = this._options.locale; allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', this._options.locale)]);
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(allHeaders) }); await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: allHeaders });
} }
async setOffline(offline: boolean): Promise<void> { async setOffline(offline: boolean): Promise<void> {

View file

@ -75,10 +75,7 @@ export class FFNetworkManager {
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
return Buffer.from(response.base64body, 'base64'); return Buffer.from(response.base64body, 'base64');
}; };
const headers: types.Headers = {}; const response = new network.Response(request.request, event.status, event.statusText, event.headers, getResponseBody);
for (const {name, value} of event.headers)
headers[name.toLowerCase()] = value;
const response = new network.Response(request.request, event.status, event.statusText, headers, getResponseBody);
this._page._frameManager.requestReceivedResponse(response); this._page._frameManager.requestReceivedResponse(response);
} }
@ -150,14 +147,11 @@ class InterceptableRequest implements network.RouteDelegate {
this._id = payload.requestId; this._id = payload.requestId;
this._session = session; this._session = session;
const headers: types.Headers = {};
for (const {name, value} of payload.headers)
headers[name.toLowerCase()] = value;
let postDataBuffer = null; let postDataBuffer = null;
if (payload.postData) if (payload.postData)
postDataBuffer = Buffer.from(payload.postData, 'base64'); postDataBuffer = Buffer.from(payload.postData, 'base64');
this.request = new network.Request(payload.isIntercepted ? this : null, frame, redirectedFrom ? redirectedFrom.request : null, payload.navigationId, this.request = new network.Request(payload.isIntercepted ? this : null, frame, redirectedFrom ? redirectedFrom.request : null, payload.navigationId,
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, headers); payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
} }
async continue(overrides: types.NormalizedContinueOverrides) { async continue(overrides: types.NormalizedContinueOverrides) {
@ -188,12 +182,3 @@ class InterceptableRequest implements network.RouteDelegate {
}); });
} }
} }
export function headersArray(headers: types.Headers): Protocol.Network.HTTPHeader[] {
const result: Protocol.Network.HTTPHeader[] = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined))
result.push({name, value: headers[name] + ''});
}
return result;
}

View file

@ -28,7 +28,7 @@ import { FFBrowserContext } from './ffBrowser';
import { FFSession, FFSessionEvents } from './ffConnection'; import { FFSession, FFSessionEvents } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext'; import { FFExecutionContext } from './ffExecutionContext';
import { RawKeyboardImpl, RawMouseImpl } from './ffInput'; import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
import { FFNetworkManager, headersArray } from './ffNetworkManager'; import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { selectors } from '../selectors'; import { selectors } from '../selectors';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
@ -272,7 +272,7 @@ export class FFPage implements PageDelegate {
} }
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
await this._session.send('Network.setExtraHTTPHeaders', { headers: headersArray(this._page._state.extraHTTPHeaders || {}) }); await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page._state.extraHTTPHeaders || [] });
} }
async setViewportSize(viewportSize: types.Size): Promise<void> { async setViewportSize(viewportSize: types.Size): Promise<void> {

View file

@ -428,8 +428,9 @@ export class Frame {
return runNavigationTask(this, options, async progress => { return runNavigationTask(this, options, async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = (this._page._state.extraHTTPHeaders || {}); const headers = this._page._state.extraHTTPHeaders || [];
let referer = headers['referer'] || headers['Referer']; const refererHeader = headers.find(h => h.name === 'referer' || h.name === 'Referer');
let referer = refererHeader ? refererHeader.value : undefined;
if (options.referer !== undefined) { if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer) if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header'); throw new Error('"referer" is already specified as extra HTTP header');

View file

@ -16,8 +16,7 @@
import * as frames from './frames'; import * as frames from './frames';
import * as types from './types'; import * as types from './types';
import { assert, helper } from './helper'; import { assert } from './helper';
import { normalizeFulfillParameters, normalizeContinueOverrides } from './converters';
export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s)); const parsedURLs = urls.map(s => new URL(s));
@ -78,13 +77,13 @@ export class Request {
private _resourceType: string; private _resourceType: string;
private _method: string; private _method: string;
private _postData: Buffer | null; private _postData: Buffer | null;
private _headers: types.Headers; private _headers: types.HeadersArray;
private _frame: frames.Frame; private _frame: frames.Frame;
private _waitForResponsePromise: Promise<Response | null>; private _waitForResponsePromise: Promise<Response | null>;
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.Headers) { url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) {
assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!url.startsWith('data:'), 'Data urls should not fire requests');
assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects'); assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects');
this._routeDelegate = routeDelegate; this._routeDelegate = routeDelegate;
@ -123,8 +122,8 @@ export class Request {
return this._postData; return this._postData;
} }
headers(): {[key: string]: string} { headers(): types.HeadersArray {
return { ...this._headers }; return this._headers;
} }
response(): Promise<Response | null> { response(): Promise<Response | null> {
@ -191,15 +190,15 @@ export class Route {
await this._delegate.abort(errorCode); await this._delegate.abort(errorCode);
} }
async fulfill(response: types.FulfillResponse & { path?: string }) { async fulfill(response: types.NormalizedFulfillResponse) {
assert(!this._handled, 'Route is already handled!'); assert(!this._handled, 'Route is already handled!');
this._handled = true; this._handled = true;
await this._delegate.fulfill(await normalizeFulfillParameters(response)); await this._delegate.fulfill(response);
} }
async continue(overrides: types.ContinueOverrides = {}) { async continue(overrides: types.NormalizedContinueOverrides = {}) {
assert(!this._handled, 'Route is already handled!'); assert(!this._handled, 'Route is already handled!');
await this._delegate.continue(normalizeContinueOverrides(overrides)); await this._delegate.continue(overrides);
} }
} }
@ -215,10 +214,10 @@ export class Response {
private _status: number; private _status: number;
private _statusText: string; private _statusText: string;
private _url: string; private _url: string;
private _headers: types.Headers; private _headers: types.HeadersArray;
private _getResponseBodyCallback: GetResponseBodyCallback; private _getResponseBodyCallback: GetResponseBodyCallback;
constructor(request: Request, status: number, statusText: string, headers: types.Headers, getResponseBodyCallback: GetResponseBodyCallback) { constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, getResponseBodyCallback: GetResponseBodyCallback) {
this._request = request; this._request = request;
this._status = status; this._status = status;
this._statusText = statusText; this._statusText = statusText;
@ -247,8 +246,8 @@ export class Response {
return this._statusText; return this._statusText;
} }
headers(): types.Headers { headers(): types.HeadersArray {
return { ...this._headers }; return this._headers;
} }
finished(): Promise<Error | null> { finished(): Promise<Error | null> {
@ -348,30 +347,24 @@ export const STATUS_TEXTS: { [status: string]: string } = {
'511': 'Network Authentication Required', '511': 'Network Authentication Required',
}; };
export function verifyHeaders(headers: types.Headers): types.Headers { export function singleHeader(name: string, value: string): types.HeadersArray {
const result: types.Headers = {}; return [{ name, value }];
for (const key of Object.keys(headers)) {
const value = headers[key];
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
result[key] = value;
}
return result;
} }
export function mergeHeaders(headers: (types.Headers | undefined | null)[]): types.Headers { export function mergeHeaders(headers: (types.HeadersArray | undefined | null)[]): types.HeadersArray {
const lowerCaseToValue = new Map<string, string>(); const lowerCaseToValue = new Map<string, string>();
const lowerCaseToOriginalCase = new Map<string, string>(); const lowerCaseToOriginalCase = new Map<string, string>();
for (const h of headers) { for (const h of headers) {
if (!h) if (!h)
continue; continue;
for (const key of Object.keys(h)) { for (const { name, value } of h) {
const lower = key.toLowerCase(); const lower = name.toLowerCase();
lowerCaseToOriginalCase.set(lower, key); lowerCaseToOriginalCase.set(lower, name);
lowerCaseToValue.set(lower, h[key]); lowerCaseToValue.set(lower, value);
} }
} }
const result: types.Headers = {}; const result: types.HeadersArray = [];
for (const [lower, value] of lowerCaseToValue) for (const [lower, value] of lowerCaseToValue)
result[lowerCaseToOriginalCase.get(lower)!] = value; result.push({ name: lowerCaseToOriginalCase.get(lower)!, value });
return result; return result;
} }

View file

@ -87,7 +87,7 @@ type PageState = {
viewportSize: types.Size | null; viewportSize: types.Size | null;
mediaType: types.MediaType | null; mediaType: types.MediaType | null;
colorScheme: types.ColorScheme | null; colorScheme: types.ColorScheme | null;
extraHTTPHeaders: types.Headers | null; extraHTTPHeaders: types.HeadersArray | null;
}; };
export class Page extends EventEmitter { export class Page extends EventEmitter {
@ -214,8 +214,8 @@ export class Page extends EventEmitter {
await this._delegate.exposeBinding(binding); await this._delegate.exposeBinding(binding);
} }
setExtraHTTPHeaders(headers: types.Headers) { setExtraHTTPHeaders(headers: types.HeadersArray) {
this._state.extraHTTPHeaders = network.verifyHeaders(headers); this._state.extraHTTPHeaders = headers;
return this._delegate.updateExtraHTTPHeaders(); return this._delegate.updateExtraHTTPHeaders();
} }

View file

@ -22,6 +22,7 @@ import { Events } from './events';
import { BrowserType } from './browserType'; import { BrowserType } from './browserType';
import { headersObjectToArray } from '../../converters'; import { headersObjectToArray } from '../../converters';
import { BrowserContextOptions } from './types'; import { BrowserContextOptions } from './types';
import { validateHeaders } from './network';
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> { export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
readonly _contexts = new Set<BrowserContext>(); readonly _contexts = new Set<BrowserContext>();
@ -47,8 +48,9 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
const logger = options.logger; const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browser.newContext', async () => { return this._wrapApiCall('browser.newContext', async () => {
if (options.extraHTTPHeaders)
validateHeaders(options.extraHTTPHeaders);
const contextOptions: BrowserNewContextParams = { const contextOptions: BrowserNewContextParams = {
...options, ...options,
viewport: options.viewport === null ? undefined : options.viewport, viewport: options.viewport === null ? undefined : options.viewport,

View file

@ -147,6 +147,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
async setExtraHTTPHeaders(headers: Headers): Promise<void> { async setExtraHTTPHeaders(headers: Headers): Promise<void> {
return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => { return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => {
network.validateHeaders(headers);
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
}); });
} }

View file

@ -28,6 +28,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../../timeoutSettings'; import { TimeoutSettings } from '../../timeoutSettings';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper'; import { envObjectToArray } from './clientHelper';
import { validateHeaders } from './network';
export interface BrowserServerLauncher { export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<BrowserServer>; launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
@ -88,6 +89,8 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger; const logger = options.logger;
options = { ...options, logger: undefined }; options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launchPersistentContext', async () => { return this._wrapApiCall('browserType.launchPersistentContext', async () => {
if (options.extraHTTPHeaders)
validateHeaders(options.extraHTTPHeaders);
const persistentOptions: BrowserTypeLaunchPersistentContextParams = { const persistentOptions: BrowserTypeLaunchPersistentContextParams = {
...options, ...options,
viewport: options.viewport === null ? undefined : options.viewport, viewport: options.viewport === null ? undefined : options.viewport,

View file

@ -18,8 +18,12 @@ import { URLSearchParams } from 'url';
import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels'; import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { Frame } from './frame'; import { Frame } from './frame';
import { normalizeFulfillParameters, headersArrayToObject, normalizeContinueOverrides } from '../../converters'; import { headersArrayToObject, headersObjectToArray } from '../../converters';
import { Headers } from './types'; import { Headers } from './types';
import * as fs from 'fs';
import * as mime from 'mime';
import * as util from 'util';
import { helper } from '../../helper';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -44,19 +48,6 @@ export type SetNetworkCookieParam = {
sameSite?: 'Strict' | 'Lax' | 'None' sameSite?: 'Strict' | 'Lax' | 'None'
}; };
type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | Buffer,
};
type ContinueOverrides = {
method?: string,
headers?: Headers,
postData?: string | Buffer,
};
export class Request extends ChannelOwner<RequestChannel, RequestInitializer> { export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
private _redirectedFrom: Request | null = null; private _redirectedFrom: Request | null = null;
private _redirectedTo: Request | null = null; private _redirectedTo: Request | null = null;
@ -77,7 +68,7 @@ export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
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); this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null; this._postData = initializer.postData ? Buffer.from(initializer.postData, 'base64') : null;
} }
@ -175,17 +166,49 @@ export class Route extends ChannelOwner<RouteChannel, RouteInitializer> {
await this._channel.abort({ errorCode }); await this._channel.abort({ errorCode });
} }
async fulfill(response: FulfillResponse & { path?: string }) { async fulfill(response: { status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string }) {
const normalized = await normalizeFulfillParameters(response); let body = '';
await this._channel.fulfill(normalized); let isBase64 = false;
let length = 0;
if (response.path) {
const buffer = await util.promisify(fs.readFile)(response.path);
body = buffer.toString('base64');
isBase64 = true;
length = buffer.length;
} else if (helper.isString(response.body)) {
body = response.body;
isBase64 = false;
length = Buffer.byteLength(body);
} else if (response.body) {
body = response.body.toString('base64');
isBase64 = true;
length = response.body.length;
}
const headers: Headers = {};
for (const header of Object.keys(response.headers || {}))
headers[header.toLowerCase()] = String(response.headers![header]);
if (response.contentType)
headers['content-type'] = String(response.contentType);
else if (response.path)
headers['content-type'] = mime.getType(response.path) || 'application/octet-stream';
if (length && !('content-length' in headers))
headers['content-length'] = String(length);
await this._channel.fulfill({
status: response.status || 200,
headers: headersObjectToArray(headers),
body,
isBase64
});
} }
async continue(overrides: ContinueOverrides = {}) { async continue(overrides: { method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
const normalized = normalizeContinueOverrides(overrides); const postDataBuffer = helper.isString(overrides.postData) ? Buffer.from(overrides.postData, 'utf8') : overrides.postData;
await this._channel.continue({ await this._channel.continue({
method: normalized.method, method: overrides.method,
headers: normalized.headers, headers: overrides.headers ? headersObjectToArray(overrides.headers) : undefined,
postData: normalized.postData ? normalized.postData.toString('base64') : undefined postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
}); });
} }
} }
@ -205,7 +228,7 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
constructor(parent: ChannelOwner, type: string, guid: string, initializer: ResponseInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: ResponseInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this._headers = headersArrayToObject(initializer.headers); this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
} }
url(): string { url(): string {
@ -257,3 +280,11 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
return Request.from(this._initializer.request).frame(); return Request.from(this._initializer.request).frame();
} }
} }
export function validateHeaders(headers: Headers) {
for (const key of Object.keys(headers)) {
const value = headers[key];
if (!Object.is(value, undefined) && !helper.isString(value))
throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
}
}

View file

@ -32,7 +32,7 @@ import { Worker } from './worker';
import { Frame, FunctionWithSource, verifyLoadState, WaitForNavigationOptions } from './frame'; import { Frame, FunctionWithSource, verifyLoadState, WaitForNavigationOptions } from './frame';
import { Keyboard, Mouse } from './input'; import { Keyboard, Mouse } from './input';
import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle'; import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
import { Request, Response, Route, RouteHandler } from './network'; import { Request, Response, Route, RouteHandler, validateHeaders } from './network';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { ChromiumCoverage } from './chromiumCoverage'; import { ChromiumCoverage } from './chromiumCoverage';
@ -291,6 +291,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
async setExtraHTTPHeaders(headers: Headers) { async setExtraHTTPHeaders(headers: Headers) {
return this._wrapApiCall('page.setExtraHTTPHeaders', async () => { return this._wrapApiCall('page.setExtraHTTPHeaders', async () => {
validateHeaders(headers);
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
}); });
} }

View file

@ -24,7 +24,6 @@ import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../../chromium/crBrowser'; import { CRBrowserContext } from '../../chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Events as ChromiumEvents } from '../../chromium/events'; import { Events as ChromiumEvents } from '../../chromium/events';
import { headersArrayToObject } from '../../converters';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, BrowserContextInitializer> implements BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, BrowserContextInitializer> implements BrowserContextChannel {
private _context: BrowserContextBase; private _context: BrowserContextBase;
@ -96,7 +95,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
} }
async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise<void> { async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise<void> {
await this._context.setExtraHTTPHeaders(headersArrayToObject(params.headers)); await this._context.setExtraHTTPHeaders(params.headers);
} }
async setOffline(params: { offline: boolean }): Promise<void> { async setOffline(params: { offline: boolean }): Promise<void> {

View file

@ -23,7 +23,6 @@ import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher';
import { CRBrowser } from '../../chromium/crBrowser'; import { CRBrowser } from '../../chromium/crBrowser';
import { PageDispatcher } from './pageDispatcher'; import { PageDispatcher } from './pageDispatcher';
import { headersArrayToObject } from '../../converters';
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel { export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
constructor(scope: DispatcherScope, browser: BrowserBase, guid?: string) { constructor(scope: DispatcherScope, browser: BrowserBase, guid?: string) {
@ -37,11 +36,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> i
} }
async newContext(params: BrowserNewContextParams): Promise<{ context: BrowserContextChannel }> { async newContext(params: BrowserNewContextParams): Promise<{ context: BrowserContextChannel }> {
const options = { return { context: new BrowserContextDispatcher(this._scope, await this._object.newContext(params) as BrowserContextBase) };
...params,
extraHTTPHeaders: params.extraHTTPHeaders ? headersArrayToObject(params.extraHTTPHeaders) : undefined,
};
return { context: new BrowserContextDispatcher(this._scope, await this._object.newContext(options) as BrowserContextBase) };
} }
async close(): Promise<void> { async close(): Promise<void> {

View file

@ -21,7 +21,6 @@ import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel, BrowserTypeI
import { Dispatcher, DispatcherScope } from './dispatcher'; import { Dispatcher, DispatcherScope } from './dispatcher';
import { BrowserContextBase } from '../../browserContext'; import { BrowserContextBase } from '../../browserContext';
import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher';
import { headersArrayToObject } from '../../converters';
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeInitializer> implements BrowserTypeChannel { export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeInitializer> implements BrowserTypeChannel {
constructor(scope: DispatcherScope, browserType: BrowserTypeBase) { constructor(scope: DispatcherScope, browserType: BrowserTypeBase) {
@ -37,11 +36,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
} }
async launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams): Promise<{ context: BrowserContextChannel }> { async launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams): Promise<{ context: BrowserContextChannel }> {
const options = { const browserContext = await this._object.launchPersistentContext(params.userDataDir, params);
...params,
extraHTTPHeaders: params.extraHTTPHeaders ? headersArrayToObject(params.extraHTTPHeaders) : undefined,
};
const browserContext = await this._object.launchPersistentContext(params.userDataDir, options);
return { context: new BrowserContextDispatcher(this._scope, browserContext as BrowserContextBase) }; return { context: new BrowserContextDispatcher(this._scope, browserContext as BrowserContextBase) };
} }
} }

View file

@ -18,7 +18,6 @@ import { Request, Response, Route } from '../../network';
import { RequestChannel, ResponseChannel, RouteChannel, ResponseInitializer, RequestInitializer, RouteInitializer, Binary } from '../channels'; import { RequestChannel, ResponseChannel, RouteChannel, ResponseInitializer, RequestInitializer, RouteInitializer, Binary } from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { headersObjectToArray, headersArrayToObject } from '../../converters';
import * as types from '../../types'; import * as types from '../../types';
export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> implements RequestChannel { export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> implements RequestChannel {
@ -40,7 +39,7 @@ export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> i
resourceType: request.resourceType(), resourceType: request.resourceType(),
method: request.method(), method: request.method(),
postData: postData === null ? undefined : postData.toString('base64'), postData: postData === null ? undefined : postData.toString('base64'),
headers: headersObjectToArray(request.headers()), headers: request.headers(),
isNavigationRequest: request.isNavigationRequest(), isNavigationRequest: request.isNavigationRequest(),
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()), redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
}); });
@ -60,7 +59,7 @@ export class ResponseDispatcher extends Dispatcher<Response, ResponseInitializer
url: response.url(), url: response.url(),
status: response.status(), status: response.status(),
statusText: response.statusText(), statusText: response.statusText(),
headers: headersObjectToArray(response.headers()), headers: response.headers(),
}); });
} }
@ -85,17 +84,13 @@ export class RouteDispatcher extends Dispatcher<Route, RouteInitializer> impleme
async continue(params: { method?: string, headers?: types.HeadersArray, postData?: string }): Promise<void> { async continue(params: { method?: string, headers?: types.HeadersArray, postData?: string }): Promise<void> {
await this._object.continue({ await this._object.continue({
method: params.method, method: params.method,
headers: params.headers ? headersArrayToObject(params.headers) : undefined, headers: params.headers,
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined, postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
}); });
} }
async fulfill(params: types.NormalizedFulfillResponse): Promise<void> { async fulfill(params: types.NormalizedFulfillResponse): Promise<void> {
await this._object.fulfill({ await this._object.fulfill(params);
status: params.status,
headers: params.headers ? headersArrayToObject(params.headers) : undefined,
body: params.isBase64 ? Buffer.from(params.body, 'base64') : params.body,
});
} }
async abort(params: { errorCode?: string }): Promise<void> { async abort(params: { errorCode?: string }): Promise<void> {

View file

@ -23,7 +23,6 @@ import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, SerializedArgument, PagePdfParams, SerializedError, PageAccessibilitySnapshotResult, SerializedValue, PageEmulateMediaParams, AXNode } from '../channels'; import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, SerializedArgument, PagePdfParams, SerializedError, PageAccessibilitySnapshotResult, SerializedValue, PageEmulateMediaParams, AXNode } from '../channels';
import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher';
import { parseError, serializeError } from '../serializers'; import { parseError, serializeError } from '../serializers';
import { headersArrayToObject } from '../../converters';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher'; import { DialogDispatcher } from './dialogDispatcher';
import { DownloadDispatcher } from './downloadDispatcher'; import { DownloadDispatcher } from './downloadDispatcher';
@ -92,7 +91,7 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
} }
async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise<void> { async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise<void> {
await this._page.setExtraHTTPHeaders(headersArrayToObject(params.headers)); await this._page.setExtraHTTPHeaders(params.headers);
} }
async reload(params: types.NavigateOptions): Promise<{ response?: ResponseChannel }> { async reload(params: types.NavigateOptions): Promise<{ response?: ResponseChannel }> {

View file

@ -208,20 +208,12 @@ export type MouseMultiClickOptions = PointerActionOptions & {
export type World = 'main' | 'utility'; export type World = 'main' | 'utility';
export type Headers = { [key: string]: string };
export type HeadersArray = { name: string, value: string }[]; export type HeadersArray = { name: string, value: string }[];
export type GotoOptions = NavigateOptions & { export type GotoOptions = NavigateOptions & {
referer?: string, referer?: string,
}; };
export type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | Buffer,
};
export type NormalizedFulfillResponse = { export type NormalizedFulfillResponse = {
status: number, status: number,
headers: HeadersArray, headers: HeadersArray,
@ -229,12 +221,6 @@ export type NormalizedFulfillResponse = {
isBase64: boolean, isBase64: boolean,
}; };
export type ContinueOverrides = {
method?: string,
headers?: Headers,
postData?: string | Buffer,
};
export type NormalizedContinueOverrides = { export type NormalizedContinueOverrides = {
method?: string, method?: string,
headers?: HeadersArray, headers?: HeadersArray,
@ -275,7 +261,7 @@ export type BrowserContextOptions = {
timezoneId?: string, timezoneId?: string,
geolocation?: Geolocation, geolocation?: Geolocation,
permissions?: string[], permissions?: string[],
extraHTTPHeaders?: Headers, extraHTTPHeaders?: HeadersArray,
offline?: boolean, offline?: boolean,
httpCredentials?: Credentials, httpCredentials?: Credentials,
deviceScaleFactor?: number, deviceScaleFactor?: number,

View file

@ -293,8 +293,8 @@ export class WKBrowserContext extends BrowserContextBase {
await this._browser._browserSession.send('Playwright.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload }); await this._browser._browserSession.send('Playwright.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
} }
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> { async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers); this._options.extraHTTPHeaders = headers;
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage).updateExtraHTTPHeaders(); await (page._delegate as WKPage).updateExtraHTTPHeaders();
} }

View file

@ -21,7 +21,7 @@ import * as network from '../network';
import * as types from '../types'; import * as types from '../types';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { headersArrayToObject } from '../converters'; import { headersArrayToObject, headersObjectToArray } from '../converters';
const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = { const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
'aborted': 'Cancellation', 'aborted': 'Cancellation',
@ -57,7 +57,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
if (event.request.postData) if (event.request.postData)
postDataBuffer = Buffer.from(event.request.postData, 'binary'); postDataBuffer = Buffer.from(event.request.postData, 'binary');
this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url, this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, event.request.url,
resourceType, event.request.method, postDataBuffer, headersObject(event.request.headers)); resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
this._interceptedPromise = new Promise(f => this._interceptedCallback = f); this._interceptedPromise = new Promise(f => this._interceptedCallback = f);
} }
@ -76,7 +76,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain'; let mimeType = response.isBase64 ? 'application/octet-stream' : 'text/plain';
const headers = headersArrayToObject(response.headers); const headers = headersArrayToObject(response.headers, false /* lowerCase */);
const contentType = headers['content-type']; const contentType = headers['content-type'];
if (contentType) if (contentType)
mimeType = contentType.split(';')[0].trim(); mimeType = contentType.split(';')[0].trim();
@ -98,7 +98,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
await this._session.sendMayFail('Network.interceptWithRequest', { await this._session.sendMayFail('Network.interceptWithRequest', {
requestId: this._requestId, requestId: this._requestId,
method: overrides.method, method: overrides.method,
headers: overrides.headers ? headersArrayToObject(overrides.headers) : undefined, headers: overrides.headers ? headersArrayToObject(overrides.headers, false /* lowerCase */) : undefined,
postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined
}); });
} }
@ -108,13 +108,6 @@ export class WKInterceptableRequest implements network.RouteDelegate {
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId }); const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
}; };
return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody); return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), getResponseBody);
} }
} }
function headersObject(headers: Protocol.Network.Headers): types.Headers {
const result: types.Headers = {};
for (const key of Object.keys(headers))
result[key.toLowerCase()] = headers[key];
return result;
}

View file

@ -37,6 +37,7 @@ import { selectors } from '../selectors';
import * as jpeg from 'jpeg-js'; import * as jpeg from 'jpeg-js';
import * as png from 'pngjs'; import * as png from 'pngjs';
import { JSHandle } from '../javascript'; import { JSHandle } from '../javascript';
import { headersArrayToObject } from '../converters';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -175,7 +176,7 @@ export class WKPage implements PageDelegate {
})); }));
} }
promises.push(this.updateEmulateMedia()); promises.push(this.updateEmulateMedia());
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() })); promises.push(session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._calculateExtraHTTPHeaders(), false /* lowerCase */) }));
if (contextOptions.offline) if (contextOptions.offline)
promises.push(session.send('Network.setEmulateOfflineState', { offline: true })); promises.push(session.send('Network.setEmulateOfflineState', { offline: true }));
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: !!contextOptions.hasTouch })); promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: !!contextOptions.hasTouch }));
@ -551,17 +552,16 @@ export class WKPage implements PageDelegate {
} }
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
await this._updateState('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }); await this._updateState('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._calculateExtraHTTPHeaders(), false /* lowerCase */) });
} }
_calculateExtraHTTPHeaders(): types.Headers { _calculateExtraHTTPHeaders(): types.HeadersArray {
const locale = this._browserContext._options.locale;
const headers = network.mergeHeaders([ const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders, this._browserContext._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders this._page._state.extraHTTPHeaders,
locale ? network.singleHeader('Accept-Language', locale) : undefined,
]); ]);
const locale = this._browserContext._options.locale;
if (locale)
headers['Accept-Language'] = locale;
return headers; return headers;
} }

View file

@ -22,10 +22,13 @@ import path from 'path';
it('should work', async({page, server}) => { it('should work', async({page, server}) => {
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.setHeader('foo', 'bar'); res.setHeader('foo', 'bar');
res.setHeader('BaZ', 'bAz');
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(response.headers()['foo']).toBe('bar');
expect(response.headers()['baz']).toBe('bAz');
expect(response.headers()['BaZ']).toBe(undefined);
}); });

View file

@ -18,13 +18,15 @@ import './base.fixture';
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'),
page.goto(server.EMPTY_PAGE), page.goto(server.EMPTY_PAGE),
]); ]);
expect(request.headers['foo']).toBe('bar'); expect(request.headers['foo']).toBe('bar');
expect(request.headers['baz']).toBe(undefined);
}); });
it('should work with redirects', async({page, server}) => { it('should work with redirects', async({page, server}) => {
@ -70,12 +72,11 @@ it('should override extra headers from browser context', async({browser, server}
expect(request.headers['bar']).toBe('foO'); expect(request.headers['bar']).toBe('foO');
}); });
it('should throw for non-string header values', async({page, server}) => { it('should throw for non-string header values', async({browser, page}) => {
let error = null; const error1 = await page.setExtraHTTPHeaders({ 'foo': 1 as any }).catch(e => e);
try { expect(error1.message).toContain('Expected value of header "foo" to be String, but "number" is found.');
await page.setExtraHTTPHeaders({ 'foo': 1 as any }); const error2 = await page.context().setExtraHTTPHeaders({ 'foo': true as any }).catch(e => e);
} catch (e) { expect(error2.message).toContain('Expected value of header "foo" to be String, but "boolean" is found.');
error = e; const error3 = await browser.newContext({ extraHTTPHeaders: { 'foo': null as any } }).catch(e => e);
} expect(error3.message).toContain('Expected value of header "foo" to be String, but "object" is found.');
expect(error.message).toContain('Expected value of header "foo" to be String, but "number" is found.');
}); });