Is PRG a valid technique in Phoenix?

Hi! I’ve been experiencing with Phoenix lately and was wondering if the PRG (Post Redirect Get) technique is relevant or there’s a better way to approach complex form building and validation.
Apart from its original purpose, the PRG pattern is very useful when you need to consistently build form data. Consider something like the code below:

def new(conn, _params) do
  changeset = ProductionLine.change_car(%Car{})
  # Loads the data necessary to render the form
  car_colors = ProductionLine.list_car_colors()
  car_optionals = ProductionLine.list_car_optionals()

  render(conn, "new.html",
    changeset: changeset,
    car_colors: car_colors,
    car_optionals: car_optionals)
end

def create(conn, %{"car" => car_params}) do
  case ProductionLine.create_car(car_params) do
    {:ok, car} ->
      conn
      |> put_flash(:info, "Car created successfully.")
      |> redirect(to: Routes.car_path(conn, :show, car))

    {:error, %Ecto.Changeset{} = changeset} ->
      # Ops, error! Don't have the assigns to build the template 
      # Won't even show the error messages because of missing data
      render(conn, "new.html", changeset: changeset)
  end
end

In the first scenario, there are traditionally two options I’ve seen to solve the problem of re-populating the form:

  • Extracting the logic to another function (which does not solve the form resubmission problem)
  • Using the PRG pattern to separate responsibilities and centralizing the form initialization logic

With PRG, we would make the “new” action always responsible for knowing how to build the form. Then, when you submit your form, the resulting action of the post is always a redirect…
If there are any problems, you should redirect to the “new” action passing the current state of the form so it knows how to display the errors properly.

def new(conn, _params) do
  changeset = ProductionLine.change_car(%Car{})
  # Loads necessary data
  car_colors = ProductionLine.list_car_colors()
  car_optionals = ProductionLine.list_car_optinals()

  render(conn, "new.html",
    changeset: changeset,
    car_colors: car_colors,
    car_optionals: car_optionals)
end

def create(conn, %{"car" => car_params}) do
  case ProductionLine.create_car(car_params) do
    {:ok, car} ->
      conn
      |> put_flash(:info, "Car created successfully.")
      |> redirect(to: Routes.car_path(conn, :show, car))

    {:error, %Ecto.Changeset{} = changeset} ->
      # render(conn, "new.html", changeset: changeset)
      redirect(conn, to: Routes.car_path(conn, :new), changeset: changeset)
  end
end

For this second example, it would still be necessary to pass the state of the form (which contains the errors) to the “new” action to be able to display it (I don’t know how this would be done in Phoenix though)

This is a very common approach that I’ve been using with aspnet, so I was wondering how it was solved over here. However, with aspnet, there’s a lot of “smoke and mirrors” to make this work…
After a post (on error), I would normally serialize the “model-state” that contains the errors, place it in a “temp-data” container that is short-lived, redirect to the “new” action, import the “model-state”, load the form data and then, call the view to display the form with the errors.

To avoid this whole processing there’s also another technique called “unobtrusive validation”, which prevents having to collect data from the database every time there’s an error on the form. It consists of making a post request to the controller and if the state of the form is invalid, it prevents reloading the page and instead displays the errors. Besides the obvious advantage of not having to hit the database every time to repopulate the form, you’ll still have a fallback rendering mechanism if the user has disabled javascript in the browser.

To me the PRG way you described seems like a lot of hassle for little benefit. I’ve used the way you first mentioned: put the assigns in a function that you can use both in the empty form case and the “failed submit” case.

2 Likes

For building data, maybe yes. But still does not solve the form resubmission issue.

Why do you care about resubmissions of invalid forms? This is usually only a problem for successful form submissions, which likely have side-effects.

1 Like

I guess that if the server is always responding with a 200, it’s still a “valid form submission” for the browser’s point fo view (usually, frameworks don’t return a 500 when there’s a validation problem). Then, if you want to prevent that you should use PRG - which states that a post request is always followed by a redirect.

To me the redirect is meant to protect against side effects happening more than once. Invalid forms should not cause side effects. If they really do, then a redirect is certainly useful.

Are you certain of the bolded statements?

Gets or sets the client validation mode for the application.

A quick scan of various web articles seems to suggest that unobtrusive validation is entirely client based - historically using https://jqueryvalidation.org/.

HTML5 (no-JS) form validation has improved quite a bit in recent years

And nothing is stopping you from using your own HTML5-based, JS-based, or LiveView-based validation in Phoenix.

There’s also that annoying form resubmission popup browsers show when you refresh or navigate back and forth, this behavior might be undesirable if the person, for instance, wants to start fresh in the form.

One thing that I noticed is that Phoenix changes the url when there’s an error. If I’m at /car/new and the submit yields an error, I’m redirected to /car - iterestanly actually makes a post request to /car. How do you guys manage this?

@peerreynders Yes, it is client-based validation.
In the case of aspnet specifically, it reuses the validation from the server so you don’t have to write client code (there’s a “plugin” that acts on top of that jquery validation you’ve mentioned).
About the second statement: I meant that reloading form data from the server would be a fallback in case the user has disabled javascript - should’ve made myself clearer.

Yes, I wanted to know what is the approach people normally use and if there’s something equivalent.
I’ve seen something about that in the past, but I wasn’t sure if LiveView does that or there’s another built-in form of “unobtrusive validation” (I saw a live WebSocket connection on the console and thought this could be the case).

<form accept-charset="UTF-8" action="/car" method="post">
  ...
</form>

It’s going to /car regardless based on the action set in the form element. It’s new.html.eex that is reused.

From Programming Phoenix 1.4:

User Controller
https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/controllers/user_controller.change3.ex

new.html.eex
https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/templates/user/new.change1.html.eex

Note how new renders with new.html while create renders with new.html in case there is an error (in case of success it redirects to index which renders with index.html).

But your code does the same thing.

That’s very interesting because in aspnet the action of the form would be just “create”, so if there’s an error it wouldn’t change the url (which I find weird).

I know this is a bit off-topic, but now I’m curious… Is this a Phoenix convention?
Seems to me it’s telling the form to look for the resource instead of the specific action and then uses a convention to select the “create” action, is that right? Do you know why’s done that way instead of just using the action itself? (Besides to avoid hardcoding the convention in the form)

My impression from ASP.NET webforms was that it tried to emulate desktop apps on the web and from that perspective pages were treated as screens - so if you were creating something you’re stuck on the “create screen” until you get it right.

I rationalize the Phoenix approach with the REST-model of the Web

  • /users/new is the URL to GET the (fresh) form that is capable of creating a new user
  • That form POSTs to /users in an attempt to create a new user
  • The error page is the response to the failed creation of a user from the /users resource
  • Upon successful creation the /users resource issues a redirect to the new resource /users/{id}.

and then uses a convention to select the “create” action,

It’s not convention, it’s in the routing:
https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/router.change1.ex

  scope "/", RumblWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController, only: [:index, :show, :new, :create]
  end

https://hexdocs.pm/phoenix/Phoenix.Router.html#resources/4

The resources/4 macro’s conventions (without the :singleton option):

  • GET /users => :index
  • GET /users/new => :new
  • POST /users => :create
  • GET /users/:id => :show
  • GET /users/:id/edit => :edit
  • PATCH /users/:id => :update
  • PUT /users/:id => :update
  • DELETE /users/:id => :delete
$ mix phx.routes
   page_path  GET     /                        RumblWeb.PageController :index
   user_path  GET     /users                   RumblWeb.UserController :index
   user_path  GET     /users/new               RumblWeb.UserController :new
   user_path  GET     /users/:id               RumblWeb.UserController :show
   user_path  POST    /users                   RumblWeb.UserController :create
...
$
1 Like

I’m actually talking about aspnet mvc (not webforms) which works very much like other web mvc frameworks out there. I’ve made a little research and found that Laravel behaves more like aspnet while Phoenix does exactly the same thing that Rails does which is changing the url on post.

After playing with some code over here, I think I finally got what’s happening. Just to draw a comparison…
When you create a controller in aspnet, the default routing convention is to use the method names in the controller to compose the route. So if you have a User controller and a method named New then you have an exposed route called ~/user/new automatically. In Phoenix the “create” function is not known as a route, so you can’t use that on the form action. Then, the thing I was finding unsettling is that, since the browser will follow the form action on submit, there’s this minor difference on the route that changes. (you learn something new every day :smile:). Also, thanks for the explanation @peerreynders.

So, getting back on the topic about the PRG pattern… What are the options here (if any)?

To me it’s not clear what your remaining issues are - other than perhaps a misaligned mental model which can easily happen when moving between frameworks.

A far as I can tell Post/Redirect/Get is what is happening in

https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/controllers/user_controller.change3.ex

in the success case - though perhaps create should redirect to show rather than index.

Then, when you submit your form, the resulting action of the post is always a redirect…

I don’t see any reference of what is supposed to happen in the failure case.

With PRG, we would make the “new” action always responsible for knowing how to build the form.

This is where there is some potential misalignment - it’s the view (new.html.eex) which builds the form - both the new and create functions have the responsibility of providing the necessary data for the view to do its job.

  • the car_colors/car_optionals issue is typically resolved with extracting a common function (likely a context function)
  • alternate solution: cache the encoded data in the server side session so the failure case doesn’t have to hit the database again (but the result could still be in the RDBMS cache anyway).

I guess that if the server is always responding with a 200, it’s still a “valid form submission” for the browser’s point fo view (usually, frameworks don’t return a 500 when there’s a validation problem).

200 OK

The HTTP 200 OK success status response code indicates that the request has succeeded.

The request has succeeded inasfar it was able to reply with a meaningful response - the error page.

If a new resource is created the redirect will cause a 302 Found (ideally it should be 303 See Other (Plug.Conn.put_status/2) while an API should simply use 201 created but that wouldn’t cause a redirect on a browser).

So the fundamental problem doesn’t seem to be the PRG pattern but the fact that you would like the new function to be solely responsible for the “new form” - and Phoenix doesn’t work that way.

That’s for sure one of the reasons I brought this topic for discussion.

But regarding the PRG pattern in general, if you check the Wikipedia article, you’ll see that it states that any post request should be followed by a redirect, not only “successful” ones. This, of course, helps in some cases:

Side note: I even saw someone on Stack Overflow suggesting the Strict PRG and Loose PRG terminology - I’m talking mainly about using the former since the latter is already used by default.

  • Avoid form resubmission on page refresh or navigation
  • Bookmarks (keeping resource “state”)
  • Form data building (SRP by extension)

Currently, only the last issue can be solved - by reusing the code (which does not depend on the framework).

In the Stack Overflow thread you linked, there are some assumptions on the “Loose PRG” type mainly because it does not touch more uses-cases for the “Strict PRG” approach, for example…

About the form resubmission issue: as I explained here, it still may be undesirable to allow that behavior, which might turn into a UX problem.

For bookmarks (in the specific case of Phoenix ), there’s that particularity about the routes. By default, if the form contains invalid data, ~/user/create becomes ~/user and ~/user/1/edit becomes ~/user/1 which both are different views (templates) if accessed without the operation context. Then, if the user is presented with an error and I ask him to send me the url where the problem happened, that will be an issue (of course, this is the most simplistic use-case I could think of).

Regarding rebuilding the view, I’ve seen people argue that using the post action to render a view violates REST. I’m not completely sure if this is technically correct, but it’d be good to allow that when possible as a best practice IMO.

I couldn’t agree more (I kept my last paragraph for reference). Thinking that way, I guess you are saying that the view is responsible for rendering the template, so I’m assuming you are also saying that there’s not SRP problem (Could you expand on that?). If so (and that’s also the case), would it still be a REST violation, considering that the action is started by the controller (because the view is used indirectly for the same purpose)?

Not exactly, but I think that this is covered by what I’ve already mentioned.
Also, as you already pointed out before, I’m adapting my mental model by trying to see what could/ should be transferred to Phoenix. So I think is plausible to have these exchanges on how others are approaching it - it’s certainly productive to me, at least.

and Phoenix doesn’t work that way

I opened this thread already thinking that since everything has to be passed explicitly as a param it probably couldn’t be achieved - since the “Strict PRG” presumes that the form-state data has to be sent over on the redirect (or at least kept in some way).

PS.: Was searching for solutions in other frameworks and stumbled upon this rails gem called rails-prg. It seems that there’s another issue that PRG solves that I was not aware of - don’t know if this affects Phoenix too, but thought would be nice to share.

1 Like

Professional ASP.NET MVC 2 (2010) attributes that to Phil Haack

Note that redirecting the request only upon success technically violates the PRG pattern, but because no changes where made to the state of anything when validation failed, it doesn’t suffer from most of the problems with not following the PRG pattern. Phil likes to call this loose PRG as opposed to strict PRG.

The pattern itself seems to go back to this 2004 article/2003 discussion:

https://www.theserverside.com/discussions/thread/20936.html

This approach provides a clean Model-View-Controller solution.

Aside: Server MVC has always played it fast and loose with Trygve Reenskaug MVC.

But please dear browser, do not save snapshots of a live program, because they may not represent actual Model state anymore.

Now I ask the same question again: what would a user see if he clicks Back button after submitting a form? You know the correct answer already: the user of a well-designed web application would see a View which represents current Model state. This View would be presented in a way that resubmitting of the same data would be impossible.

Sorry, in 2019 browsers still don’t work that way (bfcache, “Backward-Forward Cache”). Even after a successful redirect I can back up to the submit form and those last valid values will reappear in the form fields and I can click that submit button again.

If you need to defeat cached pages you need to employ token matching and remove the hidden form token from the server session after the resource has been successfully created (the standard CSRF token doesn’t stop double submission from a recently cached form).

Browser-side the pageshow event could be used to clear the input fields - but that will only work when JavaScript is enabled.

(1) Create Item is called from a link on some other web page when a new object should be created. This action constructs empty business object and stores it in the temporary area called Current Items, which itself can be stored in the database or in the session; then redirects to Display Item.

This suggests that each user has their own /users/new resource (otherwise it couldn’t be stored in the session) - this violates the notion that /users/new uniquely addresses a single resource.

(2-2) User fills out object value and submits HTML form to the Store Item action. If object is not accepted, it is kept in the Current Items area, server redirects back to Display Item action, which reads invalid object along with error messages from the Current Items and redisplays it in the form. If Item Page needs to be is refreshed, it loads the same object from Current Items again.

i.e. once again - my /users/new would show my last errors while your /users/new would show your last errors - so /users/new isn’t uniquely addressing a resource.

Furthermore we’re now in a situation where errors will change “model state” because the errors are part of the “model state”. So rather than saying that redirect on failure is a requirement, it is more to the point that redirect on state change is a requirement.

If your philosophy is to not accept invalid data then you aren’t going to store “half-baked”, inconsistent items - which is what is happening in the “Current Items” area.

If a user clicks Back button on result page (3) after successfully creating and storing new object, he returns to Display Item action (2).

This completely ignores the existence of the “Backward-Forward Cache” in browsers. (Though it’s presumably preventable with Cache-Control: no-store)

Ultimately the article reads like somebody is trying to coerce their vision of MVC/OO onto the web.

As I explained above with “Current Items Storage” each user/session would have a different /users/new resource anyway - there is no way you could GET /user/new with the validation errors unless you are in the same user/session context.

Phoenix is pragmatic about it - the form validation errors are ephemeral - they aren’t associated with an addressable resource. The aim is to maintain only valid resources, validation errors are ephemeral (short of what is written to the log).

  • when you GET \users you will always get a list of users
  • when you GET \users\new you will always get an empty form

I’ve seen people argue that using the post action to render a view violates REST.

RFC 2616 9.5 POST

The action performed by the POST method might not result in a resource that can be identified by a URI. In this case, either 200 (OK) or 204 (No Content) is the appropriate response status, depending on whether or not the response includes an entity that describes the result.

So 200 OK for the validation errors is permissible as no resource is created.

If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header (see section 14.30).

This is the ideal REST case - typically used by APIs.

However, the 303 (See Other) response can be used to direct the user agent to retrieve a cacheable resource.

This is what is used to guide browsers to the newly created resource.

FYI: SRP/SOLID

“Gather together those things that change for the same reason, and separate those things that change for different reasons.”

  • new.html combines for user convenience entry fields with “validation error placeholders”.
  • UserController.new uses new.html strictly for the entry fields.
  • UserController.create uses new.html for both the entry values and validation errors.
  • Separate new.html and create_error.html views would duplicate markup and functionality. It makes sense to combine them into one new.html, especially as they are essentially coupled anyway.

If so (and that’s also the case), would it still be a REST violation, considering that the action is started by the controller (because the view is used indirectly for the same purpose)?

  • new.html is the representation for GET /users/new (i.e. it’s an empty form)
  • index.html is the representation for GET /users
  • in the case of create (POST /users) new.html is “reused” to convey the error information - i.e .it is not a representation of any particular resource.

I opened this thread already thinking that since everything has to be passed explicitly as a param it probably couldn’t be achieved.

It’s just not something that is covered out-of-the-box.

You are free to implement your own “Current Items” scheme - though in my personal view it’s an un-webby thing to do.

@peerreynders Although I don’t think that my main concerns were addressed so far, there’s certainly a lot of useful information in this thread…

Since the reason I’ve posted was basically to see how this is being solved on the Phoenix side, I’m gonna be a little bit more practical and reformulate the question because I think if we keep expanding on other definitions we are not going to reach a conclusion.

By default, if the user is filling a form on ~/user/new and there are errors on the form, because of how the routing works, the url changes to ~/user. I see two issues here that I want to be able to solve and using PRG helps me:

  1. If the user clicks the refresh button, it shows the resubmission message which may not be desirable for UX purposes.
    The user might expect the page to be rebuilt instead (which is also a question posed in the “Redirect After Post” discussion and was not addressed).
    - There’s also the suggestion of using tokens, which does not solve the problem for other complex scenarios as described in the same discussion.

  2. If the user renavigates to the current url after posting (selecting the search bar and pressing enter for example), it’ll load the ~/user url, which shows the index page and not the form page. Then, the only way the user can start fresh in the form is pressing the backwards button - which will navigate to the previous ~/users/new route.
    - This can also be counter-intuitive for mobile browsers where the forward/ backward buttons are hidden. Also, I’ve seen that some users tend not to use this feature and instead always reload the page. This combined with the fact that some devices (I guess mainly Android) have inconsistent backwards navigation, it’s a huge UX problem.
    - Beyond that, the problem of ‘bookmarkability’ that I think was misunderstood is not about preserving/ sharing error state, is about correctly identifying the resource (form). So in an error scenario, the user will be presented with the /users url instead of the right url of the page he’s at (~/user/new).

  3. I want to separate responsibilities of building the form and actually creating it (but you pointed out that Phoenix does not work like that before. So I wonder if there are any other options).

It’s just not something that is covered out-of-the-box.

I’m completely fine with that, aspnet also does not support PRG by default but there’s at least a way to transfer data on redirects (Laravel also does that).

Do you think that the concept of a session temp-data that is short-lived in between redirects would break the Phoenix paradigm of doing things? Right now, I’m not sure how something similar could be achieved.

in the case of create (POST /users) new.html is “reused” to convey the error information - i.e .it is not a representation of any particular resource

But the route belongs to another resource. As I pointed above this can cause confusion.

“Gather together those things that change for the same reason, and separate those things that change for different reasons.”

I’m familiar with this definition. But it seems that this “change for the same reason” is sometimes drawn arbitrarily. So, for example: UserController.new beeing only responsible for building and displaying the form (with and without errors) and UserController.create only beeing responsible for actually posting data and redirecting is also a valid interpretation. I find difficult to argue that this does not make sense, also from the resource point of view of REST where “GET only reads and POST only writes”, but that’s not the point.

I just want to add that when you don’t want the default urls generated by the resources/4 macro, you could always use directly get/4 and post/4 like below:

# Registration
  scope "/registration", DemoWeb do
    pipe_through :browser
    get "/", RegistrationController, :new
    post "/", RegistrationController, :create
  end

This way the url won’t change between empty new form and create_error form. In my case I just do that for SEO purpose (for example I don’t want the url in English). And if someone shared the url, clicking on it will always result on the GET new form.

If you still want to redirect on invalid submit, you could assign the invalid changeset to the conn passed to redirect/2. Then you could have two clauses for your new action, one for empty form and the other for invalid form:

 # invalid form
  def new(%{error_changeset: changeset} = conn, _params) do
    render(conn, "new.html", changeset: changeset)
  end

  # empty form 
  def new(conn, _params) do
    changeset = Context.change_resource()
    render(conn, "new.html", changeset: changeset)
  end
2 Likes

Wow! That’s a very simple solution, thanks for that :blush:

If the conn.error_changeset is kept intact on a redirect, that means that I could also just assign the changeset variable conditionally whether or not I have errors coming through - is that right?
I guess this solves the problem of sending the form state over and allows me to do a full PRG.