tangui

tangui

HTTP-caching LiveView's first render

Hi all!

Since I released plug_http_cache, I’ve been asked a few times how to use it to cache LiveView’s initial responses. I’m not very well versed in LiveView, and I think there are many ways one could shoot himself in the foot doing that. However, it seems to be there are some reasonable use-cases for doing that.

One of them is to use http caching to improve SEO ranking for public pages that have little interactivity whether the user is logged in or not. For instance, one LiveView page could render financial charts in real-time as an aside, while the main content is almost static.

I tried plug_http_cache and it almost works out-of-the box:

  • initial mount replies with a 200 response, which is cached
  • second mount replies with a 101 HTTP response, which is not cacheable. The websocket connection is established

So far so good, but LiveView uses a CSRF token that is cached and sent along the second request - and LiveView WS connection fails when opening the same page in another browser with:

[debug] LiveView session was misconfigured or the user token is outdated.

1) Ensure your session configuration in your endpoint is in a module attribute:

    @session_options [
      ...
    ]

2) Change the `plug Plug.Session` to use said attribute:

    plug Plug.Session, @session_options

3) Also pass the `@session_options` to your LiveView socket:

    socket "/live", Phoenix.LiveView.Socket,
      websocket: [connect_info: [session: @session_options]]

4) Ensure the `protect_from_forgery` plug is in your router pipeline:

    plug :protect_from_forgery

5) Define the CSRF meta tag inside the `<head>` tag in your layout:

    <meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />

6) Pass it forward in your app.js:

    let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
    let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});

I figured out it’s possible to disable checking of this CSRF token when configuring the live endpoint, at the cost of disabling live sessions:

endpoint.ex before:

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

endpoint.ex after:

socket "/live", Phoenix.LiveView.Socket, websocket: []

My first batch of questions would be:

  • is it safe to do so?
  • what are the other side-effects, other than disabling live session?
  • is that possible to have 2 sockets: one where live sessions are disabled for this specific use-case, and another one with session information enabled?

Another thing I noticed is that some information related to LiveView is stored in the HTML upon initial rendering:

<div
data-phx-main
data-phx-session="SFMyNTY.g2gDaAJhBXQAAAAIdwJpZG0AAAAUcGh4LUY1Q0hZRndnM2I0N0lRSkN3B3Nlc3Npb250AAAAAHcKcGFyZW50X3BpZHcDbmlsdwR2aWV3dy1FbGl4aXIuSHR0cENhY2hlV2l0aExpdmV2aWV3V2ViLlVzZXJMaXZlLlNob3d3BnJvdXRlcncmRWxpeGlyLkh0dHBDYWNoZVdpdGhMaXZldmlld1dlYi5Sb3V0ZXJ3DGxpdmVfc2Vzc2lvbmgCdwdkZWZhdWx0bggAt3Fq2ugnkBd3CHJvb3RfcGlkdwNuaWx3CXJvb3Rfdmlld3ctRWxpeGlyLkh0dHBDYWNoZVdpdGhMaXZldmlld1dlYi5Vc2VyTGl2ZS5TaG93bgYA-_cJWYsBYgABUYA.25eSbZuNO6oxbt4AEcignF3HVsHExGqPcoc8BI8E5nI"
data-phx-static="SFMyNTY.g2gDaAJhBXQAAAADdwJpZG0AAAAUcGh4LUY1Q0hZRndnM2I0N0lRSkN3BWZsYXNodAAAAAB3CmFzc2lnbl9uZXdqbgYA-_cJWYsBYgABUYA.k0KyuD5mMq4arpXYUO2poQAdDrnFipHxVJ1D6B32tyI" id="phx-F5CHYFwg3b47IQJC">

After decoding it, it seems it doesn’t contain private session data, which remains stored in the cookie. However, is there any reason this data shouldn’t be served to users other than the one who initiated the request in the first place?

Lastly, I’d like to write guidance on how to handle authenticated LiveView’s with static content. The idea is to render only public data on initial mount, and load private, session-based data only when the LiveView goes live:

def mount(params, session, socket) do
  if not connected?(socket) do
    # Initial request: here we render only **public** data.
    # The response can be cached by shared caches
    products = Products.list(params)

    {:ok, assign(socket, products: products)}
  else
    # Check user authentication
    socket =
      case session do
        %{"user_id" => user_id} ->
          assign(socket, user: Accounts.get_user!(user_id))

        _ ->
          socket
      end

    # Perform stateful stuff that don't depend on the user
    Phoenix.PubSub.subscribe(MyApp.PubSub, "product-updates")

    if socket.assigns.user do
      # Prepare rendering for authenticated users
      socket =
        socket
        |> assign(:pending_orders, User.get_pending_orders!(socket.assigns.user))
        |> assign(:pending_notifications, User.get_pending_notifications(socket.assigns.user))

      {:ok, socket}
    else
      # Prepare rendering for anonymous users
      {:ok, socket}
    end
  end
end

What do you think? Did I miss something? Any security issue I didn’t anticipate?

Cheers!

Most Liked Responses

tangui

tangui

PR created: https://github.com/phoenixframework/phoenix/pull/5952

Feel free to comment, improve the idea.

tangui

tangui

The PR has been merged :partying_face: Thanks to the Phoenix team :heart: The new option will be available as of Phoenix 1.7.15

I’ve updated the instructions and the demo app.

Happy caching!

LostKobrakai

LostKobrakai

Seems like I got the actual implementation mixed up with how it works conceptually and what you eventually get in mount/3. But at least in the current implementation of socket you still need a csrf token to get access to the cookie. Iirc earlier versions didn’t allow that at all.

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement