1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Avoid parsing plain-text errors as JSON

It's somewhat unhelpful to spam over the actual error from the reverse-proxy or
whatever with a SyntaxError.
This commit is contained in:
Richard van der Hoff
2017-07-03 18:29:24 +01:00
parent cc16cb9281
commit 5f6e4bdfe9
4 changed files with 129 additions and 31 deletions

View File

@@ -49,6 +49,7 @@
"dependencies": { "dependencies": {
"another-json": "^0.2.0", "another-json": "^0.2.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"content-type": "^1.0.2",
"q": "^1.4.1", "q": "^1.4.1",
"request": "^2.53.0" "request": "^2.53.0"
}, },

View File

@@ -49,13 +49,13 @@ describe("MatrixClient", function() {
httpBackend.when( httpBackend.when(
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/v1/upload",
).check(function(req) { ).check(function(req) {
expect(req.data).toEqual(buf); expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt"); expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken); expect(req.queryParams.access_token).toEqual(accessToken);
expect(req.headers["Content-Type"]).toEqual("text/plain"); expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined); expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content"); }).respond(200, "content", true);
const prom = client.uploadContent({ const prom = client.uploadContent({
stream: buf, stream: buf,
@@ -86,7 +86,7 @@ describe("MatrixClient", function() {
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/v1/upload",
).check(function(req) { ).check(function(req) {
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" })); }).respond(200, { "content_uri": "uri" });
client.uploadContent({ client.uploadContent({
stream: buf, stream: buf,
@@ -102,16 +102,15 @@ describe("MatrixClient", function() {
}); });
it("should parse errors into a MatrixError", function(done) { it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when( httpBackend.when(
"POST", "/_matrix/media/v1/upload", "POST", "/_matrix/media/v1/upload",
).check(function(req) { ).check(function(req) {
expect(req.data).toEqual(buf); expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({ }).respond(400, {
"errcode": "M_SNAFU", "errcode": "M_SNAFU",
"error": "broken", "error": "broken",
})); });
client.uploadContent({ client.uploadContent({
stream: buf, stream: buf,

View File

@@ -126,10 +126,15 @@ HttpBackend.prototype = {
} }
testResponse = matchingReq.response; testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path); console.log(" responding to %s", matchingReq.path);
let body = testResponse.body; let body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") { if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data); body = body(req.path, req.data);
} }
if (!testResponse.rawBody) {
body = JSON.stringify(body);
}
req.callback( req.callback(
testResponse.err, testResponse.response, body, testResponse.err, testResponse.response, body,
); );
@@ -210,17 +215,22 @@ ExpectedRequest.prototype = {
/** /**
* Respond with the given data when this request is satisfied. * Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code. * @param {Number} code The HTTP status code.
* @param {Object|Function} data The HTTP JSON body. If this is a function, * @param {Object|Function?} data The response body object. If this is a function,
* it will be invoked when the JSON body is required (which should be returned). * it will be invoked when the response body is required (which should be returned).
* @param {Boolean} rawBody true if the response should be returned directly rather
* than json-stringifying it first.
*/ */
respond: function(code, data) { respond: function(code, data, rawBody) {
this.response = { this.response = {
response: { response: {
statusCode: code, statusCode: code,
headers: {}, headers: {
'content-type': 'application/json',
},
}, },
body: data, body: data || "",
err: null, err: null,
rawBody: rawBody || false,
}; };
}, },
@@ -265,6 +275,12 @@ function Request(opts, callback) {
}); });
Object.defineProperty(this, 'data', { Object.defineProperty(this, 'data', {
get: function() {
return opts.body ? JSON.parse(opts.body) : opts.body;
},
});
Object.defineProperty(this, 'rawData', {
get: function() { get: function() {
return opts.body; return opts.body;
}, },

View File

@@ -19,6 +19,8 @@ limitations under the License.
* @module http-api * @module http-api
*/ */
const q = require("q"); const q = require("q");
const parseContentType = require('content-type').parse;
const utils = require("./utils"); const utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in // we use our own implementation of setTimeout, so that if we get suspended in
@@ -623,7 +625,31 @@ module.exports.MatrixHttpApi.prototype = {
} }
} }
const headers = utils.extend({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json; const json = opts.json === undefined ? true : opts.json;
let bodyParser = opts.bodyParser;
// we handle the json encoding/decoding here, because request and
// browser-request make a mess of it. Specifically, they attempt to
// json-decode plain-text error responses, which in turn means that the
// actual error gets swallowed by a SyntaxError.
if (json) {
if (data) {
data = JSON.stringify(data);
headers['content-type'] = 'application/json';
}
if (!headers['accept']) {
headers['accept'] = 'application/json';
}
if (bodyParser === undefined) {
bodyParser = function(rawBody) {
return JSON.parse(rawBody);
};
}
}
const defer = q.defer(); const defer = q.defer();
@@ -662,7 +688,7 @@ module.exports.MatrixHttpApi.prototype = {
withCredentials: false, withCredentials: false,
qs: queryParams, qs: queryParams,
body: data, body: data,
json: json, json: false,
timeout: localTimeoutMs, timeout: localTimeoutMs,
headers: opts.headers || {}, headers: opts.headers || {},
_matrix_opts: this.opts, _matrix_opts: this.opts,
@@ -675,13 +701,9 @@ module.exports.MatrixHttpApi.prototype = {
} }
} }
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
const parseErrorJson = !json;
const handlerFn = requestCallback( const handlerFn = requestCallback(
defer, callback, self.opts.onlyData, defer, callback, self.opts.onlyData,
parseErrorJson, bodyParser,
opts.bodyParser,
); );
handlerFn(err, response, body); handlerFn(err, response, body);
}, },
@@ -718,15 +740,18 @@ module.exports.MatrixHttpApi.prototype = {
* that will either resolve or reject the given defer as well as invoke the * that will either resolve or reject the given defer as well as invoke the
* given userDefinedCallback (if any). * given userDefinedCallback (if any).
* *
* If onlyData is true, the defer/callback is invoked with the body of the * HTTP errors are transformed into javascript errors and the deferred is rejected.
* response, otherwise the result code.
* *
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error. * If bodyParser is given, it is used to transform the body of the successful
* responses before passing to the defer/callback.
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result object (with `code` and `data` fields)
* *
*/ */
const requestCallback = function( const requestCallback = function(
defer, userDefinedCallback, onlyData, defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser, bodyParser,
) { ) {
userDefinedCallback = userDefinedCallback || function() {}; userDefinedCallback = userDefinedCallback || function() {};
@@ -734,19 +759,12 @@ const requestCallback = function(
if (!err) { if (!err) {
try { try {
if (response.statusCode >= 400) { if (response.statusCode >= 400) {
if (parseErrorJson) { err = parseErrorResponse(response, body);
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
} else if (bodyParser) { } else if (bodyParser) {
body = bodyParser(body); body = bodyParser(body);
} }
} catch (e) { } catch (e) {
err = e; err = new Error(`Error parsing server response: ${e}`);
}
if (err) {
err.httpStatus = response.statusCode;
} }
} }
@@ -756,6 +774,9 @@ const requestCallback = function(
} else { } else {
const res = { const res = {
code: response.statusCode, code: response.statusCode,
// XXX: why do we bother with this? it doesn't work for
// XMLHttpRequest, so clearly we don't use it.
headers: response.headers, headers: response.headers,
data: body, data: body,
}; };
@@ -765,6 +786,67 @@ const requestCallback = function(
}; };
}; };
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @param {String} body raw body of the response
* @returns {Error}
*/
function parseErrorResponse(response, body) {
const httpStatus = response.statusCode;
const contentType = getResponseContentType(response);
let err;
if (contentType) {
if (contentType.type === 'application/json') {
err = new module.exports.MatrixError(JSON.parse(body));
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
}
if (!err) {
err = new Error(`Server returned ${httpStatus} error`);
}
err.httpStatus = httpStatus;
return err;
}
/**
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response) {
let contentType;
if (response.getResponseHeader) {
// XMLHttpRequest provides getResponseHeader
contentType = response.getResponseHeader("Content-Type");
} else if (response.headers) {
// request provides http.IncomingMessage which has a message.headers map
contentType = response.headers['content-type'] || null;
}
if (!contentType) {
return null;
}
try {
return parseContentType(contentType);
} catch(e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/** /**
* Construct a Matrix error. This is a JavaScript Error with additional * Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response. * information specific to the standard Matrix error response.