Skip to content

Commit 9c136e9

Browse files
committed
update: Allow customizing validation error
This PR adds a `onValidationError` in the endpoint options to allow devs to intercept the validation error and throw their own custom error message if desired.
1 parent b6f1948 commit 9c136e9

5 files changed

Lines changed: 78 additions & 19 deletions

File tree

src/context.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { EndpointOptions } from "./endpoint";
2-
import { _statusCode, APIError, type Status } from "./error";
2+
import { _statusCode, APIError, ValidationError, type Status } from "./error";
33
import type {
44
InferParamPath,
55
InferParamWildCard,
@@ -183,10 +183,7 @@ export const createInternalContext = async (
183183
const headers = new Headers();
184184
const { data, error } = await runValidation(options, context);
185185
if (error) {
186-
throw new APIError(400, {
187-
message: error.message,
188-
code: "VALIDATION_ERROR",
189-
});
186+
throw new ValidationError(error.message, error.issues);
190187
}
191188
const requestHeaders: Headers | null =
192189
"headers" in context

src/endpoint.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import {
1414
type Method,
1515
} from "./context";
1616
import type { CookieOptions, CookiePrefixOptions } from "./cookies";
17-
import { APIError, type _statusCode, type Status } from "./error";
17+
import { APIError, ValidationError, type _statusCode, type Status } from "./error";
1818
import type { OpenAPIParameter, OpenAPISchemaType } from "./openapi";
1919
import type { StandardSchemaV1 } from "./standard-schema";
20-
import { isAPIError } from "./utils";
20+
import { isAPIError, tryCatch } from "./utils";
2121

2222
export interface EndpointOptions {
2323
/**
@@ -155,6 +155,14 @@ export interface EndpointOptions {
155155
* @returns - The response to return
156156
*/
157157
onAPIError?: (e: APIError) => void | Promise<void>;
158+
/**
159+
* A callback to run before a validation error is thrown
160+
* You can customize the validation error message by throwing your own APIError
161+
*/
162+
onValidationError?: ({
163+
issues,
164+
message,
165+
}: { message: string; issues: readonly StandardSchemaV1.Issue[] }) => void | Promise<void>;
158166
}
159167

160168
export type EndpointContext<Path extends string, Options extends EndpointOptions, Context = {}> = {
@@ -329,10 +337,31 @@ export const createEndpoint = <Path extends string, Options extends EndpointOpti
329337
: [(Context & { asResponse?: AsResponse; returnHeaders?: ReturnHeaders })?]
330338
) => {
331339
const context = (inputCtx[0] || {}) as InputContext<any, any>;
332-
const internalContext = await createInternalContext(context, {
333-
options,
334-
path,
335-
});
340+
const { data: internalContext, error: validationError } = await tryCatch(
341+
createInternalContext(context, {
342+
options,
343+
path,
344+
}),
345+
);
346+
347+
if (validationError) {
348+
// If it's not a validation error, we throw it
349+
if (!(validationError instanceof ValidationError)) throw validationError;
350+
351+
// Check if the endpoint has a custom onValidationError callback
352+
if (options.onValidationError) {
353+
// This can possibly throw an APIError in order to customize the validation error message
354+
await options.onValidationError({
355+
message: validationError.message,
356+
issues: validationError.issues,
357+
});
358+
}
359+
360+
throw new APIError(400, {
361+
message: validationError.message,
362+
code: "VALIDATION_ERROR",
363+
});
364+
}
336365
const response = await handler(internalContext as any).catch(async (e) => {
337366
if (isAPIError(e)) {
338367
const onAPIError = options.onAPIError;

src/error.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { StandardSchemaV1 } from "./standard-schema";
2+
13
export const _statusCode = {
24
OK: 200,
35
CREATED: 201,
@@ -145,3 +147,17 @@ export class APIError extends Error {
145147
this.stack = "";
146148
}
147149
}
150+
151+
export class ValidationError extends APIError {
152+
constructor(
153+
public message: string,
154+
public issues: readonly StandardSchemaV1.Issue[],
155+
) {
156+
super(400, {
157+
message: message,
158+
code: "VALIDATION_ERROR",
159+
});
160+
161+
this.issues = issues;
162+
}
163+
}

src/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,24 @@ export function tryDecode(str: string) {
6464
return str;
6565
}
6666
}
67+
68+
type Success<T> = {
69+
data: T;
70+
error: null;
71+
};
72+
73+
type Failure<E> = {
74+
data: null;
75+
error: E;
76+
};
77+
78+
export type Result<T, E = Error> = Success<T> | Failure<E>;
79+
80+
export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
81+
try {
82+
const data = await promise;
83+
return { data, error: null };
84+
} catch (error) {
85+
return { data: null, error: error as E };
86+
}
87+
}

src/validator.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type ValidationResponse =
1414
data: null;
1515
error: {
1616
message: string;
17+
issues: readonly StandardSchemaV1.Issue[];
1718
};
1819
};
1920

@@ -56,13 +57,13 @@ export async function runValidation(
5657
if (options.requireHeaders && !context.headers) {
5758
return {
5859
data: null,
59-
error: { message: "Headers is required" },
60+
error: { message: "Headers is required", issues: [] },
6061
};
6162
}
6263
if (options.requireRequest && !context.request) {
6364
return {
6465
data: null,
65-
error: { message: "Request is required" },
66+
error: { message: "Request is required", issues: [] },
6667
};
6768
}
6869
return {
@@ -72,13 +73,8 @@ export async function runValidation(
7273
}
7374

7475
export function fromError(error: readonly StandardSchemaV1.Issue[], validating: string) {
75-
const errorMessages: string[] = [];
76-
77-
for (const issue of error) {
78-
const message = issue.message;
79-
errorMessages.push(message);
80-
}
8176
return {
8277
message: `Invalid ${validating} parameters`,
78+
issues: error,
8379
};
8480
}

0 commit comments

Comments
 (0)