Skip to content

feat(hono): one fetch adapter, and Hono on top of it - #31

Merged
cport1 merged 1 commit into
mainfrom
feat/adapter-core
Aug 22, 2026
Merged

feat(hono): one fetch adapter, and Hono on top of it#31
cport1 merged 1 commit into
mainfrom
feat/adapter-core

Conversation

@cport1

@cport1 cport1 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #727 and #736.

The duplication (#736)

Express, Fastify and Next.js had each grown their own copy of the same decision tree: skip-path matching, monitor-versus-enforce, honeytoken arming, the 429 with a Retry-After, fail-open error handling.

Three copies is three places for the branch that matters to be subtly different — and it already had been. The leftmost-X-Forwarded-For bug (#725) survived in two adapters after the WordPress plugin had fixed the same class of bug, precisely because there was no one place to fix it.

createFetchGuard() is that tree written once, over WHATWG Request/Response:

const guard = createFetchGuard({ mode: 'enforce', rules: [tripwire()] });

export default {
  async fetch(request: Request): Promise<Response> {
    const { response } = await guard.check(request);
    if (response) return response;
    return guard.decorate(await handle(request));
  },
};

It's also the answer to "which framework do you support?". The issue's suggestion was to start with a generic Request recipe rather than adding packages speculatively, and that's what this is: Bun, Deno, Astro, Nitro, SvelteKit and Remix all work through it with no package at all, documented in the README.

I did not refactor Express/Fastify/Next onto it in this PR. Their honeytoken injection hooks into framework-specific response streaming — Express intercepts res.write/res.end, Fastify uses an onSend hook — and that machinery carries hard-won detail (the Angular SSR headersSent case, Content-Length correction). Rewriting it on top of a shared core is a behaviour-preserving refactor that deserves its own PR with those tests as the contract, not a rider on a feature. Filed as a follow-up rather than left implied.

Hono (#727)

Hono gets a real package because it has a middleware contract worth fitting, and because it's the default on Workers, Bun and Deno — the runtimes the rest of our stack already fronts. The Cloudflare edge sensor has been tagging every request it forwards and readEdgeVerdict() exists so the origin can act on that tag; there was no origin middleware there to do it.

app.use('*', webdecoy({ rules: [tripwire()], skipPaths: ['/health'] }));

c.get('webdecoy') carries the decision — in monitor mode, which is the default, that's the only place the verdict surfaces, so there's a test for it.

Honeytoken injection works through the fetch shape. The Express implementation needed response-stream interception to get there; reading and rewriting a Response is enough, so Hono got it for free — including Content-Length correction, which has its own test because a stale one truncates the body at the client.

Verification

12 new tests through a real Hono app via app.request() — the same fetch-shaped entry point Workers and Bun call, so this exercises the actual runtime contract rather than a mock. 406 tests total, 20/20 turbo tasks. check:edge now covers three entry points (core, Next.js, Hono); Hono is externalised in the gate alongside next/express/fastify.

Express, Fastify and Next.js had each grown their own copy of the same
decision tree: skip-path matching, monitor versus enforce, honeytoken
arming, the 429 with a Retry-After, fail-open error handling. Three copies
is three places for the branch that matters to be subtly different, and it
already had been -- the leftmost-X-Forwarded-For bug survived in two
adapters after the WordPress plugin fixed it.

createFetchGuard() is that tree written once, over WHATWG Request and
Response. It is also the answer to "which framework do you support": Bun,
Deno, Astro, Nitro, SvelteKit and Remix all hand you a Request and want a
Response, so they need a documented recipe rather than a package.

Hono gets a package because it has a middleware contract worth fitting,
and because it is the default on Workers, Bun and Deno -- the runtimes the
rest of our stack already fronts. The Cloudflare edge sensor has been
tagging every request it forwards and readEdgeVerdict() has existed so the
origin can act on that tag; there was no origin middleware there to do it.

Honeytoken injection works through the fetch shape too, which the Express
implementation needed response-stream interception to achieve. Reading and
rewriting a Response is enough, so Hono got it for free.

check:edge now covers three entry points.

Closes WebDecoy/app#727
Closes WebDecoy/app#736
@cport1
cport1 merged commit 184ef07 into main Aug 22, 2026
2 checks passed
@cport1
cport1 deleted the feat/adapter-core branch August 22, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant