✍️

Laravel×外部APIエラー調査録:500エラーと NOT FOUND の裏にいた 環境変数

に公開

はじめに

Laravel で占い系の鑑定サイトを開発している中で、

「鑑定フォーム → 鑑定結果ページ」

というよくある画面遷移を実装していたところ、

  • フォーム送信後に 鑑定結果ページではなく非会員トップページに飛ばされる
  • ブラウザのネットワークを見ると 500 Internal Server Error

という現象にハマりました。

最終的な原因は、

開発環境から、本番環境の外部API(EXAMPLE_API_URL)を叩いていた

という、実務あるあるな「環境設定ミス」でした。

この記事では、

  • どんな実装だったのか
  • どうやって原因にたどり着いたのか
  • 「NOT FOUND」の裏にあった落とし穴
  • 再発防止で入れた工夫

をまとめます。

※ 実際のプロジェクト名・ドメイン・商品コードなどは、記事中ではサンプル名に置き換えています。


前提:外部APIから鑑定結果を取得する実装

占いの鑑定結果は、外部の API から取得する構成になっていました。

コントローラ側の流れ

ざっくりとした流れはこんな感じです。

  1. フォームから送信された値を元に $resultRequestParams を組み立てる
  2. ApiAccessComponent::GetResult() で外部APIにリクエスト
  3. 戻り値 $result をチェックし、エラーなら 500 を返す
  4. 正常なら鑑定結果ページを表示する

エラー判定部分のコードは次のようになっていました。

$result = ApiAccessComponent::GetResult($itemcd, $resultRequestParams);

if (is_int($result)) {
    // API呼び出しで例外が発生した場合
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

if ($result["error_msg"] != "") {
    // APIは返ってきたが、エラーメッセージが入っている場合も 500
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

API呼び出し部分(コンポーネント)

外部APIへの HTTP リクエストはコンポーネントで行っています。

public static function GetResult($itemcd, $params, $contentKey = EXAMPLE_CONTENT_KEY)
{
    try {
        $response = HttpRequestComponent::GetBasicAuth(
            EXAMPLE_API_URL . "/" . EXAMPLE_PLATFORM . "/" . $contentKey . "/" . $itemcd . "/result",
            EXAMPLE_API_ID,
            EXAMPLE_API_PW,
            $params
        );

        return HttpRequestComponent::EncodeJson($response);
    } catch (Exception $e) {
        // 例外が発生した場合は int を返す
        return 1;
    }
}

ポイントはこんな感じです。

  • EXAMPLE_API_URL:外部APIのベースURL(.env で管理)
  • EXAMPLE_PLATFORM:環境やプラットフォーム種別(例: stg / prod
  • $contentKey:コンテンツ指定用のキー
  • $itemcd:鑑定メニューのコード(例:freexxx_guest
  • 例外が発生した場合は 1 を返し、呼び出し側で is_int($result) によって検知

症状:鑑定結果ページに行けず、トップに飛ばされる

実際に鑑定フォームから送信してみると、

  • 期待:鑑定結果ページが表示される
  • 現実:非会員トップページへリダイレクトされる

ブラウザの DevTools(Network タブ)を確認すると、

  • 鑑定結果取得用のリクエストが HTTP 500 を返している

という状態でした。

アプリ全体の仕様として、

  • 500 や特定のエラーが返った場合に共通処理で非会員トップへリダイレクトする

という仕組みになっていたため、

「鑑定結果ページに行きたいのにトップに戻される」

という見え方になっていました。


調査ステップ1:Laravelログを仕込む

まずは、外部APIから何が返ってきているのか を知るために、ログを仕込みました。

use Illuminate\Support\Facades\Log;

$result = ApiAccessComponent::GetResult($itemcd, $resultRequestParams);

Log::debug('GetResult response', [
    'itemcd' => $itemcd,
    'params' => $resultRequestParams,
    'result' => $result,
]);

if (is_int($result)) {
    Log::error('GetResult failed: result is int', [
        'itemcd' => $itemcd,
        'params' => $resultRequestParams,
        'result' => $result,
    ]);
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

if (!is_array($result) || !array_key_exists('error_msg', $result)) {
    Log::error('GetResult invalid response format', ['result' => $result]);
    return response("internal server error. invalid result json. line : " . __LINE__, 500, []);
}

if ($result["error_msg"] != "") {
    Log::error('GetResult api error', ['result' => $result]);
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

実際に出力されたログ(一部マスク済み)

storage/logs/laravel.log を見ると、こんなログが出ていました。

[2025-11-13 11:12:17] local.DEBUG: GetResult response {
  "itemcd":"freexxx_guest",
  "params":{
    "full_name":"テスト太郎",
    "birthday":"1990-01-01",
    "target_full_name":"",
    "target_birthday":"",
    "custom_birthday":"2025-11-13",
    "birthtime":"1200",
    "target_birthtime":"00"
  },
  "result":{
    "status":0,
    "error_msg":"NOT FOUND"
  }
}

[2025-11-13 11:12:17] local.ERROR: GetResult api error {
  "result":{"status":0,"error_msg":"NOT FOUND"}
}

ここから分かったこと:

  • $itemcd は意図どおり freexxx_guest
  • パラメータも想定通り
  • 外部APIからのレスポンスが status: 0, error_msg: "NOT FOUND"

という状態になっている、ということです。


調査ステップ2:レスポンスをブラウザにも出してみる

状況を確認するため、一時的に API エラー時のレスポンスを そのまま JSON で返す ようにしました。

if (!empty($result['error_msg'])) {
    Log::error('GetResult api error', ['result' => $result]);

    return response()->json([
        'message' => 'API error',
        'result'  => $result,
    ], 500);
}

ブラウザでリクエストしてみると、こんな JSON が返ってきます。

{
  "message": "API error",
  "result": {
    "status": 0,
    "error_msg": "NOT FOUND"
  }
}

ここまでで、

  • Laravel 側の処理は「ログ通りに動いている」
  • 問題は API 側のレスポンス、特に error_msg: "NOT FOUND" にある

というところまでは切り分けられました。


「NOT FOUND」が意味していたもの

ここで少しハマったのが、

「API側には freexxx_guest が存在しているはずなのに、なぜ NOT FOUND なのか?」

という点です。

API側の管理画面(テスト環境)で確認すると、

  • freexxx_guest という itemcd 自体は登録されている

なので最初は、

「itemcd が間違っているわけではなさそう」

と考えていました。

しかし、ログと環境変数をよくよく見直してみると、
根本的な勘違い に気づきます。


真犯人:EXAMPLE_API_URL が本番を向いていた

改めて .env の設定を確認したところ、
開発環境だと思って動かしていたコンテナ内の設定が、こうなっていました。

# 想定していた設定(開発・テスト用)
# EXAMPLE_API_URL=https://api-test.example.com

# 実際に入っていた設定(本番用)
EXAMPLE_API_URL=https://api.example.com

つまり、

  • 自分は「テスト環境のAPIを叩いているつもり」
  • 実際には 本番環境のAPI にリクエストを投げていた

という状態でした。

そして本番側のAPIを確認すると、

  • 本番にはまだ freexxx_guest の結果データが反映されていない

そのため、本番APIは

{
  "status": 0,
  "error_msg": "NOT FOUND"
}

というレスポンスを返していた、というわけです。

テスト環境にはある
でも、本番環境にはまだない
なのに開発環境から本番を叩いていた

という構図ですね。


修正:環境ごとに API URL を正しく設定

原因がわかったので、まずは .env を修正しました。

開発・テスト環境の .env

APP_ENV=local
APP_DEBUG=true

RSA_API_URL=https://api-test.example.com
RSA_PLATFORM=stg

本番環境の .env(例)

APP_ENV=production
APP_DEBUG=false

RSA_API_URL=https://api.example.com
RSA_PLATFORM=prod

ポイント:

  • 開発用の .env からは 本番APIのURLを排除 する
  • 本番用の .env は本番サーバにだけ置き、Git などには載せない

設定を修正したあと再度リクエストすると、

  • テスト環境のAPIから正常な鑑定結果が返ってくる
  • 500 エラーが消え、無事に鑑定結果ページへ遷移できるようになりました 🎉

再発防止のためにやったこと

今回の件をきっかけに、いくつか再発防止の工夫も入れました。

1. 設定を config/services.php に集約

env() をコード内で直接呼ぶのではなく、
config/services.php に API 関連の設定をまとめました。

// config/services.php

return [
    'rsa' => [
        'url'      => env('RSA_API_URL'),
        'platform' => env('RSA_PLATFORM', 'stg'),
        'id'       => env('RSA_API_ID'),
        'password' => env('RSA_API_PW'),
    ],
];

利用側ではこう書きます。

$url = sprintf(
    '%s/%s/%s/%s/result',
    config('services.rsa.url'),
    RSA_PLATFORM,
    $contentKey,
    $itemcd
);

設定の入口が config/services.php にまとまることで、

  • 「どこでURLが決まっているのか」
  • 「今どの環境のAPIを叩いているのか」

が追いやすくなりました。

2. 実際に叩いている URL をログに出す

GetResult() の中で、
実際に投げている URL とパラメータをログに残すようにしました。

public static function GetResult($itemcd, $params, $contentKey = RSA_CONTENT_KEY)
{
    try {
        $url = RSA_API_URL . "/" . RSA_PLATFORM . "/" . $contentKey . "/" . $itemcd . "/result";

        Log::debug('GetResult request url', [
            'url'    => $url,
            'itemcd' => $itemcd,
            'params' => $params,
        ]);

        $response = HttpRequestComponent::GetBasicAuth(
            $url,
            RSA_API_ID,
            RSA_API_PW,
            $params
        );

        Log::debug('GetResult raw response', ['response' => $response]);

        return HttpRequestComponent::EncodeJson($response);
    } catch (Exception $e) {
        Log::error('GetResult exception', [
            'itemcd'  => $itemcd,
            'params'  => $params,
            'message' => $e->getMessage(),
        ]);

        return 1;
    }
}

これで、

  • 本当にテストAPIを叩いているのか
  • 間違って本番APIを叩いていないか

がログを見れば一目でわかるようになりました。

3. 「NOT FOUND」を全部 500 にしない

それまでは、error_msg に何か入っていれば 全部 500 にしていましたが、
ユーザー体験的にも開発者的にも扱いづらいので、分岐を見直しました。

$result = ApiAccessComponent::GetResult($itemcd, $resultRequestParams);

// ネットワークや例外系
if (is_int($result)) {
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

// レスポンス形式チェック
if (!is_array($result) || !array_key_exists('status', $result)) {
    Log::error('GetResult invalid format', ['result' => $result]);
    return response("internal server error. invalid result json. line : " . __LINE__, 500, []);
}

// 「NOT FOUND」は専用画面へ
if (($result['status'] ?? 0) === 0 && ($result['error_msg'] ?? '') === 'NOT FOUND') {
    Log::warning('GetResult NOT FOUND', [
        'itemcd' => $itemcd,
        'result' => $result,
    ]);

    return view('result.not_found', [
        'itemcd'  => $itemcd,
        'message' => 'この鑑定結果は現在準備中です。',
    ]);
}

// その他のAPIエラー
if (!empty($result['error_msg'])) {
    Log::error('GetResult other api error', ['result' => $result]);
    return response("internal server error. result json request error. line : " . __LINE__, 500, []);
}

// 正常系:鑑定結果ページへ
return view('result.show', [
    'result' => $result,
]);
  • インフラ的なエラー(接続不可・例外)は 500
  • ビジネスロジック上の「データがない(NOT FOUND)」は専用ページでハンドリング

と切り分けることで、

  • ユーザーには「準備中」「現在ご利用できません」と伝えられる
  • 開発者はログを見て原因を追いやすい

という状態にできました。


学びとまとめ

今回の一件で学んだことは、ざっくり以下の3つです。

  1. 環境変数の向き先は想像以上に重要

    • 「開発中のつもりで本番APIを叩いていた」は普通に起こる
    • 本番URLは本番環境にだけ置く、という運用にしておくと安全
  2. ログは惜しまず出したほうが早く原因にたどり着ける

    • 実際のURL
    • リクエストパラメータ
    • 生レスポンス
      この3つを一度出してみるだけで、原因の見当がつきやすくなる
  3. APIのエラーを「全部 500」にしない

    • ネットワークや例外系の 500 と
    • ビジネスロジック上の「データがない(NOT FOUND)」は分ける
    • 後者は専用画面やメッセージでユーザーに伝えるほうが UX 的にも◎

同じように、

「itemcd は存在しているはずなのに、APIが NOT FOUND を返してくる…」

という状況にハマっている方の、

「もしかして、.env の向き先では?」

という気づきにつながればうれしいです。

Discussion