mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: verbose error handling, batch processing, and multi-file support
- Replace [object Object] errors with readable messages across all 20+ API routes by normalizing Zod validation errors to strings (formatZodErrors) - Add parseApiError() on frontend to defensively handle any details type - Add global Fastify error handler with full stack traces in logs - Fix image-to-pdf auth: Object.entries(headers) → headers.forEach() - Fix passport-photo: safeParse + formatZodErrors, safe error extraction - Fix OCR silent fallbacks: log exception type/message when falling back, include actual engine used in API response and Docker logs - Fix split tool: process all uploaded images, combine into ZIP with subfolders per image - Fix batch support for blur-faces, strip-metadata, edit-metadata, vectorize: add processAllFiles branch for multi-file uploads - Docker: LOG_LEVEL=debug, PYTHONWARNINGS=default for visibility - Add Playwright e2e tests verifying all fixes against Docker container
This commit is contained in:
@@ -38,6 +38,18 @@ const app = Fastify({
|
||||
bodyLimit: env.MAX_UPLOAD_SIZE_MB * 1024 * 1024,
|
||||
});
|
||||
|
||||
app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) => {
|
||||
const statusCode = error.statusCode ?? 500;
|
||||
request.log.error(
|
||||
{ err: error, url: request.url, method: request.method },
|
||||
"Unhandled request error",
|
||||
);
|
||||
reply.status(statusCode).send({
|
||||
error: statusCode >= 500 ? "Internal server error" : error.message,
|
||||
details: error.stack ?? error.message,
|
||||
});
|
||||
});
|
||||
|
||||
// Plugins
|
||||
await app.register(cors, {
|
||||
origin: env.CORS_ORIGIN
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ZodIssue } from "zod";
|
||||
|
||||
export function formatZodErrors(issues: ZodIssue[]): string {
|
||||
return issues
|
||||
.map((i) => (i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message))
|
||||
.join("; ");
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import PQueue from "p-queue";
|
||||
import { env } from "../config.js";
|
||||
import { autoOrient } from "../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../lib/errors.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
@@ -88,12 +89,7 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map(
|
||||
(i: { path: (string | number)[]; message: string }) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
}),
|
||||
),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { z } from "zod";
|
||||
import { env } from "../config.js";
|
||||
import { db, schema } from "../db/index.js";
|
||||
import { autoOrient } from "../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../lib/errors.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
@@ -130,10 +131,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid pipeline definition",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
pipeline = result.data;
|
||||
@@ -431,10 +429,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid pipeline definition",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
pipeline = result.data;
|
||||
|
||||
@@ -7,6 +7,7 @@ import sharp from "sharp";
|
||||
import type { z } from "zod";
|
||||
import { db, schema } from "../db/index.js";
|
||||
import { autoOrient } from "../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../lib/errors.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
@@ -185,10 +186,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { basename, extname } from "node:path";
|
||||
import archiver from "archiver";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
pattern: z.string().min(1).max(200).default("image-{{index}}"),
|
||||
@@ -53,7 +54,9 @@ export function registerBulkRename(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -467,7 +468,9 @@ export function registerCollage(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -74,7 +75,9 @@ export function registerCompose(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { seamCarve } from "@ashim/ai";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -82,10 +83,7 @@ export function registerContentAwareResize(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { basename, join } from "node:path";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import {
|
||||
buildTagArgs,
|
||||
type EditMetadataSettings,
|
||||
@@ -150,10 +151,7 @@ export function registerEditMetadata(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
|
||||
@@ -6,6 +6,7 @@ import PDFDocument from "pdfkit";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -62,7 +63,9 @@ export function registerImageToPdf(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { basename } from "node:path";
|
||||
import { extractText } from "@ashim/ai";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -66,7 +67,7 @@ export function registerOcr(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: result.error.issues });
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
@@ -135,10 +136,18 @@ export function registerOcr(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
if (result.engine && result.engine !== tier) {
|
||||
request.log.warn(
|
||||
{ toolId: "ocr", requested: tier, actual: result.engine },
|
||||
`OCR engine fallback: requested ${tier} but used ${result.engine}`,
|
||||
);
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
jobId,
|
||||
filename,
|
||||
text: result.text,
|
||||
engine: result.engine,
|
||||
});
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
@@ -117,10 +118,7 @@ export function registerOptimizeForWeb(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js";
|
||||
@@ -275,15 +276,14 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
||||
app.post(
|
||||
"/api/v1/tools/passport-photo/generate",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
let parsed: z.infer<typeof generateSettingsSchema>;
|
||||
try {
|
||||
parsed = generateSettingsSchema.parse(request.body);
|
||||
} catch (err) {
|
||||
const parseResult = generateSettingsSchema.safeParse(request.body);
|
||||
if (!parseResult.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
details: formatZodErrors(parseResult.error.issues),
|
||||
});
|
||||
}
|
||||
const parsed = parseResult.data;
|
||||
|
||||
const {
|
||||
jobId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import * as mupdf from "mupdf";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -290,7 +291,9 @@ export function registerPdfToImage(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import QRCode from "qrcode";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -37,10 +38,7 @@ export function registerQrGenerate(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({
|
||||
path: i.path.join("."),
|
||||
message: i.message,
|
||||
})),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -67,7 +68,9 @@ export function registerSplit(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -91,7 +92,9 @@ export function registerStitch(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -7,6 +7,7 @@ import PQueue from "p-queue";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { env } from "../../config.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeHeic, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { isSvgBuffer, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
@@ -147,7 +148,7 @@ export function registerSvgToRaster(app: FastifyInstance) {
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({
|
||||
error: "Invalid settings",
|
||||
details: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
|
||||
details: formatZodErrors(result.error.issues),
|
||||
});
|
||||
}
|
||||
settings = result.data;
|
||||
@@ -365,7 +366,9 @@ export function registerSvgToRaster(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -7,6 +7,7 @@ import potrace from "potrace";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -82,7 +83,9 @@ export function registerVectorize(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -56,7 +57,9 @@ export function registerWatermarkImage(app: FastifyInstance) {
|
||||
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
|
||||
const result = settingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return reply.status(400).send({ error: "Invalid settings", details: result.error.issues });
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
|
||||
}
|
||||
settings = result.data;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user