garrison
I have been thinking about how I might write a new web framework in Elixir. (Not committing to anything, just thinking about it.)
One thing I feel pretty strongly about is that building Phoenix on top of Plug has had some unfortunate downstream effects on its API design, particularly when it comes to LiveView. I think I would prefer to build directly on an HTTP server. In general I am pretty wary of becoming “trapped” in abstractions, and I like to avoid dependencies where possible for that reason.
Naturally I started looking into how one might build directly on top of Bandit, and I was very surprised to find out that Bandit depends on Plug rather than the other way around. As I understand it, Cowboy was an existing (Erlang) web server and Plug (with plug_cowboy) was written as an abstraction on top of that.
But it seems that Bandit inverts that dependency, which is not at all what I was expecting!
I am wondering:
- Why was this decision made? Is there any historical context to read through?
- How hard would it be to reverse this decision? I.e. is the dependency shallow or very deep?
I am very deliberately stopping short of claiming this decision is wrong, as I fear I may be walking into Chesterton’s Fence here ![]()
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #metaprogramming
- #hex










First Post!- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
Schultzer
I didn’t know this, and it feels kind of weird since Plug is just a generalization, but not in a way where you could support sockets/conns in a unified abstraction.
Looking forward to hear @mtrudel on this.
Most Liked
mtrudel
Bandit was written specifically with the goal of serving Plug / WebSock and nothing more (it’s literally the very first line of the project README :). The reason for this is that at some point, you need to come up with some sort of abstraction to decouple your business logic from the underlying HTTP server logic, and Plug is about as simple of an abstraction as is possible to still be useful in practice. Cowboy’s underlying abstraction (‘handlers’) aren’t somehow any more magical than Plug; it’s still just a codified abstraction between an HTTP server and your business logic. Why José went with plug_cowboy instead of just building directly on top of Cowboy’s handler behaviour is a question for him, but my guess is that handler is actually a fairly awkward API to work with. plug_cowboy actually did (and does) a great job of smoothing that API out to something more usable in the form of Plug’s single
call/2based structure.This isn’t to say that Plug is perfect, far from it. In particular, its pattern of ‘everything happens within a single function call’ means that it’s very awkward to do anything not directly related to servicing the request on the request process, at least not without involving other processes. Things like gRPC or implementing an event-aware MCP server are either not possible or else require some delegate coordination with another process. This isn’t entirely bad, or even an anti-pattern in the OTP world, but it does feel awkward in use at times.
Nor is WebSock perfect. Its spec came from extracting the existing (Cowboy-based) Websocket behaviour from Phoenix, and as a consequence is only able to send messages to the client as a return value from one of the
handle_*callbacks; there’s no way to proactively send a message to the client.My original design goal with Bandit was to very much intentionally lean on Plug as a simple abstraction, and to build the simplest thing that could turn HTTP requests into Plug calls. Every line of code in the project is laser focused on this goal; the fact that there is no lower level abstraction is kinda the whole point.
I guess my question back to you would be: if not Plug, then what?
derek-zhou
This is the fact of life of using any non-trivial library. You use at most 20% of the code/functionality. I have to constantly remind me to keep away from 2 tendencies:
I am not doing embedded programming, so unused code don’t bother me. In fact, I am against excessive package splitting. like: Phoenix, Phoenix_template, Phoenix_view, Phoenix_html? It only causes clutter and confusion.
mtrudel
Ah, I see what you might be thinking. When I say ‘Bandit has Plug as a dependency’, I mean relatively few parts of the Plug Library:
Plugbehaviour (ie: definitions ofc:Plug.init/1andc:Plug.call/2)Plug.Conn.Statusas a ready-made list of status codes,Plug.SSLas an expert-built guide for configuring SSL cipher suites securely, andPlug.Conn.Utilsfor some header parsing functions)Bandit doesn’t use any of the more ‘user-focused’ modules in Plug, such as
Plug.CSRF,Plug.Router,Plug.Builderetc. Bandit’s mandate is to get from an HTTP request to a bare Plug call as quickly and minimally as possible (“Aim for minimal internal policy and HTTP-level configuration.” is Bandit’s second directive, subordinate only to HTTP correctness). We very much do not handle sessions, cookies (beyond crumbling as mandated in RFC9113) or any higher order functionality at all. The intent is that, if those are things you’re interested in, you build them into your app using the building blocks in the Plug library.As explained above, your intuition here is correct!
If you replace
handle_request(headers, body)withPlug.call(conn, opts), you’re basically correct. The two primary differences are first, that the passed in header/body state is encoded inside aPlug.Connstructure, and second, that there are facilities for building a response in thePlug.Conn.put_*functions.Last Post!
garrison
I was referring in particular to new guidance which came out of the Go community recently. I came across this article maybe a month ago at random, but I’ve been seeing it pop up more and more. I’d imagine within a year most will be aware of it.
Your quote is missing the “library” part, which substantially changes the meaning. Of course we must still protect against CSRF, but the method Plug uses is (as of fairly recently) outdated.