feat: support Response.buffer(), Response.json() and Response.text() (#4063)
This patch: - implements Response.buffer() and other methods - splits out relevant tests into a separate test suites - implements `testServer.enableGzip()` method to optionally gzip certain routes in tests - adds tests to make sure `Response.text()` returns expected results for binary and compressed responses.
This commit is contained in:
parent
3bea5d6017
commit
56dafd7424
@ -60,7 +60,7 @@ class NetworkManager extends EventEmitter {
|
|||||||
const request = this._requests.get(event.requestId);
|
const request = this._requests.get(event.requestId);
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = new Response(request, event);
|
const response = new Response(this._session, request, event);
|
||||||
request._response = response;
|
request._response = response;
|
||||||
this.emit(Events.NetworkManager.Response, response);
|
this.emit(Events.NetworkManager.Response, response);
|
||||||
}
|
}
|
||||||
@ -71,8 +71,12 @@ class NetworkManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
// Keep redirected requests in the map for future reference in redirectChain.
|
// Keep redirected requests in the map for future reference in redirectChain.
|
||||||
const isRedirected = request.response().status() >= 300 && request.response().status() <= 399;
|
const isRedirected = request.response().status() >= 300 && request.response().status() <= 399;
|
||||||
if (!isRedirected)
|
if (isRedirected) {
|
||||||
|
request.response()._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
||||||
|
} else {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
|
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||||
|
}
|
||||||
this.emit(Events.NetworkManager.RequestFinished, request);
|
this.emit(Events.NetworkManager.RequestFinished, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,6 +85,8 @@ class NetworkManager extends EventEmitter {
|
|||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
|
if (request.response())
|
||||||
|
request.response()._bodyLoadedPromiseFulfill.call(null);
|
||||||
request._errorText = event.errorCode;
|
request._errorText = event.errorCode;
|
||||||
this.emit(Events.NetworkManager.RequestFailed, request);
|
this.emit(Events.NetworkManager.RequestFailed, request);
|
||||||
}
|
}
|
||||||
@ -200,7 +206,8 @@ class Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Response {
|
class Response {
|
||||||
constructor(request, payload) {
|
constructor(session, request, payload) {
|
||||||
|
this._session = session;
|
||||||
this._request = request;
|
this._request = request;
|
||||||
this._remoteIPAddress = payload.remoteIPAddress;
|
this._remoteIPAddress = payload.remoteIPAddress;
|
||||||
this._remotePort = payload.remotePort;
|
this._remotePort = payload.remotePort;
|
||||||
@ -210,6 +217,44 @@ class Response {
|
|||||||
this._securityDetails = payload.securityDetails ? new SecurityDetails(payload.securityDetails) : null;
|
this._securityDetails = payload.securityDetails ? new SecurityDetails(payload.securityDetails) : null;
|
||||||
for (const {name, value} of payload.headers)
|
for (const {name, value} of payload.headers)
|
||||||
this._headers[name.toLowerCase()] = value;
|
this._headers[name.toLowerCase()] = value;
|
||||||
|
this._bodyLoadedPromise = new Promise(fulfill => {
|
||||||
|
this._bodyLoadedPromiseFulfill = fulfill;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Promise<!Buffer>}
|
||||||
|
*/
|
||||||
|
buffer() {
|
||||||
|
if (!this._contentPromise) {
|
||||||
|
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
||||||
|
if (error)
|
||||||
|
throw error;
|
||||||
|
const response = await this._session.send('Network.getResponseBody', {
|
||||||
|
requestId: this._request._id
|
||||||
|
});
|
||||||
|
if (response.evicted)
|
||||||
|
throw new Error(`Response body for ${this._request.method()} ${this._request.url()} was evicted!`);
|
||||||
|
return Buffer.from(response.base64body, 'base64');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._contentPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Promise<string>}
|
||||||
|
*/
|
||||||
|
async text() {
|
||||||
|
const content = await this.buffer();
|
||||||
|
return content.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {!Promise<!Object>}
|
||||||
|
*/
|
||||||
|
async json() {
|
||||||
|
const content = await this.text();
|
||||||
|
return JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
securityDetails() {
|
securityDetails() {
|
||||||
|
@ -17,6 +17,12 @@ pref("browser.newtabpage.enabled", false);
|
|||||||
// Disable topstories
|
// Disable topstories
|
||||||
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||||
|
|
||||||
|
// DevTools JSONViewer sometimes fails to load dependencies with its require.js.
|
||||||
|
// This doesn't affect Puppeteer operations, but spams console with a lot of
|
||||||
|
// unpleasant errors.
|
||||||
|
// (bug 1424372)
|
||||||
|
pref("devtools.jsonview.enabled", false);
|
||||||
|
|
||||||
// Increase the APZ content response timeout in tests to 1 minute.
|
// Increase the APZ content response timeout in tests to 1 minute.
|
||||||
// This is to accommodate the fact that test environments tends to be
|
// This is to accommodate the fact that test environments tends to be
|
||||||
// slower than production environments (with the b2g emulator being
|
// slower than production environments (with the b2g emulator being
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"node": ">=8.9.4"
|
"node": ">=8.9.4"
|
||||||
},
|
},
|
||||||
"puppeteer": {
|
"puppeteer": {
|
||||||
"firefox_revision": "f7b25713dd00f0deda7032dc25a72d4c7b42446e"
|
"firefox_revision": "3ba79216e3c5ae4e85006047cdd93eac4197427d"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "node install.js",
|
"install": "node install.js",
|
||||||
|
@ -499,7 +499,7 @@ module.exports.addTests = function({testRunner, expect, Errors, CHROME}) {
|
|||||||
const error = await navigationPromise;
|
const error = await navigationPromise;
|
||||||
expect(error.message).toBe('Navigating frame was detached');
|
expect(error.message).toBe('Navigating frame was detached');
|
||||||
});
|
});
|
||||||
it_fails_ffox('should return matching responses', async({page, server}) => {
|
it('should return matching responses', async({page, server}) => {
|
||||||
// Disable cache: otherwise, chromium will cache similar requests.
|
// Disable cache: otherwise, chromium will cache similar requests.
|
||||||
await page.setCacheEnabled(false);
|
await page.setCacheEnabled(false);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
@ -156,6 +156,86 @@ module.exports.addTests = function({testRunner, expect, CHROME}) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Response.text', function() {
|
||||||
|
it('should work', async({page, server}) => {
|
||||||
|
const response = await page.goto(server.PREFIX + '/simple.json');
|
||||||
|
expect(await response.text()).toBe('{"foo": "bar"}\n');
|
||||||
|
});
|
||||||
|
it('should return uncompressed text', async({page, server}) => {
|
||||||
|
server.enableGzip('/simple.json');
|
||||||
|
const response = await page.goto(server.PREFIX + '/simple.json');
|
||||||
|
expect(response.headers()['content-encoding']).toBe('gzip');
|
||||||
|
expect(await response.text()).toBe('{"foo": "bar"}\n');
|
||||||
|
});
|
||||||
|
it('should throw when requesting body of redirected response', async({page, server}) => {
|
||||||
|
server.setRedirect('/foo.html', '/empty.html');
|
||||||
|
const response = await page.goto(server.PREFIX + '/foo.html');
|
||||||
|
const redirectChain = response.request().redirectChain();
|
||||||
|
expect(redirectChain.length).toBe(1);
|
||||||
|
const redirected = redirectChain[0].response();
|
||||||
|
expect(redirected.status()).toBe(302);
|
||||||
|
let error = null;
|
||||||
|
await redirected.text().catch(e => error = e);
|
||||||
|
expect(error.message).toContain('Response body is unavailable for redirect responses');
|
||||||
|
});
|
||||||
|
it('should wait until response completes', async({page, server}) => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
// Setup server to trap request.
|
||||||
|
let serverResponse = null;
|
||||||
|
server.setRoute('/get', (req, res) => {
|
||||||
|
serverResponse = res;
|
||||||
|
// In Firefox, |fetch| will be hanging until it receives |Content-Type| header
|
||||||
|
// from server.
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.write('hello ');
|
||||||
|
});
|
||||||
|
// Setup page to trap response.
|
||||||
|
let requestFinished = false;
|
||||||
|
page.on('requestfinished', r => requestFinished = requestFinished || r.url().includes('/get'));
|
||||||
|
// send request and wait for server response
|
||||||
|
const [pageResponse] = await Promise.all([
|
||||||
|
page.waitForResponse(r => !utils.isFavicon(r.request())),
|
||||||
|
page.evaluate(() => fetch('./get', { method: 'GET'})),
|
||||||
|
server.waitForRequest('/get'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(serverResponse).toBeTruthy();
|
||||||
|
expect(pageResponse).toBeTruthy();
|
||||||
|
expect(pageResponse.status()).toBe(200);
|
||||||
|
expect(requestFinished).toBe(false);
|
||||||
|
|
||||||
|
const responseText = pageResponse.text();
|
||||||
|
// Write part of the response and wait for it to be flushed.
|
||||||
|
await new Promise(x => serverResponse.write('wor', x));
|
||||||
|
// Finish response.
|
||||||
|
await new Promise(x => serverResponse.end('ld!', x));
|
||||||
|
expect(await responseText).toBe('hello world!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Response.json', function() {
|
||||||
|
it('should work', async({page, server}) => {
|
||||||
|
const response = await page.goto(server.PREFIX + '/simple.json');
|
||||||
|
expect(await response.json()).toEqual({foo: 'bar'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Response.buffer', function() {
|
||||||
|
it('should work', async({page, server}) => {
|
||||||
|
const response = await page.goto(server.PREFIX + '/pptr.png');
|
||||||
|
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
|
||||||
|
const responseBuffer = await response.buffer();
|
||||||
|
expect(responseBuffer.equals(imageBuffer)).toBe(true);
|
||||||
|
});
|
||||||
|
it('should work with compression', async({page, server}) => {
|
||||||
|
server.enableGzip('/pptr.png');
|
||||||
|
const response = await page.goto(server.PREFIX + '/pptr.png');
|
||||||
|
const imageBuffer = fs.readFileSync(path.join(__dirname, 'assets', 'pptr.png'));
|
||||||
|
const responseBuffer = await response.buffer();
|
||||||
|
expect(responseBuffer.equals(imageBuffer)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Network Events', function() {
|
describe('Network Events', function() {
|
||||||
it('Page.Events.Request', async({page, server}) => {
|
it('Page.Events.Request', async({page, server}) => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
@ -193,56 +273,6 @@ module.exports.addTests = function({testRunner, expect, CHROME}) {
|
|||||||
expect(response.statusText()).toBe('cool!');
|
expect(response.statusText()).toBe('cool!');
|
||||||
});
|
});
|
||||||
|
|
||||||
it_fails_ffox('Page.Events.Response should provide body', async({page, server}) => {
|
|
||||||
let response = null;
|
|
||||||
page.on('response', r => response = r);
|
|
||||||
await page.goto(server.PREFIX + '/simple.json');
|
|
||||||
expect(response).toBeTruthy();
|
|
||||||
expect(await response.text()).toBe('{"foo": "bar"}\n');
|
|
||||||
expect(await response.json()).toEqual({foo: 'bar'});
|
|
||||||
});
|
|
||||||
it_fails_ffox('Page.Events.Response should throw when requesting body of redirected response', async({page, server}) => {
|
|
||||||
server.setRedirect('/foo.html', '/empty.html');
|
|
||||||
const response = await page.goto(server.PREFIX + '/foo.html');
|
|
||||||
const redirectChain = response.request().redirectChain();
|
|
||||||
expect(redirectChain.length).toBe(1);
|
|
||||||
const redirected = redirectChain[0].response();
|
|
||||||
expect(redirected.status()).toBe(302);
|
|
||||||
let error = null;
|
|
||||||
await redirected.text().catch(e => error = e);
|
|
||||||
expect(error.message).toContain('Response body is unavailable for redirect responses');
|
|
||||||
});
|
|
||||||
it_fails_ffox('Page.Events.Response should not report body unless request is finished', async({page, server}) => {
|
|
||||||
await page.goto(server.EMPTY_PAGE);
|
|
||||||
// Setup server to trap request.
|
|
||||||
let serverResponse = null;
|
|
||||||
server.setRoute('/get', (req, res) => {
|
|
||||||
serverResponse = res;
|
|
||||||
res.write('hello ');
|
|
||||||
});
|
|
||||||
// Setup page to trap response.
|
|
||||||
let pageResponse = null;
|
|
||||||
let requestFinished = false;
|
|
||||||
page.on('response', r => pageResponse = r);
|
|
||||||
page.on('requestfinished', () => requestFinished = true);
|
|
||||||
// send request and wait for server response
|
|
||||||
await Promise.all([
|
|
||||||
page.evaluate(() => fetch('./get', { method: 'GET'})),
|
|
||||||
utils.waitEvent(page, 'response')
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(serverResponse).toBeTruthy();
|
|
||||||
expect(pageResponse).toBeTruthy();
|
|
||||||
expect(pageResponse.status()).toBe(200);
|
|
||||||
expect(requestFinished).toBe(false);
|
|
||||||
|
|
||||||
const responseText = pageResponse.text();
|
|
||||||
// Write part of the response and wait for it to be flushed.
|
|
||||||
await new Promise(x => serverResponse.write('wor', x));
|
|
||||||
// Finish response.
|
|
||||||
await new Promise(x => serverResponse.end('ld!', x));
|
|
||||||
expect(await responseText).toBe('hello world!');
|
|
||||||
});
|
|
||||||
it('Page.Events.RequestFailed', async({page, server}) => {
|
it('Page.Events.RequestFailed', async({page, server}) => {
|
||||||
await page.setRequestInterception(true);
|
await page.setRequestInterception(true);
|
||||||
page.on('request', request => {
|
page.on('request', request => {
|
||||||
|
@ -80,6 +80,8 @@ class TestServer {
|
|||||||
this._auths = new Map();
|
this._auths = new Map();
|
||||||
/** @type {!Map<string, string>} */
|
/** @type {!Map<string, string>} */
|
||||||
this._csp = new Map();
|
this._csp = new Map();
|
||||||
|
/** @type {!Set<string>} */
|
||||||
|
this._gzipRoutes = new Set();
|
||||||
/** @type {!Map<string, !Promise>} */
|
/** @type {!Map<string, !Promise>} */
|
||||||
this._requestSubscribers = new Map();
|
this._requestSubscribers = new Map();
|
||||||
}
|
}
|
||||||
@ -111,6 +113,10 @@ class TestServer {
|
|||||||
this._auths.set(path, {username, password});
|
this._auths.set(path, {username, password});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableGzip(path) {
|
||||||
|
this._gzipRoutes.add(path);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @param {string} csp
|
* @param {string} csp
|
||||||
@ -169,6 +175,7 @@ class TestServer {
|
|||||||
this._routes.clear();
|
this._routes.clear();
|
||||||
this._auths.clear();
|
this._auths.clear();
|
||||||
this._csp.clear();
|
this._csp.clear();
|
||||||
|
this._gzipRoutes.clear();
|
||||||
const error = new Error('Static Server has been reset');
|
const error = new Error('Static Server has been reset');
|
||||||
for (const subscriber of this._requestSubscribers.values())
|
for (const subscriber of this._requestSubscribers.values())
|
||||||
subscriber[rejectSymbol].call(null, error);
|
subscriber[rejectSymbol].call(null, error);
|
||||||
@ -230,14 +237,22 @@ class TestServer {
|
|||||||
if (this._csp.has(pathName))
|
if (this._csp.has(pathName))
|
||||||
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
|
response.setHeader('Content-Security-Policy', this._csp.get(pathName));
|
||||||
|
|
||||||
fs.readFile(filePath, function(err, data) {
|
fs.readFile(filePath, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
response.statusCode = 404;
|
response.statusCode = 404;
|
||||||
response.end(`File not found: ${filePath}`);
|
response.end(`File not found: ${filePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.setHeader('Content-Type', mime.getType(filePath));
|
response.setHeader('Content-Type', mime.getType(filePath));
|
||||||
response.end(data);
|
if (this._gzipRoutes.has(pathName)) {
|
||||||
|
response.setHeader('Content-Encoding', 'gzip');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
zlib.gzip(data, (_, result) => {
|
||||||
|
response.end(result);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response.end(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user