2

I'm developing a simple website in Elixir with Phoenix. I'd like to add some custom middleware that runs after a response has been generated. For example, in order to log the total number of bytes in each response I'd like to have a Plug like this

defmodule HelloWeb.Plugs.ByteLogger do
  import Plug.Conn
  require Logger

  def init(default), do: default

  def call(conn, default) do
    log("bytes sent: #{String.length(conn.resp_body)}")
  end
end

Trying to use this plug in one of the Phoenix pipelines in the router won't work though, they are all run before the response is rendered. Instead it causes a FunctionClauseError since conn.resp_body is nil. I'm not sure how to use this plug so it can run after the response is rendered.

Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
Segfault
  • 8,036
  • 3
  • 35
  • 54

1 Answers1

2

I think you are looking for register_before_send/2.

This allows to register callbacks that will be called before resp_body gets set to nil as explained here.

Should look like:

defmodule HelloWeb.Plugs.ByteLogger do
  import Plug.Conn
  require Logger

  def init(default), do: default

  def call(conn, default) do
    register_before_send(conn, fn conn ->
      log("bytes sent: #{String.length(conn.resp_body)}")

      conn
    end)
  end
end

Edit: I don't think you should be using String.length for the byte size:

  • resp_body is not necessarily a string, can be an I/O-list
  • byte_size/1 should be used to count bytes, String.length/1 returns UTF-8 grapheme count

The following could do the job, but with a significant performance impact due to the need of concatenating the body:

conn.resp_body |> to_string() |> byte_size()

:erlang.iolist_size/1 seems to work well and I suppose is much better performance-wise.

sabiwara
  • 2,775
  • 6
  • 11
  • 1
    Thanks! I switched to `IO.iodata_length(conn.resp_body)` for the response size and it seems to be working – Segfault Aug 19 '20 at 13:10
  • Nice, I didn't know about `IO.iodata_length/1`. Apparently it is literally the same under the hood (`&IO.iodata_length/1 ` evaluates to `&:erlang.iolist_size/1` because it is inlined), but feels nicer to use. TIL, thanks! – sabiwara Aug 19 '20 at 23:40