mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 10:02:41 +00:00
🧹 fix: Normalize MIME parameters; add legacy CSV MIME variant
Two related Codex catches on PR #12934 — both about MIME-routing inconsistencies between backend and client that would cause extensionless CSV files to render as broken (raw text under an HTML slot) or skip the artifact panel entirely. P2 — backend MIME normalization: `officeHtmlBucket` matched MIME strings exactly, so a real-world `text/csv; charset=utf-8` Content-Type slipped through and the backend returned raw CSV text. The client's `baseMime` helper strips parameters before its own MIME lookup, so it routed the same file to the SPREADSHEET bucket expecting an HTML body that never arrived. Mirror the client's normalization on the backend (strip everything from `;` onward, lowercase) before bucket matching. P3 — client legacy CSV MIME: Backend's `CSV_MIME_PATTERN` accepts three variants (`text/csv`, `application/csv`, `text/comma-separated-values`); the client's `MIME_TO_TOOL_ARTIFACT_TYPE` only had the first two. An extensionless file with `text/comma-separated-values` would have backend HTML produced but the client would skip the artifact panel entirely. Add the missing variant. Tests: - 9 new parameterized-MIME cases on backend covering charset/ boundary/case variants for every bucket. - 1 new client routing case for `text/comma-separated-values`.
This commit is contained in:
parent
ab71b322d3
commit
6be1d6f6b7
4 changed files with 73 additions and 7 deletions
|
|
@ -107,6 +107,11 @@ describe('detectArtifactTypeFromFile', () => {
|
|||
['application/vnd.oasis.opendocument.spreadsheet', TOOL_ARTIFACT_TYPES.SPREADSHEET],
|
||||
['text/csv', TOOL_ARTIFACT_TYPES.SPREADSHEET],
|
||||
['application/csv', TOOL_ARTIFACT_TYPES.SPREADSHEET],
|
||||
/* Legacy CSV MIME variant — backend's `CSV_MIME_PATTERN` accepts
|
||||
* it, so the client must too or extensionless CSVs with this MIME
|
||||
* would be skipped despite the backend producing valid HTML.
|
||||
* Regression for Codex P3 review on PR #12934. */
|
||||
['text/comma-separated-values', TOOL_ARTIFACT_TYPES.SPREADSHEET],
|
||||
[
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
TOOL_ARTIFACT_TYPES.PRESENTATION,
|
||||
|
|
|
|||
|
|
@ -609,6 +609,13 @@ const MIME_TO_TOOL_ARTIFACT_TYPE: Record<string, ToolArtifactType> = {
|
|||
'application/vnd.oasis.opendocument.spreadsheet': TOOL_ARTIFACT_TYPES.SPREADSHEET,
|
||||
'text/csv': TOOL_ARTIFACT_TYPES.SPREADSHEET,
|
||||
'application/csv': TOOL_ARTIFACT_TYPES.SPREADSHEET,
|
||||
/* `text/comma-separated-values` is a legacy CSV MIME variant — rare
|
||||
* in modern HTTP traffic but still emitted by some sandboxes. Kept
|
||||
* in lock-step with the backend's `CSV_MIME_PATTERN` in
|
||||
* `packages/api/src/files/documents/html.ts` so an extensionless CSV
|
||||
* with this MIME doesn't slip through the client routing while the
|
||||
* backend has already produced full HTML for it. */
|
||||
'text/comma-separated-values': TOOL_ARTIFACT_TYPES.SPREADSHEET,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
|
||||
TOOL_ARTIFACT_TYPES.PRESENTATION,
|
||||
// Note: bare `text/plain` is NOT mapped here. The extension map handles
|
||||
|
|
|
|||
|
|
@ -290,6 +290,38 @@ describe('Office HTML producers', () => {
|
|||
])('extension wins over conflicting MIME: (%s, %s) → %s', (name, mime, expected) => {
|
||||
expect(officeHtmlBucket(name, mime)).toBe(expected);
|
||||
});
|
||||
|
||||
/* Regression for Codex P2 review on PR #12934. Real Content-Type
|
||||
* headers carry parameters like `; charset=utf-8` and `; boundary`;
|
||||
* the predicate must strip them before matching, otherwise the
|
||||
* backend silently falls through to raw text while the client's
|
||||
* `baseMime` strips the same parameters and routes the file to the
|
||||
* spreadsheet bucket — yielding a broken preview. */
|
||||
it.each([
|
||||
['data', 'text/csv; charset=utf-8', 'csv'],
|
||||
['data', 'text/csv;charset=utf-8', 'csv'],
|
||||
['data', 'TEXT/CSV; CHARSET=UTF-8', 'csv'],
|
||||
['data', 'application/csv; charset=ascii', 'csv'],
|
||||
['data', 'text/comma-separated-values', 'csv'],
|
||||
['data', 'text/comma-separated-values; charset=utf-8', 'csv'],
|
||||
[
|
||||
'workbook',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary',
|
||||
'spreadsheet',
|
||||
],
|
||||
[
|
||||
'report',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=binary',
|
||||
'docx',
|
||||
],
|
||||
[
|
||||
'deck',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation; foo=bar',
|
||||
'pptx',
|
||||
],
|
||||
])('strips MIME parameters before matching: (%s, %s) → %s', (name, mime, expected) => {
|
||||
expect(officeHtmlBucket(name, mime)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zip-bomb defense (SEC review on PR #12934)', () => {
|
||||
|
|
|
|||
|
|
@ -675,6 +675,24 @@ function extensionOf(name: string): string {
|
|||
return name.slice(dot + 1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip MIME parameters (`; charset=utf-8`, `; boundary=...`) and
|
||||
* lowercase the result so exact-match MIME comparisons survive servers
|
||||
* and tool sandboxes that decorate Content-Type headers. Without this,
|
||||
* an extensionless `text/csv; charset=utf-8` would slip past the bucket
|
||||
* predicate (returning null) while the client's MIME router — which
|
||||
* already strips parameters via `baseMime` — would route it to
|
||||
* SPREADSHEET expecting an HTML body that the backend never produced.
|
||||
* Mirrors `client/src/utils/artifacts.ts:baseMime`.
|
||||
*/
|
||||
function baseMime(mime: string): string {
|
||||
if (!mime) {
|
||||
return '';
|
||||
}
|
||||
const semi = mime.indexOf(';');
|
||||
return (semi < 0 ? mime : mime.slice(0, semi)).trim().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* The four buckets the dispatcher can route to. Used by both
|
||||
* `bufferToOfficeHtml` (which actually renders) and `officeHtmlBucket`
|
||||
|
|
@ -716,19 +734,23 @@ export function officeHtmlBucket(name: string, mimeType: string): OfficeHtmlBuck
|
|||
if (byExtension) {
|
||||
return byExtension;
|
||||
}
|
||||
/* MIME-only fallback. Order doesn't matter here — there are no
|
||||
* overlapping MIME patterns across buckets, and the extension table
|
||||
* has already taken precedence above for any conflicting input. */
|
||||
if (mimeType === DOCX_MIME) {
|
||||
/* MIME-only fallback. Strip parameters first — Content-Type headers
|
||||
* routinely carry `; charset=utf-8` or `; boundary=...` decorations
|
||||
* that would otherwise fail exact-match comparisons. Order within the
|
||||
* fallback doesn't matter: there are no overlapping MIME patterns
|
||||
* across buckets, and the extension table already took precedence
|
||||
* above for any conflicting input. */
|
||||
const normalized = baseMime(mimeType);
|
||||
if (normalized === DOCX_MIME) {
|
||||
return 'docx';
|
||||
}
|
||||
if (CSV_MIME_PATTERN.test(mimeType)) {
|
||||
if (CSV_MIME_PATTERN.test(normalized)) {
|
||||
return 'csv';
|
||||
}
|
||||
if (excelMimeTypes.test(mimeType) || mimeType === ODS_MIME) {
|
||||
if (excelMimeTypes.test(normalized) || normalized === ODS_MIME) {
|
||||
return 'spreadsheet';
|
||||
}
|
||||
if (mimeType === PPTX_MIME) {
|
||||
if (normalized === PPTX_MIME) {
|
||||
return 'pptx';
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue