🙌

# GoとRustのAPI疎通テストで詰まった全記録 — 401→404→SUCCESSまでの深夜格闘記

に公開

前回のあらすじ

前回の記事では、RustとGoをWindowsでFFI連携させて vault-api.exe を生成するまでを書きました。

あの記事の最後、[Done] vault-api.exe has been created! が出た瞬間にガッツポーズした話をしましたが、その直後に気づきました。

動くか確かめてないじゃないか。

「作れた」と「動く」は全然違う。この記事はその続きです。


今回の戦場

vault-api.exe を実際に起動して、GoのゲートウェイからRustのエンジンを叩いてレスポンスが返ってくるまでを確認するテストです。

テスト構成はシンプルで、こういうイメージです。

test_connection.ps1(テストスクリプト)

Go Gateway(vault-api.exe):ポート8443

Rust Engine(FFI経由)

レスポンスが返ってくれば完成

この「シンプルな構成」で、深夜に何時間も溶かしました。


第1ラウンド:バッチファイルの中身が消えた

最初のトラブルは技術的な話ですらなかった。

起動用の vault_launcher.bat を実行しようとしたら、コマンドプロンプトに package middleware と表示されました。

え、なにこれ。バッチファイルが喋った?

原因は単純で、vault_launcher.bat の中身を auth.go のコードで上書きしていました。集中しすぎると本当にこういうことが起きます。バッチファイルだと思って開いたファイルがGoのソースコードだった、という状態です。

バッチファイルを書き直して再実行。

@echo off
echo Go API ゲートウェイを再構築中...
go build -o vault-api.exe ./cmd/vault-api/
if %ERRORLEVEL% neq 0 (
    echo [Error] Build failed.
    exit /b 1
)
set VAULT_ENCRYPTION_KEY=your-key-here
set DEV_MODE=true
vault-api.exe

サーバーが起動して Listening on: :8443 が出た。

よし、次。


第2ラウンド:401 Unauthorized

テストスクリプトを実行したら即座にこれが出ました。

[1/2] Checking API Health... [SUCCESS]
[2/2] Testing Rust FFI...    [FAILED] (401) Unauthorized

ヘルスチェックは通るのにRustへのリクエストだけ弾かれる。

ヘルスチェックは通るじゃないか。なんで2つ目だけ401なんだ。

コードを確認したら理由がわかりました。ヘルスチェック(/health)には「このエンドポイントは認証なしで通す」という特例処理が入っていて、Rustを叩くエンドポイント(/api/v1/generate-ppid)には認証が必要だった。

auth.go の認証ロジックを確認すると、Authorizationヘッダーのトークン検証が厳しすぎる設定になっていました。開発中は DEV_MODE=true の時だけ認証をスキップする逃げ道を作っておくと便利です。

// auth.go の一部
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 開発モードは認証スキップ
        if os.Getenv("DEV_MODE") == "true" {
            next.ServeHTTP(w, r)
            return
        }
        // 本番の認証処理
        token := r.Header.Get("Authorization")
        if !validateToken(token) {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

DEV_MODE=true を環境変数に設定して再実行。401は消えました。

第1関門突破。


第3ラウンド:404 Not Found

401を突破したら今度はこれです。

[1/2] Checking API Health... [SUCCESS]
[2/2] Testing Rust FFI...    [FAILED] (404) Not Found

お城の中には入れたけど部屋が見つからない、ってやつだ。

原因はURLのズレでした。

テストスクリプト側が叩いているURL:/api/v1/generate-ppid
ルーター側が定義しているURL:/api/generate-ppid

v1 が入っているかどうかの違いだけで404になります。

// router.go(修正前)
mux.HandleFunc("/api/generate-ppid", GeneratePPIDHandler)

// router.go(修正後)
mux.HandleFunc("/api/v1/generate-ppid", GeneratePPIDHandler)

テストスクリプト側のURLも合わせて修正。再実行。

まだ404だ。

あれ、と思ってコードを確認したら、GeneratePPIDHandlerrouter.go に書いてあるけど定義されていませんでした。地図に「この部屋」と書いてあるのに、部屋自体が存在しない状態です。

// router.go(完成版)
func RegisterRoutes(mux *http.ServeMux) {
    mux.HandleFunc("/health", HealthHandler)
    mux.HandleFunc("/api/v1/generate-ppid", GeneratePPIDHandler)
}

func GeneratePPIDHandler(w http.ResponseWriter, r *http.Request) {
    // ここでRustのFFI関数を呼ぶ
    input := []byte(r.FormValue("input"))
    ppid, err := vault.GeneratePPID(input)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"ppid": "%s"}`, string(ppid))
}

ルーターとハンドラーを同じファイルに書くか、別ファイルに分けるかはプロジェクトの規模次第ですが、この時点ではまとめて書く方が混乱しにくかったです。


第4ラウンド:外部ライブラリがない

ビルドを走らせたらこれが出ました。

no required module provides github.com/gorilla/mux

gorilla/mux、入れてないじゃないか。

前のセッションで router.go を書き直した時に、標準ライブラリではなく gorilla/mux を使ったコードになっていました。go get で入れるか、標準ライブラリの net/http だけで書き直すかの二択です。

今の段階では依存関係を増やしたくなかったので、標準ライブラリだけで書き直しました。

Goの標準ライブラリだけでルーティングを実装する場合、パスパラメータの取り扱いが少し面倒ですが、シンプルなRESTなら net/http だけで十分です。

// 標準ライブラリだけで書いたルーター
func RegisterRoutes(mux *http.ServeMux) {
    mux.HandleFunc("/health", HealthHandler)
    mux.HandleFunc("/api/v1/generate-ppid", GeneratePPIDHandler)
}

エンドポイントが増えてきたら gorilla/muxchi を入れる、という判断で良いと思います。


第5ラウンド:型の不一致(またこれか)

cannot use ppid (variable of type []byte) as string value

また型だ。前回もこれで詰まったのに。

RustのFFI関数から返ってくるデータは []byte です。それをそのままJSONに入れようとすると型が合いません。string() で変換するだけです。

// ❌ []byteをそのまま使おうとしている
fmt.Fprintf(w, `{"ppid": "%s"}`, ppid)

// ✅ stringに変換してから使う
fmt.Fprintf(w, `{"ppid": "%s"}`, string(ppid))

あと、Rustの関数から複数の戻り値が返ってくる場合(結果とエラーのセット)、Go側で全部受け取らないといけません。

// ❌ 戻り値を1つしか受け取っていない
ppid := vault.GeneratePPID(input)

// ✅ エラーも受け取る
ppid, err := vault.GeneratePPID(input)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

第6ラウンド:PowerShellの文字化け

テストスクリプト(.ps1)を実行したら赤いエラーが出ました。コードの構文エラーのような見た目だけど、書き方は合っている。

なんで構文エラーになるんだ。同じコードのはずなのに。

原因はファイルのエンコードでした。メモ帳で保存する時にエンコードを指定しないと、環境によってはUTF-8ではなくShift-JISで保存されることがあります。PowerShellでShift-JISのスクリプトを実行すると、日本語のコメントや文字列が化けてコードとして解釈されてしまいます。

解決策:保存時に「UTF-8(BOM付き)」を選ぶ。

まさかファイルの保存方法で詰まるとは思わなかった。


第7ラウンド:undefined: api.NewRouter

ここまでくると笑えてきます。

undefined: api.NewRouter

router.go を書き直す過程で、関数名が NewRouter から RegisterRoutes に変わっていました。でも main.go はまだ api.NewRouter を呼んでいる。

// main.go(古い)
router := api.NewRouter()

// main.go(修正後)
mux := http.NewServeMux()
api.RegisterRoutes(mux)

一度決めた関数名は、変えるなら全ファイルを一緒に変える。当たり前のことだけど、何度もファイルを書き直していると追いきれなくなります。


ついに、SUCCESS

全部直してビルドして、テストスクリプトを実行したら。

[1/2] Checking API Health... [SUCCESS]
[2/2] Testing Rust Engine... [SUCCESS]
{"ppid": "PPID-PREXUS-akiny-victory-2026-final"}

出た。

画面を見つめながら、しばらく動けませんでした。GoのゲートウェイがHTTPリクエストを受け取って、FFI経由でRustのエンジンを叩いて、結果がネットワークを通じて返ってきた。それが動いている。

深夜に一人で、ガッツポーズしていました。また。

ちなみに結果の末尾に \u0000(ヌル文字)が付いていましたが、それは後で直しました。完璧じゃなくてもSUCCESSはSUCCESSです。


今回の詰まりポイントまとめ

□ バッチファイルを別のコードで上書きしていないか確認する
□ 認証スキップの仕組み(DEV_MODE等)を最初に用意しておく
□ ルーターのURLとテストスクリプトのURLが一字一句一致しているか
□ ハンドラー関数が実際に定義されているか(地図だけ書いて部屋がない状態に注意)
□ 外部ライブラリを使うなら go get を忘れずに
□ []byteとstringの変換を意識する
□ 複数の戻り値は全部受け取る
□ PowerShellのスクリプトはUTF-8(BOM付き)で保存する
□ 関数名を変えたら全ファイルで一緒に変える

おわりに

vault-api.exe を作った時もガッツポーズしたけど、実際にRustのエンジンがHTTP越しに応答してきた瞬間の方が、もっと大きかったです。「動いている」という実感が全然違う。

これでGoとRustの連携は一通り完成しました。次回は**「15MBのバイナリをいかに削り落としたか」**という話を書く予定です。-ldflags="-s -w" から始まってステージビルドまで、軽量化の記録を残しておきます。

PrexusのGateway部分はオープンソース化予定です。公開のタイミングはXかZennでお知らせします。


Vault内部の暗号化ロジック・鍵管理の実装詳細については、セキュリティ上の理由から公開していません。

Discussion