From 33d3df24f925c01eb0254e393140501f18f357cf Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Fri, 12 Sep 2025 11:28:40 +0100 Subject: [PATCH] Fix handling of 413 server response when uploading media (#30737) --- src/ContentMessages.ts | 18 ++++++++++++----- test/unit-tests/ContentMessages-test.ts | 26 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 54aaea3ae1..e608b3470a 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -63,7 +63,12 @@ import { blobIsAnimated } from "./utils/Image.ts"; const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01]; export class UploadCanceledError extends Error {} -export class UploadFailedError extends Error {} +export class UploadFailedError extends Error { + public constructor(cause: any) { + super(); + this.cause = cause; + } +} interface IMediaConfig { "m.upload.size"?: number; @@ -367,7 +372,7 @@ export async function uploadFile( } catch (e) { if (abortController.signal.aborted) throw new UploadCanceledError(); console.error("Failed to upload file", e); - throw new UploadFailedError(); + throw new UploadFailedError(e); } if (abortController.signal.aborted) throw new UploadCanceledError(); @@ -386,7 +391,7 @@ export async function uploadFile( } catch (e) { if (abortController.signal.aborted) throw new UploadCanceledError(); console.error("Failed to upload file", e); - throw new UploadFailedError(); + throw new UploadFailedError(e); } if (abortController.signal.aborted) throw new UploadCanceledError(); // If the attachment isn't encrypted then include the URL directly. @@ -638,15 +643,18 @@ export default class ContentMessages { dis.dispatch({ action: Action.UploadFinished, upload }); dis.dispatch({ action: "message_sent" }); } catch (error) { + // Unwrap UploadFailedError to get the underlying error + const unwrappedError = error instanceof UploadFailedError && error.cause ? error.cause : error; + // 413: File was too big or upset the server in some way: // clear the media size limit so we fetch it again next time we try to upload - if (error instanceof HTTPError && error.httpStatus === 413) { + if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) { this.mediaConfig = null; } if (!upload.cancelled) { let desc = _t("upload_failed_generic", { fileName: upload.fileName }); - if (error instanceof HTTPError && error.httpStatus === 413) { + if (unwrappedError instanceof HTTPError && unwrappedError.httpStatus === 413) { desc = _t("upload_failed_size", { fileName: upload.fileName, }); diff --git a/test/unit-tests/ContentMessages-test.ts b/test/unit-tests/ContentMessages-test.ts index 80b400568e..51bb73fbfd 100644 --- a/test/unit-tests/ContentMessages-test.ts +++ b/test/unit-tests/ContentMessages-test.ts @@ -10,6 +10,7 @@ import { mocked } from "jest-mock"; import { type ISendEventResponse, type MatrixClient, + MatrixError, RelationType, type UploadResponse, } from "matrix-js-sdk/src/matrix"; @@ -20,6 +21,9 @@ import ContentMessages, { UploadCanceledError, uploadFile } from "../../src/Cont import { doMaybeLocalRoomAction } from "../../src/utils/local-room"; import { createTestClient, flushPromises, mkEvent } from "../test-utils"; import { BlurhashEncoder } from "../../src/BlurhashEncoder"; +import Modal from "../../src/Modal"; +import ErrorDialog from "../../src/components/views/dialogs/ErrorDialog"; +import { _t } from "../../src/languageHandler"; jest.mock("matrix-encrypt-attachment", () => ({ encryptAttachment: jest.fn().mockResolvedValue({}) })); @@ -291,6 +295,28 @@ describe("ContentMessages", () => { }), ); }); + + it("handles 413 error", async () => { + mocked(client.uploadContent).mockRejectedValue( + new MatrixError( + { + errcode: "M_TOO_LARGE", + error: "File size limit exceeded", + }, + 413, + ), + ); + const file = new File([], "fileName", { type: "image/jpeg" }); + const dialogSpy = jest.spyOn(Modal, "createDialog"); + await contentMessages.sendContentToRoom(file, roomId, undefined, client, undefined); + expect(dialogSpy).toHaveBeenCalledWith( + ErrorDialog, + expect.objectContaining({ + description: _t("upload_failed_size", { fileName: "fileName" }), + }), + ); + dialogSpy.mockRestore(); + }); }); describe("getCurrentUploads", () => {