From d9fcadb72735700dfc7fdfb0437a785dfe4b0c10 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 11 May 2026 21:25:46 -0700 Subject: [PATCH] fix(host-service): accept unknown mediaType on attachment upload Browsers report empty File.type for unknown extensions (e.g. custom simulator extensions like .bmad), which was bouncing real uploads at the schema and at the mimeTypes.extension check. Fall back to application/octet-stream so the file lands; original filename is still preserved in metadata. --- .../router/attachments/attachments.test.ts | 30 ++++++++++++++----- .../trpc/router/attachments/attachments.ts | 15 +++++----- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/host-service/src/trpc/router/attachments/attachments.test.ts b/packages/host-service/src/trpc/router/attachments/attachments.test.ts index 5dc6ac0cec1..8b606ac8a10 100644 --- a/packages/host-service/src/trpc/router/attachments/attachments.test.ts +++ b/packages/host-service/src/trpc/router/attachments/attachments.test.ts @@ -84,14 +84,30 @@ describe("attachmentsRouter.upload", () => { } }); - it("rejects unrecognized media type", async () => { + it("falls back to application/octet-stream for unrecognized media type", async () => { const caller = createCaller(); - await expect( - caller.upload({ - data: { kind: "base64", data: PNG_BASE64 }, - mediaType: "application/x-totally-fake", - }), - ).rejects.toThrow(/unrecognized media type/i); + const result = await caller.upload({ + data: { kind: "base64", data: PNG_BASE64 }, + mediaType: "application/x-totally-fake", + originalFilename: "racetrack.bmad", + }); + expect(result.mediaType).toBe("application/octet-stream"); + const filePath = getAttachmentFilePath( + result.attachmentId, + "application/octet-stream", + ); + expect(filePath).toMatch(/\.bin$/); + expect(existsSync(filePath)).toBe(true); + }); + + it("falls back to application/octet-stream for empty media type", async () => { + const caller = createCaller(); + const result = await caller.upload({ + data: { kind: "base64", data: PNG_BASE64 }, + mediaType: "", + originalFilename: "racetrack.bmad", + }); + expect(result.mediaType).toBe("application/octet-stream"); }); it("accepts a single decoded byte", async () => { diff --git a/packages/host-service/src/trpc/router/attachments/attachments.ts b/packages/host-service/src/trpc/router/attachments/attachments.ts index 59414349050..2b01619aed1 100644 --- a/packages/host-service/src/trpc/router/attachments/attachments.ts +++ b/packages/host-service/src/trpc/router/attachments/attachments.ts @@ -15,10 +15,12 @@ const uploadInputSchema = z.object({ kind: z.literal("base64"), data: z.string().min(1), }), - mediaType: z.string().min(1), + mediaType: z.string(), originalFilename: z.string().optional(), }); +const FALLBACK_MEDIA_TYPE = "application/octet-stream"; + /** * Cheap size estimate from a base64 string without allocating the * decoded buffer. Used to reject oversized uploads before Buffer.from @@ -36,12 +38,9 @@ export const attachmentsRouter = router({ * renderer never sees the on-disk path. */ upload: protectedProcedure.input(uploadInputSchema).mutation(({ input }) => { - if (!mimeTypes.extension(input.mediaType)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Unrecognized media type: ${input.mediaType}`, - }); - } + const mediaType = mimeTypes.extension(input.mediaType) + ? input.mediaType + : FALLBACK_MEDIA_TYPE; // Reject before allocating the decoded buffer so a 1GB base64 // payload doesn't spike host memory only to be rejected at the end. @@ -65,7 +64,7 @@ export const attachmentsRouter = router({ const metadata: AttachmentMetadata = { attachmentId: randomUUID(), - mediaType: input.mediaType, + mediaType, originalFilename: input.originalFilename, sizeBytes: bytes.length, createdAt: Date.now(),