Packages
raxx
0.12.3
1.1.0
1.0.1
1.0.0
1.0.0-rc.3
1.0.0-rc.2
retired
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.18.1
0.18.0
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
retired
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.14
0.14.13
0.14.12
0.14.11
0.14.10
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.1
0.11.0
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
0.0.1
Interface for HTTP webservers, frameworks and clients.
Current section
Files
Jump to
Current section
Files
lib/raxx/server.ex
defmodule Raxx.Server do
@moduledoc """
Interface to handle server side communication in an HTTP message exchange.
*Using `Raxx.Server` allows an application to be run on multiple adapters.
For example [Ace](https://github.com/CrowdHailer/Ace)
has several adapters for different versions of the HTTP protocol, HTTP/1.x and HTTP/2*
## Getting Started
**Send complete response after receiving complete request.**
defmodule EchoServer do
use Raxx.Server
def handle_request(%Raxx.Request{method: :POST, path: [], body: body}, _state) do
response(:ok)
|> set_header("content-type", "text/plain")
|> set_body(body)
end
end
**Send complete response as soon as request headers are received.**
defmodule SimpleServer do
use Raxx.Server
def handle_headers(%Raxx.Request{method: :GET, path: []}, _state) do
response(:ok)
|> set_header("content-type", "text/plain")
|> set_body("Hello, World!")
end
end
**Store data as it is available from a clients request**
defmodule StreamingRequest do
use Raxx.Server
def handle_headers(%Raxx.Request{method: :PUT, body: true}, _state) do
{:ok, io_device} = File.open("my/path")
{[], {:file, device}}
end
def handle_fragment(fragment, state = {:file, device}) do
IO.write(device, fragment)
{[], state}
end
def handle_trailers(_trailers, state) do
response(:see_other)
|> set_header("location", "/")
end
end
**Subscribe server to event source and forward notifications to client.**
defmodule SubscribeToMessages do
use Raxx.Server
def handle_headers(_request, _state) do
{:ok, _} = ChatRoom.join()
response(:ok)
|> set_header("content-type", "text/plain")
|> set_body(true)
end
def handle_info({ChatRoom, data}, config) do
{[fragment(data)], config}
end
end
### Notes
- `handle_headers/2` will always be called with a request that has body as a boolean.
For small requests where buffering the whole request is acceptable a simple middleware can be used.
- Acceptable return values are the same for all callbacks;
either a `Raxx.Response`, which must be complete or
a list of message parts and a new state.
## Streaming
`Raxx.Server` defines an interface to stream the body of request and responses.
This has several advantages:
- Large payloads do not need to be help in memory
- Server can push information as it becomes available, using Server Sent Events.
- If a request has invalid headers then a reply can be set without handling the body.
- Content can be generated as requested using HTTP/2 flow control
The body of a Raxx message (Raxx.Request or `Raxx.Response`) may be one of three types:
- `io_list` - This is the complete body for the message.
- `:false` - There **is no** body, for example `:GET` requests never have a body.
- `:true` - There **is** a body, it can be processed as it is received
## Server Isolation
To start an exchange a client sends a request.
The server, upon receiving this message, sends a reply.
A logical HTTP exchange consists of a single request and response.
Methods such as [pipelining](https://en.wikipedia.org/wiki/HTTP_pipelining)
and [multiplexing](http://qnimate.com/what-is-multiplexing-in-http2/)
combine multiple logical exchanges onto a single connection.
This is done to improve performance and is a detail not exposed a server.
A Raxx server handles a single HTTP exchange.
Therefore a single connection my have multiple servers each isolated in their own process.
## Termination
An exchange can be stopped early by terminating the server process.
Support for early termination is not consistent between versions of HTTP.
- HTTP/2: server exit with reason `:normal`, stream reset with error `CANCEL`.
- HTTP/2: server exit any other reason, stream reset with error `INTERNAL_ERROR`.
- HTTP/1.x: server exit with any reason, connection is closed.
`Raxx.Server` does not provide a terminate callback.
Any cleanup that needs to be done from an aborted exchange should be handled by monitoring the server process.
"""
@typedoc """
State of application server.
Original value is the configuration given when starting the raxx application.
"""
@type state :: any()
@type request :: %Raxx.Request{}
@type response :: %Raxx.Response{}
@type fragment :: %Raxx.Fragment{}
@type trailer :: %Raxx.Trailer{}
@typedoc """
Set of all components that make up a message to or from server.
"""
@type message_part :: request | response | fragment | trailer
@typedoc """
Possible return values instructing server to send client data and update state if appropriate.
"""
@type return :: {[message_part], state} | response
@doc """
Called with a complete request once the whole body is received.
Passed a `Raxx.Request` and server configuration.
Note the value of the request body will be a string.
This callback will never be called if handle_headers/handle_fragment/handle_trailers are overwritten.
"""
@callback handle_request(request, state()) :: return
@doc """
Called once when a client starts a stream,
Passed a `Raxx.Request` and server configuration.
Note the value of the request body will be a boolean.
This callback can be relied upon to execute before any other callbacks
"""
@callback handle_headers(request, state()) :: return
@doc """
Called every time data from the request body is received
"""
@callback handle_fragment(binary(), state()) :: return
@doc """
Called once when a request finishes.
This will be called with an empty list of headers is request is completed without trailers.
"""
@callback handle_trailers([{binary(), binary()}], state()) :: return
@doc """
Called for all other messages the server may recieve
"""
@callback handle_info(any(), state()) :: return
defmacro __using__(_opts) do
quote do
import Raxx
alias Raxx.{Request, Response}
@behaviour unquote(__MODULE__)
@impl unquote(__MODULE__)
def handle_request(_request, _state) do
home_page = Raxx.html_escape("<h1>Home Page</h1>")
not_found_page = Raxx.html_escape("<h1>Ooops! Page not found.</h1>")
Raxx.response(:ok)
|> Raxx.set_header("content-type", "text/html")
|> Raxx.set_body("""
<h1>Welcome to Raxx</h1>
<p>Get started with your web application.</p>
<pre>
defmodule #{Macro.to_string(__MODULE__)} do
use #{Macro.to_string(unquote(__MODULE__))}
@impl #{Macro.to_string(unquote(__MODULE__))}
def handle_request(%{method: :GET, path: []}, _state) do
Raxx.response(:ok)
|> Raxx.set_header("content-type", "text/html")
|> Raxx.set_body(#{home_page})
end
def handle_request(_request, _state) do
Raxx.response(:not_found)
|> Raxx.set_header("content-type", "text/html")
|> Raxx.set_body(#{not_found_page})
end
end
</pre>
<p>See <a href="https://hexdocs.pm/raxx/Raxx.Server.html">documentation</a> for full details.</p>
""")
end
@impl unquote(__MODULE__)
def handle_headers(request = %{body: false}, state) do
handle_request(%{request | body: ""}, state)
end
def handle_headers(request = %{body: true}, state) do
{[], {request, "", state}}
end
@impl unquote(__MODULE__)
def handle_fragment(fragment, {request, buffer, state}) do
{[], {request, buffer <> fragment, state}}
end
@impl unquote(__MODULE__)
def handle_trailers([], {request, body, state}) do
handle_request(%{request | body: body}, state)
end
@impl unquote(__MODULE__)
def handle_info(message, state) do
require Logger
Logger.warn("#{inspect(self())} received unexpected message in handle_info/2: #{inspect(message)}")
{[], state}
end
defoverridable unquote(__MODULE__)
end
end
end