Implement Request object

This patch does a step towards Fetch API:
- implements Request object to some extend. The Request object will be
  sent in RequestWillBeSent event.
- implements InterceptedRequest which extends from Request and allows
  for request modification. The InterceptedRequest does not
  conform to Fetch API spec - there seems to be nothing related to
  amending in-flight request.
- adds test to make sure that request can change headers.

References #26
This commit is contained in:
Andrey Lushnikov 2017-06-27 23:31:38 -07:00
parent 5ed71fcb8f
commit 7b59a89695
5 changed files with 178 additions and 92 deletions

View File

@ -26,7 +26,7 @@ var address = process.argv[2];
var browser = new Browser({headless: false}); var browser = new Browser({headless: false});
browser.newPage().then(async page => { browser.newPage().then(async page => {
page.setRequestInterceptor(request => { page.setRequestInterceptor(request => {
if (request.url().endsWith('.css')) if (request.url.endsWith('.css'))
request.abort(); request.abort();
else else
request.continue(); request.continue();

View File

@ -17,7 +17,7 @@
let fs = require('fs'); let fs = require('fs');
let EventEmitter = require('events'); let EventEmitter = require('events');
let mime = require('mime'); let mime = require('mime');
let Request = require('./Request'); let {InterceptedRequest} = require('./Request');
let Navigator = require('./Navigator'); let Navigator = require('./Navigator');
let Dialog = require('./Dialog'); let Dialog = require('./Dialog');
let FrameManager = require('./FrameManager'); let FrameManager = require('./FrameManager');
@ -57,7 +57,7 @@ class Page extends EventEmitter {
this._extraHeaders = {}; this._extraHeaders = {};
/** @type {!Map<string, function>} */ /** @type {!Map<string, function>} */
this._inPageCallbacks = new Map(); this._inPageCallbacks = new Map();
/** @type {?function(!Request)} */ /** @type {?function(!InterceptedRequest)} */
this._requestInterceptor = null; this._requestInterceptor = null;
/** @type {?Promise<number>} */ /** @type {?Promise<number>} */
this._rootNodeIdPromise = null; this._rootNodeIdPromise = null;
@ -94,7 +94,7 @@ class Page extends EventEmitter {
} }
/** /**
* @param {?function(!Request)} interceptor * @param {?function(!InterceptedRequest)} interceptor
*/ */
async setRequestInterceptor(interceptor) { async setRequestInterceptor(interceptor) {
this._requestInterceptor = interceptor; this._requestInterceptor = interceptor;
@ -105,7 +105,7 @@ class Page extends EventEmitter {
* @param {!Object} event * @param {!Object} event
*/ */
_onRequestIntercepted(event) { _onRequestIntercepted(event) {
let request = new Request(this._client, event.InterceptionId, event.request); let request = new InterceptedRequest(this._client, event.InterceptionId, event.request);
this._requestInterceptor(request); this._requestInterceptor(request);
} }

View File

@ -14,86 +14,116 @@
* limitations under the License. * limitations under the License.
*/ */
class Headers {
/**
* @param {?Object} payload
* @return {!Headers}
*/
static fromPayload(payload) {
let headers = new Headers();
if (!payload)
return headers;
for (let key in payload)
headers.set(key, payload[key]);
return headers;
}
constructor() {
/** @type {!Map<string, string>} */
this._headers = new Map();
}
/**
* @param {string} name
* @param {string} value
*/
append(name, value) {
name = name.toLowerCase();
this._headers.set(name, value);
}
/**
* @param {string} name
*/
delete(name) {
name = name.toLowerCase();
this._headers.delete(name);
}
/**
* @return {!Iterator}
*/
entries() {
return this._headers.entries();
}
/**
* @param {string} name
* @return {?string}
*/
get(name) {
name = name.toLowerCase();
return this._headers.get(name);
}
/**
* @param {string} name
* @return {boolean}
*/
has(name) {
name = name.toLowerCase();
return this._headers.has(name);
}
/**
* @return {!Iterator}
*/
keys() {
return this._headers.keys();
}
/**
* @return {!Iterator}
*/
values() {
return this._headers.values();
}
/**
* @param {string} name
* @param {string} value
*/
set(name, value) {
name = name.toLowerCase();
this._headers.set(name, value);
}
}
class Request { class Request {
/**
* @param {!Object} payload
*/
constructor(payload) {
this.url = payload.url;
this.method = payload.method;
this.headers = Headers.fromPayload(payload.headers);
this.postData = payload.postData;
}
}
class InterceptedRequest extends Request {
/** /**
* @param {!Connection} client * @param {!Connection} client
* @param {string} interceptionId * @param {string} interceptionId
* @param {!Object} payload * @param {!Object} payload
*/ */
constructor(client, interceptionId, payload) { constructor(client, interceptionId, payload) {
super(payload);
this._client = client; this._client = client;
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
this._url = payload.url;
this._method = payload.method;
this._headers = payload.headers;
this._postData = payload.postData;
this._urlOverride = undefined;
this._methodOverride = undefined;
this._postDataOverride = undefined;
this._handled = false; this._handled = false;
} }
/**
* @return {string}
*/
url() {
return this._urlOverride || this._url;
}
/**
* @param {string} url
*/
setUrl(url) {
this._urlOverride = url;
}
/**
* @return {string}
*/
method() {
return this._methodOverride || this._method;
}
/**
* @param {string} method
*/
setMethod(method) {
this._methodOverride = method;
}
/**
* @return {!Object}
*/
headers() {
return Object.assign({}, this._headersOverride || this._headers);
}
/**
* @param {string} key
* @param {string} value
*/
setHeader(key, value) {
if (!this._headersOverride)
this._headersOverride = Object.assign({}, this._headers);
this._headersOverride[key] = value;
}
/**
* @return {(string|undefined)}
*/
postData() {
return this._postDataOverride || this._postData;
}
/**
* @return {(string|undefined)}
*/
setPostData(data) {
this._postDataOverride = data;
}
abort() { abort() {
console.assert(!this._handled, 'This request is already handled!'); console.assert(!this._handled, 'This request is already handled!');
this._handled = true; this._handled = true;
@ -106,21 +136,24 @@ class Request {
continue() { continue() {
console.assert(!this._handled, 'This request is already handled!'); console.assert(!this._handled, 'This request is already handled!');
this._handled = true; this._handled = true;
let headers = {};
for (let entry of this.headers.entries())
headers[entry[0]] = entry[1];
this._client.send('Network.continueInterceptedRequest', { this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId, interceptionId: this._interceptionId,
url: this._urlOverride, url: this.url,
method: this._methodOverride, method: this.method,
postData: this._postDataOverride, postData: this.postData,
headers: this._headersOverride headers: headers
}); });
} }
/** /**
* @return {boolean} * @return {boolean}
*/ */
handled() { isHandled() {
return this._handled; return this._handled;
} }
} }
module.exports = Request; module.exports = {Request, InterceptedRequest};

View File

@ -79,15 +79,15 @@ class WebPage {
this._page.setRequestInterceptor(callback ? resourceInterceptor : null); this._page.setRequestInterceptor(callback ? resourceInterceptor : null);
/** /**
* @param {!Request} request * @param {!InterceptedRequest} request
*/ */
function resourceInterceptor(request) { function resourceInterceptor(request) {
let requestData = { let requestData = new RequestData(request);
url: request.url(), let phantomRequest = new PhantomRequest(request);
headers: request.headers() callback(requestData, phantomRequest);
}; if (phantomRequest._aborted)
callback(requestData, request); request.abort();
if (!request.handled()) else
request.continue(); request.continue();
} }
} }
@ -338,6 +338,46 @@ class WebPageSettings {
} }
} }
class PhantomRequest {
/**
* @param {!InterceptedRequest} request
*/
constructor(request) {
this._request = request;
}
/**
* @param {string} key
* @param {string} value
*/
setHeader(key, value) {
this._request.headers.set(key, value);
}
abort() {
this._aborted = true;
}
/**
* @param {string} url
*/
changeUrl(newUrl) {
this._request.url = newUrl;
}
}
class RequestData {
/**
* @param {!InterceptedRequest} request
*/
constructor(request) {
this.url = request.url,
this.headers = {};
for (let entry in request.headers.entries())
this.headers[entry[0]] = entry[1];
}
}
// To prevent reenterability, eventemitters should emit events // To prevent reenterability, eventemitters should emit events
// only being in a consistent state. // only being in a consistent state.
// This is not the case for 'ws' npm module: https://goo.gl/sy3dJY // This is not the case for 'ws' npm module: https://goo.gl/sy3dJY

View File

@ -190,10 +190,10 @@ describe('Puppeteer', function() {
describe('Page.setRequestInterceptor', function() { describe('Page.setRequestInterceptor', function() {
it('should intercept', SX(async function() { it('should intercept', SX(async function() {
page.setRequestInterceptor(request => { page.setRequestInterceptor(request => {
expect(request.url()).toContain('empty.html'); expect(request.url).toContain('empty.html');
expect(request.headers()['User-Agent']).toBeTruthy(); expect(request.headers.has('User-Agent')).toBeTruthy();
expect(request.method()).toBe('GET'); expect(request.method).toBe('GET');
expect(request.postData()).toBe(undefined); expect(request.postData).toBe(undefined);
request.continue(); request.continue();
}); });
let success = await page.navigate(EMPTY_PAGE); let success = await page.navigate(EMPTY_PAGE);
@ -204,7 +204,7 @@ describe('Puppeteer', function() {
foo: 'bar' foo: 'bar'
}); });
page.setRequestInterceptor(request => { page.setRequestInterceptor(request => {
expect(request.headers()['foo']).toBe('bar'); expect(request.headers.get('foo')).toBe('bar');
request.continue(); request.continue();
}); });
let success = await page.navigate(EMPTY_PAGE); let success = await page.navigate(EMPTY_PAGE);
@ -212,7 +212,7 @@ describe('Puppeteer', function() {
})); }));
it('should be abortable', SX(async function() { it('should be abortable', SX(async function() {
page.setRequestInterceptor(request => { page.setRequestInterceptor(request => {
if (request.url().endsWith('.css')) if (request.url.endsWith('.css'))
request.abort(); request.abort();
else else
request.continue(); request.continue();
@ -223,6 +223,19 @@ describe('Puppeteer', function() {
expect(success).toBe(true); expect(success).toBe(true);
expect(failedResources).toBe(1); expect(failedResources).toBe(1);
})); }));
it('should amend HTTP headers', SX(async function() {
await page.navigate(EMPTY_PAGE);
page.setRequestInterceptor(request => {
request.headers.set('foo', 'bar');
request.continue();
});
let serverRequest = staticServer.waitForRequest('/sleep.zzz');
page.evaluate(() => {
fetch('/sleep.zzz');
});
let request = await serverRequest;
expect(request.headers['foo']).toBe('bar');
}));
}); });
describe('Page.Events.Dialog', function() { describe('Page.Events.Dialog', function() {