起こったこと
自分で開発したAPIを叩いて、戻ってきたレスポンスをそのまま返すAWS Lambdaを作っていたのですが、固定のヘッダーをつけてリクエストするときは問題なく呼び出せるのに、API Gatewayに渡されたリクエストヘッダーをそのままつけてリクエストすると、原因どころか発生箇所すらもよく分からないエラーが起きるようになりました。
import json
import urllib.request
def lambda_handler(event, context):
headers = event.get('headers')
request_body = event.get('body').encode('utf-8')
# リクエスト送信
req = urllib.request.Request('https://xxxx/api/v1/yyyy', headers=headers, method='POST', data=request_body)
with urllib.request.urlopen(req) as res:
body = res.read()
code = res.getcode()
return {
'statusCode': code,
'body': body
}
[ERROR] Runtime.MarshalError: Unable to marshal response: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte
Traceback (most recent call last):
なぜかスタックトレースが何も表示されない…
APIのレスポンスが圧縮されていた
このエラーメッセージについて調べてみたところ、ひとつの鍵はbyte 0x8b in position 1
でした。
python - UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte, while reading csv file in pandas - Stack Overflow
0x8b
はgzipで使われるマジックナンバーとのこと。gzip圧縮されたレスポンスは確かにUTF-8デコードできません。
そういえば、このAPIはASP.NETで開発しており、応答圧縮を有効にしていたのでした。
この機能はクライアントから送られたリクエストヘッダーのAccept-Encodingを見て、レスポンスを圧縮していいかを判断しています。
実際に、送ろうとしたヘッダーには以下が含まれていました。
'accept-encoding': 'gzip, deflate, br'
このヘッダーを除外してAPIを呼び出すことで、レスポンスの圧縮を防ぎ、問題を回避することができました。
headers = event.get('headers')
+ headers.pop('accept-encoding', None) # 応答圧縮を回避
request_body = event.get('body').encode('utf-8')
エラーはLambdaランライムで起きていた
直接の原因と回避策は分かったものの、Lambdaのコードの中でUTF-8デコードしている箇所がないのになぜこのエラーが起きるのか、気持ち悪さが残ります。
しかも、エラーのスタックトレースが何もないので、発生箇所が分かりません。
上記のプログラムにログを仕込んで調べたところ、returnの直前まで実行されていることが分かりました。
Runtime.MarshalError
について調べたところ、Lambdaは戻り値をJSONシリアライズしてから呼び出し元に返しており、戻り値がシリアライズできない場合にこのエラーが起きることが分かりました。
Python の Lambda 関数ハンドラー
スタックトレースが何もなかったのは、エラーの発生箇所がLambdaラインタイムだったからのようです。
ちなみに、戻り値をBase64エンコードしてから返せばこのエラーを回避できるとの情報もありましたが、試していません。