tompave
Plug: forward options to nested child plugs
UPDATE:
I originally forgot to mention that my MainPlug is a Plug.Router. I only realized it while responding to Overmind, below. I’ve now updated the initial message to make it clear.
Hello,
I’m working on a project where I have a main Plug that acts as the public API of the library. This module is a Plug.Router and it includes its own plug pipeline. Users are supposed to use it in their routers and forward traffic to it. Let’s call it MyPackage.MainPlug.
This main plug router then uses a number of child plugs to get some work done. These must be separate plugs, because the idea is that users can still use these building blocks to create their own router, if they need some custom logic.
My problem is that I can’t find a way to forward options to the nested “child” plugs.
For example, this is my Phoenix router, where I forward to my plug and pass some options to it:
defmodule MyPhoenixApp.Web.Router do
use MyPhoenixApp.Web, :router
pipeline :api do
plug :accepts, ["json"]
end
scope path: "/stuff" do
pipe_through :api
forward "/", MyPackage.MainPlug, my_options: [foo: "foo", bar: "bar"]
end
end
In the next snippet are my main plug (first) and the child plug (second):
defmodule MyPackage.MainPlug do
use Plug.Router
plug MyPackage.ChildPlug
def init(opts) do
opts # == [my_options: [foo: "foo", bar: "bar"]]
end
def call(conn, opts) do
# opts == [my_options: [foo: "foo", bar: "bar"]]
conn = Plug.Conn.assign(conn, :my_options, opts[:my_options])
# do more stuff with conn
end
end
defmodule MyPackage.ChildPlug do
def init(opts) do
opts # == []
end
def call(conn, opts) do
# I need the extra options here!
conn
end
end
My problem is that I want to be able to customize ChildPlug from the outside, but I can’t find a clear way to do it.
As you can see, in MainPlug.call/2 I’m assigning the options to the conn just as an example, but it doesn’t really help me because, by then, ChildPlug has already been run.
I guess I could avoid the plug macro and invoke the child plugs directly in MainPlug.call/2, for example:
defmodule MyPackage.MainPlug do
use Plug.Router
alias MyPackage.PrivatePlug
def init(opts) do
opts # == [my_options: [foo: "foo", bar: "bar"]]
end
def call(conn, opts) do
conn = PrivatePlug.call(conn, PrivatePlug.init(opts))
# do more stuff with conn
end
end
That would work, but I’d lose the nice performance gain of being able to manipulate the opts in the init function ahead of time.
Also, this seems a common enough requirement that hopefully there is already a clean API to do what I need.
Thanks!
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security











First Post!
OvermindDL1
The
optsbinding in your call is defined at compile-time, so not really easy to pass down.But you could always load stuff into the
:privatekey of theconnand access that down the line later.Most Liked
tompave
Ok, I’ve finally had the time to work on this.
I think I’ve managed to implement what you described @OvermindDL1. It seems to work, thanks!
I’ll share it here in case it’s helpful to anyone else. I would also appreciate suggestions on how to improve it.
My plugs:
With this setup, I can plug the main module in a Plug router:
And then on the Plug server I get:
bvobart
Thanks a lot for sharing your implementation! I was running into a similar problem while writing a small wrapper for a public plug and I was able to fix it with due to this topic and your solution!
I realize it’s been 7 years since this topic opened, but there is one important detail to add that can save future readers some problems and headaches:
init/1(and therefore the module-generating macro inside it) is evaluated at compile-time, meaning that in order to be able to use your wrapping plug in multiple places in your code, you must also incorporate a unique ID in your generated module’s name, otherwise you’ll just end up redefining the same module, overwriting its previous implementations, meaning only the latest compiled module will remain. That likely causes the wrong set of options to be used in most places where you’re using this plug. For me, it also caused some flakiness in my tests due to compilation race conditions.Here’s what my solution incorporating such a unique ID looks like:
tompave
Yes, I get that, but so is the output of
MainPlug.init/1, and I was hoping that there was some macro to store it in a module attribute and pass it down to the other plugs.Looking into the code of
Plug.Builder, however, I can see that the plug pipeline is “resolved and frozen” in__before_compile__, and I guess that theMainPlugmodule (or any module plug, really) is compiled before it gets referenced in theforward "/" ...call.I can, but my child plugs are executed before
MainPlug.call/2is called, so it doesn’t really help me unless I manually invoke the plugs instead of relying on theplugmacro.Last Post!
bvobart
I can’t edit my original response anymore, but I’ve found out that
:erlang.unique_integer([:positive])may return the same integer twice between two separate runs of the Erlang VM, which could already happen with a console command as simple asmix compile && mix test.It’s better to use a
:crypto.hashof the module name plus options object: