Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Adding /blog proxy to a Phoenix app
Byron Salty
Byron Salty

Posted on

     

Adding /blog proxy to a Phoenix app

Let's say you have a product you are creating using Elixir/Phoenix but for marketing purposes you also setup a blog on a separate site like Wordpress.

It makes sense to keep this separation of concerns and let some off the shelf software worry about creating a blog experience, while you work on your primary product.

However, for user experience and SEO purposes you may want to have your applications appear to come from a single domain. I did a quick search for the importance of a single domain vs subdomainsand the first article I found also uses this exact blog use case as their example.

Today I'll use just a couple bits of code and configuration to make to solve this problem directly in Phoenix. No access to your https proxy (nginx, caddy, etc) required!


Goal:

Create a proxy endpoint that will take all traffic to my primary domain mydomain.com/blog and route it over to the actual host of blog.mydomain.com

One reason I wanted to do this all in Elixir/Phoenix was I like to use Fly.io and their setup is super simple and effective... but I don't have direct access to their load balancer where I might otherwise setup a proxy like this.

The other thing that is nice about this setup is I will be able to use Elixir to manipulate the upstream request and response, which will come in handy as I make the experience seamless.


Step 1: The Proxy

I used the Elixir libraryreverse_proxy_plug which has the very nice benefit of not assuming anything about the hosting for the upstream service. For instance, it didn't need to be another Phoenix app or a service running on the same host.

Add this to your deps inmix.exs:

      {:reverse_proxy_plug, "~> 2.3"},
Enter fullscreen modeExit fullscreen mode

Then you can add this to yourrouter.ex:

  scope "/blog" do    pipe_through [:browser]    forward "/", ReverseProxyPlug, upstream: "https://blog.mydomain.com", response_mode: :buffer  end
Enter fullscreen modeExit fullscreen mode

Note that my use case is not high volume so I didn't really worry about different response modes. I believe the:buffer option works well enough for me here.

At this point, you should be able to hit your project at /blog and get some html that really came from your upstream service.

But there's a problem...


Step 2: Rewrite the content

The problem now is that the html that you will get will not know that you're trying to serve frommydomain.com/blog and will still have all links etc pointing toblog.mydomain.com.

No problem, we just need to alter the html response on the way out so that all of these references are updated.

This was a bit tricky because theconn struct is very particular about how it can be interacted with, stemming from the fact that HTTP is similarly and correctly picky. It wouldn't make sense if you could alter a response after you've already sent or even began sending it back to the client. So we need to pinpoint the moment where we have the response body but we are allowed to alter it.

Luckily, Plugs give us this exact ability in the standard functionPlug.Conn.register_before_send/2. This gives us the opportunity to define a function that will be called with the Conn after the response is ready but before it's actually given back to the client.

We just need to define a custom plug.

Update the scope and define a new pipeline inrouter.ex:

  pipeline :transform_blog do    plug BuddyWeb.Plugs.TransformBlog  end  scope "/blog" do    pipe_through [:transform_blog]    forward "/", ReverseProxyPlug, upstream: "https://blog.mydomain.com", response_mode: :buffer  end
Enter fullscreen modeExit fullscreen mode

And here's the entireTransformBlog plug:

defmodule BuddyWeb.Plugs.TransformBlog do  def init(options), do: options  def call(%Plug.Conn{} = conn, _ \\ []) do    Plug.Conn.register_before_send(conn, &BuddyWeb.Plugs.TransformBlog.transform_body/1)  end  def transform_body(%Plug.Conn{} = conn) do    case List.keyfind(conn.resp_headers, "content-type", 0) do      {_, "text/html" <> _} ->        body =          conn.resp_body          |> String.replace("blog.mydomain.com", "mydomain.com/blog")        %Plug.Conn{conn | resp_body: body}      type ->        IO.inspect(type, label: "content type header")        conn    end  endend
Enter fullscreen modeExit fullscreen mode

Thanks to Curiosumfor their article on this using Plugs in this way. My code borrows heavily from their example.

But there's one more problem...

Note: If you test the code at this point you may notice that you get expected results incurl orwget but not in a browser.


Step 3: Dealing with encodings

The reason why browsers give a different response than the command line tools are theaccept-encoding headers are different.

The command line tools are essentially asking for a response in a text format so our string replace is working and we get the result we expect.

But browsers will ask for a response in gzip or br encoding. Binary encodings that will only get turned back into text on the end-user's client so our string replace will not work.

The fix I employed here was to override theaccept-encoding header to force a non-binary response from our upstream service. This will be less efficient but fine for my purposes and scale.

Update the sameTransformBlog plug to change the request headers on the way in:

  def call(%Plug.Conn{} = conn, _ \\ []) do    Plug.Conn.register_before_send(conn, &BuddyWeb.Plugs.TransformBlog.transform_body/1)    |> Plug.Conn.update_req_header(      "accept-encoding",      "identity",      fn _ -> "identity" end    )  end
Enter fullscreen modeExit fullscreen mode

Theupdate_req_header() function asks you to specify both the default value (in case the header doesn't already exist) and a function to manipulate the existing header. In my case, I don't care what the previous header was - I'm just overriding to useidentity which meansno modification or compression.


That's all there is to it - one dependency to configure and one plug.

Happy Proxying~!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

25+ year veteran from the Media industry. Interested in learning, coding and building cool stuff.
  • Location
    Chicago, IL
  • Education
    Georgia Tech, University of Illinois
  • Work
    25+ years in tech. Technology Leader. EdTech. Media.
  • Joined

More fromByron Salty

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp