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(),