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.postData](#requestpostdata)
* [request.resourceType](#requestresourcetype)
* [request.respond(response)](#requestrespondresponse)
* [request.response()](#requestresponse)
* [request.url](#requesturl)
- [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.
- 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
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)
- `userAgent` <[string]> Specific user agent to use in this page
- 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.
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()
- 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
*/
@ -486,4 +534,67 @@ NetworkManager.Events = {
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;

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);
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() {