feat(interception): Implement request.mockResponse method (#1064)

feat(interception): Implement request.respond method

This patch implements a new Request.respond method. This
allows users to fulfill the intercepted request with a hand-crafted
response if they wish so.

References #1020.
This commit is contained in:
Andrey Lushnikov 2017-10-20 16:55:15 -07:00 committed by GitHub
parent 7f60e33a63
commit bcc969ccc4
5 changed files with 193 additions and 2 deletions

View File

@ -169,6 +169,7 @@
* [request.method](#requestmethod) * [request.method](#requestmethod)
* [request.postData](#requestpostdata) * [request.postData](#requestpostdata)
* [request.resourceType](#requestresourcetype) * [request.resourceType](#requestresourcetype)
* [request.respond(response)](#requestrespondresponse)
* [request.response()](#requestresponse) * [request.response()](#requestresponse)
* [request.url](#requesturl) * [request.url](#requesturl)
- [class: Response](#class-response) - [class: Response](#class-response)
@ -974,9 +975,10 @@ The extra HTTP headers will be sent with every request the page initiates.
- `value` <[boolean]> Whether to enable request interception. - `value` <[boolean]> Whether to enable request interception.
- returns: <[Promise]> - returns: <[Promise]>
Activating request interception enables `request.abort` and `request.continue`. Activating request interception enables `request.abort`, `request.continue` and
`request.respond` methods.
An example of a naïve request interceptor which aborts all image requests: An example of a naïve request interceptor that aborts all image requests:
```js ```js
const puppeteer = require('puppeteer'); const puppeteer = require('puppeteer');
@ -994,6 +996,9 @@ puppeteer.launch().then(async browser => {
}); });
``` ```
> **NOTE** Request interception doesn't work with data URLs. Calling `abort`,
> `continue` or `respond` on requests for data URLs is a noop.
#### page.setUserAgent(userAgent) #### page.setUserAgent(userAgent)
- `userAgent` <[string]> Specific user agent to use in this page - `userAgent` <[string]> Specific user agent to use in this page
- returns: <[Promise]> Promise which resolves when the user agent is set. - returns: <[Promise]> Promise which resolves when the user agent is set.
@ -1899,6 +1904,32 @@ Contains the request's post body, if any.
Contains the request's resource type as it was perceived by the rendering engine. Contains the request's resource type as it was perceived by the rendering engine.
ResourceType will be one of the following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`. ResourceType will be one of the following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttrack`, `xhr`, `fetch`, `eventsource`, `websocket`, `manifest`, `other`.
#### request.respond(response)
- `response` <[Object]> Response that will fulfill this request
- `status` <[number]> Response status code, defaults to `200`.
- `headers` <[Object]> Optional response headers
- `contentType` <[string]> If set, equals to setting `Content-Type` response header
- `body` <[Buffer]|[string]> Optional response body
- returns: <[Promise]>
Fulfills request with given response. To use this, request interception should
be enabled with `page.setRequestInterceptionEnabled`. Exception is thrown if
request interception is not enabled.
An example of fulfilling all requests with 404 responses:
```js
await page.setRequestInterceptionEnabled(true);
page.on('request', request => {
request.respond({
status: 404,
contentType: 'text/plain',
body: 'Not Found!'
});
});
```
#### request.response() #### request.response()
- returns: <[Response]> A matching [Response] object, or `null` if the response has not been received yet. - returns: <[Response]> A matching [Response] object, or `null` if the response has not been received yet.

View File

@ -340,6 +340,54 @@ class Request {
}); });
} }
/**
* @param {!{status: number, headers: Object, contentType: string, body: (string|Buffer)}} response
*/
async respond(response) {
// DataURL's are not interceptable. In this case, do nothing.
if (this.url.startsWith('data:'))
return;
console.assert(this._allowInterception, 'Request Interception is not enabled!');
console.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true;
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null);
const responseHeaders = {};
if (response.headers) {
for (const header of Object.keys(response.headers))
responseHeaders[header.toLowerCase()] = response.headers[header];
}
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders)) {
// @ts-ignore
responseHeaders['content-length'] = Buffer.byteLength(responseBody);
}
const statusCode = response.status || 200;
const statusText = statusTexts[statusCode] || '';
const statusLine = `HTTP/1.1 ${statusCode} ${statusText}`;
const CRLF = '\r\n';
let text = statusLine + CRLF;
for (const header of Object.keys(responseHeaders))
text += header + ': ' + responseHeaders[header] + CRLF;
text += CRLF;
let responseBuffer = Buffer.from(text, 'utf8');
if (responseBody)
responseBuffer = Buffer.concat([responseBuffer, responseBody]);
await this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId,
rawResponse: responseBuffer.toString('base64')
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
});
}
/** /**
* @param {string=} errorCode * @param {string=} errorCode
*/ */
@ -486,4 +534,67 @@ NetworkManager.Events = {
RequestFinished: 'requestfinished', RequestFinished: 'requestfinished',
}; };
const statusTexts = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'209': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'306': 'Switch Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': 'I\'m a teapot',
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'510': 'Not Extended',
'511': 'Network Authentication Required',
};
module.exports = NetworkManager; module.exports = NetworkManager;

BIN
test/assets/pptr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -1337,6 +1337,55 @@ describe('Page', function() {
await request.continue().catch(e => error = e); await request.continue().catch(e => error = e);
expect(error).toBe(null); expect(error).toBe(null);
})); }));
it('should throw if interception is not enabled', SX(async function() {
let error = null;
page.on('request', async request => {
try {
await request.continue();
} catch (e) {
error = e;
}
});
await page.goto(EMPTY_PAGE);
expect(error.message).toContain('Request Interception is not enabled');
}));
});
describe('Request.respond', function() {
it('should work', SX(async function() {
await page.setRequestInterceptionEnabled(true);
page.on('request', request => {
request.respond({
status: 201,
headers: {
foo: 'bar'
},
body: 'Yo, page!'
});
});
const response = await page.goto(EMPTY_PAGE);
expect(response.status).toBe(201);
expect(response.headers.foo).toBe('bar');
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
}));
it('should allow mocking binary responses', SX(async function() {
await page.setRequestInterceptionEnabled(true);
page.on('request', request => {
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
request.respond({
contentType: 'image/png',
body: imageBuffer
});
});
await page.evaluate(PREFIX => {
const img = document.createElement('img');
img.src = PREFIX + '/does-not-exist.png';
document.body.appendChild(img);
return new Promise(fulfill => img.onload = fulfill);
}, PREFIX);
const img = await page.$('img');
expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
}));
}); });
describe('Page.Events.Dialog', function() { describe('Page.Events.Dialog', function() {