diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index d19795993e..9b416fb7f1 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -28,7 +28,7 @@ import { Events } from './events'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { Waiter } from './waiter'; import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; -import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString } from '../utils/utils'; +import { isUnderTest, headersObjectToArray, mkdirIfNeeded, isString, headersArrayToObject } from '../utils/utils'; import { isSafeCloseError } from '../utils/errors'; import * as api from '../../types/types'; import * as structs from '../../types/structs'; @@ -86,8 +86,8 @@ export class BrowserContext extends ChannelOwner this._onRequest(network.Request.from(request), Page.fromNullable(page))); this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page))); - this._channel.on('requestFinished', ({ request, responseEndTiming, page, requestSizes }) => - this._onRequestFinished(network.Request.from(request), responseEndTiming, requestSizes, Page.fromNullable(page)) + this._channel.on('requestFinished', ({ request, response, responseEndTiming, responseHeaders, page, requestSizes }) => + this._onRequestFinished(network.Request.from(request), network.Response.fromNullable(response), responseEndTiming, responseHeaders, requestSizes, Page.fromNullable(page)) ); this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page))); this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); @@ -126,10 +126,12 @@ export class BrowserContext extends ChannelOwner implements api.Response { - private _headers: Headers; + _headers: Headers; private _request: Request; static from(response: channels.ResponseChannel): Response { diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 647b59d8ab..4d1a2a9385 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -83,8 +83,10 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('requestFinished', { + context.on(BrowserContext.Events.RequestFinished, ({ request, response}: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request), + response: ResponseDispatcher.fromNullable(scope, response), + responseHeaders: response?.headers(), responseEndTiming: request._responseEndTiming, requestSizes: request.sizes(), page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()), diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 6dc91d051e..fe9024f778 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -146,10 +146,7 @@ export type InterceptedResponse = { request: RequestChannel, status: number, statusText: string, - headers: { - name: string, - value: string, - }[], + headers: NameValue[], }; export type FetchResponse = { @@ -787,7 +784,9 @@ export type BrowserContextRequestFailedEvent = { }; export type BrowserContextRequestFinishedEvent = { request: RequestChannel, + response?: ResponseChannel, responseEndTiming: number, + responseHeaders?: NameValue[], requestSizes: RequestSizes, page?: PageChannel, }; @@ -2577,10 +2576,7 @@ export type RequestInitializer = { resourceType: string, method: string, postData?: Binary, - headers: { - name: string, - value: string, - }[], + headers: NameValue[], isNavigationRequest: boolean, redirectedFrom?: RequestChannel, }; @@ -2671,14 +2667,8 @@ export type ResponseInitializer = { url: string, status: number, statusText: string, - requestHeaders: { - name: string, - value: string, - }[], - headers: { - name: string, - value: string, - }[], + requestHeaders: NameValue[], + headers: NameValue[], timing: ResourceTiming, }; export interface ResponseChannel extends Channel { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 6f531980d5..735052d433 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -213,11 +213,7 @@ InterceptedResponse: statusText: string headers: type: array - items: - type: object - properties: - name: string - value: string + items: NameValue FetchResponse: @@ -755,7 +751,11 @@ BrowserContext: requestFinished: parameters: request: Request + response: Response? responseEndTiming: number + responseHeaders: + type: array? + items: NameValue requestSizes: RequestSizes page: Page? @@ -2112,11 +2112,7 @@ Request: postData: binary? headers: type: array - items: - type: object - properties: - name: string - value: string + items: NameValue isNavigationRequest: boolean redirectedFrom: Request? @@ -2190,18 +2186,10 @@ Response: statusText: string requestHeaders: type: array - items: - type: object - properties: - name: string - value: string + items: NameValue headers: type: array - items: - type: object - properties: - name: string - value: string + items: NameValue timing: ResourceTiming diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index f604dcca6e..5b24ce3574 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -144,10 +144,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { request: tChannel('Request'), status: tNumber, statusText: tString, - headers: tArray(tObject({ - name: tString, - value: tString, - })), + headers: tArray(tType('NameValue')), }); scheme.FetchResponse = tObject({ url: tString, diff --git a/src/server/chromium/crNetworkManager.ts b/src/server/chromium/crNetworkManager.ts index 696e11c516..6e71f6e4d2 100644 --- a/src/server/chromium/crNetworkManager.ts +++ b/src/server/chromium/crNetworkManager.ts @@ -39,6 +39,7 @@ export class CRNetworkManager { private _requestIdToRequestPausedEvent = new Map(); private _eventListeners: RegisteredListener[]; private _requestIdToExtraInfo = new Map(); + private _responseExtraInfoTracker = new ResponseExtraInfoTracker(); constructor(client: CRSession, page: Page, parentManager: CRNetworkManager | null) { this._client = client; @@ -54,6 +55,7 @@ export class CRNetworkManager { eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, workerFrame)), eventsHelper.addEventListener(session, 'Network.requestWillBeSentExtraInfo', this._onRequestWillBeSentExtraInfo.bind(this)), eventsHelper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), + eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)), eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)), eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)), @@ -116,6 +118,8 @@ export class CRNetworkManager { } _onRequestWillBeSent(workerFrame: frames.Frame | undefined, event: Protocol.Network.requestWillBeSentPayload) { + this._responseExtraInfoTracker.requestWillBeSent(event); + // Request interception doesn't happen for data URLs with Network Service. if (this._protocolRequestInterceptionEnabled && !event.request.url.startsWith('data:')) { const requestId = event.requestId; @@ -327,6 +331,7 @@ export class CRNetworkManager { validFrom: responsePayload?.securityDetails?.validFrom, validTo: responsePayload?.securityDetails?.validTo, }); + this._responseExtraInfoTracker.processResponse(request._requestId, response, request.wasFulfilled()); return response; } @@ -337,10 +342,16 @@ export class CRNetworkManager { if (request._interceptionId) this._attemptedAuthentications.delete(request._interceptionId); this._page._frameManager.requestReceivedResponse(response); - this._page._frameManager.requestFinished(request.request); + this._page._frameManager.requestFinished(request.request, response); + } + + _onResponseReceivedExtraInfo(event: Protocol.Network.responseReceivedExtraInfoPayload) { + this._responseExtraInfoTracker.responseReceivedExtraInfo(event); } _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { + this._responseExtraInfoTracker.responseReceived(event); + const request = this._requestIdToRequest.get(event.requestId); // FileUpload sends a response without a matching request. if (!request) @@ -350,6 +361,8 @@ export class CRNetworkManager { } _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) { + this._responseExtraInfoTracker.loadingFinished(event); + let request = this._requestIdToRequest.get(event.requestId); if (!request) request = this._maybeAdoptMainRequest(event.requestId); @@ -369,10 +382,12 @@ export class CRNetworkManager { this._requestIdToRequest.delete(request._requestId); if (request._interceptionId) this._attemptedAuthentications.delete(request._interceptionId); - this._page._frameManager.requestFinished(request.request); + this._page._frameManager.requestFinished(request.request, response); } _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { + this._responseExtraInfoTracker.loadingFailed(event); + let request = this._requestIdToRequest.get(event.requestId); if (!request) request = this._maybeAdoptMainRequest(event.requestId); @@ -410,11 +425,11 @@ export class CRNetworkManager { class InterceptableRequest { readonly request: network.Request; - _requestId: string; - _interceptionId: string | null; - _documentId: string | undefined; - _timestamp: number; - _wallTime: number; + readonly _requestId: string; + readonly _interceptionId: string | null; + readonly _documentId: string | undefined; + readonly _timestamp: number; + readonly _wallTime: number; private _route: RouteImpl | null; private _redirectedFrom: InterceptableRequest | null; @@ -455,6 +470,10 @@ class InterceptableRequest { request = request._redirectedFrom; return request._route; } + + wasFulfilled() { + return this._routeForRedirectChain()?._wasFulfilled || false; + } } class RouteImpl implements network.RouteDelegate { @@ -463,6 +482,7 @@ class RouteImpl implements network.RouteDelegate { private _responseInterceptedPromise: Promise; _responseInterceptedCallback: ((event: Protocol.Fetch.requestPausedPayload) => void) = () => {}; _interceptingResponse: boolean = false; + _wasFulfilled = false; constructor(client: CRSession, interceptionId: string) { this._client = client; @@ -501,6 +521,7 @@ class RouteImpl implements network.RouteDelegate { } async fulfill(response: types.NormalizedFulfillResponse) { + this._wasFulfilled = true; const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64'); // In certain cases, protocol will return error if the request was already canceled @@ -542,3 +563,138 @@ const errorReasons: { [reason: string]: Protocol.Network.ErrorReason } = { 'timedout': 'TimedOut', 'failed': 'Failed', }; + +type RequestInfo = { + requestId: string, + responseReceived: (Protocol.Network.Response | undefined)[], + responseReceivedExtraInfo: Protocol.Network.responseReceivedExtraInfoPayload[], + responses: network.Response[], + loadingFinished?: Protocol.Network.loadingFinishedPayload, + loadingFailed?: Protocol.Network.loadingFailedPayload, + sawResponseWithoutConnectionId: boolean +}; + +// This class aligns responses with response headers from extra info: +// - Network.requestWillBeSent, Network.responseReceived, Network.loadingFinished/loadingFailed are +// dispatched using one channel. +// - Network.requestWillBeSentExtraInfo and Network.responseReceivedExtraInfo are dispatches on +// another channel. Those channels are not associated, so events come in random order. +// +// This class will associate responses with the new headers. These extra info headers will become +// available to client reliably upon requestfinished event only. It consumes CDP +// signals on one end and processResponse(network.Response) signals on the other hands. It then makes +// sure that responses have all the extra headers in place by the time request finises. +// +// The shape of the instrumentation API is deliberately following the CDP, so that it +// what clear what is called when and what this means to the tracker without extra +// documentation. +class ResponseExtraInfoTracker { + private _requests = new Map(); + + constructor() { + } + + requestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) { + const info = this._requests.get(event.requestId); + if (info) { + // This is redirect. + this._innerResponseReceived(info, event.redirectResponse); + } else { + this._requests.set(event.requestId, { + requestId: event.requestId, + responseReceived: [], + responseReceivedExtraInfo: [], + responses: [], + sawResponseWithoutConnectionId: false + }); + } + } + + responseReceived(event: Protocol.Network.responseReceivedPayload) { + const info = this._requests.get(event.requestId); + if (!info) + return; + this._innerResponseReceived(info, event.response); + } + + private _innerResponseReceived(info: RequestInfo, response: Protocol.Network.Response | undefined) { + info.responseReceived.push(response); + if (!response?.connectionId) { + // Starting with this response we no longer can guarantee that response and extra info correspond to the same index. + info.sawResponseWithoutConnectionId = true; + } + } + + responseReceivedExtraInfo(event: Protocol.Network.responseReceivedExtraInfoPayload) { + const info = this._requests.get(event.requestId); + if (!info) + return; + info.responseReceivedExtraInfo.push(event); + this._patchResponseHeaders(info, info.responseReceivedExtraInfo.length - 1); + this._checkFinished(info); + } + + processResponse(requestId: string, response: network.Response, wasFulfilled: boolean) { + // We are not interested in ExtraInfo tracking for fulfilled requests, our Blink + // headers are the ones that contain fulfilled headers. + if (wasFulfilled) { + this._stopTracking(requestId); + return; + } + + const info = this._requests.get(requestId); + if (!info || info.sawResponseWithoutConnectionId) + return; + response.setWillReceiveExtraHeaders(); + info.responses.push(response); + this._patchResponseHeaders(info, info.responses.length - 1); + } + + loadingFinished(event: Protocol.Network.loadingFinishedPayload) { + const info = this._requests.get(event.requestId); + if (!info) + return; + info.loadingFinished = event; + this._checkFinished(info); + } + + loadingFailed(event: Protocol.Network.loadingFailedPayload) { + const info = this._requests.get(event.requestId); + if (!info) + return; + info.loadingFailed = event; + this._checkFinished(info); + } + + private _patchResponseHeaders(info: RequestInfo, index: number) { + const response = info.responses[index]; + const extraInfo = info.responseReceivedExtraInfo[index]; + if (response && extraInfo) + response.extraHeadersReceived(headersObjectToArray(extraInfo.headers)); + } + + private _checkFinished(info: RequestInfo) { + if (!info.loadingFinished && !info.loadingFailed) + return; + + // Loading finished, check that we have all ExtraInfo events in place. + if (!info.responseReceived.length) { + // loading finished without responses, finish it immediately. + this._stopTracking(info.requestId); + return; + } + + // We could have more extra infos because we stopped collecting responses at some point. + if (info.responses.length <= info.responseReceivedExtraInfo.length) { + // We have extra info for each response. + this._stopTracking(info.requestId); + return; + } + + // We are not done yet. + } + + private _stopTracking(requestId: string) { + this._requests.delete(requestId); + } +} diff --git a/src/server/firefox/ffNetworkManager.ts b/src/server/firefox/ffNetworkManager.ts index c98edc4d5c..c8ba9198dc 100644 --- a/src/server/firefox/ffNetworkManager.ts +++ b/src/server/firefox/ffNetworkManager.ts @@ -128,7 +128,7 @@ export class FFNetworkManager { this._requests.delete(request._id); response._requestFinished(this._relativeTiming(event.responseEndTime), undefined); } - this._page._frameManager.requestFinished(request.request); + this._page._frameManager.requestFinished(request.request, response); } _onRequestFailed(event: Protocol.Network.requestFailedPayload) { diff --git a/src/server/frames.ts b/src/server/frames.ts index b5b07696e1..72743b9846 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -74,7 +74,6 @@ export class FrameManager { readonly _consoleMessageTags = new Map(); readonly _signalBarriers = new Set(); private _webSockets = new Map(); - readonly _responses: network.Response[] = []; _dialogCounter = 0; constructor(page: Page) { @@ -204,7 +203,6 @@ export class FrameManager { frame._onClearLifecycle(); const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument }; frame.emit(Frame.Events.Navigation, navigationEvent); - this._responses.length = 0; if (!initial) { debugLogger.log('api', ` navigated to "${url}"`); this._page.frameNavigatedToNewDocument(frame); @@ -274,15 +272,21 @@ export class FrameManager { requestReceivedResponse(response: network.Response) { if (response.request()._isFavicon) return; - this._responses.push(response); this._page._browserContext.emit(BrowserContext.Events.Response, response); } - requestFinished(request: network.Request) { + requestFinished(request: network.Request, response: network.Response | null) { this._inflightRequestFinished(request); if (request._isFavicon) return; - this._page._browserContext.emit(BrowserContext.Events.RequestFinished, request); + this._dispatchRequestFinished(request, response).catch(() => {}); + } + + private async _dispatchRequestFinished(request: network.Request, response: network.Response | null) { + // Avoid unnecessary microtask, we want to report finished early for regular redirects. + if (response?.willWaitForExtraHeaders()) + await response?.waitForExtraHeadersIfNeeded(); + this._page._browserContext.emit(BrowserContext.Events.RequestFinished,{ request, response }); } requestFailed(request: network.Request, canceled: boolean) { diff --git a/src/server/network.ts b/src/server/network.ts index fb26bf52b7..364008059d 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -329,6 +329,7 @@ export class Response extends SdkObject { private _timing: ResourceTiming; private _serverAddrPromise = new ManualPromise(); private _securityDetailsPromise = new ManualPromise(); + private _extraHeadersPromise: ManualPromise | undefined; private _httpVersion: string | undefined; constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, httpVersion?: string) { @@ -363,6 +364,25 @@ export class Response extends SdkObject { this._httpVersion = httpVersion; } + setWillReceiveExtraHeaders() { + this._extraHeadersPromise = new ManualPromise(); + } + + willWaitForExtraHeaders(): boolean { + return !!this._extraHeadersPromise && !this._extraHeadersPromise.isDone(); + } + + async waitForExtraHeadersIfNeeded(): Promise { + await this._extraHeadersPromise; + } + + extraHeadersReceived(headers: types.HeadersArray) { + this._headers = headers; + for (const { name, value } of this._headers) + this._headersMap.set(name.toLowerCase(), value); + this._extraHeadersPromise?.resolve(); + } + url(): string { return this._url; } diff --git a/src/server/supplements/har/harTracer.ts b/src/server/supplements/har/harTracer.ts index b58ca2a3f7..9ad7e273a8 100644 --- a/src/server/supplements/har/harTracer.ts +++ b/src/server/supplements/har/harTracer.ts @@ -61,7 +61,7 @@ export class HarTracer { this._eventListeners = [ eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)), eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, (request: network.Request) => this._onRequestFinished(request).catch(() => {})), + eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})), eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)), ]; } @@ -193,14 +193,13 @@ export class HarTracer { this._delegate.onEntryStarted(harEntry); } - private async _onRequestFinished(request: network.Request) { + private async _onRequestFinished(request: network.Request, response: network.Response | null) { + if (!response) + return; const page = request.frame()._page; const harEntry = this._entryForRequest(request); if (!harEntry) return; - const response = await request.response(); - if (!response) - return; const httpVersion = response.httpVersion(); const transferSize = response.transferSize() || -1; diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index 229cded4c6..25b5ae9d1f 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -981,7 +981,7 @@ export class WKPage implements PageDelegate { response._requestFinished(responsePayload.timing ? helper.secondsToRoundishMillis(timestamp - request._timestamp) : -1, 'Response body is unavailable for redirect responses'); this._requestIdToRequest.delete(request._requestId); this._page._frameManager.requestReceivedResponse(response); - this._page._frameManager.requestFinished(request.request); + this._page._frameManager.requestFinished(request.request, response); } _onRequestIntercepted(session: WKSession, event: Protocol.Network.requestInterceptedPayload) { @@ -1056,7 +1056,7 @@ export class WKPage implements PageDelegate { this._requestIdToResponseReceivedPayloadEvent.delete(request._requestId); this._requestIdToRequest.delete(request._requestId); - this._page._frameManager.requestFinished(request.request); + this._page._frameManager.requestFinished(request.request, response); } _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d2359817e3..1a45db48cd 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -396,6 +396,7 @@ export function wrapInASCIIBox(text: string, padding = 0): string { export class ManualPromise extends Promise { private _resolve!: (t: T) => void; private _reject!: (e: Error) => void; + private _isDone: boolean; constructor() { let resolve: (t: T) => void; @@ -404,15 +405,22 @@ export class ManualPromise extends Promise { resolve = f; reject = r; }); + this._isDone = false; this._resolve = resolve!; this._reject = reject!; } + isDone() { + return this._isDone; + } + resolve(t: T) { + this._isDone = true; this._resolve(t); } reject(e: Error) { + this._isDone = true; this._reject(e); } diff --git a/tests/page/page-network-request.spec.ts b/tests/page/page-network-request.spec.ts index 57f7c28db3..b30b7bd4a6 100644 --- a/tests/page/page-network-request.spec.ts +++ b/tests/page/page-network-request.spec.ts @@ -314,3 +314,29 @@ it('should should set bodySize to 0 when there was no response body', async ({pa expect(response.request().sizes().responseHeadersSize).toBeGreaterThanOrEqual(150); expect(response.request().sizes().responseTransferSize).toBeGreaterThanOrEqual(160); }); + +it('should report raw response headers in redirects', async ({ page, server, browserName }) => { + it.skip(browserName === 'webkit', `WebKit won't give us raw headers for redirects`); + server.setExtraHeaders('/redirect/1.html', { 'sec-test-header': '1.html' }); + server.setExtraHeaders('/redirect/2.html', { 'sec-test-header': '2.html' }); + server.setExtraHeaders('/empty.html', { 'sec-test-header': 'empty.html' }); + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + + const expectedUrls = ['/redirect/1.html', '/redirect/2.html', '/empty.html'].map(s => server.PREFIX + s); + const expectedHeaders = ['1.html', '2.html', 'empty.html']; + + const response = await page.goto(server.PREFIX + '/redirect/1.html'); + await response.finished(); + + const redirectChain = []; + const headersChain = []; + for (let req = response.request(); req; req = req.redirectedFrom()) { + redirectChain.unshift(req.url()); + const res = await req.response(); + headersChain.unshift(res.headers()['sec-test-header']); + } + + expect(redirectChain).toEqual(expectedUrls); + expect(headersChain).toEqual(expectedHeaders); +}); diff --git a/utils/testserver/index.d.ts b/utils/testserver/index.d.ts index f495ecb44f..6a954dc2ca 100644 --- a/utils/testserver/index.d.ts +++ b/utils/testserver/index.d.ts @@ -24,6 +24,7 @@ export class TestServer { setAuth(path: string, username: string, password: string); enableGzip(path: string); setCSP(path: string, csp: string); + setExtraHeaders(path: string, headers: { [key: string]: string }); stop(): Promise; setRoute(path: string, handler: (message: IncomingMessage & { postBody: Promise }, response: ServerResponse) => void); setRedirect(from: string, to: string); diff --git a/utils/testserver/index.js b/utils/testserver/index.js index 83d639f30d..354781c098 100644 --- a/utils/testserver/index.js +++ b/utils/testserver/index.js @@ -97,6 +97,8 @@ class TestServer { this._auths = new Map(); /** @type {!Map} */ this._csp = new Map(); + /** @type {!Map} */ + this._extraHeaders = new Map(); /** @type {!Set} */ this._gzipRoutes = new Set(); /** @type {!Map} */ @@ -153,6 +155,14 @@ class TestServer { this._csp.set(path, csp); } + /** + * @param {string} path + * @param {Object} object + */ + setExtraHeaders(path, object) { + this._extraHeaders.set(path, object); + } + async stop() { this.reset(); for (const socket of this._sockets) @@ -175,7 +185,8 @@ class TestServer { */ setRedirect(from, to) { this.setRoute(from, (req, res) => { - res.writeHead(302, { location: to }); + let headers = this._extraHeaders.get(req.url) || {}; + res.writeHead(302, { ...headers, location: to }); res.end(); }); } @@ -203,6 +214,7 @@ class TestServer { this._routes.clear(); this._auths.clear(); this._csp.clear(); + this._extraHeaders.clear(); this._gzipRoutes.clear(); const error = new Error('Static Server has been reset'); for (const subscriber of this._requestSubscribers.values()) @@ -280,6 +292,12 @@ class TestServer { if (this._csp.has(pathName)) response.setHeader('Content-Security-Policy', this._csp.get(pathName)); + if (this._extraHeaders.has(pathName)) { + const object = this._extraHeaders.get(pathName); + for (const key in object) + response.setHeader(key, object[key]); + } + const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err})); // The HTTP transaction might be already terminated after async hop here - do nothing in this case. if (response.writableEnded)