12k
All articles

Eigene Hono-Middleware schreiben

Erstellen Sie Hono-Middleware mit typisierten Context-Variablen, Request-Timing, frühem Abbruch und Pfad-Scoping. Verstehen Sie next() und die Reihenfolge.

OpenReplay Team
OpenReplay Team
Eigene Hono-Middleware schreiben

Eine Hono-Middleware ist eine async-Funktion, die (c, next) entgegennimmt. Sie kann auf zwei Arten enden: Sie ruft next() mit await auf und gibt nichts zurück, wodurch der Request weitergereicht wird, oder sie gibt eine Response zurück – und damit endet der Request an dieser Stelle.

logger() und cors() werden als Factories aufgerufen, weshalb diese Signatur so lange verborgen bleibt, bis man selbst eine Middleware schreibt. Die erste funktioniert meist auf Anhieb. Die Schwierigkeiten beginnen einen Schritt später: dann nämlich, wenn ein Handler einen Wert liest, den die Middleware gesetzt hat, und TypeScript nicht sagen kann, welchen Typ dieser Wert hat.

Dieser Artikel baut Middleware von der Signatur aus auf: die beiden Phasen und ihre Ausführungsreihenfolge, ein Request-Timer, der beide Phasen benötigt, typisierte Context-Variablen, die ihre Typen behalten, sobald die Middleware in eine eigene Datei wandert, vorzeitiger Abbruch und Pfad-Scoping. Vorausgesetzt wird, dass bereits Routen laufen, wie in Getting started with Hono beschrieben. Wer von Express kommt, findet im Framework-Vergleich und in den Porting-Hinweisen genau das Terrain, hinter dem dieser Artikel ansetzt. Die Beispiele beziehen sich auf die auf npm veröffentlichte Hono-4.13.x-Linie.

Die wichtigsten Erkenntnisse

  • Eine Hono-Middleware ist (c, next): Code vor await next() läuft auf dem Hinweg, Code danach auf dem Rückweg.
  • Middleware läuft auf dem Hinweg in Registrierungsreihenfolge und auf dem Rückweg in umgekehrter Registrierungsreihenfolge – die zuerst registrierte Middleware sieht den Request also als Erste und die Response als Letzte.
  • createMiddleware() aus hono/factory mit einem Variables-Generic hält c und next typisiert, wenn die Middleware in eine eigene Datei wandert.
  • Wird eine Response zurückgegeben, ohne next() aufzurufen, endet der Request: Nichts, was danach registriert wurde, läuft noch.
  • Hono fängt Fehler aus Handlern und Middleware ab und leitet sie an onError() oder an einen 500er weiter – next() wirft also nie eine Exception.

Die Signatur und die zwei Arten, wie Middleware endet

Middleware nimmt den Context und die next-Funktion entgegen, und der Hono Middleware Guide lässt nur zwei Endungen zu. Sie kann die Kontrolle weiter nach unten in der Kette reichen, indem sie next() mit await aufruft und überhaupt nichts zurückgibt, oder sie kann den Request an Ort und Stelle beenden, indem sie eine eigene Response zurückliefert. Das andere Primitiv ist der Handler, der immer eine Response erzeugt – und ein einzelner Request erreicht immer nur einen davon.

import { Hono } from 'hono'

const app = new Hono()

app.use(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`)
  await next()
})

app.get('/', (c) => c.text('Hello!'))

export default app

Lässt man das await weg, behält die Funktion ihre Bedeutung, verliert aber ihre Reihenfolgengarantie – schreiben Sie daher immer await next().

Die Zwiebel: In welcher Reihenfolge läuft Hono-Middleware?

Code vor await next() läuft auf dem Hinweg, Code danach auf dem Rückweg. Genau deshalb kann eine einzige Funktion vor dem Handler einen Timer starten und nach ihm den verstrichenen Wert in einen Response-Header schreiben. Die Konzept-Dokumentation von Hono stellt diese Anordnung als Zwiebel dar: Jede Schicht umhüllt den Handler und kommt auf beiden Seiten davon einmal zum Zug. Middleware läuft auf dem Hinweg in Registrierungsreihenfolge und auf dem Rückweg in umgekehrter Registrierungsreihenfolge – die zuerst registrierte Middleware sieht den Request also als Erste und die Response als Letzte.

import { Hono } from 'hono'

const app = new Hono()

app.use(async (_, next) => {
  console.log('timing start')
  await next()
  console.log('timing end')
})
app.use(async (_, next) => {
  console.log('  auth start')
  await next()
  console.log('  auth end')
})
app.use(async (_, next) => {
  console.log('    flags start')
  await next()
  console.log('    flags end')
})

app.get('/', (c) => {
  console.log('      handler')
  return c.text('Hello!')
})

export default app

Ein Request erzeugt:

timing start
  auth start
    flags start
      handler
    flags end
  auth end
timing end

Die Einrückung steckt in den Log-Strings selbst und wird nicht von console.log erzeugt. Sie ist bewusst gesetzt, denn genau um die Verschachtelung geht es: Die Rückweg-Hälfte jeder Middleware wird von der Rückweg-Hälfte all dessen umhüllt, was vor ihr registriert wurde.

Ein Request-Timer, über beide Phasen hinweg geschrieben

Hono bringt eine Server-Timing-Middleware mit, die Server-Timing-Header aus hono/timing ausgibt – und die ist die richtige Wahl, wenn Ihnen dieses Format zusagt. Ein selbst geschriebener Timer bleibt dennoch die klarste Demonstration der zweiphasigen Ausführung, weil der Wert, den er meldet, in keiner der beiden Phasen allein existiert.

import { createMiddleware } from 'hono/factory'

export const responseTime = createMiddleware(async (c, next) => {
  const start = Date.now()
  await next()
  c.header('X-Response-Time', `${Date.now() - start}ms`)
})

Nach await next() existiert bereits eine Response, weshalb c.header() direkt in die Header dieser Response schreibt und nicht in vorbereitete Header. Dieselbe Middleware-Seite enthält einen Vorbehalt für Cloudflare Workers: Ein Timer ist dort an die letzte I/O-Operation gekoppelt statt an die tatsächlich verstrichene Zeit, weshalb die Zahlen in die Irre führen können.

Wie reicht man typisierte Werte die Kette hinunter?

Alles, was mit c.set() geschrieben wird, gehört zu genau einem Request und verschwindet mit ihm – ein Wert lässt sich also weder in den nächsten Request übertragen noch zwischen Requests teilen. Nachgelagerter Code liest ihn mit c.get('key') oder c.var.key wieder aus; beides ist auf der Context-API-Seite dokumentiert. Inline gesetzt, kommen die Werte untypisiert weiter unten an:

app.use(async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  await next()
})

app.get('/', (c) => c.json({ id: c.var.requestId })) // no type information

createMiddleware() aus hono/factory hält die Typen von c und next intakt, wenn eine Middleware in eine eigene Datei wandert, und ein mitgegebenes Variables-Generic macht jeden von der Middleware gesetzten Wert für die nachfolgenden Handler typsicher.

// middleware/request-id.ts
import { createMiddleware } from 'hono/factory'

export const requestId = createMiddleware<{
  Variables: { requestId: string }
}>(async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  await next()
})
// index.ts
import { Hono } from 'hono'
import { requestId } from './middleware/request-id'

const app = new Hono().use(requestId).get('/', (c) => {
  return c.json({ id: c.var.requestId }) // string
})

export default app

Für die Typisierung von Context-Variablen gibt es drei Mechanismen. Greifen Sie standardmäßig zur mittleren Zeile.

MechanismusWo der Typ sichtbar istKosten
new Hono<{ Variables }>()Jede Route dieser App-InstanzVon Hand gepflegter kombinierter Typ, an jeder Deklarationsstelle dupliziert
createMiddleware<{ Variables }>()Routen unterhalb dieser MiddlewareEin Generic pro Middleware
Augmentierung von ContextVariableMapJeder Context in der gesamten AnwendungTypisiert auch Routen, auf denen die Middleware nie lief

Augmentiert man ContextVariableMap, landet der Typ auf jedem Context der Anwendung – unabhängig davon, ob die Middleware, die den Wert setzt, auf dieser Route überhaupt je gelaufen ist. Dort, wo sie nicht lief, sieht c.get() weiterhin typisiert aus, während der Wert zur Laufzeit fehlt. Die Warnung in der Context-API-Dokumentation verdeutlicht das anhand zweier Routen: einer, die die Middleware nutzt, und einer, die es nicht tut. Beschränken Sie den Typ stattdessen auf die Middleware.

Wie bricht man ab, ohne next() aufzurufen?

Geben Sie eine Response zurück und überspringen Sie next() – der Request endet dort: keine spätere Middleware, kein Handler.

import { createMiddleware } from 'hono/factory'

export const apiKeyAuth = createMiddleware(async (c, next) => {
  if (c.req.header('X-API-Key') !== 'expected-key') {
    return c.text('Unauthorized', 401)
  }
  await next()
})

Alles, was nach dieser Middleware registriert wurde, läuft nie – einschließlich der Rückweg-Hälfte jeder Middleware, die sie umhüllt hätte. Die Rückweg-Hälften der davor registrierten Middleware laufen dagegen weiterhin, sodass ein zuerst registrierter Timer auch den 401er noch mit einem Stempel versieht.

Wie beschränkt man Hono-Middleware auf einen Pfad?

Übergeben Sie ein Pfadmuster als erstes Argument an app.use(), um eine Middleware auf passende Routen zu beschränken – und denken Sie daran, dass die Reihenfolge die Registrierungsreihenfolge ist.

app.use(responseTime)          // every route
app.use('/api/*', apiKeyAuth)  // only /api/**
app.get('/api/orders', (c) => c.json([]))

responseTime wird zuerst registriert, umhüllt also apiKeyAuth und misst auch abgewiesene Requests. Vertauscht man die beiden Zeilen, bekommt der Timer keine 401er mehr zu sehen.

Zwei Verhaltensweisen, die immer wieder überraschen

Ein Fehler, der irgendwo in der Kette geworfen wird – sei es von einem Handler oder von irgendeiner Middleware –, wird von Hono selbst abgefangen: Er geht an app.onError(), sofern vorhanden, und kommt andernfalls als 500er zurück. Deshalb wirft next() nie eine Exception, und deshalb bringt es auch nichts, es in try/catch zu verpacken. Nach await next() ist der Fehler weiterhin über c.error erreichbar, falls Sie ihn protokollieren möchten.

Typen summieren sich entlang einer .use()-Kette: Ein Handler, der hinter zwei typisierten Middlewares sitzt, sieht die Variablen aus beiden. Jedes .use() liefert eine Instanz zurück, deren Typ bereits alles Vorangegangene enthält – weshalb der Middleware-Guide festhält, dass die meisten Anwendungen nie einen kombinierten Env-Typ vorab ausformulieren müssen.

Wie es weitergeht

Custom Middleware ist eine Funktionsform mit zwei Hälften und einer Regel dazu, wie sie endet. Setzen Sie das Variables-Generic von der ersten Zeile an darauf, halten Sie jede Middleware in ihrem eigenen Modul, und die Registrierungsreihenfolge erledigt den Rest. Nehmen Sie den obigen Timer, verschieben Sie ihn nach middleware/ und ergänzen Sie ein Konfigurationsargument, indem Sie createMiddleware() in eine schlichte Funktion einpacken, die es zurückgibt: Das ist bereits das gesamte Muster für eine parametrisierte Middleware.

FAQs

Wie lasse ich eine Middleware nur auf einer einzigen Route statt auf allen Routen laufen?

Registrieren Sie sie über die Routen-Methode statt über ein nacktes app.use(). Hono erlaubt es, Middleware über app.HTTP_METHOD anzuhängen: app.post('/posts/*', basicAuth()) greift damit nur bei POST-Requests auf diesem Pfad, und wenn Sie die Middleware als Argument vor dem Handler übergeben, wie in app.get('/echo', echoMiddleware, handler), beschränkt sich ihre Wirkung auf genau diese eine Route, ohne den Rest der Anwendung zu berühren.

Gilt Middleware auch für Routen, die vor ihr registriert wurden?

Registrieren Sie Middleware oberhalb der Handler, die sie umhüllen soll. Hono führt Handler und Middleware in Registrierungsreihenfolge aus, und die Routing-Dokumentation formuliert die Regel unmissverständlich: Alles, was vor einem Handler laufen soll, muss vor ihm registriert werden. Eine Route, die vor dem app.use()-Aufruf deklariert wurde, wird zuerst gematcht, weshalb die später hinzugefügte Middleware nicht verlässlich Teil der Kette dieser Route ist.

Was ist der Unterschied zwischen Bindings und Variables im Env-Generic?

Bindings beschreiben Plattform-Ressourcen und Umgebungswerte, die die Runtime injiziert und die über c.env gelesen werden – etwa eine D1-Datenbank oder ein Secret auf Cloudflare Workers. Variables beschreiben die requestbezogenen Werte, die Ihr eigener Code mit c.set() schreibt und mit c.get() oder c.var liest. Beide sitzen auf demselben Env-Objekt, das als new Hono<{ Bindings: ...; Variables: ... }>() übergeben wird.

Kann ich eine Context-Variable außerhalb eines Handlers oder einer Middleware lesen?

Ja, mit der eingebauten Context-Storage-Middleware. Registrieren Sie app.use(contextStorage()) aus 'hono/context-storage' und rufen Sie dann getContext() innerhalb einer beliebigen Funktion auf, um an den Context des aktuellen Requests zu gelangen – inklusive c.var und, auf Cloudflare Workers, der Bindings auf c.env. tryGetContext() verhält sich genauso, gibt aber undefined zurück, wo getContext() eine Exception werfen würde, was sich für Code eignet, der möglicherweise außerhalb eines Requests läuft.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.