-
-
Notifications
You must be signed in to change notification settings - Fork 616
WIP: Fetch support #2166
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
WIP: Fetch support #2166
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,6 +161,8 @@ import { IThreepid } from "./@types/threepids"; | |
import { CryptoStore } from "./crypto/store/base"; | ||
import { MediaHandler } from "./webrtc/mediaHandler"; | ||
|
||
import fetch from 'cross-fetch'; | ||
|
||
export type Store = IStore; | ||
export type SessionStore = WebStorageSessionStore; | ||
|
||
|
@@ -212,12 +214,10 @@ export interface ICreateClientOpts { | |
scheduler?: MatrixScheduler; | ||
|
||
/** | ||
* The function to invoke for HTTP | ||
* requests. The value of this property is typically <code>require("request") | ||
* </code> as it returns a function which meets the required interface. See | ||
* {@link requestFunction} for more information. | ||
* The function to invoke for HTTP requests. | ||
* See {@link fetch} for more information. | ||
*/ | ||
request?: IHttpOpts["request"]; | ||
fetch?: typeof fetch; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think maybe instead of depending on cross-fetch we could just have the caller pass their favourite fetch, as for typing a shim in |
||
|
||
userId?: string; | ||
|
||
|
@@ -845,7 +845,7 @@ export class MatrixClient extends EventEmitter { | |
baseUrl: opts.baseUrl, | ||
idBaseUrl: opts.idBaseUrl, | ||
accessToken: opts.accessToken, | ||
request: opts.request, | ||
fetch: opts.fetch, | ||
prefix: PREFIX_R0, | ||
onlyData: true, | ||
extraParams: opts.queryParams, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,6 @@ import { parse as parseContentType, ParsedMediaType } from "content-type"; | |
import EventEmitter from "events"; | ||
|
||
import type { IncomingHttpHeaders, IncomingMessage } from "http"; | ||
import type { Request as _Request, CoreOptions } from "request"; | ||
// we use our own implementation of setTimeout, so that if we get suspended in | ||
// the middle of a /sync, we cancel the sync as soon as we awake, rather than | ||
// waiting for the delay to elapse. | ||
|
@@ -35,6 +34,7 @@ import { IDeferred } from "./utils"; | |
import { Callback } from "./client"; | ||
import * as utils from "./utils"; | ||
import { logger } from './logger'; | ||
const queryString = require('qs'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imports not requires please |
||
|
||
/* | ||
TODO: | ||
|
@@ -73,16 +73,6 @@ export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2"; | |
*/ | ||
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0"; | ||
|
||
type RequestProps = "method" | ||
| "withCredentials" | ||
| "json" | ||
| "headers" | ||
| "qs" | ||
| "body" | ||
| "qsStringifyOptions" | ||
| "useQuerystring" | ||
| "timeout"; | ||
|
||
export interface IHttpOpts { | ||
baseUrl: string; | ||
idBaseUrl?: string; | ||
|
@@ -92,24 +82,12 @@ export interface IHttpOpts { | |
extraParams?: Record<string, string>; | ||
localTimeoutMs?: number; | ||
useAuthorizationHeader?: boolean; | ||
request(opts: Pick<CoreOptions, RequestProps> & { | ||
uri: string; | ||
method: Method; | ||
// eslint-disable-next-line camelcase | ||
_matrix_opts: IHttpOpts; | ||
}, callback: RequestCallback): IRequest; | ||
} | ||
|
||
interface IRequest extends _Request { | ||
onprogress?(e: unknown): void; | ||
fetch: typeof fetch; | ||
} | ||
|
||
interface IRequestOpts<T> { | ||
prefix?: string; | ||
localTimeoutMs?: number; | ||
headers?: Record<string, string>; | ||
json?: boolean; // defaults to true | ||
qsStringifyOptions?: CoreOptions["qsStringifyOptions"]; | ||
bodyParser?(body: string): T; | ||
} | ||
|
||
|
@@ -333,7 +311,7 @@ export class MatrixHttpApi { | |
// (browser-request doesn't support progress either, which is also kind | ||
// of important here) | ||
|
||
const upload = { loaded: 0, total: 0 } as IUpload; | ||
const upload = {loaded: 0, total: 0} as IUpload; | ||
let promise: IAbortablePromise<UploadContentResponseType<O>>; | ||
|
||
// XMLHttpRequest doesn't parse JSON for us. request normally does, but | ||
|
@@ -342,7 +320,7 @@ export class MatrixHttpApi { | |
// way, we have to JSON-parse the response ourselves. | ||
let bodyParser = null; | ||
if (!rawResponse) { | ||
bodyParser = function(rawBody: string) { | ||
bodyParser = function (rawBody: string) { | ||
let body = JSON.parse(rawBody); | ||
if (onlyContentUri) { | ||
body = body.content_uri; | ||
|
@@ -359,15 +337,15 @@ export class MatrixHttpApi { | |
const xhr = new global.XMLHttpRequest(); | ||
const cb = requestCallback(defer, opts.callback, this.opts.onlyData); | ||
|
||
const timeoutFn = function() { | ||
const timeoutFn = function () { | ||
xhr.abort(); | ||
cb(new Error('Timeout')); | ||
}; | ||
|
||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification | ||
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); | ||
|
||
xhr.onreadystatechange = function() { | ||
xhr.onreadystatechange = function () { | ||
let resp: string; | ||
switch (xhr.readyState) { | ||
case global.XMLHttpRequest.DONE: | ||
|
@@ -392,7 +370,7 @@ export class MatrixHttpApi { | |
break; | ||
} | ||
}; | ||
xhr.upload.addEventListener("progress", function(ev) { | ||
xhr.upload.addEventListener("progress", function (ev) { | ||
callbacks.clearTimeout(timeoutTimer); | ||
upload.loaded = ev.loaded; | ||
upload.total = ev.total; | ||
|
@@ -440,7 +418,7 @@ export class MatrixHttpApi { | |
promise = this.authedRequest( | ||
opts.callback, Method.Post, "/upload", queryParams, body, { | ||
prefix: "/_matrix/media/r0", | ||
headers: { "Content-Type": contentType }, | ||
headers: {"Content-Type": contentType}, | ||
json: false, | ||
bodyParser, | ||
}, | ||
|
@@ -497,26 +475,21 @@ export class MatrixHttpApi { | |
} | ||
|
||
const opts = { | ||
uri: fullUri, | ||
method, | ||
withCredentials: false, | ||
json: true, // we want a JSON response if we can | ||
_matrix_opts: this.opts, | ||
headers: {}, | ||
} as Parameters<IHttpOpts["request"]>[0]; | ||
|
||
if (method === Method.Get) { | ||
opts.qs = params; | ||
} else if (typeof params === "object") { | ||
opts.json = params; | ||
} | ||
|
||
if (accessToken) { | ||
opts.headers['Authorization'] = `Bearer ${accessToken}`; | ||
} | ||
headers: accessToken | ||
? {Authorization: `Bearer ${accessToken}`} | ||
: {}, | ||
body: (method !== Method.Get) | ||
? JSON.stringify(params) | ||
: null, | ||
} as RequestInfo; | ||
|
||
const defer = utils.defer<T>(); | ||
this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData)); | ||
const url = (method === Method.Get) | ||
? fullUri + queryString.stringify(params) | ||
: fullUri; | ||
this.requestOtherUrl(callback, method, url, null, null, opts); | ||
return defer.promise; | ||
} | ||
|
||
|
@@ -782,7 +755,7 @@ export class MatrixHttpApi { | |
} | ||
|
||
if (bodyParser === undefined) { | ||
bodyParser = function(rawBody: string) { | ||
bodyParser = function (rawBody: string) { | ||
return JSON.parse(rawBody); | ||
}; | ||
} | ||
|
@@ -792,17 +765,15 @@ export class MatrixHttpApi { | |
|
||
let timeoutId: number; | ||
let timedOut = false; | ||
let req: IRequest; | ||
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs; | ||
|
||
const resetTimeout = () => { | ||
if (localTimeoutMs) { | ||
if (timeoutId) { | ||
callbacks.clearTimeout(timeoutId); | ||
} | ||
timeoutId = callbacks.setTimeout(function() { | ||
timeoutId = callbacks.setTimeout(function () { | ||
timedOut = true; | ||
req?.abort?.(); | ||
defer.reject(new MatrixError({ | ||
error: "Locally timed out waiting for a response", | ||
errcode: "ORG.MATRIX.JSSDK_TIMEOUT", | ||
|
@@ -815,58 +786,38 @@ export class MatrixHttpApi { | |
|
||
const reqPromise = defer.promise as IAbortablePromise<ResponseType<T, O>>; | ||
|
||
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, | ||
}, | ||
(err, response, body) => { | ||
if (localTimeoutMs) { | ||
callbacks.clearTimeout(timeoutId); | ||
if (timedOut) { | ||
return; // already rejected promise | ||
} | ||
} | ||
|
||
const handlerFn = requestCallback(defer, callback, this.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); | ||
const qs = queryString.stringify(queryParams || {}, opts.qsStringifyOptions); | ||
let res; | ||
const controller = new AbortController(); | ||
const signal = controller.signal; | ||
this.opts.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 | ||
} | ||
} | ||
} catch (ex) { | ||
defer.reject(ex); | ||
if (callback) { | ||
callback(ex); | ||
} | ||
} | ||
return res.json(); | ||
}).then((body) => { | ||
const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser); | ||
handlerFn(null, res, body.toString()); | ||
}).catch((ex) => { | ||
const handlerFn = requestCallback( | ||
defer, callback, this.opts.onlyData, | ||
); | ||
handlerFn(ex, null, null); | ||
}); | ||
reqPromise.abort = () => { | ||
controller.abort(); | ||
}; | ||
return reqPromise; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth investigating but this package uses whatwg-fetch (browser polyfill) which may create downstream issues in Element Web inflating bundle size with a polyfill which we never care about as all compatible browsers already support fetch.