Search code examples
elixirplug

How to do Plug pipelines in Elixir?


Background

I have an app that is a webserver with cowboy and uses Plugs. Since this app was inherited, using Phoenix is out of the question unless we remake the entire thing, which is not happening.

My objective is to instead of having everything inside one huge file, to have several plugs and connect them via pipelines.

Code

Let’s assume I have a main router Plug, that looks like this:

defmodule MyApp.Web.Router do
  use Plug.Router

  plug(:match)

  forward "/check", to: MyApp.Route.Check
  forward "/dispatch", to: MyApp.Plug.Dispatch      
end

So here I have 2 things. A Route for the endpoint /check, which looks like this:

defmodule MyApp.Route.Check do
  use Plug.Router

  plug(:dispatch)

  get "/", do: send_resp(conn, 200, "ok")
end

And a Plug pipeline for /dispatch that looks like this:

defmodule MyApp.Plug.Dispatch do
  use Plug.Builder

  plug(Plug.Parsers, parsers: [:urlencoded])   #parses parameters
  plug(MyApp.Plug.Metrics)                     #exposes /metrics path
  plug(Cipher.ValidatePlug)                    #typical URL validation
  plug(MyApp.Route.Dispatch)                   #forwards to dispatch Route 
end

This pipeline parses the parameters, notifies a metrics service, validates the request and then sends it to the proper Router, which looks like this:

defmodule MyApp.Route.Dispatch do
  use Plug.Router

  plug(:dispatch)

  get "/", do: send_resp(conn, 200, "Info dispatched")
end

Problem

The problem here is that nothing works. Quite literally if I launch the application and try to access even the dummest endpoint ( /check ) the code blows with errors:

17:44:03.330 [error] #PID<0.402.0> running MyApp.Web.Router (connection #PID<0.401.0>, stream id 1) terminated
Server: localhost:4003 (http)
Request: GET /check
** (exit) an exception was raised:
    ** (Plug.Conn.NotSentError) a response was neither set nor sent from the connection
        (plug_cowboy) lib/plug/cowboy/handler.ex:37: Plug.Cowboy.Handler.maybe_send/2
        (plug_cowboy) lib/plug/cowboy/handler.ex:13: Plug.Cowboy.Handler.init/2
        (...)

I have now spent the entirety of my day reading documentation and this was as far as I got. The app is super simple, it is pretty much the hello world for plugs:

https://elixirschool.com/en/lessons/specifics/plug/

But with MyApp.Web.Router instead of the one they use.

A MWE can be seen here:

https://github.com/Fl4m3Ph03n1x/plug-pipeline-problem

What am I doing wrong?


Solution

  • Route was not matching on Example.Route.Dispatch and Example.Route.Check. To fix that you need to make two changes:

    defmodule Example.Route.Dispatch do
      use Plug.Router
    
      plug(:dispatch)
    
      get "/*glob", do: send_resp(conn, 200, "Info dispatched")
    end
    

    and

    defmodule Example.Route.Check do
      use Plug.Router
    
      plug :match
      plug :dispatch
    
      get "/*glob" do
        send_resp(conn, 200, "ok")
      end
    end
    

    Alternatively, you can do this: https://github.com/Fl4m3Ph03n1x/plug-pipeline-problem/pull/1

    To understand more about why it is not matching you can use Plug.Router.match_path/1 on a "catch all" match.