From 973438477c979ff5c78143ead740b68cf4777211 Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 06:47:35 +0100 Subject: [PATCH 1/6] Improve error handling in image-to-text and stream-child-process --- apps/tesseract/src/main.ts | 7 ++++++- libs/binary/tesseract/src/lib/image-to-text.ts | 2 +- libs/stream/src/lib/stream-child-process.ts | 14 +++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/tesseract/src/main.ts b/apps/tesseract/src/main.ts index 4e1c034d..86235a4d 100644 --- a/apps/tesseract/src/main.ts +++ b/apps/tesseract/src/main.ts @@ -8,7 +8,12 @@ httpServer( connect( post({ path: '/image-to-text' }, async ({ req, res }) => { res.setHeader('Content-Type', 'text/plain'); - imageToText({ input: req, output: res }); + try { + await imageToText({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'tesseract failed'); + } }), ...healthEndpoints, ), diff --git a/libs/binary/tesseract/src/lib/image-to-text.ts b/libs/binary/tesseract/src/lib/image-to-text.ts index 93c94cd9..9ccdc984 100644 --- a/libs/binary/tesseract/src/lib/image-to-text.ts +++ b/libs/binary/tesseract/src/lib/image-to-text.ts @@ -2,7 +2,7 @@ import { spawn } from 'node:child_process'; import type { Writable } from 'node:stream'; import { type InputType, streamChildProcess, streamChildProcessToBuffer } from '@container/stream'; -export function imageToText(options: { input: InputType; output: Writable }): void; +export function imageToText(options: { input: InputType; output: Writable }): Promise; export function imageToText(options: { input: InputType }): Promise; export function imageToText({ input, output }: { input: InputType; output?: Writable }) { const tesseract = spawn('tesseract', ['-', '-']); diff --git a/libs/stream/src/lib/stream-child-process.ts b/libs/stream/src/lib/stream-child-process.ts index 64bc691e..00521926 100644 --- a/libs/stream/src/lib/stream-child-process.ts +++ b/libs/stream/src/lib/stream-child-process.ts @@ -33,9 +33,21 @@ export async function streamChildProcess( const { end = true } = options ?? {}; child.stdin.on('error', (error) => { console.error(error); + if (!output.destroyed) { + output.destroy(error); + } + child.kill(); }); - await streamInputToWriteable(input, child.stdin, { end: true }); + try { + await streamInputToWriteable(input, child.stdin, { end: true }); + } catch (error) { + if (!output.destroyed) { + output.destroy(error instanceof Error ? error : undefined); + } + child.kill(); + throw error; + } child.stdout .on('error', (error) => { From c7580ebaccfa65ba90f7bb0114c765429e445624 Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 06:47:40 +0100 Subject: [PATCH 2/6] Add error handling to all pdftk endpoint streams --- apps/pdftk/src/main.ts | 56 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/apps/pdftk/src/main.ts b/apps/pdftk/src/main.ts index b99f3e98..73097093 100644 --- a/apps/pdftk/src/main.ts +++ b/apps/pdftk/src/main.ts @@ -20,26 +20,56 @@ const PORT = process.env.PORT || '3000'; httpServer( connect( post({ path: '/compress' }, async ({ req, res }) => { - await compressStream({ input: req, output: res }); + try { + await compressStream({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), post({ path: '/uncompress' }, async ({ req, res }) => { - await uncompressStream({ input: req, output: res }); + try { + await uncompressStream({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), post( { path: '/encrypt' }, middlewareQuery(encryptSchema), async ({ req, res, query: { password, userPassword, allow } }) => { - await encryptStream({ input: req, output: res, password, userPassword, allow }); + try { + await encryptStream({ input: req, output: res, password, userPassword, allow }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }, ), post({ path: '/decrypt' }, middlewareQuery(decryptSchema), async ({ req, res, query: { password } }) => { - await decryptStream({ input: req, output: res, password }); + try { + await decryptStream({ input: req, output: res, password }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), post({ path: '/data/fields' }, async ({ req, res }) => { - await dataFieldsStream({ input: req, output: res }); + try { + await dataFieldsStream({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), post({ path: '/data/dump' }, async ({ req, res }) => { - await dataDumpStream({ input: req, output: res }); + try { + await dataDumpStream({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), // post({ path: '/data/annots' }, async ({ req, res }) => { // const pdftkSpawn = spawn('java', ['-jar', '/pdftk/pdftk.jar', '-', 'dump_data_annots', '-']); @@ -57,11 +87,21 @@ httpServer( { path: '/form/fill' }, middlewareQuery(formFillSchema), async ({ req, res, query: { flag, fontName, ...data } }) => { - await formFillStream({ input: req, output: res, flag, fontName, data }); + try { + await formFillStream({ input: req, output: res, flag, fontName, data }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }, ), post({ path: '/data/fdf' }, async ({ req, res }) => { - await dataFdfStream({ input: req, output: res }); + try { + await dataFdfStream({ input: req, output: res }); + } catch (error) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } }), ...healthEndpoints, ), From dbdc2e2cc1d9c750d36d5b50764bed4423d448f6 Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 06:52:47 +0100 Subject: [PATCH 3/6] Remove output.destroy calls from error handling --- libs/stream/src/lib/stream-child-process.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/stream/src/lib/stream-child-process.ts b/libs/stream/src/lib/stream-child-process.ts index 00521926..6b032e7e 100644 --- a/libs/stream/src/lib/stream-child-process.ts +++ b/libs/stream/src/lib/stream-child-process.ts @@ -33,18 +33,12 @@ export async function streamChildProcess( const { end = true } = options ?? {}; child.stdin.on('error', (error) => { console.error(error); - if (!output.destroyed) { - output.destroy(error); - } child.kill(); }); try { await streamInputToWriteable(input, child.stdin, { end: true }); } catch (error) { - if (!output.destroyed) { - output.destroy(error instanceof Error ? error : undefined); - } child.kill(); throw error; } From d1bfa44391a355a4b7c94e2ebdaa9ba573facb9c Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 06:57:29 +0100 Subject: [PATCH 4/6] Improve stderr handling in streamChildProcess --- libs/stream/src/lib/stream-child-process.ts | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/libs/stream/src/lib/stream-child-process.ts b/libs/stream/src/lib/stream-child-process.ts index 6b032e7e..114a285d 100644 --- a/libs/stream/src/lib/stream-child-process.ts +++ b/libs/stream/src/lib/stream-child-process.ts @@ -31,6 +31,15 @@ export async function streamChildProcess( options?: StreamChildProcessOptions, ): Promise { const { end = true } = options ?? {}; + const stderrChunks: Buffer[] = []; + child.stderr.on('data', (chunk) => { + stderrChunks.push(Buffer.from(chunk)); + }); + + child.stderr.on('error', (error) => { + console.error(error); + }); + child.stdin.on('error', (error) => { console.error(error); child.kill(); @@ -40,6 +49,11 @@ export async function streamChildProcess( await streamInputToWriteable(input, child.stdin, { end: true }); } catch (error) { child.kill(); + const stderrOutput = Buffer.concat(stderrChunks).toString('utf-8'); + if (stderrOutput) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`${message}: ${stderrOutput}`); + } throw error; } @@ -49,15 +63,6 @@ export async function streamChildProcess( }) .pipe(output, { end }); - const stderrChunks: Buffer[] = []; - child.stderr.on('data', (chunk) => { - stderrChunks.push(Buffer.from(chunk)); - }); - - child.stderr.on('error', (error) => { - console.error(error); - }); - output.on('close', () => { child.kill(); }); From 6d44b0966e3846e1024e4c4da4bbcfe61996e5df Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 07:15:49 +0100 Subject: [PATCH 5/6] Prevent double response on error if headers already sent --- apps/pdftk/src/main.ts | 48 +++++++++++++++++++++++++------------- apps/tesseract/src/main.ts | 6 +++-- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/pdftk/src/main.ts b/apps/pdftk/src/main.ts index 73097093..214bcdac 100644 --- a/apps/pdftk/src/main.ts +++ b/apps/pdftk/src/main.ts @@ -23,16 +23,20 @@ httpServer( try { await compressStream({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), post({ path: '/uncompress' }, async ({ req, res }) => { try { await uncompressStream({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), post( @@ -42,8 +46,10 @@ httpServer( try { await encryptStream({ input: req, output: res, password, userPassword, allow }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }, ), @@ -51,24 +57,30 @@ httpServer( try { await decryptStream({ input: req, output: res, password }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), post({ path: '/data/fields' }, async ({ req, res }) => { try { await dataFieldsStream({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), post({ path: '/data/dump' }, async ({ req, res }) => { try { await dataDumpStream({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), // post({ path: '/data/annots' }, async ({ req, res }) => { @@ -90,8 +102,10 @@ httpServer( try { await formFillStream({ input: req, output: res, flag, fontName, data }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }, ), @@ -99,8 +113,10 @@ httpServer( try { await dataFdfStream({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'pdftk failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'pdftk failed'); + } } }), ...healthEndpoints, diff --git a/apps/tesseract/src/main.ts b/apps/tesseract/src/main.ts index 86235a4d..67a84e40 100644 --- a/apps/tesseract/src/main.ts +++ b/apps/tesseract/src/main.ts @@ -11,8 +11,10 @@ httpServer( try { await imageToText({ input: req, output: res }); } catch (error) { - res.statusCode = 500; - res.end(error instanceof Error ? error.message : 'tesseract failed'); + if (!res.headersSent) { + res.statusCode = 500; + res.end(error instanceof Error ? error.message : 'tesseract failed'); + } } }), ...healthEndpoints, From c5b4ad5d1288dc06dea507fdb5e085121c321ae0 Mon Sep 17 00:00:00 2001 From: Philip Lehmann Date: Sat, 21 Feb 2026 07:17:49 +0100 Subject: [PATCH 6/6] Handle child process exit signals and improve error reporting --- libs/stream/src/lib/stream-child-process.ts | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/libs/stream/src/lib/stream-child-process.ts b/libs/stream/src/lib/stream-child-process.ts index 114a285d..a08a2f8a 100644 --- a/libs/stream/src/lib/stream-child-process.ts +++ b/libs/stream/src/lib/stream-child-process.ts @@ -63,24 +63,32 @@ export async function streamChildProcess( }) .pipe(output, { end }); + let stdoutFinished = false; + output.on('close', () => { - child.kill(); + if (!stdoutFinished) { + child.kill(); + } }); // Set up exit listener before awaiting to avoid race condition - const exitPromise = new Promise((resolve) => { - child.on('exit', (code) => { - resolve(code); + const exitPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => { + child.on('exit', (code, signal) => { + resolve({ code, signal }); }); }); await finished(child.stdout); + stdoutFinished = true; // Wait for the process to exit and check the exit code - const exitCode = await exitPromise; + const { code, signal } = await exitPromise; - if (exitCode !== 0) { + if (code !== 0) { const stderrOutput = Buffer.concat(stderrChunks).toString('utf-8'); - throw new Error(`Child process exited with code ${exitCode}${stderrOutput ? `: ${stderrOutput}` : ''}`); + if (code === null && signal) { + throw new Error(`Child process exited with signal ${signal}${stderrOutput ? `: ${stderrOutput}` : ''}`); + } + throw new Error(`Child process exited with code ${code}${stderrOutput ? `: ${stderrOutput}` : ''}`); } }