Browse Source

Add {switch_handler, Module} return value to cowboy_rest

Also {switch_handler, Module, Opts}.

Allows switching to a different handler type. This is
particularly useful for processing most of the request
with cowboy_rest and then streaming the response body
using cowboy_loop.
Loïc Hoguin 7 years ago
parent
commit
836342abb8

+ 6 - 2
doc/src/guide/rest_handlers.asciidoc

@@ -43,8 +43,12 @@ you need.
 All callbacks take two arguments, the Req object and the State,
 All callbacks take two arguments, the Req object and the State,
 and return a three-element tuple of the form `{Value, Req, State}`.
 and return a three-element tuple of the form `{Value, Req, State}`.
 
 
-All callbacks can also return `{stop, Req, State}` to stop execution
-of the request.
+Nearly all callbacks can also return `{stop, Req, State}` to
+stop execution of the request, and
+`{{switch_handler, Module}, Req, State}` or
+`{{switch_handler, Module, Opts}, Req, State}` to switch to
+a different handler type. The exceptions are `expires`
+`generate_etag`, `last_modified` and `variances`.
 
 
 The following table summarizes the callbacks and their default values.
 The following table summarizes the callbacks and their default values.
 If the callback isn't defined, then the default value will be used.
 If the callback isn't defined, then the default value will be used.

+ 12 - 1
doc/src/manual/cowboy_rest.asciidoc

@@ -25,11 +25,15 @@ init(Req, State)
 Callback(Req, State)
 Callback(Req, State)
     -> {Result, Req, State}
     -> {Result, Req, State}
      | {stop, Req, State}
      | {stop, Req, State}
+     | {{switch_handler, Module}, Req, State}
+     | {{switch_handler, Module, Opts}, Req, State}
 
 
 terminate(Reason, Req, State) -> ok  %% optional
 terminate(Reason, Req, State) -> ok  %% optional
 
 
 Req    :: cowboy_req:req()
 Req    :: cowboy_req:req()
 State  :: any()
 State  :: any()
+Module :: module()
+Opts   :: any()
 Reason :: normal
 Reason :: normal
         | {crash, error | exit | throw, any()}
         | {crash, error | exit | throw, any()}
 
 
@@ -51,7 +55,14 @@ implemented. They otherwise all follow the same interface.
 
 
 The `stop` tuple can be returned to stop REST processing.
 The `stop` tuple can be returned to stop REST processing.
 If no response was sent before then, Cowboy will send a
 If no response was sent before then, Cowboy will send a
-'204 No Content'.
+'204 No Content'. The `stop` tuple can be returned from
+any callback, excluding `expires`, `generate_etag`,
+`last_modified` and `variances`.
+
+A `switch_handler` tuple can be returned from these same
+callbacks to stop REST processing and switch to a different
+handler type. This is very useful to, for example, to stream
+the response body.
 
 
 The optional `terminate/3` callback will ultimately be called
 The optional `terminate/3` callback will ultimately be called
 with the reason for the termination of the handler.
 with the reason for the termination of the handler.

+ 67 - 8
src/cowboy_rest.erl

@@ -20,6 +20,9 @@
 -export([upgrade/4]).
 -export([upgrade/4]).
 -export([upgrade/5]).
 -export([upgrade/5]).
 
 
+-type switch_handler() :: {switch_handler, module()}
+	| {switch_handler, module(), any()}.
+
 %% Common handler callbacks.
 %% Common handler callbacks.
 
 
 -callback init(Req, any())
 -callback init(Req, any())
@@ -35,162 +38,181 @@
 -callback allowed_methods(Req, State)
 -callback allowed_methods(Req, State)
 	-> {[binary()], Req, State}
 	-> {[binary()], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([allowed_methods/2]).
 -optional_callbacks([allowed_methods/2]).
 
 
 -callback allow_missing_post(Req, State)
 -callback allow_missing_post(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([allow_missing_post/2]).
 -optional_callbacks([allow_missing_post/2]).
 
 
 -callback charsets_provided(Req, State)
 -callback charsets_provided(Req, State)
 	-> {[binary()], Req, State}
 	-> {[binary()], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([charsets_provided/2]).
 -optional_callbacks([charsets_provided/2]).
 
 
 -callback content_types_accepted(Req, State)
 -callback content_types_accepted(Req, State)
 	-> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State}
 	-> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([content_types_accepted/2]).
 -optional_callbacks([content_types_accepted/2]).
 
 
 -callback content_types_provided(Req, State)
 -callback content_types_provided(Req, State)
 	-> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State}
 	-> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([content_types_provided/2]).
 -optional_callbacks([content_types_provided/2]).
 
 
 -callback delete_completed(Req, State)
 -callback delete_completed(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([delete_completed/2]).
 -optional_callbacks([delete_completed/2]).
 
 
 -callback delete_resource(Req, State)
 -callback delete_resource(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([delete_resource/2]).
 -optional_callbacks([delete_resource/2]).
 
 
 -callback expires(Req, State)
 -callback expires(Req, State)
 	-> {calendar:datetime() | binary() | undefined, Req, State}
 	-> {calendar:datetime() | binary() | undefined, Req, State}
-	| {stop, Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([expires/2]).
 -optional_callbacks([expires/2]).
 
 
 -callback forbidden(Req, State)
 -callback forbidden(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([forbidden/2]).
 -optional_callbacks([forbidden/2]).
 
 
 -callback generate_etag(Req, State)
 -callback generate_etag(Req, State)
 	-> {binary() | {weak | strong, binary()}, Req, State}
 	-> {binary() | {weak | strong, binary()}, Req, State}
-	| {stop, Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([generate_etag/2]).
 -optional_callbacks([generate_etag/2]).
 
 
 -callback is_authorized(Req, State)
 -callback is_authorized(Req, State)
 	-> {true | {false, iodata()}, Req, State}
 	-> {true | {false, iodata()}, Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([is_authorized/2]).
 -optional_callbacks([is_authorized/2]).
 
 
 -callback is_conflict(Req, State)
 -callback is_conflict(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([is_conflict/2]).
 -optional_callbacks([is_conflict/2]).
 
 
 -callback known_methods(Req, State)
 -callback known_methods(Req, State)
 	-> {[binary()], Req, State}
 	-> {[binary()], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([known_methods/2]).
 -optional_callbacks([known_methods/2]).
 
 
 -callback languages_provided(Req, State)
 -callback languages_provided(Req, State)
 	-> {[binary()], Req, State}
 	-> {[binary()], Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([languages_provided/2]).
 -optional_callbacks([languages_provided/2]).
 
 
 -callback last_modified(Req, State)
 -callback last_modified(Req, State)
 	-> {calendar:datetime(), Req, State}
 	-> {calendar:datetime(), Req, State}
-	| {stop, Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([last_modified/2]).
 -optional_callbacks([last_modified/2]).
 
 
 -callback malformed_request(Req, State)
 -callback malformed_request(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([malformed_request/2]).
 -optional_callbacks([malformed_request/2]).
 
 
 -callback moved_permanently(Req, State)
 -callback moved_permanently(Req, State)
 	-> {{true, iodata()} | false, Req, State}
 	-> {{true, iodata()} | false, Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([moved_permanently/2]).
 -optional_callbacks([moved_permanently/2]).
 
 
 -callback moved_temporarily(Req, State)
 -callback moved_temporarily(Req, State)
 	-> {{true, iodata()} | false, Req, State}
 	-> {{true, iodata()} | false, Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([moved_temporarily/2]).
 -optional_callbacks([moved_temporarily/2]).
 
 
 -callback multiple_choices(Req, State)
 -callback multiple_choices(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([multiple_choices/2]).
 -optional_callbacks([multiple_choices/2]).
 
 
 -callback options(Req, State)
 -callback options(Req, State)
 	-> {ok, Req, State}
 	-> {ok, Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([options/2]).
 -optional_callbacks([options/2]).
 
 
 -callback previously_existed(Req, State)
 -callback previously_existed(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([previously_existed/2]).
 -optional_callbacks([previously_existed/2]).
 
 
 -callback resource_exists(Req, State)
 -callback resource_exists(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([resource_exists/2]).
 -optional_callbacks([resource_exists/2]).
 
 
 -callback service_available(Req, State)
 -callback service_available(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([service_available/2]).
 -optional_callbacks([service_available/2]).
 
 
 -callback uri_too_long(Req, State)
 -callback uri_too_long(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([uri_too_long/2]).
 -optional_callbacks([uri_too_long/2]).
 
 
 -callback valid_content_headers(Req, State)
 -callback valid_content_headers(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([valid_content_headers/2]).
 -optional_callbacks([valid_content_headers/2]).
 
 
 -callback valid_entity_length(Req, State)
 -callback valid_entity_length(Req, State)
 	-> {boolean(), Req, State}
 	-> {boolean(), Req, State}
 	| {stop, Req, State}
 	| {stop, Req, State}
+	| {switch_handler(), Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([valid_entity_length/2]).
 -optional_callbacks([valid_entity_length/2]).
 
 
 -callback variances(Req, State)
 -callback variances(Req, State)
 	-> {[binary()], Req, State}
 	-> {[binary()], Req, State}
-	| {stop, Req, State}
 	when Req::cowboy_req:req(), State::any().
 	when Req::cowboy_req:req(), State::any().
 -optional_callbacks([variances/2]).
 -optional_callbacks([variances/2]).
 
 
@@ -233,11 +255,17 @@
 
 
 -spec upgrade(Req, Env, module(), any())
 -spec upgrade(Req, Env, module(), any())
 	-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
 	-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
-upgrade(Req0, Env, Handler, HandlerState) ->
+upgrade(Req0, Env, Handler, HandlerState0) ->
 	Method = cowboy_req:method(Req0),
 	Method = cowboy_req:method(Req0),
-	{ok, Req, Result} = service_available(Req0, #state{method=Method,
-		handler=Handler, handler_state=HandlerState}),
-	{ok, Req, Env#{result => Result}}.
+	case service_available(Req0, #state{method=Method,
+			handler=Handler, handler_state=HandlerState0}) of
+		{ok, Req, Result} ->
+			{ok, Req, Env#{result => Result}};
+		{Mod, Req, HandlerState} ->
+			Mod:upgrade(Req, Env, Handler, HandlerState);
+		{Mod, Req, HandlerState, Opts} ->
+			Mod:upgrade(Req, Env, Handler, HandlerState, Opts)
+	end.
 
 
 -spec upgrade(Req, Env, module(), any(), any())
 -spec upgrade(Req, Env, module(), any(), any())
 	-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
 	-> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env().
@@ -260,6 +288,8 @@ known_methods(Req, State=#state{method=Method}) ->
 			next(Req, State, 501);
 			next(Req, State, 501);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{List, Req2, HandlerState} ->
 		{List, Req2, HandlerState} ->
 			State2 = State#state{handler_state=HandlerState},
 			State2 = State#state{handler_state=HandlerState},
 			case lists:member(Method, List) of
 			case lists:member(Method, List) of
@@ -285,6 +315,8 @@ allowed_methods(Req, State=#state{method=Method}) ->
 				[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]);
 				[<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{List, Req2, HandlerState} ->
 		{List, Req2, HandlerState} ->
 			State2 = State#state{handler_state=HandlerState},
 			State2 = State#state{handler_state=HandlerState},
 			case lists:member(Method, List) of
 			case lists:member(Method, List) of
@@ -316,6 +348,8 @@ is_authorized(Req, State) ->
 			forbidden(Req, State);
 			forbidden(Req, State);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{true, Req2, HandlerState} ->
 		{true, Req2, HandlerState} ->
 			forbidden(Req2, State#state{handler_state=HandlerState});
 			forbidden(Req2, State#state{handler_state=HandlerState});
 		{{false, AuthHead}, Req2, HandlerState} ->
 		{{false, AuthHead}, Req2, HandlerState} ->
@@ -346,6 +380,8 @@ options(Req, State=#state{allowed_methods=Methods, method= <<"OPTIONS">>}) ->
 				= << << ", ", M/binary >> || M <- Methods >>,
 				= << << ", ", M/binary >> || M <- Methods >>,
 			Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req),
 			Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req),
 			respond(Req2, State, 200);
 			respond(Req2, State, 200);
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
 		{ok, Req2, HandlerState} ->
 		{ok, Req2, HandlerState} ->
@@ -387,6 +423,8 @@ content_types_provided(Req, State) ->
 			end;
 			end;
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{[], Req2, HandlerState} ->
 		{[], Req2, HandlerState} ->
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 		{CTP, Req2, HandlerState} ->
 		{CTP, Req2, HandlerState} ->
@@ -489,6 +527,8 @@ languages_provided(Req, State) ->
 			charsets_provided(Req, State);
 			charsets_provided(Req, State);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{[], Req2, HandlerState} ->
 		{[], Req2, HandlerState} ->
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 		{LP, Req2, HandlerState} ->
 		{LP, Req2, HandlerState} ->
@@ -549,6 +589,8 @@ charsets_provided(Req, State) ->
 			set_content_type(Req, State);
 			set_content_type(Req, State);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{[], Req2, HandlerState} ->
 		{[], Req2, HandlerState} ->
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 			not_acceptable(Req2, State#state{handler_state=HandlerState});
 		{CP, Req2, HandlerState} ->
 		{CP, Req2, HandlerState} ->
@@ -832,6 +874,8 @@ moved_permanently(Req, State, OnFalse) ->
 			OnFalse(Req2, State#state{handler_state=HandlerState});
 			OnFalse(Req2, State#state{handler_state=HandlerState});
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		no_call ->
 		no_call ->
 			OnFalse(Req, State)
 			OnFalse(Req, State)
 	end.
 	end.
@@ -853,6 +897,8 @@ moved_temporarily(Req, State) ->
 			is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState}, 410);
 			is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState}, 410);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		no_call ->
 		no_call ->
 			is_post_to_missing_resource(Req, State, 410)
 			is_post_to_missing_resource(Req, State, 410)
 	end.
 	end.
@@ -903,6 +949,8 @@ accept_resource(Req, State) ->
 			respond(Req, State, 415);
 			respond(Req, State, 415);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{CTA, Req2, HandlerState} ->
 		{CTA, Req2, HandlerState} ->
 			CTA2 = [normalize_content_types(P) || P <- CTA],
 			CTA2 = [normalize_content_types(P) || P <- CTA],
 			State2 = State#state{handler_state=HandlerState},
 			State2 = State#state{handler_state=HandlerState},
@@ -938,6 +986,8 @@ process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) ->
 	try case call(Req, State, Fun) of
 	try case call(Req, State, Fun) of
 		{stop, Req2, HandlerState2} ->
 		{stop, Req2, HandlerState2} ->
 			terminate(Req2, State#state{handler_state=HandlerState2});
 			terminate(Req2, State#state{handler_state=HandlerState2});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{true, Req2, HandlerState2} when Exists ->
 		{true, Req2, HandlerState2} when Exists ->
 			State2 = State#state{handler_state=HandlerState2},
 			State2 = State#state{handler_state=HandlerState2},
 			next(Req2, State2, fun has_resp_body/2);
 			next(Req2, State2, fun has_resp_body/2);
@@ -1019,6 +1069,8 @@ set_resp_body(Req, State=#state{content_type_a={_, Callback}}) ->
 	try case call(Req, State, Callback) of
 	try case call(Req, State, Callback) of
 		{stop, Req2, HandlerState2} ->
 		{stop, Req2, HandlerState2} ->
 			terminate(Req2, State#state{handler_state=HandlerState2});
 			terminate(Req2, State#state{handler_state=HandlerState2});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{Body, Req2, HandlerState2} ->
 		{Body, Req2, HandlerState2} ->
 			State2 = State#state{handler_state=HandlerState2},
 			State2 = State#state{handler_state=HandlerState2},
 			Req3 = cowboy_req:set_resp_body(Body, Req2),
 			Req3 = cowboy_req:set_resp_body(Body, Req2),
@@ -1114,6 +1166,8 @@ expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
 			next(Req, State, OnTrue);
 			next(Req, State, OnTrue);
 		{stop, Req2, HandlerState} ->
 		{stop, Req2, HandlerState} ->
 			terminate(Req2, State#state{handler_state=HandlerState});
 			terminate(Req2, State#state{handler_state=HandlerState});
+		{Switch, Req2, HandlerState} when element(1, Switch) =:= switch_handler ->
+			switch_handler(Switch, Req2, HandlerState);
 		{Expected, Req2, HandlerState} ->
 		{Expected, Req2, HandlerState} ->
 			next(Req2, State#state{handler_state=HandlerState}, OnTrue);
 			next(Req2, State#state{handler_state=HandlerState}, OnTrue);
 		{_Unexpected, Req2, HandlerState} ->
 		{_Unexpected, Req2, HandlerState} ->
@@ -1148,6 +1202,11 @@ next(Req, State, StatusCode) when is_integer(StatusCode) ->
 respond(Req, State, StatusCode) ->
 respond(Req, State, StatusCode) ->
 	terminate(cowboy_req:reply(StatusCode, Req), State).
 	terminate(cowboy_req:reply(StatusCode, Req), State).
 
 
+switch_handler({switch_handler, Mod}, Req, HandlerState) ->
+	{Mod, Req, HandlerState};
+switch_handler({switch_handler, Mod, Opts}, Req, HandlerState) ->
+	{Mod, Req, HandlerState, Opts}.
+
 -spec error_terminate(cowboy_req:req(), #state{}, atom(), any()) -> no_return().
 -spec error_terminate(cowboy_req:req(), #state{}, atom(), any()) -> no_return().
 error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, Reason) ->
 error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, Reason) ->
 	cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler),
 	cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler),

+ 36 - 0
test/handlers/switch_handler_h.erl

@@ -0,0 +1,36 @@
+-module(switch_handler_h).
+
+-export([init/2]).
+-export([content_types_provided/2]).
+-export([provide/2]).
+-export([info/3]).
+
+init(Req, State) ->
+	{cowboy_rest, Req, State}.
+
+content_types_provided(Req, State) ->
+	{[{<<"text/plain">>, provide}], Req, State}.
+
+provide(Req0, run) ->
+	Req = cowboy_req:stream_reply(200, Req0),
+	send_after(0),
+	{{switch_handler, cowboy_loop}, Req, 0};
+provide(Req0, hibernate) ->
+	Req = cowboy_req:stream_reply(200, Req0),
+	send_after(0),
+	{{switch_handler, cowboy_loop, hibernate}, Req, 0}.
+
+send_after(N) ->
+	erlang:send_after(100, self(), {stream, msg(N)}).
+
+msg(0) -> <<"Hello\n">>;
+msg(1) -> <<"streamed\n">>;
+msg(2) -> <<"world!\n">>;
+msg(3) -> stop.
+
+info({stream, stop}, Req, State) ->
+	{stop, Req, State};
+info({stream, What}, Req, State) ->
+	cowboy_req:stream_body(What, nofin, Req),
+	send_after(State + 1),
+	{ok, Req, State + 1}.

+ 70 - 0
test/rest_handler_SUITE.erl

@@ -0,0 +1,70 @@
+%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(rest_handler_SUITE).
+-compile(export_all).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+-import(cowboy_test, [gun_open/1]).
+
+%% ct.
+
+all() ->
+	cowboy_test:common_all().
+
+groups() ->
+	cowboy_test:common_groups(ct_helper:all(?MODULE)).
+
+init_per_group(Name, Config) ->
+	cowboy_test:init_common_groups(Name, Config, ?MODULE).
+
+end_per_group(Name, _) ->
+	cowboy:stop_listener(Name).
+
+%% Dispatch configuration.
+
+init_dispatch(_) ->
+	cowboy_router:compile([{'_', [
+		{"/switch_handler", switch_handler_h, run},
+		{"/switch_handler_opts", switch_handler_h, hibernate}
+	]}]).
+
+%% Internal.
+
+do_decode(Headers, Body) ->
+	case lists:keyfind(<<"content-encoding">>, 1, Headers) of
+		{_, <<"gzip">>} -> zlib:gunzip(Body);
+		_ -> Body
+	end.
+
+%% Tests.
+
+switch_handler(Config) ->
+	doc("Switch REST to loop handler for streaming the response body."),
+	ConnPid = gun_open(Config),
+	Ref = gun:get(ConnPid, "/switch_handler", [{<<"accept-encoding">>, <<"gzip">>}]),
+	{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
+	{ok, Body} = gun:await_body(ConnPid, Ref),
+	<<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body),
+	ok.
+
+switch_handler_opts(Config) ->
+	doc("Switch REST to loop handler for streaming the response body; with options."),
+	ConnPid = gun_open(Config),
+	Ref = gun:get(ConnPid, "/switch_handler_opts", [{<<"accept-encoding">>, <<"gzip">>}]),
+	{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
+	{ok, Body} = gun:await_body(ConnPid, Ref),
+	<<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body),
+	ok.