Skip to content

Instantly share code, notes, and snippets.

@timkelty
Last active July 31, 2025 13:55
Show Gist options
  • Save timkelty/8809fb0c0cbc9e840fa421376ab28a8b to your computer and use it in GitHub Desktop.
Save timkelty/8809fb0c0cbc9e840fa421376ab28a8b to your computer and use it in GitHub Desktop.
Hono ESI
import { Hono, MiddlewareHandler } from 'hono';
import { getContext } from "hono/context-storage";
import esi from "cloudflare-esi";
import { type CloudflareBindings, type ContextVariables } from "../types";
import { createMiddleware } from 'hono/factory'
type OriginRequestOptions = {
app: Hono<any, any, any>,
middleware: MiddlewareHandler<any, any, any>[],
}
export const handleEsi = (options: OriginRequestOptions) => createMiddleware(async (c, next) => {
await next();
c.res = await new esi(
undefined,
undefined,
createEsiFetcher(options),
).parse(c.req.raw.clone());
});
const isOriginRequest = (request: RequestInfo): boolean => {
const c = getContext<{
Bindings: CloudflareBindings;
Variables: ContextVariables;
}>();
const kvData = c.get("kvData");
const url = new URL(request instanceof Request ? request.url : request.toString());
return kvData.hostnames.includes(url.hostname);
};
const createEsiFetcher = (options: OriginRequestOptions) => {
return async function(input: RequestInfo, esiContext: Request[]): Promise<Response> {
const c = getContext<{
Bindings: CloudflareBindings;
Variables: ContextVariables;
}>();
const { app, middleware = [] } = options;
const request = new Request(input);
middleware.forEach((middleware) => app.use(middleware));
const response = isOriginRequest(request)
? await app.fetch(await createOriginRequest(request), {
// Forward all request data to ESI requests
...c.req,
})
: await app.fetch(request);
// TODO: collect `hono/timing` data from response in Cloudflare WAE
return response;
};
};
const createOriginRequest = async function (
input: RequestInfo | URL | string,
options?: RequestInit,
): Promise<Request> {
const c = getContext<{
Bindings: CloudflareBindings;
Variables: ContextVariables;
}>();
const request = new Request(input, options);
const requestUrl = new URL(request.url);
const originRequestUrl = new URL(
requestUrl.pathname + requestUrl.searchParams,
c.get("kvData").originUrl,
);
const { environmentId } = c.get("kvData");
const requestHeaders = Object.fromEntries(request.headers.entries());
const originRequest = new Request(originRequestUrl, {
...request,
headers: Object.assign(requestHeaders, {
'x-forwarded-for': request.headers.get('cf-connecting-ip') || null,
'x-forwarded-host': requestUrl.hostname,
}),
cf: {
cacheTags: [
environmentId,
],
},
})
return originRequest;
}
import * as Sentry from "@sentry/cloudflare";
import { Hono } from "hono";
import { contextStorage } from "hono/context-storage";
import { logger } from "hono/logger";
import { timeout } from "hono/timeout";
import { cache } from "hono/cache";
import { timing } from "hono/timing";
import { handleEsi } from './middleware/handle-esi';
import { type ContextVariables, type CloudflareBindings } from "./types";
const maxDurationMs = 60000;
const maxBytes = 6 * 1024 * 1024;
const app = new Hono<{
Bindings: CloudflareBindings;
Variables: ContextVariables;
}>();
app.use(contextStorage());
app.use(logger());
app.use(timeout(maxDurationMs));
app.use(sizeLimit(maxBytes));
app.use(
handleEsi({ app, middleware: [
timing(),
cache({
cacheName: "origin-cache",
}),
]}));
app.use(timing());
export default app;
@timkelty
Copy link
Author

This is a simplified/distilled example of what I'm trying to do: I want to apply middleware to recursively called subrequests (from ESI parsing).

I saw that the method override middleware accepted app and arg and returned app.fetch, so I tried to follow a similar pattern.

However, this implementation currently gives me:

Error: Can not add a route since the matcher is already built.
    at SmartRouter.add (Users/timkelty/Dev/cloud-gateway-worker/node_modules/hono/dist/router/smart-router/router.js:12:13)

Am I on the right track, or is there another way to do this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment