From a29628f5dd588b2ce946054c36f9c676ba32733d Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 7 Oct 2026 13:58:54 -0300 Subject: [PATCH 1/2] fix(pdf): rethrow download errors downloadBlob only logged a failed download, so createDocument().download() resolved and onError never ran. downloadBlob rethrows the error after logging it. --- src/utils/pdf/__tests__/create-document.test.js | 12 ++++++++++++ src/utils/pdf/__tests__/download-blob.test.js | 8 ++++++++ src/utils/pdf/download-blob.js | 1 + 3 files changed, 21 insertions(+) diff --git a/src/utils/pdf/__tests__/create-document.test.js b/src/utils/pdf/__tests__/create-document.test.js index f2c656d5..8a09b984 100644 --- a/src/utils/pdf/__tests__/create-document.test.js +++ b/src/utils/pdf/__tests__/create-document.test.js @@ -52,4 +52,16 @@ describe('createDocument', () => { ).rejects.toThrow('boom'); expect(onError).toHaveBeenCalled(); }); + + it('calls onError and rejects when the browser download fails', async () => { + const { pdfMake } = createFakePdfMake(); + const onError = jest.fn(); + global.URL.createObjectURL = jest.fn(() => { + throw new Error('no blob url'); + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); + const api = await createDocument({ pdfMake, template: () => ({ content: [] }), data: {}, onError }); + await expect(api.download('receipt.pdf')).rejects.toThrow('no blob url'); + expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'no blob url' })); + }); }); diff --git a/src/utils/pdf/__tests__/download-blob.test.js b/src/utils/pdf/__tests__/download-blob.test.js index 313c4212..1fda36c1 100644 --- a/src/utils/pdf/__tests__/download-blob.test.js +++ b/src/utils/pdf/__tests__/download-blob.test.js @@ -39,4 +39,12 @@ describe('downloadBlob', () => { downloadBlob(new Blob(['%PDF']), 'file.pdf'); expect(document.body.contains(anchor)).toBe(true); }); + + it('rethrows when the browser cannot start the download', () => { + global.URL.createObjectURL = jest.fn(() => { + throw new Error('no blob url'); + }); + jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => downloadBlob(new Blob(['%PDF']), 'file.pdf')).toThrow('no blob url'); + }); }); diff --git a/src/utils/pdf/download-blob.js b/src/utils/pdf/download-blob.js index 91fb4086..09a44d0d 100644 --- a/src/utils/pdf/download-blob.js +++ b/src/utils/pdf/download-blob.js @@ -30,6 +30,7 @@ export const downloadBlob = (blob, filename) => { } catch (err) { // eslint-disable-next-line no-console console.error('[pdf] download failed', err); + throw err; } }; From ff5258e6f0622d2c2114b77c33ead5d810ec0c53 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 7 Oct 2026 13:58:55 -0300 Subject: [PATCH 2/2] fix(pdf): fall back to Helvetica for a container without its family resolveFont registered a pre-baked container even when its fonts did not define the family, and pdfmake failed later while building the file. Such a container falls back to Helvetica, like a font URL that fails to load. --- src/utils/pdf/__tests__/resolve-font.test.js | 8 ++++++++ src/utils/pdf/resolve-font.js | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/utils/pdf/__tests__/resolve-font.test.js b/src/utils/pdf/__tests__/resolve-font.test.js index 52296213..41354b1c 100644 --- a/src/utils/pdf/__tests__/resolve-font.test.js +++ b/src/utils/pdf/__tests__/resolve-font.test.js @@ -25,6 +25,14 @@ describe('resolveFont', () => { expect(pdfMake.addFontContainer).toHaveBeenCalledWith({ vfs: font.vfs, fonts: font.fonts }); }); + it('falls back to Helvetica when a pre-baked container does not define its family', async () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + const pdfMake = fakePdfMake(); + const font = { family: 'Brand', vfs: { 'BrandSans-normal.ttf': 'QUFB' }, fonts: { BrandSans: { normal: 'BrandSans-normal.ttf' } } }; + await expect(resolveFont(pdfMake, font)).resolves.toBe('Helvetica'); + expect(pdfMake.addFontContainer).not.toHaveBeenCalled(); + }); + it('fetches + base64s a URL font and registers a container', async () => { global.fetch = jest.fn(() => Promise.resolve({ ok: true, arrayBuffer: () => Promise.resolve(new Uint8Array([65, 66, 67]).buffer) })); const pdfMake = fakePdfMake(); diff --git a/src/utils/pdf/resolve-font.js b/src/utils/pdf/resolve-font.js index df72be72..4390a1b3 100644 --- a/src/utils/pdf/resolve-font.js +++ b/src/utils/pdf/resolve-font.js @@ -57,6 +57,11 @@ export const resolveFont = async (pdfMake, font) => { // Imported / pre-baked container — no fetch. if (font.family && font.vfs && font.fonts) { + if (!font.fonts[font.family]) { + // eslint-disable-next-line no-console + console.warn('[pdf] font container lacks family, using Helvetica:', font.family); + return HELVETICA; + } pdfMake.addFontContainer({ vfs: font.vfs, fonts: font.fonts }); return font.family; }