Replies: 8 comments 40 replies
-
Could we adapt express middleware to universal ones?I think so. Express middlewares can edit Hattip, and some others, can edit What does this this imply? It implies that, given any express middleware, const app = express();
app.use(middleware_a());
app.use(middleware_b());
app.use(middleware_c()); And as an example for the rest of this comment, let's assume the following behavior:
In this case, modifications applied to But in the case of hattip-like middlewares, import { compose } from "@hattip/compose";
compose(
adaptExpressToHattip(middleware_a),
adaptExpressToHattip(middleware_b),
adaptExpressToHattip(middleware_c),
); Here there are multiple cases:
|
Beta Was this translation helpful? Give feedback.
-
Maybe something like this: usage: https://github.com/nitedani/universal-handler/blob/main/hattip-entry.ts Can we assume that express middlewares are only used on node? Or we want to adapt them to serverless, for example cloudflare workers? It should be possible for middlewares that don't use node builtins, but not for others. |
Beta Was this translation helpful? Give feedback.
-
I stumbled upon a recurring issue while adapting Bati's code to universal middlewares and handlers. Some servers (mostly fastify by default and express with some middlewares) are consuming and parsing the request's body, making it hard to then create a proper I adapted type RequestAdapter = (req: IncomingMessage) => Request; To properly fix the aforementioned issue, we'll probably have to change this signature to: type RequestAdapter = (req: IncomingMessage, body?: BodyInit | null) => Request; This way, if the body is already consumed by the server, we have a way to give it back to the Request. |
Beta Was this translation helpful? Give feedback.
-
Now that I finished the MVP in Bati, I'll summarize my thoughts here. What changes where made in Bati's codebaseServers for which universal handlers (and middlewares) where applied:
For all those servers, I managed to use Handler as defined in #1:
The only middlewares I needed to implement are ones that modify the |
Beta Was this translation helpful? Give feedback.
-
What should Middlewares actually look like?Middlewares can edit the Context. This can be done at any moment (i.e. before or after the Response). Currently my proposals are: Pattern 1This is mostly an adaption of how // Should be extendable by user
interface Context extends Record<string | number | symbol, unknown>> {
next: () => Promise<Response>;
}
interface Middleware {
(request: Request, context: Context): Response | void | Promise<Response> | Promise<void>;
}
async function myMiddleware1(request: Request, context: Context) {
// do things
const response = await context.next();
// do things
// If `context.next()` is called, a Response MUST be returned
return response;
}
async function myMiddleware2(request: Request, context: Context) {
context.user = { ... };
// Here we should be able to return nothing, as we just modified the context in no particular order
return;
} Pattern 2I slightly prefer this one, as this keep the // Should be extendable by user
interface Context extends Record<string | number | symbol, unknown> {
}
interface Middleware {
(request: Request, context: Context, next: () => Promise<Response>): Response | void | Promise<Response> | Promise<void>;
}
async function myMiddleware1(request: Request, context: Context, next: () => Promise<Response>) {
// do things
const response = await next();
// do things
// If `next()` is called, a Response MUST be returned
return response;
}
async function myMiddleware2(request: Request, context: Context, next: () => Promise<Response>) {
context.user = { ... };
// Here we should be able to return nothing, as we just modified the context in no particular order
return;
} Pattern 3Taken from @mindplay-dk example. This one has a different approach than the previous ones. // Should be extendable by user
interface Context extends Record<string | number | symbol, unknown> {
}
interface Middleware {
(handler: Handler): Handler;
}
async function myMiddleware1(handler: Handler) {
return (request: Request, context: Context) => {
// do things
const response = await handler(request, context);
// do things
return response;
}
}
async function myMiddleware2(handler: Handler) {
return (request: Request, context: Context) => {
context.user = { ... };
return handler;
}
} This pattern is simple enough, but it needs to be tested further, because I have the feeling that for some servers, it would require a custom middleware composition layer (akin to Pattern 4Extracted from a discussion below. The idea would be to either only care about Middlewares that do not return anything (only updating the Context), or define 2 distinct kind a Middlewares. // Should be extendable by user
interface Context extends Record<string | number | symbol, unknown> {
}
// Those kind of middleware do not care about the Response
interface BeforeMiddleware {
(request: Request, context: Context): void | Promise<void>;
}
// Those kind of middleware interact with the Response
interface WrappingMiddleware {
// Any kind of pattern (like the ones in 1., 2. or 3.) could be valid here
// They should always return a Response
(request: Request, context: Context, next: () => Promise<Response>): Response | Promise<Response>;
}
// BeforeMiddleware
async function myMiddleware1(request: Request, context: Context) {
context.user = { ... };
}
// WrappingMiddleware
async function myMiddleware2(request: Request, context: Context, next: () => Promise<Response>) {
const response = await next();
// do things with response..., then
return response;
} If we limit our scope to Pattern 5Inspired by webroute's middlewares. // Should be extendable by user
interface Context extends Record<string | number | symbol, unknown> {
}
type Awaitable<T> = T | Promise<T>;
interface Middleware {
(request: Request, context: Context): Awaitable<Response> | Awaitable<void> | ((response: Response) => Awaitable<Response>);
}
async function myMiddleware(request: Request, context: Context) {
// If we return a response handler, it will be called AFTER the handler has executed an returned a response.
return (response: Response) => {
// manipule the response before returning it to user-land
response.headers.set('Cache-Control', '...');
return response;
};
} |
Beta Was this translation helpful? Give feedback.
-
Caching
|
Beta Was this translation helpful? Give feedback.
-
Caching computed transformationsNot only For instance, several middlewares could want to parse Currently, the ecosystem does this caching in 2 ways. The custom set of tools seems like a good thing to have anyway for universal Handlers and Middlewares, so the second solution seems like a good fit IMO. This would require us to adapt what Hono or h3 does, and make it universal, i.e. work with generic I have 2 ideas for this: // On Request
const cookies = getCookies(request);
// On Context
const cookies = getCookies(request, context); My only concern is that there could be potential limitations when writing directly to |
Beta Was this translation helpful? Give feedback.
-
Should we have our own
|
Beta Was this translation helpful? Give feedback.
-
Not sure of the actual format that those should use, but for now, let's take hattip way of doing middlewares
cc @cyco130 @mindplay-dk @brillout
Beta Was this translation helpful? Give feedback.
All reactions