Hono Context / Request の仕組みと フレームワーク間の互換性
Published on 2026年4月4日
技術はじめに
Hono + Next.js + Auth0 で認証ミドルウェアを実装していたとき、c.req.raw as NextRequest というキャストに違和感を覚えた。動いてはいるが、なぜ c.req を直接渡せないのか、なぜ .raw を経由する必要があるのか——よく分かっていなかった。
そもそも、Web標準の Request オブジェクトが各フレームワーク間でどう扱われているのか、フレームワークごとの Request 型がどのような関係にあるのかを深く考えたことがなかった。HonoRequest、NextRequest、Web標準 Request——それぞれが何を担っていて、どう繋がっているのか。
この記事は、ミドルウェアの型エラーを正しく解消する過程で、HonoRequest / NextRequest / Web標準 Request の関係からフレームワーク間の互換性の仕組みまでを掘り下げた調査ノートである。
前提: この調査が発生した経緯
個人開発のミドルウェア requireSessionMiddleware で、当初以下のようにキャスト (as) を使った実装をしていた。
// Before: キャストで型を無理やり合わせていた
export const requireSessionMiddleware = createMiddleware(async (c, next) => {
const session = await auth0.getSession(c.req.raw as NextRequest);
if (!session) {
const loginUrl = new URL("/auth/login", c.req.url);
return NextResponse.redirect(loginUrl);
}
return next();
});
c.req.raw は Web標準の Request 型であり、NextRequest ではない。
as NextRequest は型チェックを黙らせているだけで、実際には cookies や nextUrl 等のプロパティが存在しない状態だった。
as キャストの危険性
as は TypeScript のコンパイル時にだけ作用し、実行時のオブジェクトは何も変わらない。
コンパイル時(TypeScript):
c.req.raw as NextRequest → 「NextRequest型として扱ってOK」と判断
実行時(JavaScript):
c.req.raw → 中身はただの Request のまま。何も変わらない
const req = c.req.raw as NextRequest;
req.nextUrl; // TypeScript: エラーなし / 実行時: undefined(Requestに存在しない)
req.cookies; // TypeScript: エラーなし / 実行時: undefined(Requestに存在しない)
つまり、auth0.middleware や auth0.getSession の内部で req.nextUrl や req.cookies にアクセスした場合:
// auth0 の内部で起きうること
req.cookies.get("session"); // → cookies が undefined → ランタイムエラー
req.nextUrl.pathname; // → nextUrl が undefined → ランタイムエラー
動いているように見えても、たまたまそのコードパスを通っていないだけで、 ライブラリのアップデートや別のコードパスで突然壊れるリスクがある。
as と new の違い
// as: 型だけ変える。実行時のオブジェクトは Request のまま
auth0.middleware(c.req.raw as NextRequest);
// → cookies, nextUrl は実行時に存在しない
// new: 実際に NextRequest インスタンスを生成する
auth0.middleware(new NextRequest(c.req.raw));
// → cookies, nextUrl が実際に存在する本物の NextRequest
as NextRequest |
new NextRequest() |
|
|---|---|---|
| TypeScript の型 | NextRequest | NextRequest |
| 実行時のオブジェクト | Request のまま | 本物の NextRequest |
cookies |
存在しない | 存在する |
nextUrl |
存在しない | 存在する |
| 安全性 | ランタイムエラーのリスク | 安全 |
この実装を正しく修正するにあたり:
// After: 型の整合性を正しく保った実装
export const requireSessionMiddleware = createMiddleware(async (c, next) => {
const req = new NextRequest(c.req.raw);
const session = await getSessionFromRequest(req);
if (!session) {
const loginUrl = new URL("/auth/login", c.req.url);
return NextResponse.redirect(loginUrl);
}
return next();
});
- なぜ
c.req.rawを経由する必要があるのか - HonoRequest と NextRequest の関係は何か
- フレームワーク間の型の互換性はどう成り立っているのか
これらを理解するために、Request の仕組みを掘り下げた。
1. HonoRequest の役割
Web標準の Request オブジェクトをラップして、便利メソッドを追加したもの。
ブラウザからのリクエスト
↓
Web標準 Request (c.req.raw でアクセス可能)
↓
HonoRequest がラップ (c.req)
↓
便利メソッド (.query(), .param(), .header(), .json() 等)
Web標準 Request の不便な点
// Web標準 Request でクエリパラメータを取るのは冗長
const url = new URL(request.url);
const q = url.searchParams.get("q");
// body は1回しか読めない(ReadableStream)
const body = await request.json();
const body2 = await request.json(); // エラー!ストリーム消費済み
HonoRequest なら簡潔に書ける
c.req.query("q"); // クエリパラメータ
c.req.param("id"); // パスパラメータ(ルーターと連携)
c.req.header("cookie"); // ヘッダー
// 何度でも読める(bodyCacheでキャッシュしてくれる)
await c.req.json();
await c.req.json(); // OK
bodyCache の仕組み
1回目: await c.req.json() → Request.body を読む → キャッシュに保存 → 返す
2回目: await c.req.json() → キャッシュから返す(再パースなし)
Web標準の Request.body はストリームなので1回しか読めない。
HonoRequest はこれをキャッシュで解決している。
2. c.req.raw が保持している情報
console.log(c.req.raw) で出力される全データ:
c.req.raw (= Web標準 Request)
├── url: "http://localhost:3000/dashboard" ← リクエストURL
├── method: "GET" ← HTTPメソッド
├── headers: Headers { ... } ← ヘッダー(cookie含む)
├── body: ReadableStream | null ← リクエストボディ
├── bodyUsed: false ← ボディ消費済みフラグ
├── cache: "default" ← キャッシュモード
├── credentials: "same-origin" ← 資格情報モード
├── destination: "" ← リクエスト先の種別
├── integrity: "" ← SRI用ハッシュ
├── keepalive: false ← 接続維持フラグ
├── mode: "cors" ← CORSモード
├── redirect: "follow" ← リダイレクト処理
├── referrer: "about:client" ← リファラー
├── referrerPolicy: "" ← リファラーポリシー
└── signal: AbortSignal { ... } ← リクエスト中断用
ブラウザから来た生のHTTP情報をすべてインスタンス内に保持している。
3. NextRequest と HonoRequest の関係
持ち方の違い
Web標準 Request(HTTP情報の本体)
/ \
NextRequest HonoRequest
extends で持つ(is-a) raw プロパティで持つ(has-a)
├── cookies ├── param()
├── nextUrl ├── query()
└── Next.js用 └── Hono用
| 関係 | 意味 |
|---|---|
| is-a (NextRequest extends Request) | Requestである → そのまま渡せる |
| has-a (HonoRequest.raw: Request) | Requestを持っている → .raw で取り出す必要がある |
NextRequest のコンストラクタ
NextRequest は様々な入力を受け付ける:
new NextRequest("http://localhost:3000/dashboard"); // string
new NextRequest(new URL("http://localhost:3000")); // URL
new NextRequest(c.req.raw); // Request
c.req.raw を経由する理由
new NextRequest(c.req); // NG: HonoRequest は Request型ではない
new NextRequest(c.req.raw); // OK: Request型なので渡せる
NextRequest のコンストラクタが Request を受け取るようになっているため、
c.req.raw で Request を取り出して渡す。
NextRequest が Request に追加する機能
c.req.raw (Request)
├── url, method, headers, body ...
└── (Web標準の機能のみ)
↓ new NextRequest(c.req.raw)
NextRequest(Requestを継承して拡張)
├── url, method, headers, body ... ← 元のまま引き継ぎ
├── cookies: RequestCookies { ... } ← 追加:パース済みcookie
└── nextUrl: NextURL { ... } ← 追加:パース済みURL
4. フレームワーク間の互換性
Web標準 Request が「共通言語」として機能する
// Hono → Next.js
const nextReq = new NextRequest(c.req.raw); // Request で橋渡し
// Hono → Auth0
auth0.middleware(c.req.raw); // Request で橋渡し
Web標準の Request / Response をサポートしているフレームワーク・ライブラリ同士であれば、c.req.raw を経由するだけでデータを受け渡せる。
Web標準に準拠していないフレームワークの場合
独自のRequest型を持つフレームワークでは、自分でWeb標準 Request に変換する必要がある。
// 例: 独自のRequest型を持つフレームワーク
interface WeirdRequest {
uri: string;
httpMethod: string;
headerMap: Map<string, string>;
}
// 自分で Web標準 Request に変換する必要がある
const req = new Request(weirdReq.uri, {
method: weirdReq.httpMethod,
headers: Object.fromEntries(weirdReq.headerMap),
});
new NextRequest(req); // これならOK
実際に Express がこのケースに該当する:
// Express の req は Web標準 Request ではない独自オブジェクト
app.get("/", (req, res) => {
req.params; // Express独自
req.query; // Express独自
req.body; // Express独自
// → new NextRequest(req) はできない
});
5. なぜ Express は Web標準を採用していないのか
歴史的経緯
Express (2010年〜) は Fetch API (2015年〜) より約5年早く生まれた。
当時は http.IncomingMessage が Node.js の唯一の選択肢だった。
Express が今も Web標準を採用しない理由
1. 後方互換性の重さ
- npm で週数億ダウンロードの巨大なエコシステム
req.params/req.query/req.bodyなどの独自APIに依存するコードが膨大- 破壊的変更が事実上不可能
2. Node.js の http モジュールとの密結合
// Express の Request は IncomingMessage を継承している
class Request extends http.IncomingMessage { ... }
Web標準の Request に乗り換えるには、http モジュールとの密結合を解消する必要があり、現実的ではない。
3. Hono / Deno / Bun が Web標準を採用できた理由
- Node.js の
httpモジュールを使わない独自ランタイム(Deno / Bun) - または最初から Web標準前提で設計された後発フレームワーク(Hono)
- レガシーの互換性制約がなかった
6. Web標準 Request 以前の世界
「Request」が共通概念ではなかった時代
ブラウザ → XMLHttpRequest / fetch (2015年〜)
Node.js → http.IncomingMessage / http.ClientRequest で独自実装
Express → IncomingMessage を拡張した独自 Request
→ 共通の「Request」という概念が存在しなかった
各環境がバラバラに独自実装していた。
環境をまたぐには変換が必要だった
ブラウザと Node.js で同じコードを動かしたいとき(isomorphic / universal と呼ばれていた):
// 当時よく使われたパターン
if (typeof window !== "undefined") {
// ブラウザ用: XHRを使う
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
} else {
// Node.js用: http.requestを使う
http.request({ host: "..." }, callback);
}
axios や node-fetch のようなライブラリが流行ったのもこれが理由で、**「環境差を吸収するラッパー」**として需要があった。
タイムライン
| 時期 | 状況 |
|---|---|
| ~2015年 | 共通の Request 概念が存在しない。各環境が独自実装 |
| 2015年~ | Fetch API 仕様策定。ブラウザに Request / Response が標準搭載 |
| 2018年~ | Deno が Fetch API をランタイムレベルでサポート |
| 2020年~ | Cloudflare Workers が Fetch API ベースで動作 |
| 2022年~ | Node.js 18 で fetch が標準搭載。Bun も Fetch API をサポート |
| 現在 | Web標準 Request / Response が「共通言語」に。Hono はこの流れの上で設計されたフレームワーク |
まとめ
この問題の本質は、ランタイムエラーの話である前に静的解析の話だ。
TypeScript が c.req.raw を NextRequest として受け付けないのは、両者の型が実際に異なるからであり、それは正しいシグナルである。as キャストはそのシグナルを握り潰すアンチパターンで、型の不一致をコンパイル時に隠蔽したまま実行時まで持ち込む。SDKの内部実装が cookies や nextUrl にアクセスした瞬間に初めて壊れる、というのはその結果に過ぎない。
正しい対処は、TypeScript が検知した型の不一致を new NextRequest(c.req.raw) による適切な変換で解消することだ。これが成立するのも、Web標準の Request がフレームワーク間の「共通言語」として機能しているからである——HonoRequest は Web標準 Request を .raw で持ち(has-a)、NextRequest は Web標準 Request を継承している(is-a)。この構造があるからこそ、c.req.raw を橋渡しにして正しく変換できる。
TypeScript が型の不一致を検知する
→ as で黙らせる(アンチパターン)
→ new による正しい変換で解消する(正しい対処)
└── Web標準 Request が共通言語として機能しているから成立する