Skip to content

Controller Functions

Marc Worrell edited this page Jul 25, 2019 · 31 revisions

This page describes all the callback functions of a controller.

Controller Function Signature

All Cowmachine controller functions are of the signature:

f(RequestContext) -> {Result, RequestContext}

Request Context

The request Context argument and return value is one of:

  • A map, then it is assumed to be a cowboy_req:req() request map
  • A tuple, then the 2nd field must be the cowboy_req:req() request map

Example of such a tuple definition:

-record(context, { req :: cowboy_req:req(), ... }.

After the req you can add any fields for your application.

The Context is passed between the various functions of your controller. Cowmachine can modify the cowboy request in your request context state.

Result

The allowed results vary per controller function. Check the controller functions below to see which is expected.

Controller Functions Types

The following types are exported by cowmachine_req and used in the controller function definitions below:

%% The request context, either a tuple with a `cowboy_req:req()` or a `cowboy_req:req()` itself.
-type context() :: cowboy_req:req() | tuple().

%% Used to stop a request with a specific HTTP status code
-type halt() :: {error, term()} | {halt, 200..599}.

%% Response body, can be data, a file, device or streaming functions.
-type resp_body() :: iolist()
                   | {device, Size::pos_integer(), file:io_device()}
                   | {device, file:io_device()}
                   | {file, Size::pos_integer(), filename:filename()}
                   | {file, filename:filename()}
                   | {stream, streamfun()}
                   | {stream, Size::pos_integer(), streamfun()}
                   | {writer, writerfun()}.

%% Streaming function, repeatedly called to fetch the next chunk
-type streamfun() :: fun( () -> {streamdata(), streamfun_next()} ).
-type streamfun_next() :: streamfun() | done.
-type streamdata() :: iolist()
                    | {file, pos_integer(), filename:filename()}
                    | {file, filename:filename()}.

%% Writer function, calls output function till finished
-type writerfun() :: fun( (outputfun(), cowboy_req:req()) -> cowboy_req:req() ).
-type outputfun() :: fun( (iolist(), IsFinal::boolean(), cowboy_req:req()) -> cowboy_req:req() ).


%% Media types for accepted and provided content types
-type media_type() :: binary()
                    | {binary(), binary(), list( {binary(), binary()} )}
                    | {binary(), binary()}
                    | {binary(), list( {binary(), binary()} )}.

Request processing function: process

-spec process(Method, BodyContentType, ResultContentType, context()) ->
        {boolean() | resp_body() | halt(), context()}
   when BodyContentType :: media_type() | undefined,
        ResultContentType :: media_type(),
        Method :: binary().

Process a request. Called for all methods, except OPTIONS.

The BodyContentType is the main mime type of the Content-Type request header. For example, for Content-Type: text/plain; charset=utf-8 this will be {<<"text”>>, <<“plain">>, []}. If no request body is present, then the BodyContentType is undefined.

The ResultContentType is the content-type negotiated using the Accept request header and the content_types_provided callback. This is also a simple content type without options.

With this callback it is possible to easily match a combination of request method, content-type of the incoming data, and the negotiated outgoing data.

Processing functions usually want to use cowmachine_req:req_body(Context) to access the incoming request body.

Response body

As seen in the resp_body() typespec, the body can be one of the following:

  • iodata()
  • filename
  • device pid
  • streaming function (called repeatedly for every chunk)
  • writer function (called once, and supplied with a writer function)

POST requests

If post_is_create returns false, then this will be called to process any POST requests.

If POST processing succeeds without content, then process should return true.

After this a couple of things can happen:

  1. If cowmachine:set_resp_redirect/2 was set then a 303 See Other is returned. The location header should be set using either set_resp_redirect/2 or set_resp_header.
  2. If cowmachine:set_resp_redirect/2 was not set, then:
    • If a location header was set, then a 201 Created is returned.
    • If a response body was set, and the multiple_choices callback returns false then 200 OK
    • If a response body was set, and the multiple_choices callback returns true then 300 Multiple Choices
    • If there is no response body set, then 204 No Content

DELETE requests

The process function is called when a DELETE request should be enacted, and should return true if the deletion succeeded, false if not.

A returned body is handled as true value.

Halting Controllers

In the function list below if a function has a result type including halt(), it also has the option of returning either of the two following special values for Result:

Result Effect
{error, Err::term()} Immediately end processing of this request, returning a 500 Internal Server Error response. The response body will contain the Err term.
{halt, 200..599} Immediately end processing of this request, returning response code Code. It is the responsibility of the controller to ensure that all necessary response header and body elements are filled in Request in order to make that response code valid.

Controller Functions

There are over 30 controller functions you can define, but any of them can be omitted as they have reasonable defaults. Each function is described below, showing the default and allowed values that may be in the Result term. The default will be used if a controller does not export the function.

Any function which has no description is optional and the effect of its return value should be evident from examining the Diagram.

Below are all of the supported predefined controller functions. In addition to whichever of these a controller wishes to use, it also must export all of the functions named in the return values of the content_types_provided and content_types_accepted functions, with behavior as described in the last two controller functions below.

service_available

-spec service_available(context()) -> {boolean() | halt(), context()}.

This is the first function called, if needed you can perform any initialization here. Returning non-true values will result in 503 Service Unavailable.

Default : true

resource_exists

-spec resource_exists(context()) -> {boolean() | halt(), context()}.

Returning non-true values will result in 404 Not Found. If this function returns false then the callback previously_existed will be called to determine if 401 Gone should be returned. If the resource previously existed then a new location can be returned in moved_temporarily or moved_permanently.

Default : true

is_authorized

-spec is_authorized(context()) -> {true | binary() | halt(), context()}.

If this returns anything other than true, the response will be 401 Unauthorized. The binary() return value will be used as the value in the WWW-Authenticate header, for example Basic realm="Foobar".

Default : true

forbidden

-spec forbidden(context()) -> {boolean() | halt(), context()}.

Returning true will result in 403 Forbidden.

Default : false

allow_missing_post

-spec allow_missing_post(context()) -> {boolean() | halt(), context()}.

If the controller accepts POST requests to nonexistent resources, then this should return true.

Default : false

malformed_request

-spec malformed_request(context()) -> {boolean() | halt(), context()}.

Returning true will result in 400 Bad Request.

Default : false

uri_too_long

-spec uri_too_long(context()) -> {boolean() | halt(), context()}.

Returning true will result in 414 Request-URI Too Long.

Default : false

known_content_type

-spec known_content_type(context()) -> {boolean() | halt(), context()}.

Returning false will result in 415 Unsupported Media Type.

Default : true

valid_content_headers

-spec valid_content_headers(context()) -> {boolean() | halt(), context()}.

Returning false will result in 501 Not Implemented.

Default : true

valid_entity_length

-spec valid_entity_length(context()) -> {boolean() | halt(), context()}.

Returning false will result in 413 Request Entity Too Large.

Default : false

options

-spec options(context()) -> {[{binary(), binary()}], context()}.

If the OPTIONS method is supported and is used, the return value of this function is expected to be a list of pairs representing header names and values that should appear in the response.

known_methods

-spec allowed_methods(context()) -> {[ Method ], context()}
      when Method :: binary().

The list of all known methods. A 501 is returned if the method is not in this list. Note that these are all-caps and binaries.

Default : [ <<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"TRACE">>, <<"CONNECT">>, <<"OPTIONS">> ]

allowed_methods

-spec allowed_methods(context()) -> {[ Method ], context()}
      when Method :: binary().

If a Method not in this list is requested, then a 405 Method Not Allowed will be sent. Note that these are all-caps and binaries.

Default : [ <<"GET">>, <<"HEAD">> ]

validate_content_checksum

-spec validate_content_checksum(context()) -> {binary() | not_validated | halt(), context()}.

Called if a Content-MD5 header is present. This callback should calculate the md5 checksum of the request body content (i.e. not the response body). If not_validated is returned then cowmachine will fetch the complete body and calculate the checksum. Note that the checksum should be a binary value, not an hex encoded value.

Default: not_validated

delete_resource

This callback has been removed, use process instead.

delete_completed

-spec delete_resource(context()) -> {boolean() | halt(), context()}.

This is only called for DELETE requests after a successful process call and should return false if the deletion was accepted but cannot yet be guaranteed to have finished.

post_is_create

-spec post_is_create(context()) -> {boolean(), context()}.

If POST requests should be treated as a request to put content into a (potentially new) resource as opposed to being a generic submission for processing, then this function should return true. If it does return true, then create_path will be called and the rest of the request will be treated much like a PUT to the Path entry returned by that call.

Default : false

create_path

-spec create_path(context()) -> {Path::binary(), context()}.

This will be called on a POST request if post_is_create returns true. It is an error for this function not to produce a Path if post_is_create returns true. The Path returned should be a valid URI part following the dispatcher prefix. That Path will replace the previous one in the return value of cowmachine_req:disp_path(Request) for all subsequent controller function calls in the course of this request.

content_types_provided

-spec content_types_provided(context()) -> {[ MediaType::media_type() ], context()}.

This should return a list of media types. Content negotiation is driven by this return value. For example, if a client request includes an Accept header with a value that does not appear as a first element in any of the return tuples, then a 406 Not Acceptable will be sent.

The selected content type can be found with:

cowmachine_req:resp_content_type(Request)

Default : [ {<<"text”>>, <<“html">>, []} ]

content_types_accepted

-spec content_types_accepted(context()) -> {[ MediaType::media_type() ], context()}.

This is used similarly to content_types_provided, except that it is for incoming resource representations -- for example, PUT and POST requests.

Default : []

charsets_provided

-spec charsets_provided(context()) -> {no_charset | [ Charset::binary() ], context()}

If this is anything other than the atom no_charset, then it must be a list of binaries representing character sets.

The controller is responsible for returning the body in the selected character set.

The selected character set can be found with:

cowmachine_req:resp_chosen_charset(Request)

Example: [ <<"iso-8859-1">>, <<"utf-8">> ]

Default : no_charset, and for textual content-types [ <<“utf-8”>> ]`

content_encodings_provided

-spec content_encodings_provided(context()) -> {[ Encoding::binary() ], context()}

This must be a list of strings naming valid content encodings. The controller is responsible for the encoding of the returned body using the selected content encoding.

For identity and gzip the function cowmachine_req:encode_content/2 can be used to encode the content using the selected content encoding. Provide the content as the first argument and the Request as the second, the encoded content will be returned.

Example:

[ <<"identity">>, <<"gzip">> ]

Default : [ <<"identity">> ]

transfer_encodings_provided

-spec transfer_encodings_provided(context()) -> {[{Encoding::binary(), Encoder}], context()}
           when Encoder :: fun((iodata()) -> iodata()).

How the content is transferred, this is handy for auto-gzip of GET-only resources. identity and chunked are always available to HTTP/1.1 clients.

This must be a list of pairs where in each pair Encoding is a string naming a valid transfer encoding and Encoder is a callable function in the controller which will be called on the produced body in a GET and ensure that it is so encoded. One possible setting is to have the function check on method, and on GET requests return:

[{<<"identity">>, fun(X) -> X end},
 {<<"gzip">>, fun(X) -> zlib:gzip(X) end}]

as this is all that is needed to support gzip transfer encoding.

Default : [ {<<"identity">>, fun(X) -> X end} ]

variances

-spec variances(context()) -> {[HeaderName::binary()], context()}.

If this function is implemented, it should return a list of strings with header names that should be included in a given response's Vary header. The standard conneg headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) do not need to be specified here as Webmachine will add the correct elements of those automatically depending on controller behavior.

Default : []

is_conflict

-spec is_conflict(context()) -> {boolean(), context()}.

If this returns true, the client will receive a 409 Conflict.

Default : false

multiple_choices

-spec multiple_choices(context()) -> {boolean() | halt(), context()}.

If this returns true, then it is assumed that multiple representations of the response are possible and a single one cannot be automatically chosen, so a 300 Multiple Choices will be sent instead of a 200 OK.

Default : false

previously_existed

-spec previously_existed(context()) -> {boolean() | halt(), context()}.

If this returns true, the moved_permanently and moved_temporarily callbacks will be invoked to determine whether the response should be 301 Moved Permanently, 307 Temporary Redirect, or 410 Gone.

Default : false

moved_permanently

-spec moved_permanently(context()) -> {{true, URI::binary()} | false | halt(), context()}.

If this returns {true, URI}, the client will receive a 301 Moved Permanently with URI in the Location header.

Default : false

moved_temporarily

-spec moved_temporarily(context()) -> {{true, URI::binary()} | false | halt(), context()}.

If this returns {true, URI}, the client will receive a 307 Temporary Redirect with URI in the Location header.

Default : false

last_modified

-spec last_modified(context()) -> {calendar:datetime() | undefined, context()}.

If this returns a datetime(), it will be used for the Last-Modified header and for comparison in conditional requests.

Default : undefined

generate_etag

-spec generate_etag(context()) -> {binary() | undefined, context()}.

If this returns a binary(), it will be used for the ETag header and for comparison in conditional requests.

Default : undefined

finish_request

-spec finish_request(context()) -> {Result::any(), context()}.

This function, if exported, is called just before the final response is constructed and sent. The Result is ignored, so any effect of this function must be by returning a modified context.

Clone this wiki locally