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:
parent
7f60e33a63
commit
bcc969ccc4
35
docs/api.md
35
docs/api.md
@ -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.
|
||||
|
||||
|
@ -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
BIN
test/assets/pptr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
test/golden/mock-binary-response.png
Normal file
BIN
test/golden/mock-binary-response.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
49
test/test.js
49
test/test.js
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user