メインコンテンツにスキップ

CORSエラーとLambdaのパニックを修正したリファクタリング事例

タグ: 🏷 AWS ,Lambda ,CORS ,Golang ,Refactoring

はじめに

ある日、フロントエンドの担当者から「特定のAPIを叩くとCORSエラーになる」という報告を受けました。調査を開始したところ、単純なCORSの設定ミスではなく、Goで実装されたAWS Lambda関数内の予期せぬpanicが原因であることが判明しました。

本記事では、この問題の発見から原因特定、そしてリファクタリングによる解決までのプロセスを共有します。

問題の発見:謎のCORSエラー

開発者ツール(Chrome DevTools)のコンソールには、以下のような典型的なCORSエラーが表示されていました。

Access to XMLHttpRequest at ‘https://api.example.com/items' from origin ‘https://front.example.com ’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

最初に行ったのは、API GatewayのCORS設定の確認です。

  • Access-Control-Allow-Originに正しいドメインが設定されているか? -> OK
  • Access-Control-Allow-MethodsPOST, OPTIONSなどが含まれているか? -> OK
  • OPTIONSメソッドは200 OKを返しているか? -> OK

設定は一見、問題ないように見えました。しかし、特定のPOSTリクエストでのみ、このエラーが発生していました。

原因の特定:CloudWatch Logsの調査

次に、API Gatewayの背後にいるLambda関数のログをCloudWatch Logsで確認しました。すると、エラーが発生しているリクエストに対応するログに、panicの記録が残っていました。

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x...

Goのコードを追ってみると、問題の箇所は以下のようでした。

// main.go

func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var bodyData MyRequestBody
    if err := json.Unmarshal([]byte(req.Body), &bodyData); err != nil {
        // ... エラー処理
    }

    // bodyData.OptionalFieldは必須ではない
    // OptionalFieldがnilの場合、このアクセスがpanicを引き起こす
    if *bodyData.OptionalField.SomeValue == "some_condition" {
        // ...
    }

    // ... 成功レスポンスを返す処理
}

リクエストボディに含まれるOptionalFieldが任意項目であり、フロントエンドから送信されていないケースがありました。このとき、bodyData.OptionalFieldnilになります。Goでは、nilポインタに対してフィールドアクセス(*bodyData.OptionalField.SomeValue)を行うと、panicが発生します。

なぜCORSエラーになったのか?

Lambda関数がpanicで異常終了すると、API Gatewayには502 Bad Gatewayエラーが返されます。このとき、API Gatewayで設定していたCORSヘッダー(Access-Control-Allow-Originなど)はレスポンスに含まれません。

そのため、ブラウザは「CORSヘッダーがない」と判断し、CORSエラーをコンソールに表示していたのです。つまり、根本原因はサーバーサイドのpanicであり、CORSエラーはそれに伴う二次的な問題でした。

リファクタリングによる解決

原因が特定できたので、コードを修正します。修正のポイントは以下の2点です。

  1. nilチェックの追加panicを回避するために、ポインタへのアクセス前にnilでないことを確認します。
  2. エラーハンドリングの改善panicに頼るのではなく、アプリケーションのエラーとして適切に処理し、クライアントに意味のあるエラーレスポンスを返します。

まず、問題となっていたstructの定義は以下のようになっています。OptionalField自体がポインタで、さらにその中のSomeValueもポインタであるため、二重のnilチェックが必要です。

// リクエストボディの構造体
type MyRequestBody struct {
    RequiredField   string          `json:"required_field"`
    OptionalField   *OptionalStruct `json:"optional_field"`
}

type OptionalStruct struct {
    SomeValue *string `json:"some_value"`
}

修正前のコード

// main.go

func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var bodyData MyRequestBody
    if err := json.Unmarshal([]byte(req.Body), &bodyData); err != nil {
        return createErrorResponse(400, "Invalid request body"), nil
    }

    // bodyData.OptionalField や bodyData.OptionalField.SomeValueがnilの場合にpanicする
    if *bodyData.OptionalField.SomeValue == "some_condition" {
        // ...
    }

    return createSuccessResponse(), nil
}

修正後のコード

// main.go

func handler(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var bodyData MyRequestBody
    if err := json.Unmarshal([]byte(req.Body), &bodyData); err != nil {
        log.Printf("ERROR: request body unmarshal failed: %v", err)
        return createErrorResponse(400, "Invalid request body"), nil
    }

    // nilチェックを段階的に行う
    if bodyData.OptionalField != nil && 
       bodyData.OptionalField.SomeValue != nil && 
       *bodyData.OptionalField.SomeValue == "some_condition" {
        // OptionalFieldとSomeValueの両方がnilでなければ、安全に値にアクセスできる
        // ...
    }

    return createSuccessResponse(), nil
}

// エラーレスポンス生成関数
func createErrorResponse(statusCode int, message string) events.APIGatewayProxyResponse {
    return events.APIGatewayProxyResponse{
        StatusCode: statusCode,
        Headers: map[string]string{
            "Content-Type": "application/json",
            // エラー時にもCORSヘッダーを返すことが重要
            // 本番環境では "*" ではなく、特定のオリジンを指定することが推奨されます
            "Access-Control-Allow-Origin": "*", 
        },
        Body: fmt.Sprintf(`{"message": "%s"}`, message),
    }
}

さらに、API GatewayのGateway Responses設定で、DEFAULT_5XX(5xx系エラー全般)に対してもCORSヘッダーを返すように設定を変更しました。これにより、万が一未知のpanicが発生した場合でも、フロントエンドはCORSエラーではなく、サーバーエラーとして適切にハンドリングできるようになります。

まとめ

一見するとCORSの設定ミスに見える問題も、その背後にはサーバーサイドアプリケーションのpanicやクラッシュが隠れていることがあります。

今回の教訓は以下の通りです。

  • CORSエラーを鵜呑みにしない:特に、特定のリクエストでのみ発生する場合は、サーバーサイドのエラーを疑う。
  • ログは必ず確認する:CloudWatch Logsには、問題解決のヒントが必ず隠されている。
  • nilポインタに注意:Goでは、ポインタへのアクセス前にnilチェックを徹底する。
  • エラー時にもCORSヘッダーを返す:API Gatewayやアプリケーション側で、エラーレスポンスにもCORSヘッダーを含める設計にすることで、フロントエンドでのデバッグが容易になる。

地道な調査と適切なエラーハンドリングが、安定したシステム構築の鍵となります。