Skip to content

Instantly share code, notes, and snippets.

@weaversam8
Created November 3, 2023 17:47
Show Gist options
  • Save weaversam8/c77fe1df5749d6eaa459d9e1b6f61d9d to your computer and use it in GitHub Desktop.
Save weaversam8/c77fe1df5749d6eaa459d9e1b6f61d9d to your computer and use it in GitHub Desktop.

Revisions

  1. weaversam8 created this gist Nov 3, 2023.
    7 changes: 7 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    Cowboy, the popular Erlang web server, has deprecated [middleware][0] and replaced
    the concept with [stream handlers][1], a more flexible, but more complicated API.
    This page contains a documented reference implementation of a stream handler, to
    help others when developing their stream handlers in the future.

    [0]: https://ninenines.eu/docs/en/cowboy/2.8/guide/middlewares/
    [1]: https://ninenines.eu/docs/en/cowboy/2.8/manual/cowboy_stream/
    117 changes: 117 additions & 0 deletions reference_stream_h.erl
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,117 @@
    -module(reference_stream_h).
    -behavior(cowboy_stream).

    -export([init/3]).
    -export([data/4]).
    -export([info/3]).
    -export([terminate/3]).
    -export([early_error/5]).

    -record(state, {next}).

    %%
    %% Callback Functions
    %%

    % This implementation was modeled from imetrics_cowboy_stream_h.
    % This is a ready-to-use template that other stream handlers can be
    % based on.

    % This function is called when the request is first received and the
    % headers are parsed and known.
    -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
    -> {cowboy_stream:commands(), #state{}}.
    init(StreamID, Req = #{ path := Path }, Opts) ->
    % The state record here holds our "Next" tuple, which contains a reference
    % to ourselves (module) and a state maintained by the cowboy_stream module.
    % It can be passed around opaquely.
    State0 = #state{},

    % When we're ready to invoke the next handler in the chain, we call
    % cowboy_stream:init/3. It returns that "Next" tuple and a list of "commands"
    % to send to the upstream HTTP process. Those commands are documented [here][0]
    %
    % [0]: https://ninenines.eu/docs/en/cowboy/2.8/manual/cowboy_stream/#commands
    {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),

    % We pass these commands thru our "fold" function, which allows us to iterate
    % thru each command and modify it if necessary. We can also add new commands
    % here or in that function if we want to. Ultimately, the list of commands is
    % returned by this function, and passed to the next handler upstream (or cowboy,
    % which passes them to the client, if this is the last handler.)
    fold(Commands0, State0#state{next=Next}).

    % This function is called with the data sent in the request body. It can be called in
    % chunks, so make sure to check the "IsFin" flag to decide whether you've received
    % all the data.
    -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
    -> {cowboy_stream:commands(), State} when State::#state{}.
    data(StreamID, IsFin, Data, State0=#state{next=Next0}) ->
    % Invoke the next stream handler in the chain.
    {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),

    % Process any commands returned by downstream stream handlers.
    fold(Commands0, State0#state{next=Next}).

    % Any messages addressed to the stream will be sent to this function. Cowboy also uses
    % this function to inform stream handlers of internal events.
    -spec info(cowboy_stream:streamid(), any(), State)
    -> {cowboy_stream:commands(), State} when State::#state{}.
    info(StreamID, Info, State0=#state{next=Next0}) ->
    % Invoke the next stream handler in the chain.
    {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),

    % Process any commands returned by downstream stream handlers.
    fold(Commands0, State0#state{next=Next}).

    % All streams will eventually be terminated and this function will be called. The
    % only time when terminate/3 is not called is when an error occurs in init/3 of
    % any stream handler, since the state is not available.
    -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
    terminate(StreamID, Reason, #state{next=Next}) ->
    % Propagate the terminate to downstream stream handlers.
    cowboy_stream:terminate(StreamID, Reason, Next).

    % This function will be called when an error occurs before the request-line and
    % all headers have been received in an HTTP/1.1 request. It includes the partial
    % request, and the response Cowboy intends to send. The return value of this
    % function is the response, so it allows you to modify it if necessary.
    -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
    cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
    when Resp::cowboy_stream:resp_command().
    early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
    % Propagate the error to downstream stream handlers.
    cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).

    %%
    %% Internal Functions
    %%

    % Any commands (see [0]) returned from downstream handlers are passed thru this
    % fold function, giving us the chance to modify, omit, replace, or append to them.
    %
    % This module transparently passes the commands thru without any changes, but this
    % logic remains in place since this is the best documented stream handler in all of
    % our codebases, and it should exist as a reference for other stream handlers to hook
    % into in the future.
    %
    % [0]: https://ninenines.eu/docs/en/cowboy/2.8/manual/cowboy_stream/#commands
    fold(Commands, State) ->
    fold(Commands, State, []).

    % At the end of the recursion, reverse the accumulator so the original order of
    % commands is preserved.
    fold([], State, Acc) ->
    {lists:reverse(Acc), State};

    % You can add additional cases here to catch specific commands.
    % fold([{response, ResponseCode, Headers, Body0}|Tail], State0, Acc) ->
    % % do something with the body
    % Body = do_something(Body0),
    % Response = {response, ResponseCode, Headers, Body},
    % fold(Tail, State0, [Response|Acc]);

    % This is the catch-all case for any commands we didn't match above, pass it
    % thru unmodified.
    fold([Command|Tail], State, Acc) ->
    fold(Tail, State, [Command|Acc]).