Skip to content

Tracker: Hono on Perry — runtime-agnostic app.fetch contract working end-to-end #1655

Description

@proggeramlug

Goal

Hono's runtime-agnostic value proposition is that the same (req: Request) => Promise<Response> code runs unchanged on Workers, Bun, Deno, Node, Lambda. We want Perry to be one of those runtimes — same Hono app, swap only the adapter import. Today the contract breaks at multiple layers; this tracker pins down the precise set of issues to close and the acceptance test for "done".

Current state, mapped

Layer Status Tracking
Hono compiles via compilePackages: ["hono"] ✅ done #421 (closed v0.5.613)
new Hono(), app.get/app.post/etc. route registration ✅ done (covered by #421)
app.fetch(req) dispatches into route handlers ✅ partial — runs, but throws on first header access #1649
new Request(url, { headers })req.headers is a Headers instance ❌ returns numeric handle #1649
new Response(body)res.text() / res.arrayBuffer() ✅ works for string bodies
new Response(body)res.body is ReadableStream | null ❌ returns numeric handle #1650
hono/jsx<Component/> evaluates to a JSXNode; c.html(<Comp/>) renders ❌ evaluates to undefined #1653
hono/jsx/server, hono/jsx/streaming module imports ❌ throw undefined (covered by #1653)
HTTP listener path A — @hono/node-server works unchanged ❌ blocked on node:http2 exports + node:http link gap #1651, #1652
HTTP listener path B — @hono/perry-server adapter exists ❌ not shipped (deferred from #487) #1654
HTTP listener path C — perry-ext-fastify direct ✅ works (already shipped)
Streaming via hono/streaming ❌ depends on Response.body fix (covered by #1650)
WebSocket upgrade via Server.on('upgrade', …) ✅ shipped per #577 Phase 4; not exercised by Hono

Decision: which HTTP listener does the architecture commit to?

Two viable end-states; tracker leaves both open and recommends shipping both.

Path B is the closer-in deliverable (smaller scope, Perry-org owns it end-to-end). Path A is the longer-term "no Perry-specific import" win.

Out of scope (explicitly)

  • Dynamic imports (await import(...)) — call-site workarounds exist; not on the critical path for the runtime-agnostic Hono contract.
  • Full Express compatibility shim — explicitly out per Compile hono end-to-end via perry.compilePackages #421; Hono + Fastify cover the modern TS backend space, full Express is a tarpit.

Acceptance for "Hono on Perry is done"

Single 30-line program serves real HTTP traffic, identical to Node:

```typescript
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { serve } from '@hono/perry-server' // ← only Perry-specific line

const Layout = (props: { title: string; children: any }) => (

<title>{props.title}</title>{props.children} )

const app = new Hono()
app.use('*', logger())
app.get('/', (c) => c.html(

hello

))
app.get('/api/echo', (c) => c.json({ q: Object.fromEntries(new URL(c.req.url).searchParams) }))
app.post('/api/echo', async (c) => c.json({ body: await c.req.json() }))

serve({ fetch: app.fetch, port: 3000 }, ({ port }) => {
console.log(`listening :${port}`)
})
```

```bash
perry compile app.tsx -o app
./app &
curl -sS http://127.0.0.1:3000/ # → real HTML
curl -sS http://127.0.0.1:3000/api/echo?k=v # → {"q":{"k":"v"}}
curl -sS -X POST -H 'content-type: application/json' -d '{"x":1}'
http://127.0.0.1:3000/api/echo # → {"body":{"x":1}}
```

The same source file, with import { serve } from '@hono/node-server' instead, must produce byte-for-byte identical responses under node --import tsx app.tsx. That's the acceptance bar — runtime-agnostic in fact, not in marketing.

Sub-issues

Related (already closed)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions