❤️‍🔥

Honoざっくりキャッチアップ(v4.6.0〜v4.10.0)

に公開

はじめに

こんにちは、@sugar235711です。

前回の記事から約1年が経ち、Honoはv4.6.0からv4.10.0までアップデートされているのでメモ書き程度に更新内容をまとめます。

https://github.com/honojs/hono/releases

https://hono.dev/

主要な変更点

この章では、v4.6.0以降の大きな変更点について解説します。

Contextual Storage Middlewareの追加によるContext伝搬

従来の方法

前回の記事では、リクエストごとにLoggerをContextに詰めて伝播させる方法を紹介しました。

import { Hono } from 'hono'

type Env = {
  Variables: {
    logger: Logger
  }
}

const app = new Hono<Env>()
app.use(async (c, next) => {
  const logger = new AppLogger()
  c.set('logger', logger)
  await next()
})

// 従来の方法
app.get('/hello', (c) => {
  const logger = c.get("logger")
  logger.info("Hello, Hono!")
  return c.text('Hello, Hono!')
})

AsyncLocalStorageを利用すればグローバルにContextにアクセスできましたが、その方法(contextStorage)が公式にサポートされています。
https://github.com/honojs/hono/blob/main/src/middleware/context-storage/index.ts

import { Hono } from 'hono'
import { contextStorage, getContext } from 'hono/context-storage'

type Env = {
  Variables: {
    logger: Logger
  }
}

const app = new Hono<Env>()

// Context Storage Middlewareを有効化
app.use(contextStorage())

app.use(async (c, next) => {
  const logger = new AppLogger()
  c.set('logger', logger)
  await next()
})

app.get('/hello', (c) => {
  // AsyncLocalStorageを利用してグローバルにContextにアクセス可能
  const logger = getContext<Env>().var.logger
  logger.info("Hello, Hono!")
  return c.text('Hello, Hono!')
})

JWT/JWK認証の強化

JWK Auth Middleware

JWK Auth Middlewareが追加され、JWT検証に公開鍵を使用できます。これにより、Auth0、Clerk等のIdPとの連携が容易になります。
一般的なJWKSに加えて匿名リクエストや署名付きCookie(RFC6265)の検証が内部的にサポートされています。

import { Hono } from 'hono'
import { jwk } from 'hono/jwk'

const app = new Hono()

app.use(
  '/auth/*',
  jwk({
    jwks_uri: 'https://your-auth-server/.well-known/jwks.json'
    headerName: 'x-custom-auth-header'
  })
)

app.get('/auth/page', (c) => {
  const payload = c.get('jwtPayload')
  return c.json(payload)
})

verifyWithJwksを使用するとJWTの検証がミドルウェア外でできるようになります。
ちなみにjwkミドルウェア内でこのメソッドが使用され検証されています。

https://github.com/honojs/hono/blob/4b796cfb0b105418bbf806050e788741f2739125/src/middleware/jwk/jwk.ts#L131-L138

JWKSのミドルウェアに指定したCookieはHelper(getSignedCookie)を利用して内部的に取得されています。ユーザー側もHelperを使用してCookieを直接扱うことができます。
https://hono.dev/docs/helpers/cookie

app.get('/signed-cookie', (c) => {
  const secret = 'secret'

  await setSignedCookie(c, 'cookie_name0', 'cookie_value', secret)
  const fortuneCookie = await getSignedCookie(
    c,
    secret,
    'cookie_name0'
  )
  deleteCookie(c, 'cookie_name0')
  const allSignedCookies = await getSignedCookie(c, secret)
  // ...
})

このCookie HelperはRFC6265bis-13CHIPS-01に準拠しており、下記に対応していない場合はエラーをthrowするようになっています。

  • maxAgeexpiresは最大400日まで
  • __Host-/__Secure-のプレフィックス及びPartitionedに対してのSecure属性の必須化

Proxy Helperによるリバースプロキシ実装

Proxy HelperはHonoをリバースプロキシとして使用するためのヘルパーです。
https://hono.dev/docs/helpers/proxy

従来の問題

単純にfetchを使用すると、意図しないヘッダーが送信される可能性があります。Proxy Helperを使用すると明示的にヘッダーを設定できます。

Proxy Helperの使用例

app.get('/proxy/:path', async (c) => {
  const res = await proxy(
    `http://${originServer}/${c.req.param('path')}`,
    {
      headers: {
        ...c.req.header(),
        'X-Forwarded-For': '127.0.0.1',
        'X-Forwarded-Host': c.req.header('host'),
        Authorization: undefined,  // 特定のヘッダーを除外
      },
    }
  )
  res.headers.delete('Set-Cookie')  // レスポンスヘッダーの削除
  return res
})

内部的にはcontent-encodingcontent-lengthヘッダーと、hop-by-hopヘッダーが自動的に削除されます。

- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authorization
- TE
- Trailers
- Transfer-Encoding
- Upgrade

実装的には少し古いRFCが参照されていましたが、RFC9110ではConnectionヘッダーに含まれるヘッダーも削除するように記載されているため、修正しています。
https://github.com/honojs/hono/pull/4459

Language Middlewareを使った言語判定

サーバーサイドでの言語判定が容易になりました。
https://hono.dev/docs/middleware/builtin/language

import { Hono } from 'hono'
import { languageDetector } from 'hono/language'

const app = new Hono()

app.use(
  languageDetector({
    supportedLanguages: ['en', 'ar', 'ja'],
    fallbackLanguage: 'en',
  })
)

app.get('/', (c) => {
  const lang = c.get('language')
  return c.text(`Hello! Your language is ${lang}`)
})

デフォルトでは下記で言語が検出されるようになっており、Optionを調節するとルールを変更できます。

  • クエリパラメータ(?lang=ja
  • Cookie(language
  • Acceptヘッダー(Accept-Language
  • パス(/ja/page

Route Helper

ルート情報に簡単にアクセスできるようになりました。

import { Hono } from 'hono'
import { getPath, getRoutes } from 'hono/route'

const app = new Hono()
const apiApp = new Hono()

apiApp.get('/posts/:id', (c) => {
  return c.json({
    routePath: routePath(c), // '/posts/:id'
    baseRoutePath: baseRoutePath(c), // '/api'
    basePath: basePath(c), // '/api' (with actual params)
  })
})

app.route('/api', apiApp)

parseResponse Utility

Hono RPCクライアントのレスポンス処理が簡単になりました。

import { parseResponse, DetailedError } from 'hono/client'
import { hc } from 'hono/client'

const client = hc<AppType>('http://localhost:3000')

// 従来の方法
const res = await client.hello.$get({ query: { name: 'John' } })
if (!res.ok) {
  throw new Error('Request failed')
}
const data = await res.json()


// parseResponseを使用
const result = await parseResponse(client.hello.$get({ query: { name: 'John' } }))
// result.message

エラーの場合はDetailedErrorがthrowされ、ステータスコードやレスポンスボディに型付きでアクセスできます。
https://github.com/honojs/hono/blob/4b796cfb0b105418bbf806050e788741f2739125/src/client/fetch-result-please.ts#L46-L75

また、RPCを使う際のミドルウェアの型推論も強化されミドルウェア上のレスポンスもinferできるようになったそうです。
https://github.com/honojs/hono/releases/tag/v4.10.0

cloneRawRequest Utility

リクエストオブジェクトをクローンできるUtilityが追加されています。

import { cloneRawRequest } from 'hono/request'

app.post('/api', async (c) => {
  const body = await c.req.json()

  // Clone the consumed request
  const clonedRequest = cloneRawRequest(c.req)
  await externalLibrary.process(clonedRequest)
})

内部的にはbodyが含まれている場合は、HonoRequest内部で持っているキャッシュの消費を確認し、RequestInitからで再構成しているようです。
https://github.com/honojs/hono/blob/5eb7c15bb11543fca0f2fb8ee0246d3cb8ec9c96/src/request.ts#L458-L487

セキュリティ対応まとめ

新規追加でできるようになったことを中心に解説します。

セキュリティヘッダー

NONCE属性のサポート

hono/secure-headersを使用してscriptstyle要素に対してNONCE属性を付与できるようにCSPが強化されました。
https://hono.dev/docs/middleware/builtin/secure-headers#nonce-attribute

import { secureHeaders, NONCE } from 'hono/secure-headers'
import type { SecureHeadersVariables } from 'hono/secure-headers'
type Variables = SecureHeadersVariables

const app = new Hono<{ Variables: Variables }>()
app.get(
  '*',
  secureHeaders({
    contentSecurityPolicy: {
      scriptSrc: [NONCE, 'https://allowed1.example.com'],
    },
  })
)

app.get('/', (c) => {
  return c.html(
    <html>
      <body>
        {/** contents */}
        <script
          src='/js/client.js'
          nonce={c.get('secureHeadersNonce')}
        />
      </body>
    </html>
  )
})

Permission Policyの設定

ブラウザの機能の制限を目的にsecureHeadersMiddlewarepermissionsPolicyが設定できるようになりました。
https://hono.dev/docs/middleware/builtin/secure-headers#setting-permission-policy

const app = new Hono()
app.use(
  '*',
  secureHeaders({
    permissionsPolicy: {
      fullscreen: ['self'], // fullscreen=(self)
      bluetooth: ['none'], // bluetooth=(none)
      payment: ['self', 'https://example.com'], // payment=(self "https://example.com")
      syncXhr: [], // sync-xhr=()
      camera: false, // camera=none
      microphone: true, // microphone=*
      geolocation: ['*'], // geolocation=*
      usb: ['self', 'https://a.example.com', 'https://b.example.com'], // usb=(self "https://a.example.com" "https://b.example.com")
      accelerometer: ['https://*.example.com'], // accelerometer=("https://*.example.com")
      gyroscope: ['src'], // gyroscope=(src)
      magnetometer: [
        'https://a.example.com',
        'https://b.example.com',
      ], // magnetometer=("https://a.example.com" "https://b.example.com")
    },
  })

CSRF保護の強化

CSRFでFetch MetadataSec-Fetch-Siteリクエストヘッダーを検証する実装がサポートされました。
https://hono.dev/docs/middleware/builtin/csrf#secfetchsite-string-string-function

app.use(
  csrf({
    secFetchSite: (secFetchSite, c) => {
      // Always allow same-origin
      if (secFetchSite === 'same-origin') return true
      // Allow cross-site for webhook endpoints
      if (
        secFetchSite === 'cross-site' &&
        c.req.path.startsWith('/webhook/')
      ) {
        return true
      }
      return false
    },
  })
)

Third Party Middleware

MCP

https://github.com/honojs/middleware/tree/main/packages/mcp
HTTP Streaming Transportに対応した形で実装されています。

User Agent based blocker

https://github.com/honojs/middleware/tree/main/packages/ua-blocker

User-Agentベースで特定のボットをブロックするミドルウェアです。
例えばai.robots.txtに基づいて生成されたUAのリストも合わせて提供されています。
https://github.com/honojs/middleware/blob/main/packages/ua-blocker/src/generated.ts

import { uaBlocker } from '@hono/ua-blocker'
import { aiBots } from '@hono/ua-blocker/ai-bots'
import { Hono } from 'hono'

const app = new Hono()

app.use(
  '*',
  uaBlocker({
    blocklist: aiBots,
  })
)
app.get('/', (c) => c.text('Hello World'))

export default app

OpenTelemetry

OpenTelemetryに関しては別記事で詳しく解説する予定です。

Standard Schema Validatorの登場

Third Partyのミドルウェアとして、Standard Schema Validatorが追加され、複数のバリデーションライブラリを統一的に扱えるようになりました。

https://standardschema.dev/

Standard Schema Specに準拠したライブラリ(Zod、Valibot、ArkType、TypeBox等)を同じインターフェースで使用できます。

import { sValidator } from '@hono/standard-validator'
import { type } from 'arktype'
import * as v from 'valibot'
import { z } from 'zod'

const aSchema = type({
  agent: 'string',
})

const vSchema = v.object({
  slag: v.string(),
})

const zSchema = z.object({
  name: z.string(),
})

const app = new Hono()

app.get(
  '/:slag',
  sValidator('header', aSchema),    // ArkType
  sValidator('param', vSchema),     // Valibot
  sValidator('query', zSchema),     // Zod
  (c) => {
    const headerValue = c.req.valid('header')
    const paramValue = c.req.valid('param')
    const queryValue = c.req.valid('query')
    return c.json({ headerValue, paramValue, queryValue })
  }
)

最終的にこれらのバリデータを置き換えて統合しようとする動きもあります。
https://github.com/honojs/middleware/issues/1241

このバリデータを利用したOpenAPIのAPIドキュメント生成のライブラリも提供されています。
https://hono.dev/examples/hono-openapi#hono-openapi

import { openAPIRouteHandler } from 'hono-openapi'

app.get(
  '/openapi',
  openAPIRouteHandler(app, {
    documentation: {
      info: {
        title: 'Hono API',
        version: '1.0.0',
        description: 'Greeting API',
      },
      servers: [
        { url: 'http://localhost:3000', description: 'Local Server' },
      ],
    },
  })
)

具体的な使い方に関してはHonoHubで公開されています。
https://honohub.dev/docs/openapi

まとめ

Honoのv4.6.0からv4.9.12までの主要な変更点をまとめました。

Discussion