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});
browser.newPage().then(async page => {
page.setRequestInterceptor(request => {
if (request.url().endsWith('.css'))
if (request.url.endsWith('.css'))
request.abort();
else
request.continue();

View File

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

View File

@ -14,86 +14,116 @@
* 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 {
/**
* @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 {string} interceptionId
* @param {!Object} payload
*/
constructor(client, interceptionId, payload) {
super(payload);
this._client = client;
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;
}
/**
* @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() {
console.assert(!this._handled, 'This request is already handled!');
this._handled = true;
@ -106,21 +136,24 @@ class Request {
continue() {
console.assert(!this._handled, 'This request is already handled!');
this._handled = true;
let headers = {};
for (let entry of this.headers.entries())
headers[entry[0]] = entry[1];
this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId,
url: this._urlOverride,
method: this._methodOverride,
postData: this._postDataOverride,
headers: this._headersOverride
url: this.url,
method: this.method,
postData: this.postData,
headers: headers
});
}
/**
* @return {boolean}
*/
handled() {
isHandled() {
return this._handled;
}
}
module.exports = Request;
module.exports = {Request, InterceptedRequest};

View File

@ -79,15 +79,15 @@ class WebPage {
this._page.setRequestInterceptor(callback ? resourceInterceptor : null);
/**
* @param {!Request} request
* @param {!InterceptedRequest} request
*/
function resourceInterceptor(request) {
let requestData = {
url: request.url(),
headers: request.headers()
};
callback(requestData, request);
if (!request.handled())
let requestData = new RequestData(request);
let phantomRequest = new PhantomRequest(request);
callback(requestData, phantomRequest);
if (phantomRequest._aborted)
request.abort();
else
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
// only being in a consistent state.
// 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() {
it('should intercept', SX(async function() {
page.setRequestInterceptor(request => {
expect(request.url()).toContain('empty.html');
expect(request.headers()['User-Agent']).toBeTruthy();
expect(request.method()).toBe('GET');
expect(request.postData()).toBe(undefined);
expect(request.url).toContain('empty.html');
expect(request.headers.has('User-Agent')).toBeTruthy();
expect(request.method).toBe('GET');
expect(request.postData).toBe(undefined);
request.continue();
});
let success = await page.navigate(EMPTY_PAGE);
@ -204,7 +204,7 @@ describe('Puppeteer', function() {
foo: 'bar'
});
page.setRequestInterceptor(request => {
expect(request.headers()['foo']).toBe('bar');
expect(request.headers.get('foo')).toBe('bar');
request.continue();
});
let success = await page.navigate(EMPTY_PAGE);
@ -212,7 +212,7 @@ describe('Puppeteer', function() {
}));
it('should be abortable', SX(async function() {
page.setRequestInterceptor(request => {
if (request.url().endsWith('.css'))
if (request.url.endsWith('.css'))
request.abort();
else
request.continue();
@ -223,6 +223,19 @@ describe('Puppeteer', function() {
expect(success).toBe(true);
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() {