CORSエラーとLambdaのパニックを修正したリファクタリング事例
はじめに
ある日、フロントエンドの担当者から「特定の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に正しいドメインが設定されているか? -> OKAccess-Control-Allow-MethodsにPOST,OPTIONSなどが含まれているか? -> OKOPTIONSメソッドは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.OptionalFieldはnilになります。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点です。
- nilチェックの追加:
panicを回避するために、ポインタへのアクセス前にnilでないことを確認します。 - エラーハンドリングの改善:
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ヘッダーを含める設計にすることで、フロントエンドでのデバッグが容易になる。
地道な調査と適切なエラーハンドリングが、安定したシステム構築の鍵となります。
