-
Notifications
You must be signed in to change notification settings - Fork 628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(http): middleware #3785
feat(http): middleware #3785
Conversation
http/middleware.ts
Outdated
return (request, info) => { | ||
function compose(index: number): Response | Promise<Response> { | ||
if (index >= middlewares.length) { | ||
throw new RangeError("Middleware chain exhausted"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this error make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if a RangeError
is the right Error class for this. Should this be a custom error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain either. I'd be keen to hear other's thoughts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way the createHandler
function is designed here assumes that the last middleware never calls next()
. So in that sense it's fine, although it prevents composing createHandler
itself. Not sure if that is a goal here though.
const a = createHandler([m1, m2, m3]);
const b = createHandler([m1, m2, m3]);
// Not possible
const c = createHandler([a, b]);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it wasn't one of my goals to do that... I don't know the advantage of doing it and whether there'd be sufficient use cases for that.
I'm slightly -1 on this from a philosophical standpoint, but the actual code looks fine to me. |
Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
Maybe let's explore how this Also this middleware doesn't seem having ability to pass information to the later middleware such as auth info, etc (or is that possible?). I wonder if this 'MiddlewareFunction' interface is general enough to be 'standard' middleware |
@kitsonk and @marvinhagemeister, WDYT? Can you see this being useful for your respective frameworks? 🙂 |
I would suggest in general to rename the module:
And also
I think this feature would be a must for useful middleware handlers in std. Passing data to the next handler could be achieved by extending export type MiddlewareHandler<I> = (
request: Request,
info: I,
next: (info?: I) => Response | Promise<Response>,
) => Response | Promise<Response>;
export function composeHandlers<I extends Deno.ServeHandlerInfo>(
middlewares: MiddlewareHandler<I>[],
): (request: Request, info: I) => Promise<Response> | Response {
return (request, info) => {
function compose(index: number, info: I): Response | Promise<Response> {
const handler = middlewares[index];
if (!handler) throw new RangeError("Middleware chain exhausted");
return handler(
request,
info,
(newInfo = info) => compose(index + 1, newInfo),
);
}
return compose(0, info);
};
} So it can be used like this: import { composeHandlers } from "https://deno.land/std/http/handlers.ts";
interface StateHandlerInfo extends Deno.ServeHandlerInfo {
state: Record<PropertyKey, unknown>;
}
const composedHandler = composeHandlers<StateHandlerInfo>([
(_request, info, next) => next({ ...info, state: { foo: "bar" } }),
(_request, info) => Response.json(info.state),
]);
Deno.serve((request, info) => composedHandler(request, { ...info, state: {} })); |
This file is deliberately only for middleware-related APIs.
I agree. Done.
I don't want to go down a bikeshedding rabbit hole, but
I think the important question to answer is: "Is a context object a requirement here or can context be managed without adding a parameter to interface MiddlewareHandlerOptions<T> {
info?: Deno.ServeHandlerInfo;
context?: T;
}
export type MiddlewareHandler<T> = (
request: Request,
next: () => Response | Promise<Response>,
options?: MiddlewareHandlerOptions<T>,
) => Response | Promise<Response>; |
I think the context info can be stored somewhere else by using WeakMap keyed by request object. const contextMap = new WeakMap();
export function getContext(request: Request) {
return contextMap.get(request);
} const middleware1 = async (req, info, next) => {
const ctx = getContext(req);
...
}; |
Oh, I like that approach. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of exposing a composable middleware primitive in the std. One thing I'm wondering if the current signature proposed in this PR is useful enough. It allows you to call a bunch of functions in sequence, but the way it's designed here it's not possible to share state among them.
http/middleware.ts
Outdated
return (request, info) => { | ||
function compose(index: number): Response | Promise<Response> { | ||
if (index >= middlewares.length) { | ||
throw new RangeError("Middleware chain exhausted"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way the createHandler
function is designed here assumes that the last middleware never calls next()
. So in that sense it's fine, although it prevents composing createHandler
itself. Not sure if that is a goal here though.
const a = createHandler([m1, m2, m3]);
const b = createHandler([m1, m2, m3]);
// Not possible
const c = createHandler([a, b]);
If it had a state parameter, could you imagine it being something Fresh would use? |
I've added a state parameter. PTAL, @marvinhagemeister. I am still not 100% sure about having this in the Standard Library. This PR aims to explore the idea either way. The implementation does aim to be as plain and unopinionated as possible. @kitsonk, WDYT? |
PTAL @keithamus and @LionC |
http/middleware.ts
Outdated
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. | ||
|
||
/** Options for {@linkcode MiddlewareHandler}. */ | ||
export interface MiddlewareOptions<T = undefined> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once nice thing in https://github.com/denoland/deno_std/pull/1555/files#diff-d7730497c05f4870ef5336fe16c6eddb662c84d364b8ec55a1810a36a4901591R51 is that the Middleware was typed to allow for dependencies in the state which allowed for a very nice typing of middleware.
My view (#1295 (comment)) has not changed, and considering other things that are being deemed too frameworky around HTTP servers are being removed it seems that is the direction of travel... |
http/middleware.ts
Outdated
*/ | ||
export type MiddlewareHandler<T = undefined> = ( | ||
request: Request, | ||
info: Deno.ServeHandlerInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is from the Deno.serve API, but might it be cleaner to move it into either the options object or the options state object? Similar to what you suggest here #3785 (comment).
- It follows the deno_std guidelines "max 2 args, put the rest into an options object"
- Deno.ServeHandlerInfo isn't a very interesting object, no offence 😅, but it adds noise to the API when I can imagine most people will never use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm thinking that too.
Is it mandatory that every middleware in a chain has the same State type? Doesn't it make it difficult to reuse middlewares accross different chains/endpoints? |
In my draft (linked in the original post), they do not have the same type - they state what they require to already be in the context to work (so basically context inputs) and what they add to the context (context outputs) and the chaining function then composes them, combining the types correctly and in order. This makes them completely composable, as a combined chain is just a new middleware with those inputs and outputs again, and a listener is just a middleware with an empty context input. I think if we go Typescript first, we should honor order to catch errors, as it is just a function chain in the end. |
Thank you, Lion. Another question: the way this PR implements it, isn't there a chance of "state collision" between middlewares? Like, a middleware declares a |
Sorry, I replied in regards to how my original proposal and PoC PR implemented it (linked in the first post here as well), not related to this MR. |
Do you see a way to add this functionality to this PR's implementation? I'm open to suggestions. |
I will close this for now because this has gone stale and is not a priority right now. I'd be happy to look at a future PR that addresses some of the concerns mentioned here. |
createHandler()
creates a handler from a given middleware chain. This implementation aims to be plain, simple, unopinionated and align with the Deno runtime and Web APIs as much as possible.Hopefully, this can be used as a fundamental building block for web frameworks and spur the creation of middleware with a similar design philosophy.
Closes #1295