diff --git a/docs/api.md b/docs/api.md index 08fe7062307..fa11980650b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -208,6 +208,7 @@ * [request.headers()](#requestheaders) * [request.method()](#requestmethod) * [request.postData()](#requestpostdata) + * [request.redirectChain()](#requestredirectchain) * [request.resourceType()](#requestresourcetype) * [request.respond(response)](#requestrespondresponse) * [request.response()](#requestresponse) @@ -2379,6 +2380,33 @@ page.on('requestfailed', request => { #### request.postData() - returns: <[string]> Request's post body, if any. +#### request.redirectChain() +- returns: <[Array]<[Request]>> + +A `redirectChain` is a chain of requests initiated to fetch a resource. +- If there are no redirects and the request was successful, the chain will be empty. +- If a server responds with at least a single redirect, then the chain will +contain all the requests that were redirected. + +`redirectChain` is shared between all the requests of the same chain. + +For example, if the website `http://example.com` has a single redirect to +`https://example.com`, then the chain will contain one request: + +```js +const response = await page.goto('http://example.com'); +const chain = response.request().redirectChain(); +console.log(chain.length); // 1 +console.log(chain[0].url()); // 'http://example.com' +``` + +If the website `https://google.com` has no redirects, then the chain will be empty: +```js +const response = await page.goto('https://google.com'); +const chain = response.request().redirectChain(); +console.log(chain.length); // 0 +``` + #### request.resourceType() - returns: <[string]> diff --git a/lib/NetworkManager.js b/lib/NetworkManager.js index ba54e62fd41..ddbd773a01e 100644 --- a/lib/NetworkManager.js +++ b/lib/NetworkManager.js @@ -154,7 +154,7 @@ class NetworkManager extends EventEmitter { const request = this._interceptionIdToRequest.get(event.interceptionId); if (request) { this._handleRequestRedirect(request, event.responseStatusCode, event.responseHeaders, false /* fromDiskCache */, false /* fromServiceWorker */, null /* securityDetails */); - this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.resourceType, event.request, event.frameId); + this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.resourceType, event.request, event.frameId, request._redirectChain); } return; } @@ -162,10 +162,10 @@ class NetworkManager extends EventEmitter { const requestId = this._requestHashToRequestIds.firstValue(requestHash); if (requestId) { this._requestHashToRequestIds.delete(requestHash, requestId); - this._handleRequestStart(requestId, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId); + this._handleRequestStart(requestId, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId, []); } else { this._requestHashToInterceptionIds.set(requestHash, event.interceptionId); - this._handleRequestStart(null, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId); + this._handleRequestStart(null, event.interceptionId, event.request.url, event.resourceType, event.request, event.frameId, []); } } @@ -189,6 +189,7 @@ class NetworkManager extends EventEmitter { _handleRequestRedirect(request, redirectStatus, redirectHeaders, fromDiskCache, fromServiceWorker, securityDetails) { const response = new Response(this._client, request, redirectStatus, redirectHeaders, fromDiskCache, fromServiceWorker, securityDetails); request._response = response; + request._redirectChain.push(request); this._requestIdToRequest.delete(request._requestId); this._interceptionIdToRequest.delete(request._interceptionId); this._attemptedAuthentications.delete(request._interceptionId); @@ -203,12 +204,13 @@ class NetworkManager extends EventEmitter { * @param {string} resourceType * @param {!Object} requestPayload * @param {?string} frameId + * @param {!Array} redirectChain */ - _handleRequestStart(requestId, interceptionId, url, resourceType, requestPayload, frameId) { + _handleRequestStart(requestId, interceptionId, url, resourceType, requestPayload, frameId, redirectChain) { let frame = null; if (frameId) frame = this._frameManager.frame(frameId); - const request = new Request(this._client, requestId, interceptionId, this._userRequestInterceptionEnabled, url, resourceType, requestPayload, frame); + const request = new Request(this._client, requestId, interceptionId, this._userRequestInterceptionEnabled, url, resourceType, requestPayload, frame, redirectChain); if (requestId) this._requestIdToRequest.set(requestId, request); if (interceptionId) @@ -236,13 +238,16 @@ class NetworkManager extends EventEmitter { } return; } + let redirectChain = []; if (event.redirectResponse) { const request = this._requestIdToRequest.get(event.requestId); // If we connect late to the target, we could have missed the requestWillBeSent event. - if (request) + if (request) { this._handleRequestRedirect(request, event.redirectResponse.status, event.redirectResponse.headers, event.redirectResponse.fromDiskCache, event.redirectResponse.fromServiceWorker, event.redirectResponse.securityDetails); + redirectChain = request._redirectChain; + } } - this._handleRequestStart(event.requestId, null, event.request.url, event.type, event.request, event.frameId); + this._handleRequestStart(event.requestId, null, event.request.url, event.type, event.request, event.frameId, redirectChain); } /** @@ -303,8 +308,9 @@ class Request { * @param {string} resourceType * @param {!Object} payload * @param {?Puppeteer.Frame} frame + * @param {!Array} redirectChain */ - constructor(client, requestId, interceptionId, allowInterception, url, resourceType, payload, frame) { + constructor(client, requestId, interceptionId, allowInterception, url, resourceType, payload, frame, redirectChain) { this._client = client; this._requestId = requestId; this._interceptionId = interceptionId; @@ -322,6 +328,7 @@ class Request { this._postData = payload.postData; this._headers = {}; this._frame = frame; + this._redirectChain = redirectChain; for (const key of Object.keys(payload.headers)) this._headers[key.toLowerCase()] = payload.headers[key]; @@ -377,6 +384,13 @@ class Request { return this._frame; } + /** + * @return {!Array} + */ + redirectChain() { + return this._redirectChain.slice(); + } + /** * @return {?{errorText: string}} */ diff --git a/test/test.js b/test/test.js index ba5a5b8d511..c64588de661 100644 --- a/test/test.js +++ b/test/test.js @@ -1646,6 +1646,15 @@ describe('Page', function() { expect(response.url()).toContain('empty.html'); expect(requests.length).toBe(5); expect(requests[2].resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(4); + expect(redirectChain[0].url()).toContain('/non-existing-page.html'); + expect(redirectChain[2].url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]; + expect(request.redirectChain().indexOf(request)).toBe(i); + } }); it('should be able to abort redirects', async({page, server}) => { await page.setRequestInterception(true); @@ -2948,7 +2957,7 @@ describe('Page', function() { page.on('requestfailed', request => events.push(`FAIL ${request.url()}`)); server.setRedirect('/foo.html', '/empty.html'); const FOO_URL = server.PREFIX + '/foo.html'; - await page.goto(FOO_URL); + const response = await page.goto(FOO_URL); expect(events).toEqual([ `GET ${FOO_URL}`, `302 ${FOO_URL}`, @@ -2957,6 +2966,11 @@ describe('Page', function() { `200 ${server.EMPTY_PAGE}`, `DONE ${server.EMPTY_PAGE}` ]); + + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain.length).toBe(1); + expect(redirectChain[0].url()).toContain('/foo.html'); }); });