UNPKG

33.4 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3Copyright 2019 The Matrix.org Foundation C.I.C.
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16*/
17
18/**
19 * This is an internal module. See {@link MatrixHttpApi} for the public class.
20 * @module http-api
21 */
22
23import {parse as parseContentType} from "content-type";
24import * as utils from "./utils";
25import {logger} from './logger';
26
27// we use our own implementation of setTimeout, so that if we get suspended in
28// the middle of a /sync, we cancel the sync as soon as we awake, rather than
29// waiting for the delay to elapse.
30import * as callbacks from "./realtime-callbacks";
31
32/*
33TODO:
34- CS: complete register function (doing stages)
35- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
36*/
37
38/**
39 * A constant representing the URI path for release 0 of the Client-Server HTTP API.
40 */
41export const PREFIX_R0 = "/_matrix/client/r0";
42
43/**
44 * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
45 */
46export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
47
48/**
49 * URI path for v1 of the the identity API
50 * @deprecated Use v2.
51 */
52export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
53
54/**
55 * URI path for the v2 identity API
56 */
57export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
58
59/**
60 * URI path for the media repo API
61 */
62export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
63
64/**
65 * Construct a MatrixHttpApi.
66 * @constructor
67 * @param {EventEmitter} event_emitter The event emitter to use for emitting events
68 * @param {Object} opts The options to use for this HTTP API.
69 * @param {string} opts.baseUrl Required. The base client-server URL e.g.
70 * 'http://localhost:8008'.
71 * @param {Function} opts.request Required. The function to call for HTTP
72 * requests. This function must look like function(opts, callback){ ... }.
73 * @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
74 * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
75 *
76 * @param {boolean} opts.onlyData True to return only the 'data' component of the
77 * response (e.g. the parsed HTTP body). If false, requests will return an
78 * object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
79 *
80 * @param {string} opts.accessToken The access_token to send with requests. Can be
81 * null to not send an access token.
82 * @param {Object=} opts.extraParams Optional. Extra query parameters to send on
83 * requests.
84 * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
85 * before timing out the request. If not specified, there is no timeout.
86 * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
87 * Authorization header instead of query param to send the access token to the server.
88 */
89export function MatrixHttpApi(event_emitter, opts) {
90 utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
91 opts.onlyData = opts.onlyData || false;
92 this.event_emitter = event_emitter;
93 this.opts = opts;
94 this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
95 this.uploads = [];
96}
97
98MatrixHttpApi.prototype = {
99 /**
100 * Sets the baase URL for the identity server
101 * @param {string} url The new base url
102 */
103 setIdBaseUrl: function(url) {
104 this.opts.idBaseUrl = url;
105 },
106
107 /**
108 * Get the content repository url with query parameters.
109 * @return {Object} An object with a 'base', 'path' and 'params' for base URL,
110 * path and query parameters respectively.
111 */
112 getContentUri: function() {
113 const params = {
114 access_token: this.opts.accessToken,
115 };
116 return {
117 base: this.opts.baseUrl,
118 path: "/_matrix/media/r0/upload",
119 params: params,
120 };
121 },
122
123 /**
124 * Upload content to the Home Server
125 *
126 * @param {object} file The object to upload. On a browser, something that
127 * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
128 * a Buffer, String or ReadStream.
129 *
130 * @param {object} opts options object
131 *
132 * @param {string=} opts.name Name to give the file on the server. Defaults
133 * to <tt>file.name</tt>.
134 *
135 * @param {boolean=} opts.includeFilename if false will not send the filename,
136 * e.g for encrypted file uploads where filename leaks are undesirable.
137 * Defaults to true.
138 *
139 * @param {string=} opts.type Content-type for the upload. Defaults to
140 * <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
141 *
142 * @param {boolean=} opts.rawResponse Return the raw body, rather than
143 * parsing the JSON. Defaults to false (except on node.js, where it
144 * defaults to true for backwards compatibility).
145 *
146 * @param {boolean=} opts.onlyContentUri Just return the content URI,
147 * rather than the whole body. Defaults to false (except on browsers,
148 * where it defaults to true for backwards compatibility). Ignored if
149 * opts.rawResponse is true.
150 *
151 * @param {Function=} opts.callback Deprecated. Optional. The callback to
152 * invoke on success/failure. See the promise return values for more
153 * information.
154 *
155 * @param {Function=} opts.progressHandler Optional. Called when a chunk of
156 * data has been uploaded, with an object containing the fields `loaded`
157 * (number of bytes transferred) and `total` (total size, if known).
158 *
159 * @return {Promise} Resolves to response object, as
160 * determined by this.opts.onlyData, opts.rawResponse, and
161 * opts.onlyContentUri. Rejects with an error (usually a MatrixError).
162 */
163 uploadContent: function(file, opts) {
164 if (utils.isFunction(opts)) {
165 // opts used to be callback
166 opts = {
167 callback: opts,
168 };
169 } else if (opts === undefined) {
170 opts = {};
171 }
172
173 // default opts.includeFilename to true (ignoring falsey values)
174 const includeFilename = opts.includeFilename !== false;
175
176 // if the file doesn't have a mime type, use a default since
177 // the HS errors if we don't supply one.
178 const contentType = opts.type || file.type || 'application/octet-stream';
179 const fileName = opts.name || file.name;
180
181 // We used to recommend setting file.stream to the thing to upload on
182 // Node.js. As of 2019-06-11, this is still in widespread use in various
183 // clients, so we should preserve this for simple objects used in
184 // Node.js. File API objects (via either the File or Blob interfaces) in
185 // the browser now define a `stream` method, which leads to trouble
186 // here, so we also check the type of `stream`.
187 let body = file;
188 if (body.stream && typeof body.stream !== "function") {
189 logger.warn(
190 "Using `file.stream` as the content to upload. Future " +
191 "versions of the js-sdk will change this to expect `file` to " +
192 "be the content directly.",
193 );
194 body = body.stream;
195 }
196
197 // backwards-compatibility hacks where we used to do different things
198 // between browser and node.
199 let rawResponse = opts.rawResponse;
200 if (rawResponse === undefined) {
201 if (global.XMLHttpRequest) {
202 rawResponse = false;
203 } else {
204 logger.warn(
205 "Returning the raw JSON from uploadContent(). Future " +
206 "versions of the js-sdk will change this default, to " +
207 "return the parsed object. Set opts.rawResponse=false " +
208 "to change this behaviour now.",
209 );
210 rawResponse = true;
211 }
212 }
213
214 let onlyContentUri = opts.onlyContentUri;
215 if (!rawResponse && onlyContentUri === undefined) {
216 if (global.XMLHttpRequest) {
217 logger.warn(
218 "Returning only the content-uri from uploadContent(). " +
219 "Future versions of the js-sdk will change this " +
220 "default, to return the whole response object. Set " +
221 "opts.onlyContentUri=false to change this behaviour now.",
222 );
223 onlyContentUri = true;
224 } else {
225 onlyContentUri = false;
226 }
227 }
228
229 // browser-request doesn't support File objects because it deep-copies
230 // the options using JSON.parse(JSON.stringify(options)). Instead of
231 // loading the whole file into memory as a string and letting
232 // browser-request base64 encode and then decode it again, we just
233 // use XMLHttpRequest directly.
234 // (browser-request doesn't support progress either, which is also kind
235 // of important here)
236
237 const upload = { loaded: 0, total: 0 };
238 let promise;
239
240 // XMLHttpRequest doesn't parse JSON for us. request normally does, but
241 // we're setting opts.json=false so that it doesn't JSON-encode the
242 // request, which also means it doesn't JSON-decode the response. Either
243 // way, we have to JSON-parse the response ourselves.
244 let bodyParser = null;
245 if (!rawResponse) {
246 bodyParser = function(rawBody) {
247 let body = JSON.parse(rawBody);
248 if (onlyContentUri) {
249 body = body.content_uri;
250 if (body === undefined) {
251 throw Error('Bad response');
252 }
253 }
254 return body;
255 };
256 }
257
258 if (global.XMLHttpRequest) {
259 const defer = utils.defer();
260 const xhr = new global.XMLHttpRequest();
261 upload.xhr = xhr;
262 const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
263
264 const timeout_fn = function() {
265 xhr.abort();
266 cb(new Error('Timeout'));
267 };
268
269 // set an initial timeout of 30s; we'll advance it each time we get
270 // a progress notification
271 xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
272
273 xhr.onreadystatechange = function() {
274 switch (xhr.readyState) {
275 case global.XMLHttpRequest.DONE:
276 callbacks.clearTimeout(xhr.timeout_timer);
277 var resp;
278 try {
279 if (!xhr.responseText) {
280 throw new Error('No response body.');
281 }
282 resp = xhr.responseText;
283 if (bodyParser) {
284 resp = bodyParser(resp);
285 }
286 } catch (err) {
287 err.http_status = xhr.status;
288 cb(err);
289 return;
290 }
291 cb(undefined, xhr, resp);
292 break;
293 }
294 };
295 xhr.upload.addEventListener("progress", function(ev) {
296 callbacks.clearTimeout(xhr.timeout_timer);
297 upload.loaded = ev.loaded;
298 upload.total = ev.total;
299 xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
300 if (opts.progressHandler) {
301 opts.progressHandler({
302 loaded: ev.loaded,
303 total: ev.total,
304 });
305 }
306 });
307 let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
308
309 const queryArgs = [];
310
311 if (includeFilename && fileName) {
312 queryArgs.push("filename=" + encodeURIComponent(fileName));
313 }
314
315 if (!this.useAuthorizationHeader) {
316 queryArgs.push("access_token="
317 + encodeURIComponent(this.opts.accessToken));
318 }
319
320 if (queryArgs.length > 0) {
321 url += "?" + queryArgs.join("&");
322 }
323
324 xhr.open("POST", url);
325 if (this.useAuthorizationHeader) {
326 xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
327 }
328 xhr.setRequestHeader("Content-Type", contentType);
329 xhr.send(body);
330 promise = defer.promise;
331
332 // dirty hack (as per _request) to allow the upload to be cancelled.
333 promise.abort = xhr.abort.bind(xhr);
334 } else {
335 const queryParams = {};
336
337 if (includeFilename && fileName) {
338 queryParams.filename = fileName;
339 }
340
341 promise = this.authedRequest(
342 opts.callback, "POST", "/upload", queryParams, body, {
343 prefix: "/_matrix/media/r0",
344 headers: {"Content-Type": contentType},
345 json: false,
346 bodyParser: bodyParser,
347 },
348 );
349 }
350
351 const self = this;
352
353 // remove the upload from the list on completion
354 const promise0 = promise.finally(function() {
355 for (let i = 0; i < self.uploads.length; ++i) {
356 if (self.uploads[i] === upload) {
357 self.uploads.splice(i, 1);
358 return;
359 }
360 }
361 });
362
363 // copy our dirty abort() method to the new promise
364 promise0.abort = promise.abort;
365
366 upload.promise = promise0;
367 this.uploads.push(upload);
368
369 return promise0;
370 },
371
372 cancelUpload: function(promise) {
373 if (promise.abort) {
374 promise.abort();
375 return true;
376 }
377 return false;
378 },
379
380 getCurrentUploads: function() {
381 return this.uploads;
382 },
383
384 idServerRequest: function(
385 callback,
386 method,
387 path,
388 params,
389 prefix,
390 accessToken,
391 ) {
392 if (!this.opts.idBaseUrl) {
393 throw new Error("No Identity Server base URL set");
394 }
395
396 const fullUri = this.opts.idBaseUrl + prefix + path;
397
398 if (callback !== undefined && !utils.isFunction(callback)) {
399 throw Error(
400 "Expected callback to be a function but got " + typeof callback,
401 );
402 }
403
404 const opts = {
405 uri: fullUri,
406 method: method,
407 withCredentials: false,
408 json: true, // we want a JSON response if we can
409 _matrix_opts: this.opts,
410 headers: {},
411 };
412 if (method === 'GET') {
413 opts.qs = params;
414 } else if (typeof params === "object") {
415 opts.json = params;
416 }
417 if (accessToken) {
418 opts.headers['Authorization'] = `Bearer ${accessToken}`;
419 }
420
421 const defer = utils.defer();
422 this.opts.request(
423 opts,
424 requestCallback(defer, callback, this.opts.onlyData),
425 );
426 return defer.promise;
427 },
428
429 /**
430 * Perform an authorised request to the homeserver.
431 * @param {Function} callback Optional. The callback to invoke on
432 * success/failure. See the promise return values for more information.
433 * @param {string} method The HTTP method e.g. "GET".
434 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
435 * "/createRoom".
436 *
437 * @param {Object=} queryParams A dict of query params (these will NOT be
438 * urlencoded). If unspecified, there will be no query params.
439 *
440 * @param {Object} [data] The HTTP JSON body.
441 *
442 * @param {Object|Number=} opts additional options. If a number is specified,
443 * this is treated as `opts.localTimeoutMs`.
444 *
445 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
446 * timing out the request. If not specified, there is no timeout.
447 *
448 * @param {sting=} opts.prefix The full prefix to use e.g.
449 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
450 *
451 * @param {Object=} opts.headers map of additional request headers
452 *
453 * @return {Promise} Resolves to <code>{data: {Object},
454 * headers: {Object}, code: {Number}}</code>.
455 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
456 * object only.
457 * @return {module:http-api.MatrixError} Rejects with an error if a problem
458 * occurred. This includes network problems and Matrix-specific error JSON.
459 */
460 authedRequest: function(callback, method, path, queryParams, data, opts) {
461 if (!queryParams) {
462 queryParams = {};
463 }
464 if (this.useAuthorizationHeader) {
465 if (isFinite(opts)) {
466 // opts used to be localTimeoutMs
467 opts = {
468 localTimeoutMs: opts,
469 };
470 }
471 if (!opts) {
472 opts = {};
473 }
474 if (!opts.headers) {
475 opts.headers = {};
476 }
477 if (!opts.headers.Authorization) {
478 opts.headers.Authorization = "Bearer " + this.opts.accessToken;
479 }
480 if (queryParams.access_token) {
481 delete queryParams.access_token;
482 }
483 } else {
484 if (!queryParams.access_token) {
485 queryParams.access_token = this.opts.accessToken;
486 }
487 }
488
489 const requestPromise = this.request(
490 callback, method, path, queryParams, data, opts,
491 );
492
493 const self = this;
494 requestPromise.catch(function(err) {
495 if (err.errcode == 'M_UNKNOWN_TOKEN') {
496 self.event_emitter.emit("Session.logged_out", err);
497 } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
498 self.event_emitter.emit(
499 "no_consent",
500 err.message,
501 err.data.consent_uri,
502 );
503 }
504 });
505
506 // return the original promise, otherwise tests break due to it having to
507 // go around the event loop one more time to process the result of the request
508 return requestPromise;
509 },
510
511 /**
512 * Perform a request to the homeserver without any credentials.
513 * @param {Function} callback Optional. The callback to invoke on
514 * success/failure. See the promise return values for more information.
515 * @param {string} method The HTTP method e.g. "GET".
516 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
517 * "/createRoom".
518 *
519 * @param {Object=} queryParams A dict of query params (these will NOT be
520 * urlencoded). If unspecified, there will be no query params.
521 *
522 * @param {Object} [data] The HTTP JSON body.
523 *
524 * @param {Object=} opts additional options
525 *
526 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
527 * timing out the request. If not specified, there is no timeout.
528 *
529 * @param {sting=} opts.prefix The full prefix to use e.g.
530 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
531 *
532 * @param {Object=} opts.headers map of additional request headers
533 *
534 * @return {Promise} Resolves to <code>{data: {Object},
535 * headers: {Object}, code: {Number}}</code>.
536 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
537 * object only.
538 * @return {module:http-api.MatrixError} Rejects with an error if a problem
539 * occurred. This includes network problems and Matrix-specific error JSON.
540 */
541 request: function(callback, method, path, queryParams, data, opts) {
542 opts = opts || {};
543 const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
544 const fullUri = this.opts.baseUrl + prefix + path;
545
546 return this.requestOtherUrl(
547 callback, method, fullUri, queryParams, data, opts,
548 );
549 },
550
551 /**
552 * Perform a request to an arbitrary URL.
553 * @param {Function} callback Optional. The callback to invoke on
554 * success/failure. See the promise return values for more information.
555 * @param {string} method The HTTP method e.g. "GET".
556 * @param {string} uri The HTTP URI
557 *
558 * @param {Object=} queryParams A dict of query params (these will NOT be
559 * urlencoded). If unspecified, there will be no query params.
560 *
561 * @param {Object} [data] The HTTP JSON body.
562 *
563 * @param {Object=} opts additional options
564 *
565 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
566 * timing out the request. If not specified, there is no timeout.
567 *
568 * @param {sting=} opts.prefix The full prefix to use e.g.
569 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
570 *
571 * @param {Object=} opts.headers map of additional request headers
572 *
573 * @return {Promise} Resolves to <code>{data: {Object},
574 * headers: {Object}, code: {Number}}</code>.
575 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
576 * object only.
577 * @return {module:http-api.MatrixError} Rejects with an error if a problem
578 * occurred. This includes network problems and Matrix-specific error JSON.
579 */
580 requestOtherUrl: function(callback, method, uri, queryParams, data,
581 opts) {
582 if (opts === undefined || opts === null) {
583 opts = {};
584 } else if (isFinite(opts)) {
585 // opts used to be localTimeoutMs
586 opts = {
587 localTimeoutMs: opts,
588 };
589 }
590
591 return this._request(
592 callback, method, uri, queryParams, data, opts,
593 );
594 },
595
596 /**
597 * Form and return a homeserver request URL based on the given path
598 * params and prefix.
599 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
600 * "/createRoom".
601 * @param {Object} queryParams A dict of query params (these will NOT be
602 * urlencoded).
603 * @param {string} prefix The full prefix to use e.g.
604 * "/_matrix/client/v2_alpha".
605 * @return {string} URL
606 */
607 getUrl: function(path, queryParams, prefix) {
608 let queryString = "";
609 if (queryParams) {
610 queryString = "?" + utils.encodeParams(queryParams);
611 }
612 return this.opts.baseUrl + prefix + path + queryString;
613 },
614
615 /**
616 * @private
617 *
618 * @param {function} callback
619 * @param {string} method
620 * @param {string} uri
621 * @param {object} queryParams
622 * @param {object|string} data
623 * @param {object=} opts
624 *
625 * @param {boolean} [opts.json =true] Json-encode data before sending, and
626 * decode response on receipt. (We will still json-decode error
627 * responses, even if this is false.)
628 *
629 * @param {object=} opts.headers extra request headers
630 *
631 * @param {number=} opts.localTimeoutMs client-side timeout for the
632 * request. Default timeout if falsy.
633 *
634 * @param {function=} opts.bodyParser function to parse the body of the
635 * response before passing it to the promise and callback.
636 *
637 * @return {Promise} a promise which resolves to either the
638 * response object (if this.opts.onlyData is truthy), or the parsed
639 * body. Rejects
640 */
641 _request: function(callback, method, uri, queryParams, data, opts) {
642 if (callback !== undefined && !utils.isFunction(callback)) {
643 throw Error(
644 "Expected callback to be a function but got " + typeof callback,
645 );
646 }
647 opts = opts || {};
648
649 const self = this;
650 if (this.opts.extraParams) {
651 for (const key in this.opts.extraParams) {
652 if (!this.opts.extraParams.hasOwnProperty(key)) {
653 continue;
654 }
655 queryParams[key] = this.opts.extraParams[key];
656 }
657 }
658
659 const headers = utils.extend({}, opts.headers || {});
660 const json = opts.json === undefined ? true : opts.json;
661 let bodyParser = opts.bodyParser;
662
663 // we handle the json encoding/decoding here, because request and
664 // browser-request make a mess of it. Specifically, they attempt to
665 // json-decode plain-text error responses, which in turn means that the
666 // actual error gets swallowed by a SyntaxError.
667
668 if (json) {
669 if (data) {
670 data = JSON.stringify(data);
671 headers['content-type'] = 'application/json';
672 }
673
674 if (!headers['accept']) {
675 headers['accept'] = 'application/json';
676 }
677
678 if (bodyParser === undefined) {
679 bodyParser = function(rawBody) {
680 return JSON.parse(rawBody);
681 };
682 }
683 }
684
685 const defer = utils.defer();
686
687 let timeoutId;
688 let timedOut = false;
689 let req;
690 const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
691
692 const resetTimeout = () => {
693 if (localTimeoutMs) {
694 if (timeoutId) {
695 callbacks.clearTimeout(timeoutId);
696 }
697 timeoutId = callbacks.setTimeout(function() {
698 timedOut = true;
699 if (req && req.abort) {
700 req.abort();
701 }
702 defer.reject(new MatrixError({
703 error: "Locally timed out waiting for a response",
704 errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
705 timeout: localTimeoutMs,
706 }));
707 }, localTimeoutMs);
708 }
709 };
710 resetTimeout();
711
712 const reqPromise = defer.promise;
713
714 try {
715 req = this.opts.request(
716 {
717 uri: uri,
718 method: method,
719 withCredentials: false,
720 qs: queryParams,
721 qsStringifyOptions: opts.qsStringifyOptions,
722 useQuerystring: true,
723 body: data,
724 json: false,
725 timeout: localTimeoutMs,
726 headers: headers || {},
727 _matrix_opts: this.opts,
728 },
729 function(err, response, body) {
730 if (localTimeoutMs) {
731 callbacks.clearTimeout(timeoutId);
732 if (timedOut) {
733 return; // already rejected promise
734 }
735 }
736
737 const handlerFn = requestCallback(
738 defer, callback, self.opts.onlyData,
739 bodyParser,
740 );
741 handlerFn(err, response, body);
742 },
743 );
744 if (req) {
745 // This will only work in a browser, where opts.request is the
746 // `browser-request` import. Currently `request` does not support progress
747 // updates - see https://github.com/request/request/pull/2346.
748 // `browser-request` returns an XHRHttpRequest which exposes `onprogress`
749 if ('onprogress' in req) {
750 req.onprogress = (e) => {
751 // Prevent the timeout from rejecting the deferred promise if progress is
752 // seen with the request
753 resetTimeout();
754 };
755 }
756
757 // FIXME: This is EVIL, but I can't think of a better way to expose
758 // abort() operations on underlying HTTP requests :(
759 if (req.abort) reqPromise.abort = req.abort.bind(req);
760 }
761 } catch (ex) {
762 defer.reject(ex);
763 if (callback) {
764 callback(ex);
765 }
766 }
767 return reqPromise;
768 },
769};
770
771/*
772 * Returns a callback that can be invoked by an HTTP request on completion,
773 * that will either resolve or reject the given defer as well as invoke the
774 * given userDefinedCallback (if any).
775 *
776 * HTTP errors are transformed into javascript errors and the deferred is rejected.
777 *
778 * If bodyParser is given, it is used to transform the body of the successful
779 * responses before passing to the defer/callback.
780 *
781 * If onlyData is true, the defer/callback is invoked with the body of the
782 * response, otherwise the result object (with `code` and `data` fields)
783 *
784 */
785const requestCallback = function(
786 defer, userDefinedCallback, onlyData,
787 bodyParser,
788) {
789 userDefinedCallback = userDefinedCallback || function() {};
790
791 return function(err, response, body) {
792 if (!err) {
793 try {
794 if (response.statusCode >= 400) {
795 err = parseErrorResponse(response, body);
796 } else if (bodyParser) {
797 body = bodyParser(body);
798 }
799 } catch (e) {
800 err = new Error(`Error parsing server response: ${e}`);
801 }
802 }
803
804 if (err) {
805 defer.reject(err);
806 userDefinedCallback(err);
807 } else {
808 const res = {
809 code: response.statusCode,
810
811 // XXX: why do we bother with this? it doesn't work for
812 // XMLHttpRequest, so clearly we don't use it.
813 headers: response.headers,
814 data: body,
815 };
816 defer.resolve(onlyData ? body : res);
817 userDefinedCallback(null, onlyData ? body : res);
818 }
819 };
820};
821
822/**
823 * Attempt to turn an HTTP error response into a Javascript Error.
824 *
825 * If it is a JSON response, we will parse it into a MatrixError. Otherwise
826 * we return a generic Error.
827 *
828 * @param {XMLHttpRequest|http.IncomingMessage} response response object
829 * @param {String} body raw body of the response
830 * @returns {Error}
831 */
832function parseErrorResponse(response, body) {
833 const httpStatus = response.statusCode;
834 const contentType = getResponseContentType(response);
835
836 let err;
837 if (contentType) {
838 if (contentType.type === 'application/json') {
839 const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
840 err = new MatrixError(jsonBody);
841 } else if (contentType.type === 'text/plain') {
842 err = new Error(`Server returned ${httpStatus} error: ${body}`);
843 }
844 }
845
846 if (!err) {
847 err = new Error(`Server returned ${httpStatus} error`);
848 }
849 err.httpStatus = httpStatus;
850 return err;
851}
852
853
854/**
855 * extract the Content-Type header from the response object, and
856 * parse it to a `{type, parameters}` object.
857 *
858 * returns null if no content-type header could be found.
859 *
860 * @param {XMLHttpRequest|http.IncomingMessage} response response object
861 * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
862 */
863function getResponseContentType(response) {
864 let contentType;
865 if (response.getResponseHeader) {
866 // XMLHttpRequest provides getResponseHeader
867 contentType = response.getResponseHeader("Content-Type");
868 } else if (response.headers) {
869 // request provides http.IncomingMessage which has a message.headers map
870 contentType = response.headers['content-type'] || null;
871 }
872
873 if (!contentType) {
874 return null;
875 }
876
877 try {
878 return parseContentType(contentType);
879 } catch(e) {
880 throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
881 }
882}
883
884/**
885 * Construct a Matrix error. This is a JavaScript Error with additional
886 * information specific to the standard Matrix error response.
887 * @constructor
888 * @param {Object} errorJson The Matrix error JSON returned from the homeserver.
889 * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
890 * @prop {string} name Same as MatrixError.errcode but with a default unknown string.
891 * @prop {string} message The Matrix 'error' value, e.g. "Missing token."
892 * @prop {Object} data The raw Matrix error JSON used to construct this object.
893 * @prop {integer} httpStatus The numeric HTTP status code given
894 */
895export function MatrixError(errorJson) {
896 errorJson = errorJson || {};
897 this.errcode = errorJson.errcode;
898 this.name = errorJson.errcode || "Unknown error code";
899 this.message = errorJson.error || "Unknown message";
900 this.data = errorJson;
901}
902MatrixError.prototype = Object.create(Error.prototype);
903MatrixError.prototype.constructor = MatrixError;