Can I route on cookies using Routex?

hi, thanks for this

can I Route on cookies, similar to this question: Is it possible to modify Router matches based on cookies like in Varnish? - #3 by dimitarvp

I want to be able to present a static cached page using http_cache | Hex if no cookies and if there are cookies I want to route to a non cached cookied page


in Varnish you can do:

sub vcl_hash {
  if (req.http.cookie ~ "wordpress_logged_in_") {
    hash_data("wordpress_logged_in");
  }
  # the builtin.vcl will take care of also varying cache on Host/IP and URL 
}

or better

import cookie;
import directors;
import std;
import kvstore;

sub vcl_init {
    kvstore.init(0, 1000);
}

sub vcl_backend_response {
    set beresp.http.x-tmp = regsub(header.get(beresp.http.set-cookie,"JSESSIONID=", " *;.*", "");
    if (beresp.http.x-tmp != "") {
        kvstore.set(0, cookie.get("id"), beresp.backend, 15m);
    }
    unset beresp.http.x-tmp;
}

sub vcl_recv {
    cookie.parse(req.http.cookie);
    set req.http.server = kvstore.get(0, cookie.get("id"), "none");

    if (req.http.server == "s1") {
        set req.backend_hint = s1;
    } else if (req.http.server == "s2") {
        set req.backend_hint = s2;
    } else {
        if (std.rand(0, 100) < 50) {
            req.backend_hint = s1;
        } else {
            req.backend_hint = s2;
        }
    }
    return (pass);
}

https://varnish-cache.org/docs/trunk/reference/vmod_cookie.html


or in Nginx, you can define a map in http section:

map $cookie_proxy_override $my_upstream {
  default default-server-or-upstream;
  ~^(?P<name>[\w-]+) $name;
}

Then you simply use $my_upstream in location section(s):

location /original-request {
  proxy_pass http://$my_upstream$uri;
}

Nginx evaluates map variables lazily, only once (per request) and when you are using them.

server {
    ...
    set $upstream "default-server-or-upstream";
    if ($http_cookie ~ "proxy_override=([\w-]+)") {
        set $upstream $1;                                   
    }

    location /original-request {
        proxy_pass http://$upstream/original-application
    }
}

I asked Brave search AI and it gave me:

defmodule MyApp.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/hello" do
    case get_req_header(conn, "cookie") do
      [{"cookie_value"}] -> send_resp(conn, 200, "Cookie matched")
      _ -> send_resp(conn, 400, "Cookie not matched")
    end
  end
end

or

defmodule MyApp.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/set-cookie" do
    domain = PublicSuffix.registrable_domain(conn.host)
    opts = [store: :cookie, key: "_myapp_key", signing_salt: "asdadl", domain: domain, max_age: max_age, http_only: true]
    opts = Plug.Session.init(opts)
    conn = Plug.Session.call(conn, opts)
    send_resp(conn, 200, "Cookie set")
  end
end

@tangui

Hi @niccolox,

First of all thank you for showing interest in Routex.

What you are requesting is imho not in scope of Routex itself. Let me try to explain with an analogy:

Consider this analogy…

You are a real estate entrepreneur wanting to build a few houses. You have some global ideas about what houses (route definitions in route.ex) and how they should look (Routex configuration).

Routex would be the architect which takes your ideas (route definitions) and wishes (Routex config) and designs the blueprints. Routex extension are the tools the architect uses to change the blueprints in an efficient manner.

Once the architect has finalized the blueprints, the blueprints are passed to construction company Phoenix.Router to construct the actual houses (routes) following the blueprints. The blueprints are exactly how Phoenix.Router expects them to be. As if they were made for it!

This is the first part: Routex generates blueprints using route definitions and Routex config for Phoenix.Router to build.


The second part is almost a side-effect: As Routex is the architect, it also knows all specifications of the houses produced and so it can produce a few accessories (helper functions) for the houses being build by Phoenix.Router Contruction and co. A fancy electronic door lock, a perfect matching sunscreen and so on. It’s all additional functionality next to all functionality of Phoenix already build, but it makes life a bit easier.

Given this analogy…
What you are asking is a split in the road that leads to the houses so you can route to different houses upfront (before router logic kicks in [1]) OR a split in the hallway once a house entered leading visitors to different rooms (after the router did it’s job [2]).

[1] Varnish, Nginx, Plug
[2] Inside the controller of (Live)View.

Hope this makes sense,
Let me know.

Using plug_http_cache, you can always refuse a cached version to be served by setting the request cache-control: no-cache header.

The spec states that:

The no-cache request directive indicates that the client prefers a stored response not be used to satisfy the request without successful validation on the origin server.

Luckily, plug_http_cache doesn’t support revalidation* and doesn’t return a cached version in this case, so what you have to do is just to write a Plug before plug_http_cache that

  • reads if there is a specific cookie / cookie value (user authenticated for instance)
  • set this cache-control: no-cache request header if so

Cheers

* You should have a test for this, since plug_http_cache might support revalidation in the future, although it is not very likely

EDIT: might even be better to set the request header cache-control: max-age=0

1 Like

Thanks guys

I think I am going to pattern a plug on the Phx.auth.gen code such that the plug checks for cookie and then does redirects etc

I want some solution that is very close to core

havent tested this, and obviously its incomplete, but this is the direction

  Used for routes that require the user has no cookies and is probably a first time visitor.
  """
  defp no_cookies_path(_conn), do: "/static"

  def redirect_if_user_no_cookies(conn, _opts) do
    conn = fetch_cookies(conn, signed: [@remember_me_cookie])
    if conn.cookies[@remember_me_cookie] == nil do
      conn
      |> redirect(to: no_cookies_path(conn))
      |> halt()
    else
      conn
    end
  end