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>;
clearPermissions(): 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>;
setHTTPCredentials(httpCredentials?: types.Credentials): 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 setGeolocation(geolocation?: types.Geolocation): 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 _doAddInitScript(expression: string): Promise<void>;
abstract _doExposeBinding(binding: PageBinding): Promise<void>;
@ -189,9 +189,11 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
const { username, password } = proxy;
if (username) {
this._options.httpCredentials = { username, password: password! };
this._options.extraHTTPHeaders = this._options.extraHTTPHeaders || {};
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)
options.viewport = { width: 1280, height: 720 };
verifyGeolocation(options.geolocation);
if (options.extraHTTPHeaders)
options.extraHTTPHeaders = network.verifyHeaders(options.extraHTTPHeaders);
}
export function verifyGeolocation(geolocation?: types.Geolocation) {

View file

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

View file

@ -23,6 +23,7 @@ import * as network from '../network';
import * as frames from '../frames';
import * as types from '../types';
import { CRPage } from './crPage';
import { headersObjectToArray } from '../converters';
export class CRNetworkManager {
private _client: CRSession;
@ -239,7 +240,7 @@ export class CRNetworkManager {
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
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) {
@ -350,7 +351,7 @@ class InterceptableRequest implements network.RouteDelegate {
if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes)
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) {
@ -406,11 +407,3 @@ const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = {
'timedout': 'TimedOut',
'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 * as sourceMap from '../utils/sourceMap';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { headersArrayToObject } from '../converters';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -729,7 +730,7 @@ class FrameSession {
this._crPage._browserContext._options.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> {

View file

@ -19,7 +19,6 @@ import * as mime from 'mime';
import * as path from 'path';
import * as util from 'util';
import * as types from './types';
import { helper, assert } from './helper';
export async function normalizeFilePayloads(files: string | types.FilePayload | string[] | types.FilePayload[]): Promise<types.FilePayload[]> {
let ff: string[] | types.FilePayload[];
@ -43,65 +42,18 @@ export async function normalizeFilePayloads(files: string | types.FilePayload |
return filePayloads;
}
export async function normalizeFulfillParameters(params: types.FulfillResponse & { path?: string }): Promise<types.NormalizedFulfillResponse> {
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 {
export function headersObjectToArray(headers: { [key: string]: string }): types.HeadersArray {
const result: types.HeadersArray = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined)) {
const value = headers[name];
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
result.push({ name, value });
}
if (!Object.is(headers[name], undefined))
result.push({ name, value: headers[name] });
}
return result;
}
export function headersArrayToObject(headers: types.HeadersArray): types.Headers {
const result: types.Headers = {};
export function headersArrayToObject(headers: types.HeadersArray, lowerCase: boolean): { [key: string]: string } {
const result: { [key: string]: string } = {};
for (const { name, value } of headers)
result[name] = value;
result[lowerCase ? name.toLowerCase() : name] = value;
return result;
}

View file

@ -24,7 +24,6 @@ import { Page, PageBinding } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage';
import { Protocol } from './protocol';
@ -211,7 +210,7 @@ export class FFBrowserContext extends BrowserContextBase {
if (this._options.permissions)
promises.push(this.grantPermissions(this._options.permissions));
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)
promises.push(this.setHTTPCredentials(this._options.httpCredentials));
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 });
}
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
const allHeaders = { ...this._options.extraHTTPHeaders };
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = headers;
let allHeaders = this._options.extraHTTPHeaders;
if (this._options.locale)
allHeaders['Accept-Language'] = this._options.locale;
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(allHeaders) });
allHeaders = network.mergeHeaders([allHeaders, network.singleHeader('Accept-Language', this._options.locale)]);
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: allHeaders });
}
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!`);
return Buffer.from(response.base64body, 'base64');
};
const headers: types.Headers = {};
for (const {name, value} of event.headers)
headers[name.toLowerCase()] = value;
const response = new network.Response(request.request, event.status, event.statusText, headers, getResponseBody);
const response = new network.Response(request.request, event.status, event.statusText, event.headers, getResponseBody);
this._page._frameManager.requestReceivedResponse(response);
}
@ -150,14 +147,11 @@ class InterceptableRequest implements network.RouteDelegate {
this._id = payload.requestId;
this._session = session;
const headers: types.Headers = {};
for (const {name, value} of payload.headers)
headers[name.toLowerCase()] = value;
let postDataBuffer = null;
if (payload.postData)
postDataBuffer = Buffer.from(payload.postData, 'base64');
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) {
@ -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 { FFExecutionContext } from './ffExecutionContext';
import { RawKeyboardImpl, RawMouseImpl } from './ffInput';
import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { FFNetworkManager } from './ffNetworkManager';
import { Protocol } from './protocol';
import { selectors } from '../selectors';
import { rewriteErrorMessage } from '../utils/stackTrace';
@ -272,7 +272,7 @@ export class FFPage implements PageDelegate {
}
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> {

View file

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

View file

@ -16,8 +16,7 @@
import * as frames from './frames';
import * as types from './types';
import { assert, helper } from './helper';
import { normalizeFulfillParameters, normalizeContinueOverrides } from './converters';
import { assert } from './helper';
export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s));
@ -78,13 +77,13 @@ export class Request {
private _resourceType: string;
private _method: string;
private _postData: Buffer | null;
private _headers: types.Headers;
private _headers: types.HeadersArray;
private _frame: frames.Frame;
private _waitForResponsePromise: Promise<Response | null>;
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
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(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects');
this._routeDelegate = routeDelegate;
@ -123,8 +122,8 @@ export class Request {
return this._postData;
}
headers(): {[key: string]: string} {
return { ...this._headers };
headers(): types.HeadersArray {
return this._headers;
}
response(): Promise<Response | null> {
@ -191,15 +190,15 @@ export class Route {
await this._delegate.abort(errorCode);
}
async fulfill(response: types.FulfillResponse & { path?: string }) {
async fulfill(response: types.NormalizedFulfillResponse) {
assert(!this._handled, 'Route is already handled!');
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!');
await this._delegate.continue(normalizeContinueOverrides(overrides));
await this._delegate.continue(overrides);
}
}
@ -215,10 +214,10 @@ export class Response {
private _status: number;
private _statusText: string;
private _url: string;
private _headers: types.Headers;
private _headers: types.HeadersArray;
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._status = status;
this._statusText = statusText;
@ -247,8 +246,8 @@ export class Response {
return this._statusText;
}
headers(): types.Headers {
return { ...this._headers };
headers(): types.HeadersArray {
return this._headers;
}
finished(): Promise<Error | null> {
@ -348,30 +347,24 @@ export const STATUS_TEXTS: { [status: string]: string } = {
'511': 'Network Authentication Required',
};
export function verifyHeaders(headers: types.Headers): types.Headers {
const result: types.Headers = {};
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 singleHeader(name: string, value: string): types.HeadersArray {
return [{ name, value }];
}
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 lowerCaseToOriginalCase = new Map<string, string>();
for (const h of headers) {
if (!h)
continue;
for (const key of Object.keys(h)) {
const lower = key.toLowerCase();
lowerCaseToOriginalCase.set(lower, key);
lowerCaseToValue.set(lower, h[key]);
for (const { name, value } of h) {
const lower = name.toLowerCase();
lowerCaseToOriginalCase.set(lower, name);
lowerCaseToValue.set(lower, value);
}
}
const result: types.Headers = {};
const result: types.HeadersArray = [];
for (const [lower, value] of lowerCaseToValue)
result[lowerCaseToOriginalCase.get(lower)!] = value;
result.push({ name: lowerCaseToOriginalCase.get(lower)!, value });
return result;
}

View file

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

View file

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

View file

@ -28,6 +28,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../../timeoutSettings';
import { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper';
import { validateHeaders } from './network';
export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
@ -88,6 +89,8 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
const logger = options.logger;
options = { ...options, logger: undefined };
return this._wrapApiCall('browserType.launchPersistentContext', async () => {
if (options.extraHTTPHeaders)
validateHeaders(options.extraHTTPHeaders);
const persistentOptions: BrowserTypeLaunchPersistentContextParams = {
...options,
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 { ChannelOwner } from './channelOwner';
import { Frame } from './frame';
import { normalizeFulfillParameters, headersArrayToObject, normalizeContinueOverrides } from '../../converters';
import { headersArrayToObject, headersObjectToArray } from '../../converters';
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 = {
name: string,
@ -44,19 +48,6 @@ export type SetNetworkCookieParam = {
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> {
private _redirectedFrom: 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);
if (this._redirectedFrom)
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;
}
@ -175,17 +166,49 @@ export class Route extends ChannelOwner<RouteChannel, RouteInitializer> {
await this._channel.abort({ errorCode });
}
async fulfill(response: FulfillResponse & { path?: string }) {
const normalized = await normalizeFulfillParameters(response);
await this._channel.fulfill(normalized);
async fulfill(response: { status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string }) {
let body = '';
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 = {}) {
const normalized = normalizeContinueOverrides(overrides);
async continue(overrides: { method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
const postDataBuffer = helper.isString(overrides.postData) ? Buffer.from(overrides.postData, 'utf8') : overrides.postData;
await this._channel.continue({
method: normalized.method,
headers: normalized.headers,
postData: normalized.postData ? normalized.postData.toString('base64') : undefined
method: overrides.method,
headers: overrides.headers ? headersObjectToArray(overrides.headers) : 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) {
super(parent, type, guid, initializer);
this._headers = headersArrayToObject(initializer.headers);
this._headers = headersArrayToObject(initializer.headers, true /* lowerCase */);
}
url(): string {
@ -257,3 +280,11 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
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 { Keyboard, Mouse } from './input';
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 { Buffer } from 'buffer';
import { ChromiumCoverage } from './chromiumCoverage';
@ -291,6 +291,7 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
async setExtraHTTPHeaders(headers: Headers) {
return this._wrapApiCall('page.setExtraHTTPHeaders', async () => {
validateHeaders(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 { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Events as ChromiumEvents } from '../../chromium/events';
import { headersArrayToObject } from '../../converters';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, BrowserContextInitializer> implements BrowserContextChannel {
private _context: BrowserContextBase;
@ -96,7 +95,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
}
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> {

View file

@ -23,7 +23,6 @@ import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { CRBrowser } from '../../chromium/crBrowser';
import { PageDispatcher } from './pageDispatcher';
import { headersArrayToObject } from '../../converters';
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
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 }> {
const options = {
...params,
extraHTTPHeaders: params.extraHTTPHeaders ? headersArrayToObject(params.extraHTTPHeaders) : undefined,
};
return { context: new BrowserContextDispatcher(this._scope, await this._object.newContext(options) as BrowserContextBase) };
return { context: new BrowserContextDispatcher(this._scope, await this._object.newContext(params) as BrowserContextBase) };
}
async close(): Promise<void> {

View file

@ -21,7 +21,6 @@ import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel, BrowserTypeI
import { Dispatcher, DispatcherScope } from './dispatcher';
import { BrowserContextBase } from '../../browserContext';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { headersArrayToObject } from '../../converters';
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeInitializer> implements BrowserTypeChannel {
constructor(scope: DispatcherScope, browserType: BrowserTypeBase) {
@ -37,11 +36,7 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
}
async launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams): Promise<{ context: BrowserContextChannel }> {
const options = {
...params,
extraHTTPHeaders: params.extraHTTPHeaders ? headersArrayToObject(params.extraHTTPHeaders) : undefined,
};
const browserContext = await this._object.launchPersistentContext(params.userDataDir, options);
const browserContext = await this._object.launchPersistentContext(params.userDataDir, params);
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 { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { headersObjectToArray, headersArrayToObject } from '../../converters';
import * as types from '../../types';
export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> implements RequestChannel {
@ -40,7 +39,7 @@ export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> i
resourceType: request.resourceType(),
method: request.method(),
postData: postData === null ? undefined : postData.toString('base64'),
headers: headersObjectToArray(request.headers()),
headers: request.headers(),
isNavigationRequest: request.isNavigationRequest(),
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
});
@ -60,7 +59,7 @@ export class ResponseDispatcher extends Dispatcher<Response, ResponseInitializer
url: response.url(),
status: response.status(),
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> {
await this._object.continue({
method: params.method,
headers: params.headers ? headersArrayToObject(params.headers) : undefined,
headers: params.headers,
postData: params.postData ? Buffer.from(params.postData, 'base64') : undefined,
});
}
async fulfill(params: types.NormalizedFulfillResponse): Promise<void> {
await this._object.fulfill({
status: params.status,
headers: params.headers ? headersArrayToObject(params.headers) : undefined,
body: params.isBase64 ? Buffer.from(params.body, 'base64') : params.body,
});
await this._object.fulfill(params);
}
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 { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher';
import { parseError, serializeError } from '../serializers';
import { headersArrayToObject } from '../../converters';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher';
import { DownloadDispatcher } from './downloadDispatcher';
@ -92,7 +91,7 @@ export class PageDispatcher extends Dispatcher<Page, PageInitializer> implements
}
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 }> {

View file

@ -208,20 +208,12 @@ export type MouseMultiClickOptions = PointerActionOptions & {
export type World = 'main' | 'utility';
export type Headers = { [key: string]: string };
export type HeadersArray = { name: string, value: string }[];
export type GotoOptions = NavigateOptions & {
referer?: string,
};
export type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | Buffer,
};
export type NormalizedFulfillResponse = {
status: number,
headers: HeadersArray,
@ -229,12 +221,6 @@ export type NormalizedFulfillResponse = {
isBase64: boolean,
};
export type ContinueOverrides = {
method?: string,
headers?: Headers,
postData?: string | Buffer,
};
export type NormalizedContinueOverrides = {
method?: string,
headers?: HeadersArray,
@ -275,7 +261,7 @@ export type BrowserContextOptions = {
timezoneId?: string,
geolocation?: Geolocation,
permissions?: string[],
extraHTTPHeaders?: Headers,
extraHTTPHeaders?: HeadersArray,
offline?: boolean,
httpCredentials?: Credentials,
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 });
}
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
this._options.extraHTTPHeaders = headers;
for (const page of this.pages())
await (page._delegate as WKPage).updateExtraHTTPHeaders();
}

View file

@ -21,7 +21,7 @@ import * as network from '../network';
import * as types from '../types';
import { Protocol } from './protocol';
import { WKSession } from './wkConnection';
import { headersArrayToObject } from '../converters';
import { headersArrayToObject, headersObjectToArray } from '../converters';
const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
'aborted': 'Cancellation',
@ -57,7 +57,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
if (event.request.postData)
postDataBuffer = Buffer.from(event.request.postData, 'binary');
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);
}
@ -76,7 +76,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
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'];
if (contentType)
mimeType = contentType.split(';')[0].trim();
@ -98,7 +98,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
await this._session.sendMayFail('Network.interceptWithRequest', {
requestId: this._requestId,
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
});
}
@ -108,13 +108,6 @@ export class WKInterceptableRequest implements network.RouteDelegate {
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
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 png from 'pngjs';
import { JSHandle } from '../javascript';
import { headersArrayToObject } from '../converters';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -175,7 +176,7 @@ export class WKPage implements PageDelegate {
}));
}
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)
promises.push(session.send('Network.setEmulateOfflineState', { offline: true }));
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: !!contextOptions.hasTouch }));
@ -551,17 +552,16 @@ export class WKPage implements PageDelegate {
}
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([
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;
}

View file

@ -22,10 +22,13 @@ import path from 'path';
it('should work', async({page, server}) => {
server.setRoute('/empty.html', (req, res) => {
res.setHeader('foo', 'bar');
res.setHeader('BaZ', 'bAz');
res.end();
});
const response = await page.goto(server.EMPTY_PAGE);
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}) => {
await page.setExtraHTTPHeaders({
foo: 'bar'
foo: 'bar',
baz: undefined,
});
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
expect(request.headers['foo']).toBe('bar');
expect(request.headers['baz']).toBe(undefined);
});
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');
});
it('should throw for non-string header values', async({page, server}) => {
let error = null;
try {
await page.setExtraHTTPHeaders({ 'foo': 1 as any });
} catch (e) {
error = e;
}
expect(error.message).toContain('Expected value of header "foo" to be String, but "number" is found.');
it('should throw for non-string header values', async({browser, page}) => {
const error1 = await page.setExtraHTTPHeaders({ 'foo': 1 as any }).catch(e => e);
expect(error1.message).toContain('Expected value of header "foo" to be String, but "number" is found.');
const error2 = await page.context().setExtraHTTPHeaders({ 'foo': true as any }).catch(e => e);
expect(error2.message).toContain('Expected value of header "foo" to be String, but "boolean" is found.');
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.');
});