- {errors.map((err, index) => (
+ {messages.map((err, index) => (
{err}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 3fc3b656..a20a6db2 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -187,6 +187,7 @@ class ApiClientImpl {
body?.message || response.statusText,
statusToUserMessage(response.status),
response.status,
+ body?.errors,
);
}
diff --git a/src/lib/validation.ts b/src/lib/validation.ts
index ee970fe1..f449bcc6 100644
--- a/src/lib/validation.ts
+++ b/src/lib/validation.ts
@@ -1,10 +1,15 @@
import { NextResponse } from 'next/server';
-import { ZodTypeAny, ZodError, z } from 'zod';
+import { ZodTypeAny, z } from 'zod';
// ---------------------------------------------------------------------------
// Discriminated union result type — TypeScript narrows correctly on `.ok`
// ---------------------------------------------------------------------------
+export type ValidationFieldError = {
+ field: string;
+ message: string;
+};
+
type ValidationSuccess = { ok: true; data: T };
type ValidationFailure = { ok: false; error: NextResponse };
export type ValidationResult = ValidationSuccess | ValidationFailure;
@@ -19,10 +24,14 @@ export function validateBody(
): ValidationResult> {
const result = schema.safeParse(input);
if (!result.success) {
+ const errors: ValidationFieldError[] = result.error.issues.map((issue) => ({
+ field: issue.path.join('.'),
+ message: issue.message,
+ }));
return {
ok: false,
error: NextResponse.json(
- { success: false, message: formatZodError(result.error) },
+ { message: 'Validation failed', errors },
{ status: 400 },
),
};
@@ -41,11 +50,3 @@ export function validateQuery(
const raw = Object.fromEntries(searchParams.entries());
return validateBody(schema, raw);
}
-
-// ---------------------------------------------------------------------------
-// Helpers
-// ---------------------------------------------------------------------------
-
-function formatZodError(error: ZodError): string {
- return error.errors.map((e) => e.message).join('; ');
-}
diff --git a/src/services/pdf-generation.ts b/src/services/pdf-generation.ts
index aa4020c2..d8708399 100644
--- a/src/services/pdf-generation.ts
+++ b/src/services/pdf-generation.ts
@@ -1,4 +1,4 @@
-import puppeteer from 'puppeteer';
+import puppeteer, { type PDFOptions } from 'puppeteer';
/**
* Generate a PDF from the provided HTML string using Puppeteer.
@@ -13,7 +13,7 @@ import puppeteer from 'puppeteer';
*/
export async function generatePDF(
html: string,
- options?: puppeteer.PDFOptions
+ options?: PDFOptions
): Promise {
// Launch a headless browser. The flags ensure compatibility in most CI
// and server environments without a sandbox.
@@ -27,12 +27,12 @@ export async function generatePDF(
// Apply the required default timeout (25 000 ms) to prevent indefinite hangs.
page.setDefaultTimeout(25000);
- // Load the HTML content. "networkidle0" waits for all network requests to finish.
- await page.setContent(html, { waitUntil: 'networkidle0' });
+ // Load the HTML content. Use "domcontentloaded" instead of "networkidle0"
+ await page.setContent(html, { waitUntil: 'domcontentloaded' });
// Generate the PDF. Caller may supply additional options.
const pdfBuffer = await page.pdf({ format: 'A4', ...options });
- return pdfBuffer;
+ return Buffer.isBuffer(pdfBuffer) ? pdfBuffer : Buffer.from(pdfBuffer);
} finally {
// Ensure the browser process is always cleaned up, even on errors or timeouts.
await browser.close();
diff --git a/src/utils/error-handler.ts b/src/utils/error-handler.ts
index edb31476..0d493524 100644
--- a/src/utils/error-handler.ts
+++ b/src/utils/error-handler.ts
@@ -38,6 +38,7 @@ export function parseApiError(error: unknown): ErrorInfo {
timestamp: now,
retryable: error.statusCode ? error.statusCode >= 500 : true,
userMessage: error.userMessage,
+ details: error.errors ? { validationErrors: error.errors } : undefined,
};
}
@@ -51,12 +52,18 @@ export function parseApiError(error: unknown): ErrorInfo {
};
}
+export type ApiFieldError = {
+ field: string;
+ message: string;
+};
+
export class ApiError extends Error {
constructor(
public readonly type: ErrorType,
message: string,
public readonly userMessage: string,
public readonly statusCode?: number,
+ public readonly errors?: ApiFieldError[],
) {
super(message);
this.name = 'ApiError';