raxx v1.1.0 Raxx

Tooling to work with HTTP.

Several data structures are defined to model parts of the communication between client and server.

  • Raxx.Request: metadata sent by a client before sending content.
  • Raxx.Response: metadata sent by a server before sending content.
  • Raxx.Data: A part of a messages content.
  • Raxx.Tail: metadata set by client or server to conclude communication.

This module contains functions to create and manipulate these structures.

See Raxx.Server for implementing a web application.

Link to this section Summary

Types

The body of a Raxx message.

Attribute value pair that can be serialized to an HTTP request or response

List of HTTP headers.

Either a Raxx.Request.t or a Raxx.Response.t

Set of all components that make up a message to or from server.

The response status that can be parsed to e.g. response/1. This is either an integer status code like 404 or an atom that refers to a reason phrase in RFC7231 like :not_found

Functions

Does the message struct contain all the data to be sent.

Construct a Raxx.Data

Delete a header, if present from a request or response.

This function never returns an error use, get_query/1 instead.

Get the integer value for the content length of a message.

Get the value of a header field.

Return the decoded query from a request

Effect of handling this request more than once should be identical to handling it once.

The RFC7231 specified reason phrase for each known HTTP status code.

Create a response to redirect client to the given url.

Return the host, without port, for a request.

Return the host, without port, for a request.

Request does not expect any state change on the server.

Helper function that takes a list of Raxx.part/0 parts and breaks down Request/Response objects containing binary body values into their head, data and tail parts.

Set the "content-disposition" header on a response indicating the file should be downloaded.

Add a complete body to a message.

Set the content length of a request or response.

Set the value of a header field.

Add a query value to a request

Put headers that improve browser security.

Split a path on forward slashes.

Link to this section Types

Link to this type

body()

body() :: boolean() | iodata()

The body of a Raxx message.

The body can be:

  • included in the message (as iodata).
  • empty (false).
  • present but unknown (true).
Link to this type

header()

header() :: {String.t(), String.t()}

Attribute value pair that can be serialized to an HTTP request or response

Link to this type

headers()

headers() :: [header()]

List of HTTP headers.

Either a Raxx.Request.t or a Raxx.Response.t

Set of all components that make up a message to or from server.

The response status that can be parsed to e.g. response/1. This is either an integer status code like 404 or an atom that refers to a reason phrase in RFC7231 like :not_found

Link to this section Functions

Link to this function

complete?(map)

complete?(message()) :: boolean()

Does the message struct contain all the data to be sent.

Examples

iex> request(:GET, "/")
...> |> complete?()
true

iex> response(:ok)
...> |> set_body("Hello, World!")
...> |> complete?()
true

iex> response(:ok)
...> |> set_body(true)
...> |> complete?()
false

Construct a Raxx.Data

Wrap a piece of data as being part of a message's body.

Examples

iex> data("Hi").data
"Hi"
Link to this function

delete_header(message, header)

delete_header(Raxx.Request.t(), String.t()) :: Raxx.Request.t()
delete_header(Raxx.Response.t(), String.t()) :: Raxx.Response.t()

Delete a header, if present from a request or response.

Link to this function

fetch_query(request)

fetch_query(Raxx.Request.t()) :: {:ok, %{required(binary()) => binary()}}

This function never returns an error use, get_query/1 instead.

Link to this function

get_content_length(message)

get_content_length(Raxx.Request.t()) :: nil | non_neg_integer()
get_content_length(Raxx.Response.t()) :: nil | non_neg_integer()

Get the integer value for the content length of a message.

A well formed struct will always have a non negative content length, or none.

Examples

iex> response(:ok)
...> |> set_content_length(0)
...> |> get_content_length()
0

iex> response(:ok)
...> |> get_content_length()
nil
Link to this function

get_header(map, name, fallback \\ nil)

get_header(Raxx.Request.t(), String.t(), String.t() | nil) :: String.t() | nil
get_header(Raxx.Response.t(), String.t(), String.t() | nil) :: String.t() | nil

Get the value of a header field.

Examples

iex> response(:ok)
...> |> set_header("content-type", "text/html")
...> |> get_header("content-type")
"text/html"

iex> response(:ok)
...> |> set_header("content-type", "text/html")
...> |> get_header("location")
nil

iex> response(:ok)
...> |> set_header("content-type", "text/html")
...> |> get_header("content-type", "text/plain")
"text/html"

iex> response(:ok)
...> |> set_header("content-type", "text/html")
...> |> get_header("location", "/")
"/"
Link to this function

get_query(request)

Return the decoded query from a request

A map is always returned, even in the case of a request without a query string.

Examples

iex> request(:GET, "/")
...> |> get_query()
%{}

iex> request(:GET, "/?")
...> |> get_query()
%{}

iex> request(:GET, "/?foo=bar")
...> |> get_query()
%{"foo" => "bar"}
Link to this function

idempotent?(map)

idempotent?(Raxx.Request.t()) :: boolean()

Effect of handling this request more than once should be identical to handling it once.

For full definition of idempotent methods see RFC7231

Examples

iex> request(:GET, "/")
...> |> idempotent?()
true

iex> request(:POST, "/")
...> |> idempotent?()
false

iex> request(:PUT, "/")
...> |> idempotent?()
true
Link to this function

normalized_path(request)

Link to this function

reason_phrase(arg1)

reason_phrase(integer()) :: String.t() | nil

The RFC7231 specified reason phrase for each known HTTP status code.

Examples

iex> reason_phrase(200)
"OK"

iex> reason_phrase(500)
"Internal Server Error"

iex> reason_phrase(999)
nil
Link to this function

redirect(url, opts \\ [])

redirect(String.t(), status: status(), body: body(), content_type: String.t()) ::
  Raxx.Response.t()

Create a response to redirect client to the given url.

Response status can be set using the :status option. The body can be set with the :body option. Content-type header can be set with the :content_type option.

Examples

iex> redirect("/foo") ...> |> get_header("location") "/foo"

iex> redirect("/foo") ...> |> Map.get(:body) false

iex> redirect("/foo") ...> |> Map.get(:status) 303

iex> redirect("/foo", body: "Redirecting...") ...> |> Map.get(:body) "Redirecting..."

iex> redirect("/foo", body: "Redirecting...") ...> |> get_header("content-type") nil

iex> redirect("/foo", body: "Redirecting...", content_type: "text/html") ...> |> get_header("content-type") "text/html"

iex> redirect("/foo", status: 301) ...> |> Map.get(:status) 301

iex> redirect("/foo", status: :moved_permanently) ...> |> Map.get(:status) 301

Notes

This implementation was lifted from the sugar framework and is sufficient for many usecases.

I would like to implement a back function. Complication with such functionality are discussed here - https://github.com/phoenixframework/phoenix/pull/1402 Sinatra has a very complete test suite including a back implementation - https://github.com/sinatra/sinatra/blob/9bd0d40229f76ff60d81c01ad2f4b1a8e6f31e05/test/helpers_test.rb#L183

Link to this function

request(method, raw_url)

Construct a Raxx.Request.

An HTTP request must have a method and path.

If the location argument is a relative path the scheme and authority values will be unset. When these values can be inferred from the location they will be set.

The method must be an atom for one of the HTTP methods

[:GET, :POST, :PUT, :PATCH, :DELETE, :HEAD, :OPTIONS]

The request will have no body or headers. These can be added with set_header/3 and set_body/2.

Examples

iex> request(:HEAD, "/").method
:HEAD

iex> request(:GET, "/").path
[]

iex> request(:GET, "/foo/bar").path
["foo", "bar"]

iex> request(:GET, "/foo/bar").raw_path
"/foo/bar"

iex> request(:GET, "https:///").scheme
:https

iex> request(:GET, "https://example.com").authority
"example.com"

iex> request(:GET, "/").query
nil

iex> request(:GET, "/?").query
""

iex> request(:GET, "/?foo=bar").query
"foo=bar"

iex> request(:GET, "/").headers
[]

iex> request(:GET, "/").body
false

The path component of a request must contain at least /

https://tools.ietf.org/html/rfc7230#section-5.3.1

If the target URI's path component is empty, the client MUST send "/" as the path within the origin-form of request-target.

https://tools.ietf.org/html/rfc7540#section-8.1.2.3

"http" or "https" URIs that do not contain a path component MUST include a value of '/'

iex> request(:GET, "").raw_path
"/"

iex> request(:GET, "http://example.com").raw_path
"/"
Link to this function

request_host(request)

Return the host, without port, for a request.

Examples

iex> request(:GET, "http://www.example.com/hello")
...> |> request_host()
"www.example.com"

iex> request(:GET, "http://www.example.com:1234/hello")
...> |> request_host()
"www.example.com"
Link to this function

request_port(request)

Return the host, without port, for a request.

Examples

iex> request(:GET, "http://www.example.com:1234/hello")
...> |> request_port()
1234

iex> request(:GET, "http://www.example.com/hello")
...> |> request_port()
80

iex> request(:GET, "https://www.example.com/hello")
...> |> request_port()
443

iex> request(:GET, "http://www.example.com/hello")
...> |> request_port(%{http: 8080})
8080
Link to this function

request_port(request, default_ports)

request_port(Raxx.Request.t(), %{optional(atom()) => :inet.port_number()}) ::
  :inet.port_number()

See Raxx.Request.port/2.

Link to this function

response(status_code)

response(status()) :: Raxx.Response.t()

Construct a Raxx.Response.

The responses HTTP status code can be provided as an integer, or will be translated from a known atom.

The response will have no body or headers. These can be added with set_header/3 and set_body/2.

Examples

iex> response(200).status
200

iex> response(:no_content).status
204

iex> response(200).headers
[]

iex> response(200).body
false

# works for custom status codes
iex> response(299).status
299

Request does not expect any state change on the server.

For full definition of safe methods see RFC7231

Examples

iex> request(:GET, "/")
...> |> safe?()
true

iex> request(:POST, "/")
...> |> safe?()
false

iex> request(:PUT, "/")
...> |> safe?()
false
Link to this function

separate_parts(parts)

separate_parts([Raxx.part()]) :: [Raxx.part()]

Helper function that takes a list of Raxx.part/0 parts and breaks down Request/Response objects containing binary body values into their head, data and tail parts.

All other parts get left untouched.

Examples

iex> response = response(:ok)
iex> response.body
false
iex> [response] == separate_parts([response])
true

iex> response_with_body = response(:ok) |> set_body("some body")
iex> [head, data, tail] = separate_parts([response_with_body])
iex> head
%Raxx.Response{status: 200, body: true, headers: [{"content-length", "9"}]}
iex> data
%Raxx.Data{data: "some body"}
iex> tail
%Raxx.Tail{headers: []}
Link to this function

set_attachment(message, filename)

set_attachment(Raxx.Request.t(), String.t()) :: Raxx.Request.t()
set_attachment(Raxx.Response.t(), String.t()) :: Raxx.Response.t()

Set the "content-disposition" header on a response indicating the file should be downloaded.

Set's the disposition to attachment the only other value of inline is assumed when no content-disposition header.

NOTE: no integration with multipart downloads, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#As_a_response_header_for_the_main_body for full details.

NOTE: Sinatra sets content-type from filename extension, This is not done here in preference of being more explicit. https://github.com/sinatra/sinatra/blob/9590706ec6691520970c67b929776fd97d3c9ddd/lib/sinatra/base.rb#L362-L370

Examples

iex> response(:ok)
...> |> set_body("Hello, World!")
...> |> set_attachment("hello.txt")
...> |> get_header("content-disposition")
"attachment; filename=hello.txt"

iex> response(:ok)
...> |> set_body("Hello, World!")
...> |> set_attachment("hello world.txt")
...> |> get_header("content-disposition")
"attachment; filename=hello+world.txt"
Link to this function

set_body(message, body)

set_body(Raxx.Request.t(), body()) :: Raxx.Request.t()
set_body(Raxx.Response.t(), body()) :: Raxx.Response.t()

Add a complete body to a message.

All 1xx (Informational), 204 (No Content), and 304 (Not Modified) responses do not include a message body. This functional will raise an ArgumentError if a body is set on one of these reponses.

https://tools.ietf.org/html/rfc7230#section-3.1.2

Content Length

When setting the body of a message to some iodata value, then the content length is also set.

Examples

iex> request = response(:ok)
...> |> set_body("Hello, World!")
iex> request.body
"Hello, World!"
iex> Raxx.get_content_length(request)
13

iex> request = response(:ok)
...> |> set_body(true)
iex> request.body
true
iex> Raxx.get_content_length(request)
nil

Limitations

Requests using method GET or HEAD should not have a body.

An HTTP GET request includes request header fields and no payload body and is therefore transmitted as a single HEADERS frame, followed by zero or more CONTINUATION frames containing the serialized block of request header fields.

https://tools.ietf.org/html/rfc7540#section-8.1.3

Certain unusual usecases require a GET request with a body. For example elastic search

Detailed discussion here.

In such cases it is always possible to directly add the body to a request struct. Server implemenations should respect the provided body in such cases.

Response with certain status codes never have a body.

> All 1xx (Informational), 204 (No Content), and 304 (Not Modified) responses do not include a message body. All other responses do include a message body, although the body might be of zero length.

https://tools.ietf.org/html/rfc7230#section-3.3

Link to this function

set_content_length(message, content_length)

set_content_length(Raxx.Request.t(), non_neg_integer()) :: Raxx.Request.t()
set_content_length(Raxx.Response.t(), non_neg_integer()) :: Raxx.Response.t()

Set the content length of a request or response.

The content length must be a non negative integer.

Examples

iex> response(:ok)
...> |> set_content_length(13)
...> |> get_header("content-length")
"13"
Link to this function

set_header(message, name, value)

Set the value of a header field.

Examples

iex> request(:GET, "/")
...> |> set_header("referer", "example.com")
...> |> set_header("accept", "text/html")
...> |> Map.get(:headers)
[{"referer", "example.com"}, {"accept", "text/html"}]

Limitations

Raxx is protocol agnostic, i.e. it can be used to construct HTTP/1.1 or HTTP/2 messages. This limits the heads that can (or should) be set on a message

The host header should not be set, this information is encoded in the authority key of a request struct. This header is forbidden on a response.

A server MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message that lacks a Host header field and to any request message that contains more than one Host header field or a Host header field with an invalid field-value.

https://tools.ietf.org/html/rfc7230#section-5.4

Pseudo-header fields are only valid in the context in which they are defined. Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields defined for responses MUST NOT appear in requests.

https://tools.ietf.org/html/rfc7540#section-8.1.2.1

It is invalid to set a connection specific header on either a Raxx.Request or Raxx.Response. The connection specific headers are:

  • connection
  • keep-alive
  • proxy-connection,
  • transfer-encoding,
  • upgrade

Connection specific headers are not part of the end to end message, even if in HTTP/1.1 they are encoded as just another header.

The "Connection" header field allows the sender to indicate desired control options for the current connection. In order to avoid confusing downstream recipients, a proxy or gateway MUST remove or replace any received connection options before forwarding the message.

https://tools.ietf.org/html/rfc7230#section-6.1

HTTP/2 does not use the Connection header field to indicate connection-specific header fields; in this protocol, connection- specific metadata is conveyed by other means. An endpoint MUST NOT generate an HTTP/2 message containing connection-specific header fields; any message containing connection-specific header fields MUST be treated as malformed (Section 8.1.2.6)

https://tools.ietf.org/html/rfc7540#section-8.1.2.2

Link to this function

set_query(request, query)

set_query(Raxx.Request.t(), %{required(binary()) => binary()}) ::
  Raxx.Request.t()

Add a query value to a request

Examples

iex> request(:GET, "/")
...> |> set_query(%{"foo" => "bar"})
...> |> Map.get(:query)
"foo=bar"
Link to this function

set_secure_browser_headers(response)

Put headers that improve browser security.

The following headers are set:

  • x-frame-options - set to SAMEORIGIN to avoid clickjacking through iframes unless in the same origin
  • x-content-type-options - set to nosniff. This requires script and style tags to be sent with proper content type
  • x-xss-protection - set to "1; mode=block" to improve XSS protection on both Chrome and IE
  • x-download-options - set to noopen to instruct the browser not to open a download directly in the browser, to avoid HTML files rendering inline and accessing the security context of the application (like critical domain cookies)
  • x-permitted-cross-domain-policies - set to none to restrict Adobe Flash Player’s access to data

Examples

iex> response(:ok)
...> |> set_secure_browser_headers()
...> |> get_header("x-frame-options")
"SAMEORIGIN"

iex> response(:ok)
...> |> set_secure_browser_headers()
...> |> get_header("x-xss-protection")
"1; mode=block"

iex> response(:ok)
...> |> set_secure_browser_headers()
...> |> get_header("x-content-type-options")
"nosniff"

iex> response(:ok)
...> |> set_secure_browser_headers()
...> |> get_header("x-download-options")
"noopen"

iex> response(:ok)
...> |> set_secure_browser_headers()
...> |> get_header("x-permitted-cross-domain-policies")
"none"
Link to this function

split_path(path_string)

split_path(String.t()) :: [String.t()]

Split a path on forward slashes.

Examples

iex> split_path("/foo/bar")
["foo", "bar"]
Link to this function

tail(headers \\ [])

tail([{String.t(), String.t()}]) :: Raxx.Tail.t()

Construct a Raxx.Tail

Examples

iex> tail([{"digest", "opaque-data"}]).headers
[{"digest", "opaque-data"}]

iex> tail().headers
[]