Last active
July 31, 2025 13:55
-
-
Save timkelty/8809fb0c0cbc9e840fa421376ab28a8b to your computer and use it in GitHub Desktop.
Hono ESI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 returnedapp.fetch
, so I tried to follow a similar pattern.However, this implementation currently gives me:
Am I on the right track, or is there another way to do this?