Skip to content
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

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions http/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
export * from "./cookie.ts";
export * from "./cookie_map.ts";
export * from "./etag.ts";
export * from "./unstable_middleware.ts";
export * from "./status.ts";
export * from "./negotiation.ts";
export * from "./server.ts";
Expand Down
72 changes: 72 additions & 0 deletions http/unstable_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

/** Options for {@linkcode MiddlewareHandler}. */
export interface MiddlewareOptions<T = undefined> {
/** Information for a HTTP request. */
info: Deno.ServeHandlerInfo;
/** Calls the next middleware handler in the middleware chain. */
next: () => Response | Promise<Response>;
/** State that gets passed down through each middleware handler. */
state: T;
}

/**
* Middleware handler which extends {@linkcode Deno.ServeHandlerInfo}. Used
* in {@linkcode composeMiddleware}.
*/
export type MiddlewareHandler<T = undefined> = (
request: Request,
options: MiddlewareOptions<T>,
) => Response | Promise<Response>;

/**
* Creates a {@linkcode Deno.ServeHandler} from the given middleware chain,
* which can then be passed to {@linkcode Deno.serve}.
*
* @param middlewares Middleware chain
*
* @example
* ```ts
* import {
* type MiddlewareHandler,
* composeMiddleware,
* } from "https://deno.land/std@$STD_VERSION/http/unstable_middleware.ts";
*
* const middleware1: MiddlewareHandler = async (_request, { next }) => {
* const start = performance.now();
* const response = await next();
* const duration = performance.now() - start;
* response.headers.set("x-request-time", duration.toString());
* return response;
* };
*
* const middleware2: MiddlewareHandler = (request) => {
* return Response.json({ request });
* };
*
* const handler = composeMiddleware([middleware1, middleware2])
* ```
*/
export function composeMiddleware<T = undefined>(
middlewares: MiddlewareHandler<T>[],
initialState?: T,
): Deno.ServeHandler {
return (request, info) => {
const state = initialState as T;

function chainMiddleware(index: number): Response | Promise<Response> {
if (index >= middlewares.length) {
throw new RangeError("Middleware chain exhausted");
}
return middlewares[index](
request,
{
info,
next: () => chainMiddleware(index + 1),
state,
},
);
}
return chainMiddleware(0);
};
}
73 changes: 73 additions & 0 deletions http/unstable_middleware_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import { assertEquals, assertRejects } from "../assert/mod.ts";
import {
composeMiddleware,
type MiddlewareHandler,
} from "./unstable_middleware.ts";

const info: Deno.ServeHandlerInfo = {
remoteAddr: { transport: "tcp", hostname: "foo", port: 200 },
};

Deno.test("composeMiddleware() chains middlewares in order", async () => {
type State = number[];

const middleware1: MiddlewareHandler<State> = async (
_request,
{ next, state },
) => {
state.push(1);
const response = await next();
response.headers.set("X-Foo-1", "Bar-1");
return response;
};

const middleware2: MiddlewareHandler<State> = async (
_request,
{ next, state },
) => {
state.push(2);
const response = await next();
response.headers.set("X-Foo-2", "Bar-2");
return response;
};

const finalMiddleware: MiddlewareHandler<State> = (
_request,
{ state },
) => {
assertEquals(state, [1, 2]);
return new Response();
};

const handler = composeMiddleware<State>([
middleware1,
middleware2,
finalMiddleware,
], []);
const request = new Request("http://localhost");
const response = await handler(request, info);

assertEquals(response.status, 200);
assertEquals(response.headers.get("X-Foo-1"), "Bar-1");
assertEquals(response.headers.get("X-Foo-2"), "Bar-2");
});

Deno.test("composeMiddleware() throws when next() is called incorrectly", async () => {
const finalMiddleware: MiddlewareHandler = async (
_request,
{ next },
) => {
await next();
return new Response();
};

const handler = composeMiddleware([finalMiddleware]);
const request = new Request("http://localhost");
await assertRejects(
async () => await handler(request, info),
RangeError,
"Middleware chain exhausted",
);
});