🟪はじめに
本記事の構成図や登場する事象は、実際に業務で遭遇した出来事をベースにしていますが、説明用にデフォルメ(簡略化・改変)しています。
また、記事中で使用しているリクエスト文字列も検証用に用意したサンプルであり、実際にやり取りされたデータではありません。
本記事はあくまで筆者個人の技術検証であり、所属組織を代表するものではありません![]()
🟪とある日のことです・・
システムAとシステムBを、APIで新しく繋ぐことになりました。
要件を詰めて実装を終え、さあ疎通確認・・・という初回テストで、システムB側のLambda処理がエラーとなりました。

必須項目は送っているはずなのに、なぜかエラーになる・・。
ログを追っていくと、犯人が見つかりました。なんと、システムAが送ったリクエストに含まれる+という文字が、Amazon API Gatewayを通過する間に書き換わっていたのです![]()
Lambda側では期待していた値を受け取れず、エラーになっていました。
ということで、
「API Gatewayって、リクエストの中身を書き換えてしまう仕様なの?」というのが気になり、実際に検証環境を作って確かめてみました!
🟪検証してみた
🟣検証環境
構成はすんごくシンプル。
PCからインターネット経由でAmazon API Gateway + AWS Lambdaという最小構成を作り、Lambda側は受け取った値はそのまま加工せず、必要な項目だけを選んでレスポンスとして返す処理にしておきます。これなら「Lambdaに届いた時点で、値がどう変化していたか」がそのまま見えるはずです。

exports.handler = async (event) => {
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
httpMethod: event.httpMethod,
path: event.path,
queryStringParameters: event.queryStringParameters,
multiValueQueryStringParameters: event.multiValueQueryStringParameters,
rawQueryString: event.rawQueryString ?? null,
body: event.body,
isBase64Encoded: event.isBase64Encoded,
}, null, 2),
};
};
🟣検証方法
検証方法もすんごくシンプル。
今回、電話番号のような「先頭に+が付く文字列」を例に、2パターンのリクエストを送って挙動を比較しました。
| パターン | クエリ文字列 | 内容 |
|---|---|---|
| ① | tel=+81 |
+をそのまま(未エンコードで)送信 |
| ② | tel=%2B81 |
+を正しくパーセントエンコード(%2B)して送信 |
curlコマンドで、実際にAPI Gatewayの先にあるエンドポイントを叩いてみます。
# パターン①: +をそのまま送る
curl -sg "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/<APIGateのパス>?tel=+81" | jq .
# パターン②: %2Bでエンコードして送る
curl -sg "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/<APIGateのパス>?tel=%2B81" | jq .
🟣検証結果
結果もすんごくシンプル。
| パターン | クエリ文字列 | 内容 | 結果 |
|---|---|---|---|
| ① | tel=+81 |
+をそのまま(未エンコードで)送信 |
{"queryStringParameters":{"tel": " 81" }} |
| ② | tel=%2B81 |
+を正しくパーセントエンコード(%2B)して送信 |
{"queryStringParameters":{"tel": "+81" }} |
+が消えて、代わりに半角スペースになってしまっています。まさにこれが、冒頭のエラーの正体でした。
+81のまま、正しく受け取れています![]()
🟪教訓
◾ Amazon API Gatewayは、クエリ文字列に含まれる未エンコードの+を半角スペースとして解釈している。
◾ application/x-www-form-urlencoded(フォーム送信のボディ)で使われる「+=スペース」というルールが、クエリ文字列にもそのまま適用されている
◾ +のような文字をあらかじめ%2Bのようにエンコードして送れば、この問題は起きない!
◾ 電話番号・メールアドレス・Base64文字列などを含むデータをクエリ文字列に載せる場合は、送信側で確実にエンコードするか、そもそもクエリ文字列ではなくリクエストボディー(JSONボディー)に載せる、という設計変更も選択肢に入れておくのが良さそうですね。(と私のClaudeが言っています)
🟪おまけ:検証中に気づいたもう一つの罠
罠と思っているのは私だけかもしれませんが・・・
。
検証を進めている中で、もう一つ気づきがあったので載せます。
API Gatewayのコンソール画面には、その場でリクエストを試せる「テスト」ボタンがあります。
わざわざcurlを打たなくてもこれで確認できるのでは、と思って使ってみました。
そしたらなんと!同じtel=+81やtel=%2B81を投げているのに、curlで叩いたときとは違う結果が返ってきたのです![]()
![]()
| 送信した値 | curl(実際のリクエスト) | コンソールの「テスト」ボタン |
|---|---|---|
tel=+81 |
" 81"(スペースに変換) |
"+81"(そのまま) |
tel=%2B81 |
"+81"(正しくデコード) |
"%2B81"(デコードされず、そのまま) |
理由を調べてみると、コンソールの「テスト」ボタンは実際のインターネット越しのHTTPリクエストとしては送られておらず、TestInvokeMethodというAWSの管理用APIを経由した疑似的な実行になっているようでした。本物のリクエストがネットワーク越しにAPI Gatewayへ届いたときに行われる「URLのデコード処理」自体を通らないため、+も%2Bもそのままの文字列として扱われてしまう、ということのようです。(ややこしい)
なので、API Gatewayを検証するときは目的に応じて使い分けるのがよさそうです。
| 確認観点 | 検証方法 |
|---|---|
| Lambdaがちゃんと動くか、統合の設定に間違いがないかを素早く確認 | 「テスト」ボタンでの検証 |
| 実際にクライアントから送ったときにどう届いて処理されるかを正確に確認 | curlなど本物のHTTPリクエストで検証 |
「テストボタンでは問題なかったのに、本番で動かしたらエラーになった」ということも起こり得るので、この違いは覚えておいて損はないはずです。
🟪さいごに
「API Gatewayが、リクエストの中身を勝手に書き換えることがある」というのは、知らないとまず気づけないポイントだと思います。今回はたまたま初回の疎通テストで踏み抜いたので実害はありませんでしたが、これが本番運用に入ってから、しかもごく稀にしか+を含む値が来ないようなAPIで起きていたら、原因特定にもっと苦労していたはずです。
クエリ文字列に記号を含める設計をしている方は、一度自分のAPIでも同じことが起きていないか、試してみる価値はあると思います!!
同じような事象で困っている方の助けになれば幸いです![]()




