HTMLフォームからAPIGatewayを使ってAWS LambdaにPOSTする


AWSのフォーラム内で質問されている内容を日本語で解説した記事になります。
参考リンク： https://forums.aws.amazon.com/thread.jspa?messageID=663593&tstart=0#663593

## 前提

HTMLフォームによるPOSTリクエストでは、各inputタグの内容を`key=value`の形式として`&`でつないだものがリクエストボディとして送信されます。

``` 簡単なフォームとそのリクエストボディ
<form action="./endpoint">
  <input type="text" name="hoge" value="fuga"/>
  <input type="text" name="piyo" value="ihr"/>
  <input type="submit" value="送信">
</form>

---

hoge=fuga&piyo=ihr
```


しかし、Lambda function ではJSON形式での入力を期待しているため、デフォルトのままではHTMLフォームのバックエンドとして、APIGatewayおよびLambdaを利用することができません。

``` Lambdafunctionの期待するリクエストボディ
{
  "hoge": "fuga",
  "piyo": "ihr"
}
```

そこでAPIGatewayの[Mapping Template](http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)を使います。


## 設定方法

1. Amazon API Gatewayを適当な名前で作成します
7. 好きなResouceに対してPOSTメソッドを作成します
9. Integration type に "Lambda function" を選択し、Lambda functionがあるregionを指定します
11. 作成後、"Integration Request" → "Mapping Templates" → "Add mapping template"とクリックしていきます
14. Content-Type に "application/x-www-form-urlencoded" を設定し"Mapping template"として下記を保存します

``` フォームのためのMappingTemplate簡易ver
## convert HTTP POST data to JSON for insertion directly into a Lambda function
 
## first we we set up our variable that holds the tokenised key value pairs
#set($httpPost = $input.path('$').split("&"))
 
## next we set up our loop inside the output structure
{
#foreach( $kvPair in $httpPost )
 ## now we tokenise each key value pair using "="
 #set($kvTokenised = $kvPair.split("="))
 ## finally we output the JSON for this pair and add a "," if this isn't the last pair
 "$kvTokenised[0]" : "$kvTokenised[1]"#if( $foreach.hasNext ),#end
#end
}
```

全てのデータが正しく入力された状態のPOSTしかこないのであれば、上記の簡易verで問題ありませんが、一部のデータが空白だったりした場合にうまく動作しなくなります。
その場合は、下記を使います。

``` フォームのためのMappingTemplate完全ver
## convert HTML FORM POST data to JSON for insertion directly into a Lambda function
 
## get the raw post data from the AWS built-in variable and give it a nicer name
#set($rawPostData = $input.path('$'))
 
## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawPostData.length() - $rawPostData.replace("&", "").length())
 
## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawPostData + "&")
#end
 
## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawPostData.split("&"))
 
## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])
 
## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end
 
## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$kvTokenised[0]" : #if($kvTokenised[1].length() > 0)"$kvTokenised[1]"#{else}""#end#if( $foreach.hasNext ),#end
#end
}
```

この設定をしておけば、下記ようなフォームのPOSTリクエストが、良い感じにJSONにマッピングされてLambda functionを呼び出すことができます。

``` フォームでの呼び出しサンプルとLambdaに渡されるデータ
<form action="https://example.execute-api.us-east-1.amazonaws.com/production/mytestform" method="POST">
  <input type="text" name="hoge" value="fuga">
  <input type="text" name="ihr" value="buiyon">
</form>

---
{
  "hoge": "fuga",
  "ihr": "buiyon"
}
```

## ちなみに

フォームからPOSTする部分まではできましたが、APIのレスポンスをどうするかについての説明を省略しています。このままだとフォームに投稿後、ブラウザにAPIのレスポンスのJSONがそのまま表示されてしまうため、レスポンスについても適宜マッピングをしてやったり、302レスポンスを返してPOST完了の画面に飛ばしてやる必要があります。このあたりの説明はまたそのうち。