I am aware of these and was still was curious what a Rack style webserver interface would look like for Elixir, the result was Raxx. Recently, I started building a web-framework on top of Raxx. Here are my thoughts as to “Why Raxx”.
Interested in hearing anyone’s thoughts and opinions.
I think rack is simpler. It is easier to do things like modify a request after a subsequent step has run and it is easier to recover from exceptions. Plug has to add it’s own special mechanisms for that, like registering callbacks for after render and exception handlers that can’t be plugs. That said, Plug’s model better supports things like websockets and streaming HTTP. I’d say your assessment is correct.
When I started to build a Plug like interface for processing messages from RabbitMQ, I found myself tripping over the issues I mentioned. Switching to a rack like model was a better fit for the problem because I didn’t have the complex use cases that Plug did.
I’m all for alternatives, but can you really argue that Plug.Conn and the plugs that manipulate it don’t promote simplicity? I don’t see anything that is simpler about this and none of the points seem to not apply to Plug.
Logically, if I needed a barebones system, I could just do it in my cowboy handler, like so, but ultimately it’s extremely seldom a great idea to start at that level.
The part about purity seems like an odd one to make in Elixir, which really doesn’t limit you in terms of this. Despite this, we know how to do things even with the looming possibility of using impure functions.
Why should I start with Raxx, given the fact that, for minimalism, I could start with only cowboy, and why should I use it instead of Plug? I’ve read the rationale previously posted and I’ve watched the lightning talk, but I’m still not seeing what it’s doing in addition to the cowboy bit and a few response helper functions.
Yep that is me talking. It’s pretty up to date, the principles are constant and because it is based rack it has been pretty stable from the start.
I have been experimenting with an upgrade mechanism to support chunked replys. (Loosely based on the second articled I referenced), that is far more in flux and requires a bit more work but then I hope to write about that
The problem with this approach is that you need to sit with the response in memory while you undo the stack. If you have a pipeline with 10 plugs, it is most likely that only one or two need to modify the response. In Plug you would register callbacks when you need it. In the Rack model, you need to hold all 10 middleware in the stack.
I would also argue that the use of Plug is simpler for the majority of cases. You can’t get simpler than a single function that receives the connection and returns the connection. As an example, in Phoenix you can use plugs both as “callbacks” in your controller and in your pipelines. However, in Ruby, almost all frameworks and libraries, such as Rails and Sinatra, end-up introducing a new lightweight abstraction that resembles Plug, such as callbacks or filters at the controller and application level.
However, I definitely agree the Plug model is more complex than the Rack model in the scenarios you mentioned. Part of the problem here, especially in regards to exception handling, was that the issue was discovered as we moved forward due to the interplay between Plug and immutability. I believe it would be less confusing if those issues could have been articulated up front.
That’s not saying Plug is the ideal interface for everything but for HTTP the list of pros fairly outweigh the cons (IMO ), especially with HTTP 2 being available and playing even more on the strengths of streaming, data push, multiple responses, etc.
@josevalim thanks a bunch for taking the time to reply when you have already expressed your points well elsewhere.
Maybe simplicity is in the eyes of the beholder, so I won’t add more comments around that.
I am however curious about the topic of middleware. Raxx doesn’t yet define anything like a builder etc, for stacking middleware. For my own purposes I have used defoverridable to modify application modules. e.g. this module to translate HEAD requests to GET requests https://github.com/CrowdHailer/Tokumei/blob/master/app/lib/tokumei/head.ex.
Does this method have the same drawbacks with unrolling the stack?
I believe the conclusion to this is that working with a response in memory is really quite efficient, also that these advantages are exposed by EEx. If so it is certainly possible for either approach to benefit from this.
Cheers. Thinking out lout I guess it might be possible to create some kind of middleware convention to help here.
Stacking a new middleware will only overwrite handle_request (or handle_response) if the middleware module implements a transform_request or transform_response. The middleware builder can inspect each module using Module.defines? and use defoverridable sparingly.
Sounds interesting enough to be worth an experiment.
My largest issues with rack is it’s design is based on assumptions prior to HTTP 2 spec being developed and released - if you can develop an effective and performance http2 implementation in raxx then amazing!
Otherwise my concern is that other less experienced developers may take the advantages you offer without understanding that the trade offs and then find themselves caught in a corner of deprecated design technology (rack)
I think this is a very valid concern. To be honest I don’t see any reason why a pure request -> response function should not be able to be passed to a HTTP2 server. Or maybe it could return a list of responses as promises. request -> [response, promises...] and the server knows that if the original request was http1.1 to discard the promises.
I am working on integrating Raxx and chatterbox, no firm conclusions yet but results coming soon, I hope.
Thanks, writing docs is not a strength of mine so they tool a while.
there were certainly some interesting discussions in ruby about upgrading rack, i.e. the_metal. The dark side link I originally posted linked to this proposed spec which provides more callbacks as an upgrade mechanism. This is probably the closest to the way I’m thinking at the moment. I think the extra callbacks vs a direct connection object are a similar abstraction to the way a GenServer gives you callbacks instead of using send and receive on a process.
Further to this conversation I am ready to share the outline of how I intend to handle streaming as part of a pure interface.
This implementation for chunked responses is here, (currently only built in one webserver but I am in the process of extracting). It is promising but I look forward to using it more concertedly in the future. Feedback welcome.
As I recently made my HTTP/2 server available I can now return to the topic of what a streaming interface could look like.
Rather than develop it in Raxx, I am going to directly work on the interface in Ace. I hope that reducing the layers of indirection will allow me to iterate to a good solution quicker. Once a nice solution is found the idea would be to extract it too Raxx and have Ace as just one implementation.
I have opened this issue for what a streaming API could look like with an examples for simple cases, streaming up and streaming down.