【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