garrison

garrison

Why does Bandit depend on Plug?

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 :slight_smile:

Most Liked

mtrudel

mtrudel

Creator of Bandit

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/2 based 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?

11
Post #3
derek-zhou

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:

  • Rewrite the 20% I use myself. My code will be most likely more buggy and less battle tested.
  • Find some use for the 80% that I haven’t used. That will be like putting the cart in front of the horse.

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

mtrudel

Creator of Bandit

But Plug is a great deal more than an HTTP server abstraction.

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:

  • The Plug behaviour (ie: definitions of c:Plug.init/1 and c:Plug.call/2)
  • A few general purpose definitions (Plug.Conn.Status as a ready-made list of status codes, Plug.SSLas an expert-built guide for configuring SSL cipher suites securely, and Plug.Conn.Utils for some header parsing functions)

Bandit doesn’t use any of the more ‘user-focused’ modules in Plug, such as Plug.CSRF, Plug.Router, Plug.Builder etc. 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.

Really I was just surprised! I assumed Bandit was an implementation of an HTTP server and nothing more, and in my mind Plug sits “above that” as a level of abstraction with things like sessions and csrf and so on.

As explained above, your intuition here is correct!

Honestly I thought there was just going to be a callback like handle_request(headers, body) or whatever and I would take it from there. And I’m sure I could get back to that from Plug, but it’s just kinda sitting there awkwardly in the middle.

If you replace handle_request(headers, body) with Plug.call(conn, opts), you’re basically correct. The two primary differences are first, that the passed in header/body state is encoded inside a Plug.Conn structure, and second, that there are facilities for building a response in the Plug.Conn.put_* functions.

garrison

garrison

You are obviously going to have a far deeper understanding of this problem space than I do, so it’s entirely possible that my perspective here is not lining up with reality underneath.

But Plug is a great deal more than an HTTP server abstraction. As a completely random example, Plug includes an entire library for CSRF protection which is essentially no longer needed on the modern web. This is just the first thing that came to mind, but of course there is a ton of stuff in Plug that I just wouldn’t need or would want to reimplement myself. Cookies, sessions, and so on.

This is absolutely not the end of the world, and I could just treat Plug as a “dumb pipe”, using the simplest parts of its abstraction and ignoring the rest. But the question this raises for me is: why?

Really I was just surprised! I assumed Bandit was an implementation of an HTTP server and nothing more, and in my mind Plug sits “above that” as a level of abstraction with things like sessions and csrf and so on.

Like on the client side, where Mint is HTTP, Finch adds pooling and a reasonable API, and Req adds high-level stuff. It would be very surprising if Mint had a dependency on Req, no?

Honestly I thought there was just going to be a callback like handle_request(headers, body) or whatever and I would take it from there. And I’m sure I could get back to that from Plug, but it’s just kinda sitting there awkwardly in the middle.

And fwiw my first thought after realizing this was “oh, thousand_island must be the real HTTP server”. But it’s TCP! :slight_smile:

P.S. When I upgraded my Phoenix apps to Bandit I could not believe it was a one-line change. Thank you for however much work that must have been!

garrison

garrison

I suspected this was the case, though I wasn’t certain. Which is in part why I made this thread. Thanks for clarifying.

But can you also see that this is a bit weird? There are essentially two Plugs, a collection of low-level utils which you are taking advantage of, and a library of high-level stuff which you are certainly not going to need. This feels like an unfortunate mixing of concerns. Is there a universe where we could “split Plug in two”, here?

But if I want to build them without using the Plug library, which is the semi-hypothetical here, I cannot get rid of it! :slight_smile:

And so if I want to build a framework with a non-Plug abstraction I have to take them back out, which is what feels wrong to me. Of course there might just as easily be a Bandit.Conn I have to take them out of, so it’s really just the fact that the rest of Plug is along for the ride that seems unfortunate I suppose.

I’d be curious to hear what others think. Thank you for the clarifications!

Where Next?

Popular in Discussions Top

bartblast
With the core component system and HTTP/WebSocket infrastructure solid, it’s time to tackle Pub/Sub support. What We Have vs What’s Miss...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
wanton7
Our company has used virtual actor framework called Orleans that is for C#/.Net Framework. There are clones of it in Erlang GitHub - erle...
New
AstonJ
Bart’s talk about Hologram got me wondering what kind of apps or components people are thinking about building (or think could be built) ...
New
markmark206
Every time I build a web app, I worry about a bunch of basic things (persisting data, knowing when to compute which things, keeping thing...
New
dogweather
I’ve been brainstorming about ways to solve the N-dimensional code organization problem, and am thinking about developing a Smalltalk-lik...
New
Crowdhailer
It is rare to use direct calls to send in elixir. The call is normally wrapped in a function which is responsible for sending the correct...
New
michallepicki
Hello! To save some time when switching projects or development machines, I glued together an asdf plugin that uses Erlang precompiled b...
New
bartblast
I recently started a discussion about reactive patterns in Hologram, but I realized I need to step back and design the composability arch...
New
AstonJ
There seems to be a lot of buzz around DeepSeek at the moment, with some saying it’s a ChatGPT killer. The most remarkable thing (if they...
New

Other popular topics Top

stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement