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.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.
|
||||||
|
|
||||||
|
@ -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
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);
|
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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user