I'm having difficulty deciding what is the best approach to returning from a POST request to one's web app. I'd like to deliver messages to the user about the results of the request, I want to avoid some nasty POST-related browser behavior, and proper bookmarking would be sweet, too. Unfortunately, it seems I can only have 2 out of the 3 with any given strategy.
Approach 1: Direct response
Display the results of the POST (along with any messages to the user) as a direct response to the POST, that is, do not issue a redirect. Most sites do this, because it is dead simple. It is also RESTful, and session handling need not get involved.
Unfortunately, this means that when you click on a link from the results page and then hit the back button, you are presented with that irritating and useless modal dialog box "Do you want to resubmit that POSTDATA?" And forget about returning to a saved browser session.
Adding nonces (one-time processing keys) to prevent accidental reissuance of POSTs is only a band-aid, and a bad one at that.
Approach 2: Redirect with messages in URL
Redirect to a URL that has message information in the URL, either explicit or referenced.
- Redirect to something like /submitted.php?msg=Hey%2C%20it%20worked!. Often vulnerable to message spoofing.
- Trigger pre-defined, hardcoded messages using flags in the querystring: /submitted.php?msgtype=success&post=320
WordPress uses referenced messages. Any number of poorly-coded, XSS-vulnerable sites use the explicit method.
As before, the user is guaranteed to receive the messages, but as an added benefit, the back button works. Of course, there are downsides:
- Ugly, non-canonical URLs.
- Bookmarking and link-sending makes less sense.
- Refresh the page, and the messages are still there, giving the user the impression that they have just re-performed the action.
Approach 3: Store completely in session
Store any results and messages in the session, and redirect to a regular GET. That response pulls the messages and results out of the session for display.
I use this approach on tradeups.net, my web development sandbox. This technique is refresh-safe, the back button works, links can be properly bookmarked or sent, and the URLs are clean.
However... I am worried that in high-load situations, asynchronous requests will cause session "corruption" (messages may appear on the wrong page.) For example:
- User clicks a form button that POSTs to page A, and before the browser starts getting the response, also middle-clicks a link to page B (to open it in another tab.) I do this kind of thing all the time.
- Page A is finished processing and has queued several messages to the user. It responds with a redirect to page C.
- Page B is processed, and pulls the messages out of the queue and displays them to the user.
- The redirect to C is processed, but there are no messages to display. The user has not received the proper feedback.
- Upon viewing the other tab, they'll see the messages, but in the wrong context.
In other words, sharing a single message queue between clickstreams creates a race condition. In fact, any data that is stored in a clickstream-shared session variable is not guaranteed valid -- this could theoretically lead to security holes. That's why I don't like the loosely-coupled POST and GET.
Approach 4: Messages keyed to token
Store message information in the session under an autogenerated token, and redirect to something like /home.php?token=x8dnt5, where the token-keyed messages are retrieved and displayed. PHPMyAdmin uses this technique. It guarantees transmission of messages, and does not break the back button.
By including a token, clickstreams are separated within a session, and there is a tighter coupling of the POST and the following GET. The only downside is that the URLs can be somewhat ugly (and non-canonical.)
It seems that you can have at most 2 of the following 3 features:
- Back button/history works and page is refresh-safe
- Response messages conveyed properly to user
- Response URL is canonicalized, and makes sense when viewed later or by another user
Intuitively, approach #1 seems like the most rational, with the only downside being a fault of the browser, but #3 has been working well for me so far. What is your opinion?