Skip to content

Add support for fetch and cbor #797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 131 additions & 61 deletions src/http-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ limitations under the License.
* @module http-api
*/
import Promise from 'bluebird';
import borc from "borc";
import queryString from 'qs';
const parseContentType = require('content-type').parse;

const utils = require("./utils");
Expand Down Expand Up @@ -86,6 +88,7 @@ module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
this.opts = opts;
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
this.supportsCbor = true; // XXX: Validate this somewhere.
};

module.exports.MatrixHttpApi.prototype = {
Expand Down Expand Up @@ -701,18 +704,20 @@ module.exports.MatrixHttpApi.prototype = {

if (json) {
if (data) {
headers['Content-Type'] = 'application/json';
data = JSON.stringify(data);
headers['content-type'] = 'application/json';
}

if (!headers['accept']) {
headers['accept'] = 'application/json';
if (this.supportsCbor) {
// To remove undefined values, we cheekily use stringify.
data = borc.encode(JSON.parse(data));
headers['Content-Type'] = 'application/cbor';
}
}

if (bodyParser === undefined) {
bodyParser = function(rawBody) {
return JSON.parse(rawBody);
};
if (!headers['Accept']) {
headers['Accept'] = 'application/json';
if (this.supportsCbor) {
headers['Accept'] = `application/cbor, application/jsonq=0.9`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing semicolon before "q=0.9"

}
}
}

Expand Down Expand Up @@ -744,64 +749,129 @@ module.exports.MatrixHttpApi.prototype = {
resetTimeout();

const reqPromise = defer.promise;

try {
req = this.opts.request(
{
uri: uri,
method: method,
withCredentials: false,
qs: queryParams,
qsStringifyOptions: opts.qsStringifyOptions,
useQuerystring: true,
body: data,
json: false,
timeout: localTimeoutMs,
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
}

const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
bodyParser,
);
handlerFn(err, response, body);
},
);
if (req) {
// This will only work in a browser, where opts.request is the
// `browser-request` import. Currently `request` does not support progress
// updates - see https://github.com/request/request/pull/2346.
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
if ('onprogress' in req) {
req.onprogress = (e) => {
// Prevent the timeout from rejecting the deferred promise if progress is
// seen with the request
resetTimeout();
};
const qs = queryString.stringify(queryParams || {}, opts.qsStringifyOptions);
let res;
const controller = new AbortController();
const signal = controller.signal;
fetch(qs ? `${uri}?${qs}`: uri, {
method,
headers: headers || [],
body: data,
mode: "cors",
credentials: "omit",
signal,
}).then((_res) => {
res = _res;
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}

// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
if (req.abort) reqPromise.abort = req.abort.bind(req);
}
} catch (ex) {
defer.reject(ex);
if (callback) {
callback(ex);
if (this.supportsCbor) {
return res.arrayBuffer().then((buffer) => {
return borc.decode(new Uint8Array(buffer));
}).then((cborObj) => {
return convertCborToObject(cborObj);
});
}
}
return res.json();
}).then((body) => {
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
);
handlerFn(null, res, body);
}).catch((ex) => {
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
);
handlerFn(ex, null, null);
});
reqPromise.abort = () => {
controller.abort();
};
// req = this.opts.request(
// {
// uri: uri,
// method: method,
// withCredentials: false,
// qs: queryParams,
// qsStringifyOptions: opts.qsStringifyOptions,
// useQuerystring: true,
// encoding: this.supportsCbor ? null : undefined,
// body: data,
// json: false,
// timeout: localTimeoutMs,
// headers: headers || {},
// _matrix_opts: this.opts,
// },
// function(err, response, body) {
// if (localTimeoutMs) {
// callbacks.clearTimeout(timeoutId);
// if (timedOut) {
// return; // already rejected promise
// }
// }
//
// const handlerFn = requestCallback(
// defer, callback, self.opts.onlyData,
// bodyParser,
// );
// handlerFn(err, response, body);
// },
// );
// if (req) {
// // This will only work in a browser, where opts.request is the
// // `browser-request` import. Currently `request` does not support progress
// // updates - see https://github.com/request/request/pull/2346.
// // `browser-request` returns an XHRHttpRequest which exposes `onprogress`
// if ('onprogress' in req) {
// req.onprogress = (e) => {
// // Prevent the timeout from rejecting the deferred promise if progress is
// // seen with the request
// resetTimeout();
// };
// }
//
// // FIXME: This is EVIL, but I can't think of a better way to expose
// // abort() operations on underlying HTTP requests :(
// if (req.abort) reqPromise.abort = req.abort.bind(req);
// }
return reqPromise;
},
};

const convertCborToObject = function(currObject) {
const isMap = currObject instanceof Map;
const isObject = currObject instanceof Object;
const isArray = currObject instanceof Array;

if (!isMap && isObject) {
Object.keys(currObject).forEach((key) => {
currObject[key] = convertCborToObject(currObject[key]);
});
return currObject;
}

if (!isMap && !isArray) {
return currObject;
}

let obj = isArray ? [] : {};
currObject.forEach((value, key) => {
// Will be a uint8array
if (!isArray) {
key = key.toString();
}
// We don't use Uint8Array in CS, but cbor makes strings into these.
if (value instanceof Uint8Array) {
value = value.toString();
}
obj[key] = convertCborToObject(value);
});
return obj;
}

/*
* Returns a callback that can be invoked by an HTTP request on completion,
* that will either resolve or reject the given defer as well as invoke the
Expand All @@ -825,7 +895,7 @@ const requestCallback = function(
return function(err, response, body) {
if (!err) {
try {
if (response.statusCode >= 400) {
if (response.status >= 400) {
err = parseErrorResponse(response, body);
} else if (bodyParser) {
body = bodyParser(body);
Expand Down