diff --git a/.server-changes/transcript-download-media-type-case.md b/.server-changes/transcript-download-media-type-case.md new file mode 100644 index 00000000000..98ededeb7fd --- /dev/null +++ b/.server-changes/transcript-download-media-type-case.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Chat transcript downloads keep the .jsonl filename when the stored media type differs only in letter case. diff --git a/apps/webapp/app/services/realtime/transcriptDownload.server.test.ts b/apps/webapp/app/services/realtime/transcriptDownload.server.test.ts index 6768b6eb0f1..c4f60960aab 100644 --- a/apps/webapp/app/services/realtime/transcriptDownload.server.test.ts +++ b/apps/webapp/app/services/realtime/transcriptDownload.server.test.ts @@ -103,6 +103,22 @@ minioTest( } ); +test("uses a .jsonl filename for transcript media types regardless of case", () => { + for (const contentType of [ + TRANSCRIPT_BLOB_CONTENT_TYPE.toUpperCase(), + "Application/Vnd.Trigger.Transcript+Ndjson; charset=utf-8", + ]) { + const object = new Response('{"version":2}\n', { + headers: { "Content-Type": contentType }, + }); + const response = downloadTranscript(object, "packets/download/snapshot.json"); + expect(response.headers.get("Content-Disposition")).toBe( + 'attachment; filename="snapshot.jsonl"' + ); + expect(response.headers.get("Content-Type")).toBe(contentType); + } +}); + test("does not treat permission or service failures as an absent transcript", () => { expect(isTranscriptNotFound(new Error("Failed to download from object store: Forbidden"))).toBe( false diff --git a/apps/webapp/app/services/realtime/transcriptDownload.server.ts b/apps/webapp/app/services/realtime/transcriptDownload.server.ts index da477a9ca5a..cee932b690b 100644 --- a/apps/webapp/app/services/realtime/transcriptDownload.server.ts +++ b/apps/webapp/app/services/realtime/transcriptDownload.server.ts @@ -9,8 +9,9 @@ export function downloadTranscript( ): Response { const storedFilename = basename(storagePath).replace(/[^a-zA-Z0-9._-]/g, "_") || "transcript"; const contentType = object.headers.get("Content-Type") ?? "application/octet-stream"; + // Media type and subtype are case-insensitive (RFC 9110). Parameters after ";" are ignored. const filename = - contentType.split(";")[0]?.trim() === TRANSCRIPT_BLOB_CONTENT_TYPE + contentType.split(";")[0]?.trim().toLowerCase() === TRANSCRIPT_BLOB_CONTENT_TYPE ? `${basename(storedFilename, extname(storedFilename))}.jsonl` : storedFilename; // Errors after the route returns must still be logged and reach the HTTP adapter,