Routing hooks in Phoenix - How to modify rendered HTML before output?

I have a basic understanding of Plugs, Controllers, Views and flow of Phoenix routing to make a web page. But let’s say I want to get all of the rendered views before they are output to browser. For sake of argument, let’s say I want to trim all the whitespace in the HTML before sending the final output.

When I try to make a Plug and read conn, I do not see the HTML body? To be clear, I want to:

  1. Capture all rendered views / HTML before it is sent to the browser,
  2. modify the rendered HTML
  3. output modified HTML.

*p.s. - Just poking and prodding at Phoenix to understand it. Sorry it is taking me a while to figure this out. Apologies if this is the wrong category, it would not allow me to post in the “Phoenix” category.

I have changed the post category, as requested…

What You want to do might be expensive in term of rendering.

If you’re just wanting to understand how Phoenix works I’d start poking around Phoenix.Endpoint(phoenix/endpoint.ex at master · phoenixframework/phoenix · GitHub) as this is the Phoenix entry point of each request. That’s a bit of a rabbit hole so sorry I can’t be more specific… perhaps someone else here can. As far as I know, though, there is no provided way to do this since, as @kokolegorille alludes to, there is no benefit to doing this. Phoenix parses templates to iodata which, for the sake of simplicity, is a highly efficient way to build strings that avoids copying. Converting it to a string at the end to remove whitespace would almost certainly negate any benefit that came from using iodata.

EDIT: Here is a good user-provided explanation of iodata.

1 Like

There are also these videos…

1 Like

Thanks, I understand the practicality may be silly. I am exploring Phoenix to find where the “limits of my control” are. When using a library or framework we often cede some control over how things are handled for productivity benefits. So I am trying to poke around the edges and figure out what sort of control over the website I am losing by using Phoenix, if that makes sense.

I was going to include in my question something like, if this cannot be done, is it possible to apply a function to every view being called or rendered? I will have a look at the resources you provided.

I remember answering a question like this before and found it. I’m not sure if that’s a help as you’re looking to do a general hook into all render calls.

Do you have a real world example of what you’d want this behaviour for or just generally curious?

Okay so keep in mind, I am ~1 week into Elixir / Phoenix coming from PHP so my thinking is not fully converted over. But in PHP-land you can capture a rendered view before sending it out; i.e., render all the views, put it into a variable, do something with it.

Some Example Cases:

  • Strip whitespace out of rendered views.
  • Write the rendered HTML to a file (audit, logging?)

Generally I just want to be able to:

  1. Capture the routed request into a variable
  2. Stop traditional routing, and do something else

But no matter where I seem to access “conn” from in Phoenix I can’t seem to pull out the response body, as if it were compiled or rendered HTML. The conn.body always seems to be nil.

Like I get all this stuff…

:phoenix_action => :index,
:phoenix_controller => DemoWeb.PageController,
:phoenix_endpoint => DemoWeb.Endpoint,
:phoenix_flash => %{},
:phoenix_format => “html”,
:phoenix_layout => {DemoWeb.LayoutView, :app},
:phoenix_request_logger => {“request_logger”, “request_logger”},
:phoenix_root_layout => {DemoWeb.LayoutView, :root},
:phoenix_router => DemoWeb.Router,
:phoenix_template => “index.html”,
:phoenix_view => DemoWeb.PageView,

Along with response stuff, etc., but…

resp_body: nil,
resp_cookies: %{},
resp_headers: [
{“content-type”, “text/html; charset=utf-8”},
{“cache-control”, “max-age=0, private, must-revalidate”},
{“x-request-id”, “Fz89qzaSZgX7k4IAAAAm”},
{“x-frame-options”, “SAMEORIGIN”},
{“x-xss-protection”, “1; mode=block”},
{“x-content-type-options”, “nosniff”},
{“x-download-options”, “noopen”},
{“x-permitted-cross-domain-policies”, “none”},
{“cross-origin-window-policy”, “deny”}
],

So where is my resp_body (after rendering views) if I were to need that?

Hopefully I am making sense. Sorry.

It looks like you probably want Plug.Conn.read_body/2. I’ve never used it myself but it’s probably a good start!

The view is compiled to the rendering function, that’s why it is fast…

You already have full control of what is rendered, because You write your own template.

You don’t render views, views are the render functions.

It’s probably different in other frameworks.

It does not appear possible… except in testing?
https://hexdocs.pm/plug/readme.html#testing-plugs

  test "returns hello world" do
    # Create a test connection
    conn = conn(:get, "/hello")

    # Invoke the plug
    conn = MyRouter.call(conn, @opts)

    # Assert the response and status
    assert conn.state == :sent
    assert conn.status == 200
    assert conn.resp_body == "world"
  end

https://hexdocs.pm/plug/Plug.Conn.html#module-response-fields

  • resp_body - the response body, by default is an empty string. It is set to nil after the response is sent, except for test connections. The response charset used defaults to “utf-8”.

I had a plug inserted before and after Router in Endpoint (and various other places) trying to get the response body; it seems to be some sort of design decision not to allow this.

[info] GET /
%{body: nil, status: nil, where: "Endpoint, Before Router"}

[debug] Processing with DemoWeb.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]

[info] Sent 200 in 2ms

%{body: nil, status: 200, where: "Endpoint, After Router"}

Here’s a relevant issue that points toward using Plug.Conn.register_before_send/2.
I’m not sure if it’s still relevant, as the issue is dated :slight_smile: but it seems to be the same question.

1 Like

I tried this earlier and failed, was forgetting to “conn =” :sweat_smile:

  def call(conn, opts) do
    conn = register_before_send conn, fn conn ->
      IO.inspect %{status: conn.status, body: conn.resp_body, where: opts[:where]}
      conn
    end
    conn
  end

The above is a plug, inserted into Endpoint before router,

  plug DemoWeb.Plugs.ConnExplorer, where: "Endpoint, Before Router"
  plug DemoWeb.Router

It looks like Phoenix sends “HTML” content that still needs to be unpacked a bit by the JavaScript, but this works. Thank you all.

1 Like