Honoざっくりキャッチアップ(v4.6.0〜v4.10.0)
はじめに
こんにちは、@sugar235711です。
前回の記事から約1年が経ち、Honoはv4.6.0からv4.10.0までアップデートされているのでメモ書き程度に更新内容をまとめます。
主要な変更点
この章では、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
)が公式にサポートされています。
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
ミドルウェア内でこのメソッドが使用され検証されています。
JWKSのミドルウェアに指定したCookieはHelper(getSignedCookie
)を利用して内部的に取得されています。ユーザー側もHelperを使用して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-13とCHIPS-01に準拠しており、下記に対応していない場合はエラーをthrowするようになっています。
-
maxAge
とexpires
は最大400日まで -
__Host-
/__Secure-
のプレフィックス及びPartitioned
に対してのSecure属性の必須化
Proxy Helperによるリバースプロキシ実装
Proxy HelperはHonoをリバースプロキシとして使用するためのヘルパーです。
従来の問題
単純に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-encoding
とcontent-length
ヘッダーと、hop-by-hop
ヘッダーが自動的に削除されます。
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authorization
- TE
- Trailers
- Transfer-Encoding
- Upgrade
実装的には少し古いRFCが参照されていましたが、RFC9110ではConnectionヘッダーに含まれるヘッダーも削除するように記載されているため、修正しています。
Language Middlewareを使った言語判定
サーバーサイドでの言語判定が容易になりました。
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され、ステータスコードやレスポンスボディに型付きでアクセスできます。
また、RPCを使う際のミドルウェアの型推論も強化されミドルウェア上のレスポンスもinferできるようになったそうです。
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
からで再構成しているようです。
セキュリティ対応まとめ
新規追加でできるようになったことを中心に解説します。
セキュリティヘッダー
NONCE属性のサポート
hono/secure-headers
を使用してscript
やstyle
要素に対してNONCE属性を付与できるようにCSPが強化されました。
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の設定
ブラウザの機能の制限を目的にsecureHeaders
のMiddleware
でpermissionsPolicy
が設定できるようになりました。
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 Metadata
のSec-Fetch-Site
リクエストヘッダーを検証する実装がサポートされました。
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
HTTP Streaming Transportに対応した形で実装されています。
User Agent based blocker
User-Agentベースで特定のボットをブロックするミドルウェアです。
例えばai.robots.txtに基づいて生成されたUAのリストも合わせて提供されています。
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が追加され、複数のバリデーションライブラリを統一的に扱えるようになりました。
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 })
}
)
最終的にこれらのバリデータを置き換えて統合しようとする動きもあります。
このバリデータを利用したOpenAPIのAPIドキュメント生成のライブラリも提供されています。
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で公開されています。
まとめ
Honoのv4.6.0からv4.9.12までの主要な変更点をまとめました。
Discussion