From 8e193c422885a929cbf1d2f6010ac3fd6b6bcfa7 Mon Sep 17 00:00:00 2001 From: JD Date: Tue, 13 Oct 2026 20:34:56 -0400 Subject: [PATCH] fix(uploads): refuse an unsupported video before uploading it _promptAttachKind returned 'video' for any video/* MIME, so an Android 3gp or an mkv streamed up to 1GB before the ?kind=video route refused it with a 400. Mirror the server's own video-container set so those are filtered client-side and get the existing unsupported-file toast at once. Follows the maintainer's follow-up suggestion on #606. --- src/web/public/image-input.js | 12 ++++++++---- test/toolbar-attach.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/web/public/image-input.js b/src/web/public/image-input.js index b9f07e7d..c5f293c3 100644 --- a/src/web/public/image-input.js +++ b/src/web/public/image-input.js @@ -269,12 +269,16 @@ Object.assign(CodemanApp.prototype, { /** What a pick or a paste may upload: 'image', or 'video' for the camera * roll's clips (the server streams those to disk under their own cap), else * null. The one filter behind Attach, drop, the Compose and paste-dialog - * pickers and the clipboard handlers. The MIME type decides; some Android - * file managers hand over a File with none, so the name's extension is the - * fallback, mirroring the server's own allowlists. */ + * pickers and the clipboard handlers. The MIME type decides, but a video MIME + * only for the containers the server admits; some Android file managers hand + * over a File with none, so the name's extension is the fallback, mirroring + * the server's own allowlists. */ _promptAttachKind(file) { if (file.type.startsWith('image/')) return 'image'; - if (file.type.startsWith('video/')) return 'video'; + // A video MIME counts only for the five containers the server accepts on + // ?kind=video (its own set in session-routes.ts); a 3gp or mkv falls to the + // extension check below and is refused up front, not after a 1GB upload and a 400. + if (/^video\/(mp4|quicktime|x-m4v|webm|ogg)$/i.test(file.type)) return 'video'; const ext = (file.name || '').toLowerCase().match(/\.([a-z0-9]+)$/)?.[1] || ''; if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'heic', 'heif'].includes(ext)) return 'image'; if (['mp4', 'webm', 'mov', 'm4v', 'ogv'].includes(ext)) return 'video'; diff --git a/test/toolbar-attach.test.ts b/test/toolbar-attach.test.ts index b69d4c75..e4ffc690 100644 --- a/test/toolbar-attach.test.ts +++ b/test/toolbar-attach.test.ts @@ -352,6 +352,21 @@ describe('phone toolbar Attach button', () => { expect(kind('clip.ogv', 'image/jpeg')).toBe('image'); // the MIME type wins when present }); + it('counts a video MIME only for the containers the server admits on ?kind=video', () => { + const { app, window } = loadToolbar(); + const kind = (type: string, name = 'clip') => app._promptAttachKind(new window.File(['x'], name, { type })); + // The five the server's ?kind=video branch accepts (session-routes.ts). + for (const type of ['video/mp4', 'video/quicktime', 'video/x-m4v', 'video/webm', 'video/ogg']) { + expect(kind(type)).toBe('video'); + } + // A video MIME outside that set is refused client-side (no full upload then a + // 400) when the name's extension is not one the server admits either. + expect(kind('video/3gpp', 'clip.3gp')).toBeNull(); + expect(kind('video/x-matroska', 'clip.mkv')).toBeNull(); + // An admitted extension still rescues an odd MIME, as the server's path does. + expect(kind('video/3gpp', 'clip.mp4')).toBe('video'); + }); + it('flags the upload as a video by name as well as by MIME type', async () => { const { app, window, fetchMock } = loadToolbar(); await app._uploadPasteImage('session-1', new window.File(['v'], 'clip.ogv', { type: '' }));