Plugins are a bit like the handlers except they are run on request. There's currently two different type of plugins; pre_request and post_request. These can be used to create access logs, insert CORS headers or similar.

Plugins are used to handle things before and/or after a request. They are applied on all requests of a specified protocol.

A rule of thumb is that if you want to store a short-lived state (For the local request) then you could store that under the Env-object. If you want to store a long-lived state (For the global request) then you could store that under the State-object which is initialized in the optional init/0 callback and (potentially) updated in each call to pre_request/4 and post_request/4. The long-lived state is only applicable to the plugin-module while the local state belongs to the request and is distributed to each invoked plugin.

This is an example:

-module(correlation_id).
-behaviour(nova_plugin).
-export([
    pre_request/4,
    post_request/4,
    plugin_info/0
  ]).

pre_request(Req, _Env, _Opts, State) ->
    UUID = uuid:uuid_to_string(uuid:get_v4()),
    {ok, cowboy_req:set_resp_header(<<"x-correlation-id">>, UUID, Req), State}.

post_request(Req, _Env, _Opts, State) ->
    {ok, Req, State}.

plugin_info() ->
    #{
      title => <<"nova_cors_plugin">>,
      version => <<"0.2.0">>,
      url => <<"https://github.com/novaframework/nova">>,
      authors => [<<"Nova team <info@novaframework.org">>],
      description => <<"Add CORS headers to request">>,
      options => [
                 ]
     }.

This plugin injects a UUID into the headers.

Optional callbacks

There's two optional callbacks for a plugin that can be used for storing a global long-lived state. This can be useful if you want to keep track of something like a pid or similar. The first callback is init/0 and the second is stop/1. init/0 is called when the plugin is loaded and stop/1 is called when the plugin is unloaded (Usually when the nova-application is started/stopped). Following example shows how we spawn a process and returns the pid in a map - this map will become our new state. When the plugin is stopped, stop/1 will get invoked and we will send a stop message to the pid.

init() ->
    %% Setup some pids
    Pid = spawn(fun() -> ok end),
    #{my_pid => Pid}.

stop(State) ->
    %% Stop the pids
    Pid = maps:get(my_pid, State),
    Pid ! stop,
    ok.

Adding a plugin

Example: A good example of a very useful plugin is the nova_request_plugin. When we are developing a HTTP web api using json as the data format, we need the framework to decode our message so that we can process it. To do that we need to add decode_json_body => true into the options field in our sys.config.

sys.config

     {nova, [
         {environment, dev},
         {cowboy_configuration, #{
                                  port => 8080
                                 }},
         {dev_mode, true},
         {bootstrap_application, chatapp},
         {plugins, [
                    {pre_request, nova_request_plugin, #{parse_bindings => true,
                                                         decode_json_body => true}}
                   ]}
        ]}

We have added our plugin in the plugins section. As we can see this is a pre_request plugin since it processes and decodes the message to json format before we can actually use it in our nova application endpoints.

Usage:

controller

-module(test_controller).
-export([increment/1]).

increment(#{<<"json">> := #{<<"id">> := Id, <<"value">> := Value}})->
    {json,200,#{},#{<<"id">> => Id , <<"received">> => Value, <<"increment">> => Value+1}}.

Nova plugins

Nova has a couple of plugins for some general purposes.

PluginDescriptionCode
nova_correlation_pluginThis plugin will add a correlation id to header response but also add #{correlation_id => CorrelationID} to the request obj that is passed to the controller.nova_correlation_plugin
nova_cors_pluginThis plugin will handle cors and add the cors headers into the request.nova_cors_plugin
nova_multipart_pluginThis plugin reads multipart/form-data bodies and streams uploaded files to a handler.nova_multipart_plugin
nova_request_pluginThis plugin will handle incomming data like qs, form urlencoded and json. Multipart bodies are left to nova_multipart_plugin.nova_request_plugin

Nova correlation

This plugin will generate a uuid v4 and set it as a response header as X-Correlation-ID if nothing is configuered.

{pre_request; nova_correlation_plugin, #{request_correlation_header => CorrelationHeader,
                                         logger_metadata_key => LoggerMetaDataKey}}
OptionDescription
request_correlation_headerThis is if you want a different correlation header than the standard X-Correlation-ID
logger_metadata_keyThis is if you want to have a different metadata key then the standard correaltion_id

Nova cors

This plugins will make it so that if we get method OPTIONS it will just return back the CORS headers. In this case you don't need a controller to handle it and the plugin stops after this. For other methods it will add the CORS headers to the request.

{pre_request; nova_cors_plugin, #{allow_origins => <<"*">>}}
OptionDescription
allow_originsSpecifies which origins to insert into Access-Control-Allow-Origin

Nova request

This plugins handle incoming data and can transform them to erlang maps depending on what the options are.

{pre_request; nova_correlation_plugin, #{decode_json_body => true,
                                         read_urlencoded_body => true,
                                         parse_qs => true|list}}
OptionDescriptionReq
decode_json_bodyIf header is application/json it will decode the body.Req#{json => Map}
read_urlencoded_bodyIf header is application/x-www-form-urlencoded it will decode it.Req#{params => Map}
parse_qsIf the path have qs in it we will get them.Req#{parsed_qs => Map or List}

Nova multipart

nova_request_plugin never buffers a multipart/form-data body. Add nova_multipart_plugin to the routes that take uploads and give it a handler that receives every file part chunk by chunk:

#{prefix => "/files",
  plugins => [
    {pre_request, nova_multipart_plugin, #{handler => {nova_multipart_file_handler, #{dir => <<"/var/uploads">>}}}}
  ],
  routes => [
    {"/upload", fun upload_controller:upload/1, #{methods => [post]}}
  ]
}

The same tuple works in the global plugins list in sys.config when every route may take an upload.

-module(upload_controller).
-export([upload/1]).

upload(#{params := #{<<"title">> := Title}, files := Files}) ->
    Paths = [Path || #{result := #{path := Path}} <- Files],
    {json, 200, #{}, #{title => Title, uploaded => Paths}}.

Regular form fields end up in params. Every part with a filename ends up in files as a map with name, filename, content_type and result, where result is what the handler returned. On a non-multipart request files is [] and params is #{} unless another plugin set it, so the controller above answers a JSON request with an empty upload list rather than a 500.

Two handlers ship with Nova:

HandlerResult
nova_multipart_file_handlerStreams the part to a random name under dir and returns #{path => Path}. Pass extensions => [<<"png">>, <<"jpg">>] to keep a listed extension from the client filename, lowercased, on the stored name; any other extension is dropped, never rejected. No list means no extension.
nova_multipart_memory_handlerKeeps the part in memory and returns #{body => Binary}. Takes no init args: {nova_multipart_memory_handler, #{}}. For small attachments only.

Write your own by implementing the nova_multipart_handler behaviour (init/2, handle_data/2, handle_end/1, handle_abort/2). filename, content_type and name are all client controlled: never build a filesystem path from filename, and never echo content_type back as a response header. The extensions allowlist exists because dir may be served by a web server, where a stored .html or .svg is stored XSS. An extension says nothing about the bytes; check those in the controller if the type matters.

A part without a filename is a regular field no matter how large it is, and is bounded by max_field_size. When a form is protected by nova_csrf_plugin, nova_multipart_plugin must run before it so the token is in params.

OptionDescription
handlerRequired. {Mod, InitArgs} implementing nova_multipart_handler.
max_partsReply 413 after this many parts (default 32).
max_part_sizeReply 413 past this many bytes in a single file part (default 8 000 000).
max_field_sizeReply 413 past this many bytes in a single non-file field (default 65 536).
max_total_sizeReply 413 past this many bytes across all parts (default 64 000 000).
read_timeoutReply 408 if the whole body is not read within this many ms (default 60 000).

A malformed part gets a 400 and a handler error a 500. In every case the controller is never called and the handler's handle_abort/2 has run.

To read the parts yourself, leave nova_multipart_plugin off the route and use cowboy_req:read_part/1 from the controller, handing the updated request back through a five element return tuple:

upload(Req) ->
    {Files, Req1} = read_parts(Req, []),
    {json, 200, #{}, Req1, #{uploaded => length(Files)}}.

read_parts(Req0, Acc) ->
    case cowboy_req:read_part(Req0) of
        {ok, Headers, Req1} ->
            {ok, Body, Req2} = cowboy_req:read_part_body(Req1),
            read_parts(Req2, [{cow_multipart:form_data(Headers), Body}|Acc]);
        {done, Req1} ->
            {lists:reverse(Acc), Req1}
    end.