Skip to content

Commit 32a0c1a

Browse files
committed
fetch api: add support for downloading raw response
I need to make an authenticated request to the media repo, and expect to get a binary file back. AFAICT there is no easy way to do that right now.
1 parent 443f057 commit 32a0c1a

File tree

3 files changed

+82
-13
lines changed

3 files changed

+82
-13
lines changed

spec/unit/http-api/fetch.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,29 @@ describe("FetchHttpApi", () => {
131131
).resolves.toBe(text);
132132
});
133133

134+
it("should return a blob if rawResponseBody is true", async () => {
135+
const blob = new Blob(["blobby"]);
136+
const fetchFn = jest.fn().mockResolvedValue({ ok: true, blob: jest.fn().mockResolvedValue(blob) });
137+
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
138+
await expect(
139+
api.requestOtherUrl(Method.Get, "http://url", undefined, {
140+
rawResponseBody: true,
141+
}),
142+
).resolves.toBe(blob);
143+
});
144+
145+
it("should throw an error if both `json` and `rawResponseBody` are defined", async () => {
146+
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
147+
baseUrl,
148+
prefix,
149+
fetchFn: jest.fn(),
150+
onlyData: true,
151+
});
152+
await expect(
153+
api.requestOtherUrl(Method.Get, "http://url", undefined, { rawResponseBody: false, json: true }),
154+
).rejects.toThrow("Invalid call to `FetchHttpApi`");
155+
});
156+
134157
it("should send token via query params if useAuthorizationHeader=false", async () => {
135158
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
136159
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {

src/http-api/fetch.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,18 @@ export class FetchHttpApi<O extends IHttpOpts> {
266266
body?: Body,
267267
opts: BaseRequestOpts = {},
268268
): Promise<ResponseType<T, O>> {
269+
if (opts.json !== undefined && opts.rawResponseBody !== undefined) {
270+
throw new Error("Invalid call to `FetchHttpApi` sets both `opts.json` and `opts.rawResponseBody`");
271+
}
272+
269273
const urlForLogs = this.sanitizeUrlForLogs(url);
274+
270275
this.opts.logger?.debug(`FetchHttpApi: --> ${method} ${urlForLogs}`);
271276

272277
const headers = Object.assign({}, opts.headers || {});
273-
const json = opts.json ?? true;
274-
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
275-
const jsonBody = json && body?.constructor?.name === Object.name;
276-
277-
if (json) {
278-
if (jsonBody && !headers["Content-Type"]) {
279-
headers["Content-Type"] = "application/json";
280-
}
281278

279+
const jsonResponse = opts.json !== false;
280+
if (jsonResponse) {
282281
if (!headers["Accept"]) {
283282
headers["Accept"] = "application/json";
284283
}
@@ -294,9 +293,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
294293
signals.push(opts.abortSignal);
295294
}
296295

296+
// If the body is an object, encode it as JSON and set the `Content-Type` header,
297+
// unless that has been explicitly inhibited by setting `opts.json: false`.
298+
// We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref
297299
let data: BodyInit;
298-
if (jsonBody) {
300+
if (opts.json !== false && body?.constructor?.name === Object.name) {
299301
data = JSON.stringify(body);
302+
if (!headers["Content-Type"]) {
303+
headers["Content-Type"] = "application/json";
304+
}
300305
} else {
301306
data = body as BodyInit;
302307
}
@@ -338,10 +343,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
338343
throw parseErrorResponse(res, await res.text());
339344
}
340345

341-
if (this.opts.onlyData) {
342-
return (json ? res.json() : res.text()) as ResponseType<T, O>;
346+
if (!this.opts.onlyData) {
347+
return res as ResponseType<T, O>;
348+
} else if (opts.rawResponseBody) {
349+
return (await res.blob()) as ResponseType<T, O>;
350+
} else if (jsonResponse) {
351+
return await res.json();
352+
} else {
353+
return (await res.text()) as ResponseType<T, O>;
343354
}
344-
return res as ResponseType<T, O>;
345355
}
346356

347357
private sanitizeUrlForLogs(url: URL | string): string {

src/http-api/interface.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type AccessTokens = {
4747
* Can be passed to HttpApi instance as {@link IHttpOpts.tokenRefreshFunction} during client creation {@link ICreateClientOpts}
4848
*/
4949
export type TokenRefreshFunction = (refreshToken: string) => Promise<AccessTokens>;
50+
51+
/** Options object for `FetchHttpApi` and {@link MatrixHttpApi}. */
5052
export interface IHttpOpts {
5153
fetchFn?: typeof globalThis.fetch;
5254

@@ -67,7 +69,12 @@ export interface IHttpOpts {
6769
tokenRefreshFunction?: TokenRefreshFunction;
6870
useAuthorizationHeader?: boolean; // defaults to true
6971

72+
/**
73+
* Normally, methods in `FetchHttpApi` will return a {@link https://developer.mozilla.org/en-US/docs/Web/API/Response|Response} object.
74+
* If this is set to `true`, they instead return the response body.
75+
*/
7076
onlyData?: boolean;
77+
7178
localTimeoutMs?: number;
7279

7380
/** Optional logger instance. If provided, requests and responses will be logged. */
@@ -87,7 +94,36 @@ export interface BaseRequestOpts extends Pick<RequestInit, "priority"> {
8794
*/
8895
localTimeoutMs?: number;
8996
keepAlive?: boolean; // defaults to false
90-
json?: boolean; // defaults to true
97+
98+
/**
99+
* By default, we will:
100+
*
101+
* * If the `body` is an object, we will JSON-encode it and set `Content-Type: application/json` in the
102+
* request headers (again, unless overridden by {@link headers}).
103+
*
104+
* * Set `Accept: application/json` in the request headers (unless overridden by {@link headers}).
105+
*
106+
* * If `IHTTPOpts.onlyData` is set to `true` on the `FetchHttpApi` instance, parse the response as
107+
* JSON and return the parsed response.
108+
*
109+
* Setting this to `false` overrides inhibits all three behaviors, and the response is instead parsed as a UTF-8
110+
* string. It defaults to `true`, unless {@link rawResponseBody} is set.
111+
*
112+
* @deprecated Instead of setting this to `false`, set {@link rawResponseBody} to `true`.
113+
*/
114+
json?: boolean;
115+
116+
/**
117+
* By default, we will:
118+
*
119+
* * Set `Accept: application/json` in the request headers (unless overridden by {@link headers}).
120+
*
121+
* * If `IHTTPOpts.onlyData` is set to `true` on the `FetchHttpApi` instance, parse the response as
122+
* JSON and return the parsed response.
123+
*
124+
* Setting this to `true` overrides inhibits this behavior, and the raw response is returned as a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob|Blob}.
125+
*/
126+
rawResponseBody?: boolean;
91127
}
92128

93129
export interface IRequestOpts extends BaseRequestOpts {

0 commit comments

Comments
 (0)