Raxx - Interface for HTTP webservers, frameworks and clients (1.0 now released!)

Raxx is an alternative to Plug and is inspired by projects such as Rack(Ruby) and Ring(Clojure).

1.0-rc.1 is now available. To use it requires a server, the examples here can be experimented with by adding {:ace, "~> 0.15.2"} to a projects dependencies, NOTE 1.0-rc.0 is equivalent to 0.14.0

Raxx models HTTP as message passing between client and server. There are two flavours to this model.

First is the simple case where a complete request is a single message from client to server and a complete response as a single message from server to client. This is the model successfully exploited by Rack.

The second cases is for streaming where a series of messages are sent in either/both directions. For HTTP this series of messages begins with a head, consisting of all header information plus a few extra details (path for request, status for response). There is then the body broken into 1 or more parts and finally a tail which may contain no further information or may include some trailers.

Simple

           request -->
Client ============================================ Server
                                   <-- response

A server can implement the handle_request/2 callback to use this model in the simplest cases.
For example:

defmodule HomePage do
  use Raxx.Server

  @impl Raxx.Server
  def handle_request(%{method: :GET, path: []}, _state) do
    response(:ok)
    |> set_header("content-type", "text/plain")
    |> set_body("Hello, World!")
  end
end

Streaming

           tail | data(1+) | head(request) -->
Client ============================================ Server
           <-- head(response) | data(1+) | tail

A server can implement the handle_head/2, handle_data/2 & handle_tail/2 callbacks to react to parts of the request as they become available

defmodule Upload do
  use Raxx.Server

  @impl Raxx.Server
  def handle_head(%{method: :PUT, path: ["upload"] body: true}, _state) do
    {:ok, io_device} = File.open("my/path")
    {[], {:file, device}}
  end

  @impl Raxx.Server
  def handle_data(data, state = {:file, device}) do
    IO.write(device, data)
    {[], state}
  end

  @impl Raxx.Server
  def handle_tail(_trailers, state) do
    response(:see_other)
    |> set_header("location", "/")
  end
end

Raxx.Server has one final callback handle_info/2 for dealing with updates from other processes within the application. Several more examples are available in the README.

Goals

Raxx is designed as an alternative to Plug but not a replacement. This thread is meant to be just about Raxx developments, see this previous topic for why I started developing an alternative to plug.

  • model the communication between Client and Server as messaging to be consistent with the erlang/Elixir world view.
    This is the main difference to plug which uses a connection model.
  • support all HTTP patterns including streaming of requests and/or responses.
    In this way it is an evolution of Rack.
  • Require only pure functions, even when implementing a stateful server.
    Familiar anyone who has implemented a GenServer.
8 Likes

Do you get HTTP2 support automatically when you use Raxx?

2 Likes

Yes

Raxx has evolved to make streaming interactions natural, a requirement for getting all the value from HTTP/2. Ace (the first server offering a Raxx interface) will server content via HTTP/1.1 or HTTP/2 dependent on the client, as long as the server is using TLS(ssl).

2 Likes

Raxx.MethodOverride Middleware for overriding a requests method.

This is the first middleware extracted. It works for applications using streamed or atomic requests.

My plan is to add a few key middleware of the coming month. The Raxx stack will then become a productive toolkit without any of the prescriptive nature of a full framework.

4 Likes

Raxx.Static Middleware for serving static content.

Another middleware added.

example usage:

defmodule MyApp.WWW do
  use Raxx.Server
  use Raxx.Static, "./public"

  @impl Raxx.Server
  def handle_request(request, state) do
  # etc ...
end
2 Likes

I wonder about the decision to make middlewares compile-time. I generally consider the fact that plugs are compile-time the biggest design flaw for plug - it’s often very limiting and annoying. The counter-argument is that it’s better for performance, but I’m not really sure it’s such a huge difference. I could easily see a setup, where plugs are initialized once on server start instead of compile-time.

This is especially true of a “static” middleware - usually you want the path configured at runtime, not compile-time.

5 Likes

At the moment there is no rules for middleware. I’m just using native elixir constructs, i.e. defoverridable.
I have deliberately held off defining how middleware should work because I want to discover the best way. A solution that checks works at runtime should not be hard. It’s only functions after all and anything can be passed in as the config argument to handle_request(request, config)

1 Like

Raxx 0.14.3 released.

Typespecs added to all public functions and dialyzer checks added to CI.

3 Likes

Raxx 0.14.4 released.

  • Add’s two functions to provide helpful errors when starting a server is_application? and verify_application.
1 Like

Looking for help with middleware.

The Raxx interface is mostly stable, we are on release candidates for a 1.0 and just waiting for experience and feedback from a few more projects before committing to a 1.0. At this point adding middleware to the project is really valuable.

Middleware are isolated projects that anyone could own while being part of a larger effort to simplify web development in Elixir. They should be only of moderate difficulty (depending on which middleware of course). I would naturally offer any help needed in getting familiar with the API.

If interested check out the new issues on the Raxx project

Update several more issues add, beginner friendly issues now marked with the beginner tag.

Raxx 0.14.7 release includes new logging middleware.

Add request logging to raxx application with use Raxx.Logger.

https://hexdocs.pm/raxx/0.14.7/Raxx.Logger.html

2 Likes

Raxx 0.14.8 released

Configuration option allows extra statuses to be added.

e.g.

# config/config.exs
config :raxx,
  :extra_statuses, [{418, "Im A Teapot"}]


# iex -S mix
Raxx.response(:im_a_teaport)
# => %{status: 418, ...}
1 Like

Raxx 0.14.10 released

Default instructional page is now server with status 404 (was 200). Helps reduce false positive in test cases.

1 Like

Raxx 0.14.12 released, including helper for browser redirection.

From the changelog

Added
  • Raxx.get_header/2 for fetching a single header value from a request/response.
  • Raxx.redirect/2 will generate a redirect response directing browser to another url.
  • Header values checked for forbidden charachters using Raxx.set_header/3.
  • Raxx.get_header/3 return fallback value if required header is not set.
3 Likes

Basic authentication and Sessions added in 0.14.13 release.

This release adds two new modules to the core Raxx project

3 Likes

Secure browser headers and request_id middleware added in 0.14.14

Added

  • Raxx.set_secure_browser_headers/1 Adds a collection of useful headers when responding to a browser.
  • Raxx.delete_header/2 delete a header from request or response.
  • Raxx.RequestID assign a request id to every request handled by an application
2 Likes

major release 0.15.0.

%Raxx.Request{} now has query strings as a binary, to access the parsed query use Raxx.fetch_query/1

This allows consumers to access the raw query to handle parsing in different ways, such as accepting multiple values per key or implementing nested queries. It is also more performant for servers because they do not parse queries that are not used

1 Like

First of all, let me say that I think it is a great idea that other types of solutions besides Plug are being explored. I think the message-passing model could work really well, and I am very interested to see how it evolves.

However, I am not really happy with how the middlewares currently work, since they ‘decorate’ the original handle_xxxx call with their own wrapper at compile time which may result in performant code, but it makes the middleware code itself quite hard to read.

And as a secondary issue, changes in one of the middlewares will probably result in Elixir having to re-compile a lot of your application since these are compile-time behaviours that will live at the core of your webserver. This was also a problem in Plug that was recently mitigated by opting out of the compile-time consolidation-optimization in the development environment, but this would not be possible in your current implementation because of the way middlewares are currently implemented.

Of course, you have more freedom when you do not restrict middlewares to be composed in a method very similar to function composition (Which is what Plug does; plug modules are basically functions with an optional prior initialization step).

I think the idea to model client<->server communication as message-passing is a really smart one, but maybe the middlewares can work with that without having to become macro-heavy creatures. Writing middlewares as modules that contain some pattern-matches for the handle_request calls that might decide between:

  • returning whatever they want and stop the passing-through to the next middleware.
  • indicating that the request should pass through to the next middleware.

Then, secretly the middleware wrapper would call these in turn whenever the ‘passthrough’ result was returned. And maybe it would even be possible to optionally use some AST remolding to make it faster by transforming it optionally into a large set of function-clauses after all. But that would make the middleware modules themselves a lot easier to read and maintain.

2 Likes

I am certainly interested in exploring solutions like this. It is something that has come up several times in the slack channel. For the moment I am exploring just not using middleware.

Instead of

use Raxx.Session

# later
Raxx.get_session(request)

I have something that just extracts the session when asked

Raxx.Session.extract(request)
1 Like

So you’d explicitly add those statements to your server?

Interesting idea. Is it abstractable? :slight_smile: