【Hono + Next.js】Vercel or BunでIPアドレスを取得する
何がしたいか
- Next.js + HonoでWebアプリを開発している
- Honoはアダプターを使うことでNext.js上で動かしている
- 本番環境: Vercelでデプロイ
- 開発環境(ローカル):
- BunでNext.jsの開発サーバーを動かしている
- Next.jsに依存しない、HonoのみのAPIサーバーもBun上で動かせる
このWebアプリで、バックエンドであるHonoのコードからIPアドレスを取得することが目標です。
HonoにおけるIPアドレス
HonoでIPアドレスを取得するにはConnInfoヘルパーを用います。
ConnInfoはconnection infoの略で、getConnInfo関数を呼び出すと取得できます。
import { getConnInfo } from 'hono/bun'
// ...
app.get('/', (c) => {
// IPアドレスを取得
const address = getConnInfo(c).remote.address
// ...
})
getConnInfoをインポートするモジュールは、Honoを動作させる環境によって変える必要があります。
以下は一例です。(一覧はこちら)
| 環境 | インポート元のモジュール |
|---|---|
| Bun | hono/bun |
| Vercel | hono/vercel |
| Node.js | @hono/node-server/conninfo |
例えばBunで動作されせる場合はimport { getConnInfo } from 'hono/bun'と、Vercelで動作させる場合はimport { getConnInfo } from 'hono/vercel'と書く必要があります。
なぜgetConnInfoが環境別に用意されているかというと、環境によってIPアドレスの取得方法が異なるからです。
単純な環境ならどれを使えばいいかは一目瞭然ですが、今回のようにNext.jsとHonoを併用する場合は一見しただけだとわからないと思います。
ということで、何が使えるかを知るためにConnInfoの実装を見ていきます。
Vercelの場合
Vercelでは、リクエスト元のIPアドレスがx-real-ipヘッダーに含まれています。
そのためgetConnInfoの実装も単純で、このヘッダーから抜き出しているだけです。
しかし、これはあくまでVercelの仕様であって、BunやNext.jsの仕様ではありません。
そのためhono/vercelはVercel上でしか使用できません。Next.jsを使っているかは関係ないので注意が必要です。
Bunの場合
Bunにはexport default syntaxという構文があります。
これはBun.serveを呼び出す代わりにfetchメソッドがあるオブジェクトをexport defaultすることで、それを実行可能ファイルとしてみなすというものです。
例えば、以下のコードでサーバーを立てる場合を考えます。
import { Hono } from 'hono'
import { getConnInfo } from 'hono/bun'
const app = new Hono()
app.get(c => {
const address = getConnInfo(c).remote.address
return c.json({ address }) // IPアドレスを返す
})
export default app // export default syntax
bun index.ts
Honoインスタンスのfetchメソッドは、第二引数にc.envとなるオブジェクトを受け取ります。
これはBunによって、requestIP関数を含むオブジェクトを第二引数として呼び出されます。
するとc.env.requestIPに関数が入り、HonoのgetConnInfoはこれを呼び出すことでIPアドレスを取得しています。
// Bunが呼び出す
app.fetch(req, {
requestIP() {/* Bunによる実装 */}
})
// Honoによるヘルパーの実装
const getConnInfo = () => c.env.requestIP()
実際のgetConnInfoの実装はこちらをご覧ください。
なお、BunによるrequestIPの実装はこちらです。
コードはZigで書かれています。
どうやって取得しているのか気になったのですが、私には読めませんでした...
Next.jsの場合
HonoをNext.jsのRoute Handlersとして動かす場合、通常はアダプターを使うと思います。
例えばこんな感じです。
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => {
return c.json({ message: 'Hello from Hono!' })
})
// アダプターを呼び出す
export const GET = handle(app)
では、このNext.jsアプリをBunで動かした場合、getConnInfoはどこからインポートする必要があるでしょうか。
-
hono/vercel: Vercel上で動かす必要があるためNG -
hono/bun: ???(アダプターの実装による)
というわけで、アダプターの実装を見ていきます。
といってもapp.fetchを呼び出しているだけに過ぎません。
ここから考えられるのは、hono/bunも使えないということです。
前述の通り、hono/bunのgetConnInfoはapp.fetch()の第二引数に依存しています。
この引数はBunが用意する必要があるため、Next.jsでは使えないと考えられます。
結論として、Next.js + HonoでIPアドレスを取得する方法は思いつきませんでした。
Vercel下であればhono/vercelが使えるのですが、ローカル環境だと難しそうです。
いい方法があれば教えてください。
実装
説明が長くなりましたが、冒頭の状況でどうすればいいのか、自分なりの最適解を書いておこうと思います。
ここまでの内容を整理するとこうなります。
- 本番環境(Vercel):
hono/vercelが使える - 開発環境(ローカル):
- Hono + Next.js + Bun: 取得不可
- Hono + Bun(APIのみ):
hono/bunが使える
これを踏まえ、以下の方針でIPアドレスを取得します。
- 本番環境 or 開発環境 or 開発環境(APIのみ)を判別する
- 適切な
getConnInfoを呼び出す- 取得できない場合は
DEFAULT_IPとして扱う
- 取得できない場合は
環境の判別
判別には環境変数が使えます。
-
VERCEL=trueの場合はVercelだとわかる -
RUNTIMEのような環境変数をnext.config.tsで定義し、Next.js上でのみ環境変数を設定する- 今回はNext.jsでのみ
RUNTIMEを'Next.js'に設定
- 今回はNext.jsでのみ
- (
NEXT_RUNTIMEやNEXT_DEPLOYMENT_IDがあるならNext.jsだとわかる)
これらの環境変数を使うと、以下のような三項演算子が書けます。
const { remote: { address } } = !!process.env.VERCEL
? // ここにVercel
: process.env['RUNTIME'] === 'Next.js' // 自分で定義した環境変数
? // ここにHono + Next.js
: // ここにHono + Bun
ビルドエラー回避
あとはそれぞれのgetConnInfoを普通にimportするだけ...と思って実装すると、Next.jsのビルドが失敗するようになります。
エラー詳細
ビルドが失敗する理由は、Bunがない環境でhono/bunからgetConnInfoをインポートしたため、Bunが見つからないというエラーが出るからです。
私の環境ではbunコマンドを使っていたものの、内部ではNode.jsを使用していたため、このエラーが出てしまいました。
対処法には--bunフラグをつけ、強制的にBunを使う方法があります。
しかし、デプロイ先がVercelの場合そこで動いているのはNode.jsの可能性が高いので、この方法を試しても本番環境でのみビルドエラーになると思います(未検証)。
対処法として、hono/bunのみ動的インポートで読み込む方法があります。
開発環境でしか使わないはずですし、そんなに問題はないでしょう。多分。
import { getConnInfo } from 'hono/vercel'
// ...
const { remote: { address } } = !!process.env.VERCEL
? getConnInfo(c)
: process.env['RUNTIME'] === 'Next.js'
? { remote: { address: 'DEFAULT_IP' } }
// ビルドエラー回避のための動的インポート
: (await import('hono/bun')).getConnInfo(c)
実際のコードはこちら。
Discussion