📱

【Hono + Next.js】Vercel or BunでIPアドレスを取得する

2025/02/08に公開

何がしたいか

  • Next.js + HonoでWebアプリを開発している
  • 本番環境: 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の実装も単純で、このヘッダーから抜き出しているだけです。

https://github.com/honojs/hono/blob/3a7259bec224addb33c49496c3944a3bdb21ddcc/src/adapter/vercel/conninfo.ts#L3-L8

しかし、これはあくまでVercelの仕様であって、BunやNext.jsの仕様ではありません。
そのためhono/vercelはVercel上でしか使用できません。Next.jsを使っているかは関係ないので注意が必要です。

Bunの場合

Bunにはexport default syntaxという構文があります。
これはBun.serveを呼び出す代わりにfetchメソッドがあるオブジェクトをexport defaultすることで、それを実行可能ファイルとしてみなすというものです。

例えば、以下のコードでサーバーを立てる場合を考えます。

index.ts
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として動かす場合、通常はアダプターを使うと思います。
例えばこんな感じです。

app/api/[[...route]]/route.ts
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を呼び出しているだけに過ぎません。

https://github.com/honojs/hono/blob/3a7259bec224addb33c49496c3944a3bdb21ddcc/src/adapter/vercel/handler.ts#L4-L8

ここから考えられるのは、hono/bunも使えないということです。
前述の通り、hono/bungetConnInfoapp.fetch()の第二引数に依存しています。
この引数はBunが用意する必要があるため、Next.jsでは使えないと考えられます。


結論として、Next.js + HonoでIPアドレスを取得する方法は思いつきませんでした。
Vercel下であればhono/vercelが使えるのですが、ローカル環境だと難しそうです。

いい方法があれば教えてください。

実装

説明が長くなりましたが、冒頭の状況でどうすればいいのか、自分なりの最適解を書いておこうと思います。

ここまでの内容を整理するとこうなります。

  • 本番環境(Vercel): hono/vercelが使える
  • 開発環境(ローカル):
    • Hono + Next.js + Bun: 取得不可
    • Hono + Bun(APIのみ): hono/bunが使える

これを踏まえ、以下の方針でIPアドレスを取得します。

  1. 本番環境 or 開発環境 or 開発環境(APIのみ)を判別する
  2. 適切なgetConnInfoを呼び出す
    • 取得できない場合はDEFAULT_IPとして扱う

環境の判別

判別には環境変数が使えます。

  • VERCEL = trueの場合はVercelだとわかる
  • RUNTIMEのような環境変数をnext.config.tsで定義し、Next.js上でのみ環境変数を設定する
    • 今回はNext.jsでのみRUNTIME'Next.js'に設定
  • NEXT_RUNTIMENEXT_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のみ動的インポートで読み込む方法があります。
開発環境でしか使わないはずですし、そんなに問題はないでしょう。多分。

OK
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