A
arktype•2mo ago
dibbo

Generic middleware validator

Hello, I'm trying to write a middleware in my app that handles validating the incoming request body. It currently looks like this
export function validateBody<T extends Type>(
validator: T
): MiddlewareObj<ValidatedBody<T["infer"]>> {
return {
before: async ({ event }: Request) => {
if (!event.body) return { statusCode: 400 };
const result = validator(event.body);
if (result instanceof type.errors) {
throw new ClientError(result.summary);
}
event.body = result;
},
};
}
export function validateBody<T extends Type>(
validator: T
): MiddlewareObj<ValidatedBody<T["infer"]>> {
return {
before: async ({ event }: Request) => {
if (!event.body) return { statusCode: 400 };
const result = validator(event.body);
if (result instanceof type.errors) {
throw new ClientError(result.summary);
}
event.body = result;
},
};
}
The return type means that in my handler body comes through with the correct type definition given by the validator, e.g.
const schema = type({name:"string"}}
const handleRequest = middy()
.use(validateBody(schema))
.handler(async ({ body }) => {
// body.name === string!
});
const schema = type({name:"string"}}
const handleRequest = middy()
.use(validateBody(schema))
.handler(async ({ body }) => {
// body.name === string!
});
This has worked nicely so far, but unfortunately with larger schemas I'm getting a Type instantiation is excessively deep and possibly infinite error when calling validateBody. I've read answers to similar questions here and here, and tried out some of the suggestions, but I haven't been able to come up with a solution so far. Is there any way to get this working as expected, or is it not possible with the current limitations of Typescript? Am I safer just casting body in the handler to the expected type (since at that point it has passed validation and I know that it conforms to the expected type)? Any help or suggestions would be greatly appreciated, thanks!
6 Replies
ssalbdivad
ssalbdivad•5w ago
Can you post the relevant types that are resulting in the errors? It seems like it would be some combination of use, handler, MiddlewareObj and ValidatedBody. You should not have to cast just because a schema is large unless it's so big arktype has a problem inferring it directly
dibbo
dibboOP•5w ago
Sure. I've moved the call outside of the middleware chain i.e.
const test = validateBody(schema);
const test = validateBody(schema);
and I'm still seeing the error so I think we can rule out use/handler interfering with it. Here are the other types
// From @middy/core
declare type MiddlewareFn<
TEvent = any,
TResult = any,
TErr = Error,
TContext extends LambdaContext = LambdaContext,
TInternal extends Record<string, unknown> = {}
> = (request: Request<TEvent, TResult, TErr, TContext, TInternal>) => any

export interface MiddlewareObj<
TEvent = unknown,
TResult = any,
TErr = Error,
TContext extends LambdaContext = LambdaContext,
TInternal extends Record<string, unknown> = {}
> {
before?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
after?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
onError?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
name?: string
}

// My own ValidatedBody type
type ValidatedBody<T> = Omit<APIGatewayProxyEventV2, "body"> & {
body: T;
};
// From @middy/core
declare type MiddlewareFn<
TEvent = any,
TResult = any,
TErr = Error,
TContext extends LambdaContext = LambdaContext,
TInternal extends Record<string, unknown> = {}
> = (request: Request<TEvent, TResult, TErr, TContext, TInternal>) => any

export interface MiddlewareObj<
TEvent = unknown,
TResult = any,
TErr = Error,
TContext extends LambdaContext = LambdaContext,
TInternal extends Record<string, unknown> = {}
> {
before?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
after?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
onError?: MiddlewareFn<TEvent, TResult, TErr, TContext, TInternal>
name?: string
}

// My own ValidatedBody type
type ValidatedBody<T> = Omit<APIGatewayProxyEventV2, "body"> & {
body: T;
};
Here's a CodeSandbox link to the ark schemas and their usage
ssalbdivad
ssalbdivad•5w ago
Could you create a repo from this so I can see it locally? I'm not seeing errors in the codesanbox, just a bunch of stuff falling back to any
dibbo
dibboOP•3w ago
https://github.com/bgribben/type-issue I think that should show the issue, thanks for looking into this This might be an issue with the middy library, or at least how I'm using it. One call to use seems to impact the other e.g.
const handleCreatePet = middy()
.use(validateBody(createPetPayload))
.handler(async ({ body }) => {});

const handleUpdatePet = middy()
.use(validateBody(updatePetPayload))
.handler(async ({ body }) => {});
const handleCreatePet = middy()
.use(validateBody(createPetPayload))
.handler(async ({ body }) => {});

const handleUpdatePet = middy()
.use(validateBody(updatePetPayload))
.handler(async ({ body }) => {});
in this instance, commenting out the handleCreatePet gets rid of the Type instantiation is excessively deep and possibly infinite error in handleUpdatePet. I have no idea how they could impact each other, gonna do some digging to see if I'm even using the library correctly 🙃 Hey @ssalbdivad I've updated https://github.com/bgribben/type-issue to use my own recursive type for the middleware
type Chain<T = APIGatewayProxyEventV2> = {
(event: APIGatewayProxyEventV2): Promise<void>;
use: <K extends Middleware>(
middleware: K
) => Chain<
keyof ReturnType<K["before"]> extends keyof T
? Omit<T, keyof ReturnType<K["before"]>> & ReturnType<K["before"]>
: T & ReturnType<K["before"]>
>;
handler: (fn: HandlerFn<T>) => void;
};
type Chain<T = APIGatewayProxyEventV2> = {
(event: APIGatewayProxyEventV2): Promise<void>;
use: <K extends Middleware>(
middleware: K
) => Chain<
keyof ReturnType<K["before"]> extends keyof T
? Omit<T, keyof ReturnType<K["before"]>> & ReturnType<K["before"]>
: T & ReturnType<K["before"]>
>;
handler: (fn: HandlerFn<T>) => void;
};
and I'm still seeing the issue with the following usage
const validate = <T extends Type>(
validator: T
): Middleware<{ body: T["infer"] }> => ({
before: (event) => {
if (!event.body) return { statusCode: 400 };
const result = validator(event.body);
if (result instanceof type.errors) {
throw new Error(result.summary);
}
event.body = result;
return event;
},
after: () => {},
});

const handleCreatePet = chain()
.use(validate(createPetPayload))
.handler(async ({ body }) => {});

const handleUpdatePet = chain()
.use(validate(updatePetPayload))
.handler(async ({ body }) => {});
const validate = <T extends Type>(
validator: T
): Middleware<{ body: T["infer"] }> => ({
before: (event) => {
if (!event.body) return { statusCode: 400 };
const result = validator(event.body);
if (result instanceof type.errors) {
throw new Error(result.summary);
}
event.body = result;
return event;
},
after: () => {},
});

const handleCreatePet = chain()
.use(validate(createPetPayload))
.handler(async ({ body }) => {});

const handleUpdatePet = chain()
.use(validate(updatePetPayload))
.handler(async ({ body }) => {});
The strange thing is if I move handleUpdatePet above handleCreatePet, the error disappears At a glance, would you know why the order of the calls impacts the output of TS? Apologies for the direct ping, but if you get a second to check out the error in the repo, it would be greatly appreciated. Thanks!
ssalbdivad
ssalbdivad•3w ago
I'm not totally sure what's going on internally here, but changing your validate signature to the following seems to resolve the issue:
import type { distill } from "arktype/internal/attributes.ts";

const validate = <T>(
validator: type.Any<T>
): Middleware<{ body: distill.Out<T> }> => ({
import type { distill } from "arktype/internal/attributes.ts";

const validate = <T>(
validator: type.Any<T>
): Middleware<{ body: distill.Out<T> }> => ({
Unfortunately, this requires using the internal distill API for now, but I will likely expose it in an upcoming release. You'll also need to update your module settings in tsconfig to something like:
"module": "preserve",
"moduleResolution": "Bundler"
"module": "preserve",
"moduleResolution": "Bundler"
so you can correctly resolve package.json exports, and likely add "type": "module" to package.json since ArkType only exports esm.
dibbo
dibboOP•3w ago
Thank you!
Want results from more Discord servers?
Add your server