Plugg router forward no response set

Hello all,

trying out plugg + ecto instead of using full phoenix for api development and I’m getting:

09:11:22.225 [error] GenServer #PID<0.621.0> terminating
** (Plug.Conn.NotSentError) a response was neither set nor sent from the connection
    (bandit 0.7.7) lib/bandit/pipeline.ex:121: Bandit.Pipeline.commit_response/2
    (bandit 0.7.7) lib/bandit/http1/handler.ex:24: Bandit.HTTP1.Handler.handle_data/3
    (bandit 0.7.7) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
    (bandit 0.7.7) lib/thousand_island/handler.ex:332: Bandit.DelegatingHandler.handle_continue/2
    (stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.15.2) gen_server.erl:437: :gen_server.loop/7
    (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: {:continue, :handle_connection}
State: {%ThousandIsland.Socket{socket: #Port<0.17>, transport_module: ThousandIsland.Transports.TCP, read_timeout: 60000, span: %ThousandIsland.Telemetry{span_name: :connection, telemetry_span_context: #Reference<0.526008314.269221892.10052>, start_time: -576460747528887000, start_metadata: %{parent_telemetry_span_context: #Reference<0.526008314.269221892.9968>, remote_address: {127, 0, 0, 1}, remote_port: 57037, telemetry_span_context: #Reference<0.526008314.269221892.10052>}}}, %{handler_module: Bandit.InitialHandler, opts: %{http_1: [], http_2: [], websocket: []}, plug: {AuthexWeb.Endpoint, []}}}

in mi root router I have this line:

forward "/tokens", to: Routers.Token

And this is router I’m forwarding to:

defmodule AuthexWeb.Routers.Token do
  use Web.Router

  alias AuthexWeb.Plugs
  alias AuthexWeb.Controllers

  plug(Plugs.BasicClientAuthorization)

  post "/tokens" do
    IO.puts("one")
    Controllers.Token.create(conn, conn.assigns.current_client, conn.body_params)
  end

  post "/" do
    IO.puts("two")
    Controllers.Token.create(conn, conn.assigns.current_client, conn.body_params)
  end

  post _ do
    IO.puts("_")
    Controllers.Token.create(conn, conn.assigns.current_client, conn.body_params)
  end

  post "*a" do
    IO.puts("_")
    Controllers.Token.create(conn, conn.assigns.current_client, conn.body_params)
  end

  match _ do
    IO.puts("what?")
    Controllers.Fallback.not_found(conn)
  end
end

I have also debug log in Plugs.BasicClientAuthorization which get printed every time, btw. this is how it looks like:

defmodule AuthexWeb.Plugs.BasicClientAuthorization do
  use Web.Plug

  alias Plug.BasicAuth

  def init(opts), do: opts

  def call(conn, _opts) do
    case BasicAuth.parse_basic_auth(conn) do
      {id, secret} ->
        case Authex.get_client(id) do
          nil ->
            invalid_authorization(conn)
          client ->
            if Authex.client_secret_valid?(client, secret) do
              assign(conn, :current_client, client)
            else
              invalid_authorization(conn)
            end
        end
      _ ->
        conn
        |> BasicAuth.request_basic_auth()
        |> halt()
    end
    |> IO.inspect(label: "#{inspect(__MODULE__)}")
  end

  defp invalid_authorization(conn) do
    conn
    |> send_resp(403, "{\"error\": \"invalid\"}")
    |> halt()
  end
end

Do you see anything wrong? Even some hints on debugging plug would be appreciated.

thank you

Like the error message suggests, you need to set and send a response with your connection!

conn
|> Plug.Conn.resp(404, "Not found")
|> Plug.Conn.send_resp()

source: Plug.Conn.send_resp/1

Funnily enough, this is also covered in the Phoenix Controller docs under Sending responses directly.

That is true, also I got that, my problem is that I don’t know why on router I’m forwarding to nothing is matched. I have rule on root router to forward "/tokens" to Routers.Token and there I have post "/tokens", post "/", post _, post "*a" and match _ and neither of them is run, only that plug(Plugs.BasicClientAuthorization) is run. I’m doing POST /tokens request.

Hmm, forward/1 will forward requests to another plug so Routers.Token, as the target module plug needs to define an init/1 and call/2 functions.

A module plug implements an init/1 function to initialize the options and a call/2 function which receives the connection and initialized options and returns the connection:

source: Plug.Router.forward/2

Can you show this root router?

of course:

defmodule AuthexWeb.Router do
  use Web.Router

  alias AuthexWeb.Controllers
  alias AuthexWeb.Routers

  plug(:match)
  plug(:dispatch)

  get "/ping" do
    Controllers.Ping.ping(conn)
  end

  get "/users" do
    Controllers.User.list(conn, conn.query_params)
  end

  get "/users/:user_id" do
    Controllers.User.get(conn, conn.path_params["user_id"])
  end

  post "/users" do
    Controllers.User.create(conn, conn.body_params)
  end

  put "/users/:user_id" do
    Controllers.User.update(conn, conn.path_params["user_id"], conn.body_params)
  end

  delete "/users/:user_id" do
    Controllers.User.delete(conn, conn.path_params["user_id"])
  end

  get "/clients" do
    Controllers.Client.list(conn, conn.query_params)
  end

  get "/clients/:client_id" do
    Controllers.Client.get(conn, conn.path_params["client_id"])
  end

  post "/clients" do
    Controllers.Client.create(conn, conn.body_params)
  end

  put "/clients/:client_id" do
    Controllers.Client.update(conn, conn.path_params["client_id"], conn.body_params)
  end

  delete "/clients/:client_id" do
    Controllers.Client.delete(conn, conn.path_params["client_id"])
  end

  # post "/tokens" do
    # IO.inspect(conn, label: inspect(__MODULE__))
    # Routers.Token.call(conn, nil)
  # end
  forward "/tokens", to: Routers.Token

  match _ do
    Controllers.Fallback.not_found(conn)
  end
end

What does use Web.Router do? My theory is that it is injecting plug :match and plug :dispatch. If so, that’s your issue.

Plug router’s execute their plugs in the order in which it is invoked. That means your client authorization plug is happening after the match / dispatch to the route.

Some questions / requests:

  • what URL is being accessed when that crash occurs? It’s hard to debug routing without knowing what’s being routed

  • every route in Routers.Token has a puts; do any of those get hit?

  • more generally, what appears in the logs / output BEFORE the crash? One challenge with debugging Plug is that crashes like this happen after all the “user space” code has finished running, so a stacktrace alone isn’t informative.

  • what is defined in Web.Router, Web.Plug, and/or AuthexWeb.Endpoint? Extra plugs in these files could be disrupting the whole situation.

This kind of helped, I actually forget to add those plugs to nested router, I added them after basic authorization and now it matches, thank you

Thank you but other comment already solved the issue.