From 420f5baf98cb1b19209977e5552107ab3222767f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sun, 8 May 2011 17:26:21 +0200 Subject: Add chunked reply support. Send the status line and headers using cowboy_http_req:chunked_reply/3, and individual chunks with cowboy_http_req:chunk/2. --- include/http.hrl | 2 +- src/cowboy_http_protocol.erl | 13 +++++++++---- src/cowboy_http_req.erl | 18 +++++++++++++++++- test/chunked_handler.erl | 17 +++++++++++++++++ test/http_SUITE.erl | 9 +++++++-- 5 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 test/chunked_handler.erl diff --git a/include/http.hrl b/include/http.hrl index e6c37a9..20c63cb 100644 --- a/include/http.hrl +++ b/include/http.hrl @@ -65,5 +65,5 @@ buffer = <<>> :: binary(), %% Response. - resp_state = locked :: locked | waiting | done + resp_state = locked :: locked | waiting | chunks | done }). diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl index 221e316..302df26 100644 --- a/src/cowboy_http_protocol.erl +++ b/src/cowboy_http_protocol.erl @@ -198,9 +198,9 @@ handler_terminate(HandlerState, Req=#http_req{buffer=Buffer}, HandlerRes = (catch Handler:terminate( Req#http_req{resp_state=locked}, HandlerState)), BodyRes = ensure_body_processed(Req), - ensure_response(Req, State), - case {HandlerRes, BodyRes, State#state.connection} of - {ok, ok, keepalive} -> + RespRes = ensure_response(Req, State), + case {HandlerRes, BodyRes, RespRes, State#state.connection} of + {ok, ok, ok, keepalive} -> ?MODULE:parse_request(State#state{buffer=Buffer}); _Closed -> terminate(State) @@ -223,7 +223,12 @@ ensure_response(#http_req{resp_state=done}, _State) -> %% No response has been sent but everything apparently went fine. %% Reply with 204 No Content to indicate this. ensure_response(#http_req{resp_state=waiting}, State) -> - error_response(204, State). + error_response(204, State); +%% Close the chunked reply. +ensure_response(#http_req{socket=Socket, transport=Transport, + resp_state=chunks}, _State) -> + Transport:send(Socket, <<"0\r\n\r\n">>), + close. -spec error_response(Code::http_status(), State::#state{}) -> ok. error_response(Code, #state{socket=Socket, diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl index c430020..60f0b55 100644 --- a/src/cowboy_http_req.erl +++ b/src/cowboy_http_req.erl @@ -30,7 +30,7 @@ ]). %% Request Body API. -export([ - reply/4 + reply/4, chunked_reply/3, chunk/2 ]). %% Response API. -include("include/http.hrl"). @@ -192,6 +192,22 @@ reply(Code, Headers, Body, Req=#http_req{socket=Socket, Transport:send(Socket, [Head, Body]), {ok, Req#http_req{resp_state=done}}. +-spec chunked_reply(Code::http_status(), Headers::http_headers(), + Req::#http_req{}) -> {ok, Req::#http_req{}}. +chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport, + resp_state=waiting}) -> + Head = response_head(Code, Headers, [ + {<<"Connection">>, <<"close">>}, + {<<"Transfer-Encoding">>, <<"chunked">>} + ]), + Transport:send(Socket, Head), + {ok, Req#http_req{resp_state=chunks}}. + +-spec chunk(Data::iodata(), Req::#http_req{}) -> ok. +chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> + Transport:send(Socket, [integer_to_list(iolist_size(Data), 16), + <<"\r\n">>, Data, <<"\r\n">>]). + %% Internal. -spec parse_qs(Qs::binary()) -> list({Name::binary(), Value::binary() | true}). diff --git a/test/chunked_handler.erl b/test/chunked_handler.erl new file mode 100644 index 0000000..97ce27c --- /dev/null +++ b/test/chunked_handler.erl @@ -0,0 +1,17 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(chunked_handler). +-behaviour(cowboy_http_handler). +-export([init/3, handle/2, terminate/2]). + +init({_Transport, http}, Req, _Opts) -> + {ok, Req, undefined}. + +handle(Req, State) -> + {ok, Req2} = cowboy_http_req:chunked_reply(200, [], Req), + cowboy_http_req:chunk("chunked_handler\r\n", Req2), + cowboy_http_req:chunk("works fine!", Req2), + {ok, Req2, State}. + +terminate(_Req, _State) -> + ok. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 6b1a70f..b5b615d 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -18,7 +18,7 @@ -export([all/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2]). %% ct. --export([headers_dupe/1, headers_huge/1, +-export([chunked_response/1, headers_dupe/1, headers_huge/1, nc_rand/1, pipeline/1, raw/1]). %% http. -export([http_200/1, http_404/1, websocket/1]). %% http and https. @@ -29,7 +29,7 @@ all() -> groups() -> BaseTests = [http_200, http_404], - [{http, [], [headers_dupe, headers_huge, + [{http, [], [chunked_response, headers_dupe, headers_huge, nc_rand, pipeline, raw, websocket] ++ BaseTests}, {https, [], BaseTests}]. @@ -79,6 +79,7 @@ end_per_group(https, _Config) -> init_http_dispatch() -> [ {[<<"localhost">>], [ + {[<<"chunked_response">>], chunked_handler, []}, {[<<"websocket">>], websocket_handler, []}, {[<<"headers">>, <<"dupe">>], http_handler, [{headers, [{<<"Connection">>, <<"close">>}]}]}, @@ -91,6 +92,10 @@ init_https_dispatch() -> %% http. +chunked_response(Config) -> + {ok, {{"HTTP/1.1", 200, "OK"}, _Headers, "chunked_handler\r\nworks fine!"}} = + httpc:request(build_url("/chunked_response", Config)). + headers_dupe(Config) -> {port, Port} = lists:keyfind(port, 1, Config), {ok, Socket} = gen_tcp:connect("localhost", Port, -- cgit v1.2.3