Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions rel/overlay/etc/default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ prefer_minimal = Cache-Control, Content-Length, Content-Range, Content-Type, ETa
; _dbs_info in a request
max_db_number_for_dbs_info_req = 100

; set to true to delay the start of a response until the end has been calculated
;buffer_response = false

; authentication handlers
; authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
; uncomment the next line to enable proxy authentication
Expand Down
58 changes: 49 additions & 9 deletions src/chttpd/src/chttpd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@
req,
code,
headers,
first_chunk,
resp=nil
chunks,
resp=nil,
buffer_response=false
}).

start_link() ->
Expand Down Expand Up @@ -780,40 +781,54 @@ start_json_response(Req, Code, Headers0) ->
end_json_response(Resp) ->
couch_httpd:end_json_response(Resp).


start_delayed_json_response(Req, Code) ->
start_delayed_json_response(Req, Code, []).


start_delayed_json_response(Req, Code, Headers) ->
start_delayed_json_response(Req, Code, Headers, "").


start_delayed_json_response(Req, Code, Headers, FirstChunk) ->
{ok, #delayed_resp{
start_fun = fun start_json_response/3,
req = Req,
code = Code,
headers = Headers,
first_chunk = FirstChunk}}.
chunks = [FirstChunk],
buffer_response = buffer_response(Req)}}.


start_delayed_chunked_response(Req, Code, Headers) ->
start_delayed_chunked_response(Req, Code, Headers, "").


start_delayed_chunked_response(Req, Code, Headers, FirstChunk) ->
{ok, #delayed_resp{
start_fun = fun start_chunked_response/3,
req = Req,
code = Code,
headers = Headers,
first_chunk = FirstChunk}}.
chunks = [FirstChunk],
buffer_response = buffer_response(Req)}}.


send_delayed_chunk(#delayed_resp{}=DelayedResp, Chunk) ->
send_delayed_chunk(#delayed_resp{buffer_response=false}=DelayedResp, Chunk) ->
{ok, #delayed_resp{resp=Resp}=DelayedResp1} =
start_delayed_response(DelayedResp),
{ok, Resp} = send_chunk(Resp, Chunk),
{ok, DelayedResp1}.
{ok, DelayedResp1};

send_delayed_chunk(#delayed_resp{buffer_response=true}=DelayedResp, Chunk) ->
#delayed_resp{chunks = Chunks} = DelayedResp,
{ok, DelayedResp#delayed_resp{chunks = [Chunk | Chunks]}}.


send_delayed_last_chunk(Req) ->
send_delayed_chunk(Req, []).


send_delayed_error(#delayed_resp{req=Req,resp=nil}=DelayedResp, Reason) ->
{Code, ErrorStr, ReasonStr} = error_info(Reason),
{ok, Resp} = send_error(Req, Code, ErrorStr, ReasonStr),
Expand All @@ -823,6 +838,7 @@ send_delayed_error(#delayed_resp{resp=Resp, req=Req}, Reason) ->
log_error_with_stack_trace(Reason),
throw({http_abort, Resp, Reason}).


close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
% Use a separate chunk to close the streamed array to maintain strict
% compatibility with earlier versions. See COUCHDB-2724
Expand All @@ -831,10 +847,22 @@ close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
close_delayed_json_object(Resp, Buffer, Terminator, _Threshold) ->
send_delayed_chunk(Resp, [Buffer | Terminator]).

end_delayed_json_response(#delayed_resp{}=DelayedResp) ->

end_delayed_json_response(#delayed_resp{buffer_response=false}=DelayedResp) ->
{ok, #delayed_resp{resp=Resp}} =
start_delayed_response(DelayedResp),
end_json_response(Resp).
end_json_response(Resp);

end_delayed_json_response(#delayed_resp{buffer_response=true}=DelayedResp) ->
#delayed_resp{
req = Req,
code = Code,
headers = Headers,
chunks = Chunks
} = DelayedResp,
{ok, Resp} = start_response_length(Req, Code, Headers, iolist_size(Chunks)),
send(Resp, lists:reverse(Chunks)).


get_delayed_req(#delayed_resp{req=#httpd{mochi_req=MochiReq}}) ->
MochiReq;
Expand All @@ -847,7 +875,7 @@ start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) ->
req=Req,
code=Code,
headers=Headers,
first_chunk=FirstChunk
chunks=[FirstChunk]
}=DelayedResp,
{ok, Resp} = StartFun(Req, Code, Headers),
case FirstChunk of
Expand All @@ -858,6 +886,18 @@ start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) ->
start_delayed_response(#delayed_resp{}=DelayedResp) ->
{ok, DelayedResp}.


buffer_response(Req) ->
case chttpd:qs_value(Req, "buffer_response") of
"false" ->
false;
"true" ->
true;
_ ->
config:get_boolean("chttpd", "buffer_response", false)
end.


error_info({Error, Reason}) when is_list(Reason) ->
error_info({Error, couch_util:to_binary(Reason)});
error_info(bad_request) ->
Expand Down
73 changes: 73 additions & 0 deletions src/chttpd/test/eunit/chttpd_delayed_test.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-module(chttpd_delayed_test).

-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").

-define(USER, "chttpd_view_test_admin").
-define(PASS, "pass").
-define(AUTH, {basic_auth, {?USER, ?PASS}}).
-define(CONTENT_JSON, {"Content-Type", "application/json"}).
-define(DDOC, "{\"_id\": \"_design/bar\", \"views\": {\"baz\":
{\"map\": \"function(doc) {emit(doc._id, doc._id);}\"}}}").

-define(FIXTURE_TXT, ?ABS_PATH(?FILE)).
-define(i2l(I), integer_to_list(I)).
-define(TIMEOUT, 60). % seconds

setup() ->
Hashed = couch_passwords:hash_admin_password(?PASS),
ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist=false),
ok = config:set("chttpd", "buffer_response", "true"),
TmpDb = ?tempdb(),
Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
Port = mochiweb_socket_server:get(chttpd, port),
Url = lists:concat(["http://", Addr, ":", Port, "/", ?b2l(TmpDb)]),
create_db(Url),
Url.

teardown(Url) ->
delete_db(Url),
ok = config:delete("admins", ?USER, _Persist=false).

create_db(Url) ->
{ok, Status, _, _} = test_request:put(Url, [?CONTENT_JSON, ?AUTH], "{}"),
?assert(Status =:= 201 orelse Status =:= 202).


delete_db(Url) ->
{ok, 200, _, _} = test_request:delete(Url, [?AUTH]).


all_test_() ->
{
"chttpd delay tests",
{
setup,
fun chttpd_test_util:start_couch/0, fun chttpd_test_util:stop_couch/1,
{
foreach,
fun setup/0, fun teardown/1,
[
fun test_buffer_response_all_docs/1,
fun test_buffer_response_changes/1
]
}
}
}.


test_buffer_response_all_docs(Url) ->
assert_has_content_length(Url ++ "/_all_docs").


test_buffer_response_changes(Url) ->
assert_has_content_length(Url ++ "/_changes").


assert_has_content_length(Url) ->
{timeout, ?TIMEOUT, ?_test(begin
{ok, Code, Headers, _Body} = test_request:get(Url, [?AUTH]),
?assertEqual(200, Code),
?assert(lists:keymember("Content-Length", 1, Headers))
end)}.