diff --git a/apps/api/src/routes/batch.ts b/apps/api/src/routes/batch.ts index 96943bc4..70d5e734 100644 --- a/apps/api/src/routes/batch.ts +++ b/apps/api/src/routes/batch.ts @@ -13,6 +13,7 @@ import { getBundleForTool, TOOL_BUNDLE_MAP } from "@snapotter/shared"; import archiver from "archiver"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import PQueue from "p-queue"; +import sharp from "sharp"; import { env } from "../config.js"; import { autoOrient } from "../lib/auto-orient.js"; import { resolveConcurrency } from "../lib/env.js"; @@ -169,7 +170,11 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise { if (ext) processFilename = `${processFilename.slice(0, -ext.length)}.png`; } if (!skipPreprocess && needsCliDecode(validation.format)) { - processBuffer = await decodeToSharpCompat(processBuffer, validation.format); + try { + processBuffer = await decodeToSharpCompat(processBuffer, validation.format); + } catch { + await sharp(processBuffer).metadata(); + } const ext = processFilename.match(/\.[^.]+$/)?.[0]; if (ext) processFilename = `${processFilename.slice(0, -ext.length)}.png`; } diff --git a/apps/api/src/routes/files.ts b/apps/api/src/routes/files.ts index bd7b0c8b..3f4b4d61 100644 --- a/apps/api/src/routes/files.ts +++ b/apps/api/src/routes/files.ts @@ -156,17 +156,28 @@ export async function fileRoutes(app: FastifyInstance): Promise { try { buffer = await decodeToSharpCompat(buffer, validation.format); } catch { - return reply.status(422).send({ - error: `Failed to decode ${validation.format.toUpperCase()} file`, - }); + // CLI decoder unavailable -- try Sharp directly as fallback for preview + try { + await sharp(buffer).metadata(); + } catch { + return reply.status(422).send({ + error: `Failed to decode ${validation.format.toUpperCase()} file`, + }); + } } } - const webp = await sharp(buffer) - .resize(1200, 1200, { fit: "inside", withoutEnlargement: true }) - .webp({ quality: 80 }) - .toBuffer(); - return reply.header("Content-Type", "image/webp").send(webp); + try { + const webp = await sharp(buffer) + .resize(1200, 1200, { fit: "inside", withoutEnlargement: true }) + .webp({ quality: 80 }) + .toBuffer(); + return reply.header("Content-Type", "image/webp").send(webp); + } catch { + return reply.status(422).send({ + error: `Failed to generate preview for ${validation.format.toUpperCase()} file`, + }); + } }); } diff --git a/apps/api/src/routes/tool-factory.ts b/apps/api/src/routes/tool-factory.ts index 1d6e1c5b..1caa36b2 100644 --- a/apps/api/src/routes/tool-factory.ts +++ b/apps/api/src/routes/tool-factory.ts @@ -198,14 +198,18 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig try { const fileExt = filename.split(".").pop()?.toLowerCase(); fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt); - const ext = filename.match(/\.[^.]+$/)?.[0]; - if (ext) filename = `${filename.slice(0, -ext.length)}.png`; - } catch (err) { - return reply.status(422).send({ - error: `Failed to decode ${validation.format.toUpperCase()} file`, - details: err instanceof Error ? err.message : String(err), - }); + } catch { + try { + await sharp(fileBuffer).metadata(); + } catch (err) { + return reply.status(422).send({ + error: `Failed to decode ${validation.format.toUpperCase()} file`, + details: err instanceof Error ? err.message : String(err), + }); + } } + const ext = filename.match(/\.[^.]+$/)?.[0]; + if (ext) filename = `${filename.slice(0, -ext.length)}.png`; } // Sanitize SVG input to prevent XXE, SSRF, and script injection