Is PRG a valid technique in Phoenix?

Yes, you can get it with something like:

changeset = conn.assigns[:error_changeset] || Context.change_resource()

So in my previous answer the matching clause would rather be

%{assigns: %{error_changeset: changeset}} = conn

If you usePlug.Conn.assign/3 to assign the changeset it will be available in conn.assigns.
Using a dot to access a key not present in a map will throw an error that’s why it’s better to use [:key] which will return nil if not found.

1 Like

@Kurisu Do you have a working example?
I made some tests and the errors are not showing after the redirect for some reason.

# new
changeset = conn.assigns[:errors] || Context.change_resource(%Resource{})
render(conn, "new.html", changeset: changeset)

# update
conn = %{conn | assigns: Map.merge(conn.assigns, %{:errors => changeset})}
redirect(conn, to: Routes.resource_path(conn, :new))

Update: Seems that the assigns are been cleaned after the redirect. I think that this is not possible without using session.

1 Like
redirect(conn, to: Routes.resource_path(conn, :new))

sends the POST response back to the client.

The followup GET from the client is an entirely different request and therefore has a new conn with a fresh assign - nothing is getting “cleaned”.

You have to bridge the information between the two separate requests.

  • Using the redirect the invaild form data could be transferred as query parameters in the redirect URL - though that’s hardly desirable.
  • The create function could stick the invalid form data in the server side session to be retrieved by the new function to be used to initialize the changeset.
1 Like

Yes of course. I fooled myself thinking that maybe Phoenix could be doing some “magic” behind the scenes. But it makes total sense that the “conn” struct is different because it’s a different request.

I’m gonna study other options of what can be done, but I think this could be encapsulated in a helper? Something like a redirect_with/3, to put the errors on the session and then retrieve on the other side (maybe a plug?).

Update: So, I’ve read a little bit about Plug and this is what I came up with (It’s probably a naive implementation, but right now I can get full Post Redirect Get):

defmodule MyAppWeb.Plug.TempData do
  import Plug.Conn
  use MyAppWeb, :controller

  @session_key "temp_data"
  @assigns_key :loaded

  def init(opts), do: opts

  def call(%Plug.Conn{private: %{:plug_session => %{@session_key => temp}}} = conn, _opts) do
    conn
    |> assign(@assigns_key, temp)
    |> delete_session(@session_key)
  end

  def call(conn, _opts), do: conn

  def load_redirected(conn, default) do
    conn.assigns[@assigns_key] || default
  end

  def redirect_with(conn, object, opts) do
    conn
    |> fetch_session()
    |> put_session(@session_key, object)
    |> redirect(opts)
  end
end

Then it can be used like:

import MyAppWeb.Plug.TempData, only: [redirect_with: 3, load_redirected: 2]

alias MyAppWeb.Plug.TempData

plug TempData

def new(conn, _params) do
    changeset = load_redirected(conn, Context.change_resource(%Resource{}))
    render(conn, "new.html", changeset: changeset)
end

def create(conn, %{"resource" => resource_params}) do
    case Context.create_resource(resource_params) do
      {:ok, resource} ->
        conn
        |> put_flash(:info, "Resource created successfully.")
        |> redirect(to: Routes.resource_path(conn, :show, resource))

      {:error, %Ecto.Changeset{} = changeset} ->
        redirect_with(conn, changeset, to: Routes.resource_path(conn, :new))
    end
  end
1 Like

I thought it would be a good idea to extract this little functionality in its own specific package.
So, here it is: briefcase - It’s just a simple plug to scratch this little itch. If anybody is searching for a similar solution, feel free to take a look and to contribute :relaxed:

PS.: Since I’m not yet well versed in everything elixir has to offer, any suggestions are extremely welcome.

1 Like

Hello @thiagomajesk Not sure if you are still active.

I encounter the same sort of situation where I need to apply a redirect for the submission of an invalid changeset. I think I have a valid scenario. I will explain why:

  1. The user lands on the page displaying the form, allowing him to add other users (GET)

  2. The user submits the form (POST)

[the form is valid] ->
⠀⠀⠀3. A redirect is done to the next page (GET)

[the form is invalid]
⠀⠀⠀3. The page is re-rendered showing the errors from the invalid changeset (same request)
⠀⠀⠀-> Back to point 2.

  1. The user presses the ‘Back’ button of the browser.

If he submitted a valid form immediately, when clicking the ‘Back’ button the user will go back to point 1., and a GET is executed, which will not lead to any errors, because the controller action is called and reloads all the data. The flow:
[GET display form page] (A) -> [POST submit form] (B) -> [GET redirect] (.C) -> [GET pressed back button] (back to A)

If he submitted an invalid form, corrected it, and then submits again the flow is:
[GET display form page] (A) -> [POST submit form] (B) -> [POST submit form] (.C) -> [GET redirect] (D) -> [POST pressed back button] (back to B, which is POST)

The latter will lead to an error, when he entered a new user (POST), applied a correction in the form (second POST), and then it got inserted into the DB; then presses back => the form will not include the hidden input containing the ID of the user. So submitting again will cause an error where Ecto cannot find the user that is already saved in DB in the form data.

So I noticed this is how browsers behave when hitting ‘Back’: when the ‘Back’ action returns to a GET request, the server is actually called, and so the controller action will load all the correct data. However, when the ‘Back’ action returns to a POST request (and this happens for a form that must be corrected and leads to multiple subsequent POST requests), nothing is executed on the server; the browser loads the page from some cache. And so, the form will show incorrect data (more specifically here, lacking hidden inputs containing IDs for entities that have been already inserted).

Anyway, besides your quite complex solution (I imagine it is complex as even in your readme of that lib you made, you said it’s only experimental), what would be the best solution? Are there any other trivial solutions (even if it doesn’t provide a smooth UX) to overcome this problem?
Most important is to not raise an error and show an error page to the user, as my form behaves now in such specific case.

@thojanssens1 Hello!

If I understood correctly this can be a perfect use case for using a post-redirect-get approach.
One of the main advantages is again, separating the responsibilities and keeping proper page state. I’m positive this is not always the case for a lot of people and is mostly useful for server-rendered page apps.

I think you missed a step there because when failures happen on PRG, you should redirect back to the page that knows how to build the form so the user can correct it and submit it again as a clean slate.
So, from the client perspective:

  1. [GET] Renders user’s form page
  2. [POST] Submit (yields validation error on the server-side)
    This should redirect back with invalid form state
  3. [GET] Renders user’s form page with validation messages

The lib I’ve created goes into step 2, saving the changeset yielded and passing it to the action that builds the form so it can display the errors properly. Right now, because of the redirect-back, the browser history gets a little bit messy if the user keeps entering invalid data (since the page is always refreshed back).

Actually, the code itself is not complex. At its core, it’s only a Plug that adds and retrieves data from the session. It’s experimental so far because I’m not using it on production and I consider it mostly a Proof of Concept, but if it fits your needs just go ahead and use it.

Oops I wasn’t clear. I described the behavior with a default Phoenix setup, i.e. redirect on successful submit, and no redirection if errors in changeset. Not full PRG. To justify why I need in my case to redirect even when there are errors in the changesets.

I had a quick look at the library’s code and, there’s quite some:) I was wondering, why didn’t you use the Flash messages functions get_flash/put_flash, as it seems your library uses the same mechanism?

The main difference is in the lifecycle. Since Briefcase does not rely on the connection it can go back and forth as long as you have clean values (not dirty) saved in the internal store (session).
If you want to know exactly why there’s this previous response that explains (I too didn’t think of it first but it’s actually pretty obvious once someone has pointed out :sweat_smile:) .

The response you linked, says you have to send data between two different requests. So indeed, it’s quite obvious that you can’t put it in the conn’s assigns, as that data will be lost after redirection.

But this is exactly what the flash message is for, i.e. store temporarily data in session for persisting data in between two requests; and flash messages should also be marked “dirty” once they have been retrieved.

I’m not 100% sure about this but I think is just the Phoenix request lifecycle that makes the flash messages end up in the session…

The underlying %Plug.Conn.put_private/3 that is used by Phoenix.Controller.put_flash/3 only places the values inside the conn.private map. I’m thinking that Phoenix.Controller.fetch_flash/2 in the :browser pipeline is actually what makes the magic happen for us:

router.ex

pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
end

controller.ex

cond do
    is_nil(session_flash) and flash_size == 0 ->
        conn
    flash_size > 0 and conn.status in 300..308 ->
        put_session(conn, "phoenix_flash", flash)
    true ->
        delete_session(conn, "phoenix_flash")
end

Moreover (If I’m correct), because Briefcase does not depend on Phoenix we can’t rely on this lifecycle.
I guess we could actually persist our information inside conn.private as a convention and then place it on the session, but right now we just use the session directly.

The session must be used sparingly. Getting the data from conn.private and putting it into session before the response is sent is specific to the built-in Flash functionality. It’s not part of the general request lifecycle; it would be terrible if it were.

fetch_flash/2 returns a register_before_send/2 function, which is a callback that gets called before the response is sent. And from there the data from conn.private is stored in session.

After some research, a simple solution using the built-in Flash mechanism to implement full-PRG (so including redirecting after invalid form submission), are the following steps:

  1. Invalid form is submitted, create_* action is called
  2. Submitted form parameters are put into Flash, if changeset is invalid
  3. Redirect to the new_* action
  4. Rebuild the changeset based on parameters that were stored in Flash (if present)
  5. Form displays all errors

Sample code (example is a form allowing to add/edit/remove items for a user):

def new_user_items(conn, %{"user_id" => user_id}) do
  changeset =
    Business.get_user!(user_id, preload: [:items])
    |> Business.change_user(get_flash(conn, :create_user_items_params) || %{})  #  <---
    |> Map.put(:action, :update)

  render(conn, "new_user_items.html", changeset: changeset, user_id: user_id)
end
def create_user_items(conn, %{"user_id" => user_id, "user" => user_params}) do
  user = Business.get_user!(user_id)

  case Business.add_items(user, user_params) do
    {:ok, _} ->
      redirect(conn, to: Routes.registration_path(conn, :next_action, user_id))
    {:error, %Ecto.Changeset{} = changeset} ->
      conn
      |> put_flash(:create_user_items_params, user_params)  #  <---
      |> redirect(to: Routes.registration_path(conn, :new_user_items, user_id))  #  <---
  end
end

Maybe I was not clear enough: I didn’t mean it’s part of Plug’s request lifecycle - It’s actually part of Phoenix’s browser pipeline, but I’m not going to get cought in discussing semantics if I can avoid it.
In my experience whether you should or shouldn’t use the session has to be decided in a case-by-case basis.

Maybe I should have been a little more specific, but If you see my previous comment I specifically linked to the code inside the register_before_send function, since that section is only called because fetch_flash is include in the pipeline :sweat_smile:.

About using just put_flash: though, I wouldn’t go this route to write a lib (since this behaviour doesn’t exist for Plugs) I guess it’s perfectly fine if you are using Phoenix and want a “quick and dirty” solution just to pass data from one request to another.
Also my purpose with Briefcase is to do a little bit more out-of-the-box. I hope this clears things up :slightly_smiling_face:.