Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit f3ac669

Browse files
authored
Handle media download errors better (#12848)
* Handle media download errors better Signed-off-by: Michael Telatynski <[email protected]> * Add test Signed-off-by: Michael Telatynski <[email protected]> * Show error if media download failed Signed-off-by: Michael Telatynski <[email protected]> * More tests Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent b55653d commit f3ac669

File tree

6 files changed

+140
-8
lines changed

6 files changed

+140
-8
lines changed

src/components/views/messages/DownloadActionButton.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
2424
import Spinner from "../elements/Spinner";
2525
import { _t, _td, TranslationKey } from "../../../languageHandler";
2626
import { FileDownloader } from "../../../utils/FileDownloader";
27+
import Modal from "../../../Modal";
28+
import ErrorDialog from "../dialogs/ErrorDialog";
2729

2830
interface IProps {
2931
mxEvent: MatrixEvent;
@@ -53,6 +55,23 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
5355
}
5456

5557
private onDownloadClick = async (): Promise<void> => {
58+
try {
59+
await this.doDownload();
60+
} catch (e) {
61+
Modal.createDialog(ErrorDialog, {
62+
title: _t("timeline|download_failed"),
63+
description: (
64+
<>
65+
<div>{_t("timeline|download_failed_description")}</div>
66+
<div>{e instanceof Error ? e.toString() : ""}</div>
67+
</>
68+
),
69+
});
70+
this.setState({ loading: false });
71+
}
72+
};
73+
74+
private async doDownload(): Promise<void> {
5675
const mediaEventHelper = this.props.mediaEventHelperGet();
5776
if (this.state.loading || !mediaEventHelper) return;
5877

@@ -64,15 +83,15 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
6483

6584
if (this.state.blob) {
6685
// Cheat and trigger a download, again.
67-
return this.doDownload(this.state.blob);
86+
return this.downloadBlob(this.state.blob);
6887
}
6988

7089
const blob = await mediaEventHelper.sourceBlob.value;
7190
this.setState({ blob });
72-
await this.doDownload(blob);
73-
};
91+
await this.downloadBlob(blob);
92+
}
7493

75-
private async doDownload(blob: Blob): Promise<void> {
94+
private async downloadBlob(blob: Blob): Promise<void> {
7695
await this.downloader.download({
7796
blob,
7897
name: this.props.mediaEventHelperGet()!.fileName,

src/customisations/Media.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { MatrixClient, ResizeMethod } from "matrix-js-sdk/src/matrix";
17+
import { MatrixClient, parseErrorResponse, ResizeMethod } from "matrix-js-sdk/src/matrix";
1818
import { MediaEventContent } from "matrix-js-sdk/src/types";
1919
import { Optional } from "matrix-events-sdk";
2020

@@ -144,12 +144,16 @@ export class Media {
144144
* Downloads the source media.
145145
* @returns {Promise<Response>} Resolves to the server's response for chaining.
146146
*/
147-
public downloadSource(): Promise<Response> {
147+
public async downloadSource(): Promise<Response> {
148148
const src = this.srcHttp;
149149
if (!src) {
150150
throw new UserFriendlyError("error|download_media");
151151
}
152-
return fetch(src);
152+
const res = await fetch(src);
153+
if (!res.ok) {
154+
throw parseErrorResponse(res, await res.text());
155+
}
156+
return res;
153157
}
154158
}
155159

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3264,6 +3264,8 @@
32643264
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
32653265
"download_action_decrypting": "Decrypting",
32663266
"download_action_downloading": "Downloading",
3267+
"download_failed": "Download failed",
3268+
"download_failed_description": "An error occurred while downloading this file",
32673269
"edits": {
32683270
"tooltip_label": "Edited at %(date)s. Click to view edits.",
32693271
"tooltip_sub": "Click to view edits",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
import { mocked } from "jest-mock";
19+
import fetchMockJest from "fetch-mock-jest";
20+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
21+
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
22+
23+
import { stubClient } from "../../../test-utils";
24+
import DownloadActionButton from "../../../../src/components/views/messages/DownloadActionButton";
25+
import Modal from "../../../../src/Modal";
26+
import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper";
27+
import ErrorDialog from "../../../../src/components/views/dialogs/ErrorDialog";
28+
29+
describe("DownloadActionButton", () => {
30+
it("should show error if media API returns one", async () => {
31+
const cli = stubClient();
32+
// eslint-disable-next-line no-restricted-properties
33+
mocked(cli.mxcUrlToHttp).mockImplementation(
34+
(mxc) => `https://matrix.org/_matrix/media/r0/download/${mxc.slice(6)}`,
35+
);
36+
37+
fetchMockJest.get("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", {
38+
status: 404,
39+
body: { errcode: "M_NOT_FOUND", error: "Not found" },
40+
});
41+
42+
const event = new MatrixEvent({
43+
room_id: "!room:id",
44+
sender: "@user:id",
45+
type: "m.room.message",
46+
content: {
47+
body: "test",
48+
msgtype: "m.image",
49+
url: "mxc://matrix.org/1234",
50+
},
51+
});
52+
const mediaEventHelper = new MediaEventHelper(event);
53+
54+
render(<DownloadActionButton mxEvent={event} mediaEventHelperGet={() => mediaEventHelper} />);
55+
56+
const spy = jest.spyOn(Modal, "createDialog");
57+
58+
fireEvent.click(screen.getByRole("button"));
59+
await waitFor(() =>
60+
expect(spy).toHaveBeenCalledWith(
61+
ErrorDialog,
62+
expect.objectContaining({
63+
title: "Download failed",
64+
}),
65+
),
66+
);
67+
});
68+
});

test/customisations/Media-test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import fetchMockJest from "fetch-mock-jest";
18+
import { mocked } from "jest-mock";
19+
20+
import { mediaFromMxc } from "../../src/customisations/Media";
21+
import { stubClient } from "../test-utils";
22+
23+
describe("Media", () => {
24+
it("should not download error if server returns one", async () => {
25+
const cli = stubClient();
26+
// eslint-disable-next-line no-restricted-properties
27+
mocked(cli.mxcUrlToHttp).mockImplementation(
28+
(mxc) => `https://matrix.org/_matrix/media/r0/download/${mxc.slice(6)}`,
29+
);
30+
31+
fetchMockJest.get("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", {
32+
status: 404,
33+
body: { errcode: "M_NOT_FOUND", error: "Not found" },
34+
});
35+
36+
const media = mediaFromMxc("mxc://matrix.org/1234");
37+
await expect(media.downloadSource()).rejects.toThrow("Not found");
38+
});
39+
});

test/test-utils/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function createTestClient(): MatrixClient {
172172
content: {},
173173
});
174174
}),
175-
mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`,
175+
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
176176
scheduleAllGroupSessionsForBackup: jest.fn().mockResolvedValue(undefined),
177177
setAccountData: jest.fn(),
178178
setRoomAccountData: jest.fn(),

0 commit comments

Comments
 (0)