Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 05cd68eb19 fix: catch SAX XML parse errors in PDF file-type detection during upload
https://sonarly.com/issue/14764?type=bug

Uploading certain PDFs via the `UploadFilesFieldFile` mutation crashes with an unhandled SAX XML parsing error ("Unclosed root tag") thrown by the `@file-type/pdf` plugin during file type detection.

Fix: Wrapped the `fileParser.fromBuffer(file)` call in `extractFileInfo` with a try-catch to handle errors thrown by the `@file-type/pdf` plugin's internal SAX XML parser.

**What changed:**

In `extract-file-info.utils.ts`: Instead of `const { ext, mime } = (await fileParser.fromBuffer(file)) ?? {}`, the result is now obtained inside a try-catch. When `fromBuffer` throws (e.g., "Unclosed root tag" from the SAX parser parsing malformed XMP metadata inside a PDF), the error is caught and the function falls through to extension-based detection — the same path it takes when `fromBuffer` returns `undefined`.

This converts an unhandled 500 error into the existing structured error handling:
- For non-detectable types (txt, csv, json, etc.): returns extension-based MIME type as before
- For detectable types (pdf, png, etc.): throws a `FileStorageException` with a user-friendly message ("content doesn't match extension") — which is properly handled by the GraphQL error filters

In `extract-file-info.utils.spec.ts`: Added two tests:
1. Verifies that when `fromBuffer` throws, the function gracefully falls back to extension-based detection for non-detectable types
2. Verifies that when `fromBuffer` throws for a detectable type (pdf), the function throws a `FileStorageException` (not the raw SAX error)

**Why not fix deeper:** The root bug is in `@file-type/pdf` (third-party library) which doesn't catch SAX parser errors for non-PDF XML content. The `file-type` core's `fromTokenizer` also only catches `EndOfStreamError` and `ParserHardLimitError`. Both are outside this codebase. This defensive catch at the call site is the appropriate pattern.
2026-03-14 21:34:50 +00:00
Sonarly Claude Code 366a721649 window.open crashes with invalid URL when Links field has empty primaryLinkUrl
https://sonarly.com/issue/14749?type=bug

Clicking the "open link" button on a record table cell with a Links field that has an empty `primaryLinkUrl` calls `window.open('https://', '_blank')`, which Chrome rejects as an invalid URL.

Fix: Added empty-value guards in `useGetSecondaryRecordTableCellButton.ts` for all three field types (Links, Emails, Phones). When the primary value (URL, email, or phone number) is empty, the hook now returns an empty array (no action buttons) instead of creating handlers that would call `window.open` with invalid URLs like `https://`, `mailto:`, or `tel:`.

The fix uses `isNonEmptyString` from `@sniptt/guards`, which is the standard guard used throughout the twenty-front codebase for this purpose.

**Links field (the reported crash):** `primaryLinkUrl` being empty/null caused `getAbsoluteUrl('')` to return `'https://'`, which Chrome rejects as invalid in `window.open`.

**Emails/Phones fields (preventive):** The same class of bug exists — empty email would produce `mailto:` and empty phone would produce `tel:`, both invalid URLs for `window.open`.
2026-03-14 20:24:57 +00:00
2 changed files with 38 additions and 2 deletions
@@ -1,3 +1,5 @@
import { FileTypeParser } from 'file-type';
import { extractFileInfo } from '../extract-file-info.utils';
const pngBuffer = Buffer.from([
@@ -101,4 +103,27 @@ describe('extractFileInfo', () => {
);
},
);
it('should not crash when file-type detection throws an internal error', async () => {
jest
.spyOn(FileTypeParser.prototype, 'fromBuffer')
.mockRejectedValueOnce(new Error('Unclosed root tag'));
const result = await extractFileInfo({
file: textBuffer,
filename: 'notes.txt',
});
expect(result).toEqual({ mimeType: 'text/plain', ext: 'txt' });
});
it('should throw FileStorageException instead of internal error when detection fails for supported type', async () => {
jest
.spyOn(FileTypeParser.prototype, 'fromBuffer')
.mockRejectedValueOnce(new Error('Unclosed root tag'));
await expect(
extractFileInfo({ file: pdfBuffer, filename: 'document.pdf' }),
).rejects.toThrow('File content does not match its extension');
});
});
@@ -25,8 +25,19 @@ export const extractFileInfo = async ({
customDetectors: [detectPdf],
});
const { ext: detectedExt, mime: detectedMime } =
(await fileParser.fromBuffer(file)) ?? {};
let detectedExt: string | undefined;
let detectedMime: string | undefined;
try {
const detected = await fileParser.fromBuffer(file);
detectedExt = detected?.ext;
detectedMime = detected?.mime;
} catch {
// The @file-type/pdf plugin may throw SAX XML parsing errors
// on PDFs with malformed internal metadata. Fall through to
// extension-based detection.
}
if (isDefined(detectedExt) && isDefined(detectedMime)) {
return {