Extracting POST data from an HTTP request when using gen_tcp

A Simple Webserver

Let's implement a simple loop, using gen_tcp to listen on a particular port, and spawn a handler process for each incoming request (note that we have not defined handle_request/1 yet). When setting up the initial socket, we are going to specify that incoming packets be packaged according as HttpPacket types, as defined in decode_packet/3 built-in function (BIF).

		
 start(Port)->
  {ok, ListenSock}=gen_tcp:listen(Port, [list,{active, false},{packet,http}]),
  ?MODULE:loop(ListenSock).
 
 loop(ListenSock) ->	
  {ok, Sock}=gen_tcp:accept(ListenSock),
  spawn(?MODULE, handle_request, [Sock]),
  ?MODULE:loop(ListenSock).

Determining the HTTP Request Method

Since we are going to deal with POST request data we will need to differentiate between POST methods and non-POST methods (i.e. GET, PUT, etc.). The 2nd element of the HttpRequest tuple ({http_request, HttpMethod, HttpUri, HttpVersion}) will provide us with an atom matching one of the seven HTTP method types, so we can dispatch requests to the appropriate handler for a given method type.

	
 handle_request(Sock) ->
  {ok, {http_request, Method, Path, Version}}=gen_tcp:recv(Sock, 0),
  case (Method) of
   'POST' -> handle_post(Sock);
   _ -> send_unsupported_error(Sock)
  end.

Iterating over a Request's HTTP Headers

In order to retrieve the data contained in the body of a POST request we will first need to determine the length of the data. This information can be found in the Content-Length header, and we can get this information by iterating over the headers with a case statement and a little recursion, returning once we find the value we are looking for:


 get_content_length(Sock) ->
  case gen_tcp:recv(Sock, 0, 60000) of
   {ok, {http_header, _, 'Content-Length', _, Length}} -> list_to_integer(Length);
   {ok, {http_header, _, Header, _, _}}  -> get_content_length(Sock)
 end.

Extracting the Data

Now, all we need to do is iterate over the remaining headers until we get to the http_eoh atom indicated the end of header ("eoh") information...we can then turn off the HTTP type wrapping and read the raw POST data from the socket.


 get_body(Sock, Length) ->
  case gen_tcp:recv(Sock, 0) of
   {ok, http_eoh} ->inet:setopts(Sock, [{packet, raw}]),{ok,Body}=gen_tcp:recv(Sock, Length),Body;
   _ -> get_body(Sock, Length)
  end.

Putting it all together, our handle_post/1 function would look something like this:
	
 handle_post(Sock) ->
  Length=get_content_length(Sock),
  PostBody=get_body(Sock, Length),
  io:fwrite(PostBody),
  send_accept(Sock).

Misc. Items

While not really in scope of this article, let's fill in a few functions we referenced above.

	
 send_accept(Sock) ->
	gen_tcp:send(Sock, "HTTP/1.1 202 Accepted\r\nConnection: close\r\nContent-Type: text/html; charset=UTF-8\r\nCache-Control: no-cache\r\n\r\n"),
	gen_tcp:close(Sock).
	
 send_unsupported_error(Sock) ->
	gen_tcp:send(Sock, "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\nAllow: POST\r\nContent-Type: text/html; charset=UTF-8\r\nCache-Control: no-cache\r\n\r\n"),
	gen_tcp:close(Sock).