From 7f6171579b4e300329ac34ec6fab3312403be51b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 15 Jul 2020 13:21:21 -0700 Subject: [PATCH] feat(rpc): use headers array in the protocol (#2959) --- src/chromium/crNetworkManager.ts | 15 ++-------- src/firefox/ffNetworkManager.ts | 15 ++++------ src/network.ts | 8 +++--- src/rpc/channels.ts | 17 ++++------- src/rpc/client/browserContext.ts | 3 +- src/rpc/client/network.ts | 19 ++++++++----- src/rpc/client/page.ts | 4 +-- src/rpc/serializers.ts | 33 ++++++++++++++++++++-- src/rpc/server/browserContextDispatcher.ts | 5 ++-- src/rpc/server/networkDispatchers.ts | 19 ++++++++----- src/rpc/server/pageDispatcher.ts | 6 ++-- src/types.ts | 15 +++++++++- src/webkit/wkInterceptableRequest.ts | 10 ++++--- 13 files changed, 102 insertions(+), 67 deletions(-) diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index c04e283d27..656cae11ef 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -348,12 +348,12 @@ class InterceptableRequest implements network.RouteDelegate { this.request = new network.Request(allowInterception ? this : null, frame, redirectedFrom, documentId, url, type, method, postData, headersObject(headers)); } - async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) { + async continue(overrides: types.NormalizedContinueOverrides) { // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors. await this._client._sendMayFail('Fetch.continueRequest', { requestId: this._interceptionId!, - headers: overrides.headers ? headersArray(overrides.headers) : undefined, + headers: overrides.headers, method: overrides.method, postData: overrides.postData }); @@ -368,7 +368,7 @@ class InterceptableRequest implements network.RouteDelegate { requestId: this._interceptionId!, responseCode: response.status, responsePhrase: network.STATUS_TEXTS[String(response.status)], - responseHeaders: headersArray(response.headers), + responseHeaders: response.headers, body, }); } @@ -402,15 +402,6 @@ const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = { 'failed': 'Failed', }; -function headersArray(headers: { [s: string]: string; }): { name: string; value: string; }[] { - const result = []; - for (const name in headers) { - if (!Object.is(headers[name], undefined)) - result.push({name, value: headers[name] + ''}); - } - return result; -} - function headersObject(headers: Protocol.Network.Headers): types.Headers { const result: types.Headers = {}; for (const key of Object.keys(headers)) diff --git a/src/firefox/ffNetworkManager.ts b/src/firefox/ffNetworkManager.ts index a75483a052..708c335df4 100644 --- a/src/firefox/ffNetworkManager.ts +++ b/src/firefox/ffNetworkManager.ts @@ -158,17 +158,12 @@ class InterceptableRequest implements network.RouteDelegate { payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, payload.postData || null, headers); } - async continue(overrides: { method?: string; headers?: types.Headers; postData?: string }) { - const { - method, - headers, - postData - } = overrides; + async continue(overrides: types.NormalizedContinueOverrides) { await this._session.sendMayFail('Network.resumeInterceptedRequest', { requestId: this._id, - method, - headers: headers ? headersArray(headers) : undefined, - postData: postData ? Buffer.from(postData).toString('base64') : undefined + method: overrides.method, + headers: overrides.headers, + postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined }); } @@ -179,7 +174,7 @@ class InterceptableRequest implements network.RouteDelegate { requestId: this._id, status: response.status, statusText: network.STATUS_TEXTS[String(response.status)] || '', - headers: headersArray(response.headers), + headers: response.headers, base64body, }); } diff --git a/src/network.ts b/src/network.ts index e1f858a6e9..ba5dda08aa 100644 --- a/src/network.ts +++ b/src/network.ts @@ -18,7 +18,7 @@ import * as frames from './frames'; import * as types from './types'; import { assert, helper } from './helper'; import { URLSearchParams } from 'url'; -import { normalizeFulfillParameters } from './rpc/serializers'; +import { normalizeFulfillParameters, normalizeContinueOverrides } from './rpc/serializers'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); @@ -221,9 +221,9 @@ export class Route { await this._delegate.fulfill(await normalizeFulfillParameters(response)); } - async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) { + async continue(overrides: types.ContinueOverrides = {}) { assert(!this._handled, 'Route is already handled!'); - await this._delegate.continue(overrides); + await this._delegate.continue(normalizeContinueOverrides(overrides)); } } @@ -316,7 +316,7 @@ export class Response { export interface RouteDelegate { abort(errorCode: string): Promise; fulfill(response: types.NormalizedFulfillResponse): Promise; - continue(overrides: { method?: string; headers?: types.Headers; postData?: string; }): Promise; + continue(overrides: types.NormalizedContinueOverrides): Promise; } // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 5dbe4bdce1..6115b26a08 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -95,7 +95,7 @@ export interface BrowserContextChannel extends Channel { newPage(): Promise<{ page: PageChannel }>; setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void; setDefaultTimeoutNoReply(params: { timeout: number }): void; - setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; + setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise; setGeolocation(params: { geolocation: types.Geolocation | null }): Promise; setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; @@ -143,7 +143,7 @@ export interface PageChannel extends Channel { opener(): Promise<{ page: PageChannel | null }>; reload(params: types.NavigateOptions): Promise<{ response: ResponseChannel | null }>; screenshot(params: types.ScreenshotOptions): Promise<{ binary: Binary }>; - setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; + setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; setViewportSize(params: { viewportSize: types.Size }): Promise; @@ -282,7 +282,7 @@ export type RequestInitializer = { resourceType: string, method: string, postData: string | null, - headers: types.Headers, + headers: types.HeadersArray, isNavigationRequest: boolean, redirectedFrom: RequestChannel | null, }; @@ -290,13 +290,8 @@ export type RequestInitializer = { export interface RouteChannel extends Channel { abort(params: { errorCode: string }): Promise; - continue(params: { method?: string, headers?: types.Headers, postData?: string }): Promise; - fulfill(params: { - status?: number, - headers?: types.Headers, - body: string, - isBase64: boolean, - }): Promise; + continue(params: types.NormalizedContinueOverrides): Promise; + fulfill(params: types.NormalizedFulfillResponse): Promise; } export type RouteInitializer = { request: RequestChannel, @@ -312,7 +307,7 @@ export type ResponseInitializer = { url: string, status: number, statusText: string, - headers: types.Headers, + headers: types.HeadersArray, }; diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index 198f056f64..43d02a09a0 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -27,6 +27,7 @@ import { Events } from '../../events'; import { TimeoutSettings } from '../../timeoutSettings'; import { Waiter } from './waiter'; import { TimeoutError } from '../../errors'; +import { headersObjectToArray } from '../serializers'; export class BrowserContext extends ChannelOwner { _pages = new Set(); @@ -131,7 +132,7 @@ export class BrowserContext extends ChannelOwner { - await this._channel.setExtraHTTPHeaders({ headers }); + await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); } async setOffline(offline: boolean): Promise { diff --git a/src/rpc/client/network.ts b/src/rpc/client/network.ts index e08380cbab..0034ed4875 100644 --- a/src/rpc/client/network.ts +++ b/src/rpc/client/network.ts @@ -19,7 +19,7 @@ import * as types from '../../types'; import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels'; import { ChannelOwner } from './channelOwner'; import { Frame } from './frame'; -import { normalizeFulfillParameters } from '../serializers'; +import { normalizeFulfillParameters, headersArrayToObject, normalizeContinueOverrides } from '../serializers'; export type NetworkCookie = { name: string, @@ -48,6 +48,7 @@ export class Request extends ChannelOwner { private _redirectedFrom: Request | null = null; private _redirectedTo: Request | null = null; _failureText: string | null = null; + private _headers: types.Headers; static from(request: RequestChannel): Request { return (request as any)._object; @@ -62,6 +63,7 @@ export class Request extends ChannelOwner { this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom); if (this._redirectedFrom) this._redirectedFrom._redirectedTo = this; + this._headers = headersArrayToObject(initializer.headers); } url(): string { @@ -99,8 +101,8 @@ export class Request extends ChannelOwner { return JSON.parse(this._initializer.postData); } - headers(): {[key: string]: string} { - return { ...this._initializer.headers }; + headers(): types.Headers { + return { ...this._headers }; } async response(): Promise { @@ -154,14 +156,16 @@ export class Route extends ChannelOwner { await this._channel.fulfill(normalized); } - async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) { - await this._channel.continue(overrides); + async continue(overrides: types.ContinueOverrides = {}) { + await this._channel.continue(normalizeContinueOverrides(overrides)); } } export type RouteHandler = (route: Route, request: Request) => void; export class Response extends ChannelOwner { + private _headers: types.Headers; + static from(response: ResponseChannel): Response { return (response as any)._object; } @@ -172,6 +176,7 @@ export class Response extends ChannelOwner constructor(parent: ChannelOwner, type: string, guid: string, initializer: ResponseInitializer) { super(parent, type, guid, initializer); + this._headers = headersArrayToObject(initializer.headers); } url(): string { @@ -190,8 +195,8 @@ export class Response extends ChannelOwner return this._initializer.statusText; } - headers(): object { - return { ...this._initializer.headers }; + headers(): types.Headers { + return { ...this._headers }; } async finished(): Promise { diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 71abc106e4..cf80ab1710 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -21,7 +21,7 @@ import { assert, assertMaxArguments, helper, Listener } from '../../helper'; import { TimeoutSettings } from '../../timeoutSettings'; import * as types from '../../types'; import { BindingCallChannel, BindingCallInitializer, PageChannel, PageInitializer, PDFOptions } from '../channels'; -import { parseError, serializeError } from '../serializers'; +import { parseError, serializeError, headersObjectToArray } from '../serializers'; import { Accessibility } from './accessibility'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; @@ -282,7 +282,7 @@ export class Page extends ChannelOwner { } async setExtraHTTPHeaders(headers: types.Headers) { - await this._channel.setExtraHTTPHeaders({ headers }); + await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); } url(): string { diff --git a/src/rpc/serializers.ts b/src/rpc/serializers.ts index 68185a1a02..8425f4ec67 100644 --- a/src/rpc/serializers.ts +++ b/src/rpc/serializers.ts @@ -20,7 +20,7 @@ import * as path from 'path'; import * as util from 'util'; import { TimeoutError } from '../errors'; import * as types from '../types'; -import { helper } from '../helper'; +import { helper, assert } from '../helper'; export function serializeError(e: any): types.Error { @@ -82,7 +82,7 @@ export async function normalizeFulfillParameters(params: types.FulfillResponse & isBase64 = true; length = params.body.length; } - const headers: { [s: string]: string; } = {}; + const headers: types.Headers = {}; for (const header of Object.keys(params.headers || {})) headers[header.toLowerCase()] = String(params.headers![header]); if (params.contentType) @@ -94,8 +94,35 @@ export async function normalizeFulfillParameters(params: types.FulfillResponse & return { status: params.status || 200, - headers, + 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: overrides.postData, + }; +} + +export function headersObjectToArray(headers: types.Headers): 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 }); + } + } + return result; +} + +export function headersArrayToObject(headers: types.HeadersArray): types.Headers { + const result: types.Headers = {}; + for (const { name, value } of headers) + result[name] = value; + return result; +} diff --git a/src/rpc/server/browserContextDispatcher.ts b/src/rpc/server/browserContextDispatcher.ts index 5b269a5f0e..a535a7e016 100644 --- a/src/rpc/server/browserContextDispatcher.ts +++ b/src/rpc/server/browserContextDispatcher.ts @@ -24,6 +24,7 @@ import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; import { CRBrowserContext } from '../../chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { Events as ChromiumEvents } from '../../chromium/events'; +import { headersArrayToObject } from '../serializers'; export class BrowserContextDispatcher extends Dispatcher implements BrowserContextChannel { private _context: BrowserContextBase; @@ -94,8 +95,8 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.setExtraHTTPHeaders(params.headers); + async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise { + await this._context.setExtraHTTPHeaders(headersArrayToObject(params.headers)); } async setOffline(params: { offline: boolean }): Promise { diff --git a/src/rpc/server/networkDispatchers.ts b/src/rpc/server/networkDispatchers.ts index 1ad7bbfd66..91163cc6f1 100644 --- a/src/rpc/server/networkDispatchers.ts +++ b/src/rpc/server/networkDispatchers.ts @@ -15,10 +15,11 @@ */ import { Request, Response, Route } from '../../network'; -import * as types from '../../types'; 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 '../serializers'; +import * as types from '../../types'; export class RequestDispatcher extends Dispatcher implements RequestChannel { @@ -38,7 +39,7 @@ export class RequestDispatcher extends Dispatcher i resourceType: request.resourceType(), method: request.method(), postData: request.postData(), - headers: request.headers(), + headers: headersObjectToArray(request.headers()), isNavigationRequest: request.isNavigationRequest(), redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()), }); @@ -58,7 +59,7 @@ export class ResponseDispatcher extends Dispatcher impleme }); } - async continue(params: { method?: string, headers?: types.Headers, postData?: string }): Promise { - await this._object.continue(params); + async continue(params: types.NormalizedContinueOverrides): Promise { + await this._object.continue({ + method: params.method, + headers: params.headers ? headersArrayToObject(params.headers) : undefined, + postData: params.postData, + }); } - async fulfill(params: { status?: number, headers?: types.Headers, contentType?: string, body: string, isBase64: boolean }): Promise { + async fulfill(params: types.NormalizedFulfillResponse): Promise { await this._object.fulfill({ status: params.status, - headers: params.headers, + headers: params.headers ? headersArrayToObject(params.headers) : undefined, body: params.isBase64 ? Buffer.from(params.body, 'base64') : params.body, }); } diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index 130718a002..c5f2ac67f8 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -22,7 +22,7 @@ import { Page, Worker } from '../../page'; import * as types from '../../types'; import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, PDFOptions } from '../channels'; import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; -import { parseError, serializeError } from '../serializers'; +import { parseError, serializeError, headersArrayToObject } from '../serializers'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { DialogDispatcher } from './dialogDispatcher'; import { DownloadDispatcher } from './downloadDispatcher'; @@ -91,8 +91,8 @@ export class PageDispatcher extends Dispatcher implements }); } - async setExtraHTTPHeaders(params: { headers: types.Headers }): Promise { - await this._page.setExtraHTTPHeaders(params.headers); + async setExtraHTTPHeaders(params: { headers: types.HeadersArray }): Promise { + await this._page.setExtraHTTPHeaders(headersArrayToObject(params.headers)); } async reload(params: types.NavigateOptions): Promise<{ response: ResponseChannel | null }> { diff --git a/src/types.ts b/src/types.ts index 0645f8ab72..0a6aae824f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -202,6 +202,7 @@ 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, @@ -216,11 +217,23 @@ export type FulfillResponse = { export type NormalizedFulfillResponse = { status: number, - headers: Headers, + headers: HeadersArray, body: string, isBase64: boolean, }; +export type ContinueOverrides = { + method?: string, + headers?: Headers, + postData?: string, +}; + +export type NormalizedContinueOverrides = { + method?: string, + headers?: HeadersArray, + postData?: string, +}; + export type NetworkCookie = { name: string, value: string, diff --git a/src/webkit/wkInterceptableRequest.ts b/src/webkit/wkInterceptableRequest.ts index 4e94768ed5..542b2a1377 100644 --- a/src/webkit/wkInterceptableRequest.ts +++ b/src/webkit/wkInterceptableRequest.ts @@ -21,6 +21,7 @@ import * as network from '../network'; import * as types from '../types'; import { Protocol } from './protocol'; import { WKSession } from './wkConnection'; +import { headersArrayToObject } from '../rpc/serializers'; const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = { 'aborted': 'Cancellation', @@ -72,7 +73,8 @@ 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 contentType = response.headers['content-type']; + const headers = headersArrayToObject(response.headers); + const contentType = headers['content-type']; if (contentType) mimeType = contentType.split(';')[0].trim(); await this._session.sendMayFail('Network.interceptRequestWithResponse', { @@ -80,20 +82,20 @@ export class WKInterceptableRequest implements network.RouteDelegate { status: response.status, statusText: network.STATUS_TEXTS[String(response.status)], mimeType, - headers: response.headers, + headers, base64Encoded: response.isBase64, content: response.body }); } - async continue(overrides: { method?: string; headers?: types.Headers; postData?: string }) { + async continue(overrides: types.NormalizedContinueOverrides) { await this._interceptedPromise; // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors. await this._session.sendMayFail('Network.interceptWithRequest', { requestId: this._requestId, method: overrides.method, - headers: overrides.headers, + headers: overrides.headers ? headersArrayToObject(overrides.headers) : undefined, postData: overrides.postData ? Buffer.from(overrides.postData).toString('base64') : undefined }); }