MetaGuru
MetaGuru

Reputation: 43873

What is a good method for preventing a user from submitting a form twice?

I have a purchase page and I don't want the user to be able to refresh the page and resubmit the form once they get to the 'order complete' page because it automatically sets them up in our system via database values and charges their card via paypal (only want these to happen ONCE)... I have seen some sites that say 'Don't hit refresh or you will get charged twice!' but that is pretty lame to leave it open to possibility, what's a good way to only allow it to be submitted once or prevent them from refreshing, etc?

PS: I saw a few similar questions: PHP: Stop a Form from being accidentally reprocessed when Back is pressed and How do I stop the Back and Refresh buttons from resubmitting my form? but found no satisfactory answer... an ASP.NET MVC specific answer would be ideal too if there is a mechanism for this.

EDIT: Once they click submit it POSTS to my controller and then the controller does some magic and then returns a view with an order complete message, but if I click refresh on my browser it does the whole 'do you want to resend this form?' that is bad...

Upvotes: 13

Views: 17527

Answers (9)

KyleMit
KyleMit

Reputation: 30267

I 100% agree with RedFilter's generic answer, but wanted to post some relevant code for ASP.NET MVC specifically.

You can use the Post/Redirect/Get (PRG) Pattern to solve the double postback problem.

Here's an graphical illustration of the problem:

Diagram

What happens is when the user hits refresh, the browser attempts to resubmit the last request it made. If the last request was a post, the browser will attempt to do that.

Most browsers know that this isn't typically what the user wants to do, so will automatically ask:

Chrome - The page that you're looking for used information that you entered. Returning to that page might cause any action you took to be repeated. Do you want to continue?
Firefox - To display this page, Firefox must send information that will repeat any action (such as a search or order confirmation) that was performed earlier.
Safari - Are you sure you want to send a form again? To reopen this page Safari must resend a form. This might result in duplicate purchases, comments, or other actions.
Internet Explorer - To display the webpage again, the web browser needs to resend the information you've previously submitted. If you were making a purchase, you should click Cancel to avoid a duplicate transaction. Otherwise, click Retry to display the webpage again.

But the PRG pattern helps avoid this altogether by sending the client a redirect message so when the page finally appears, the last request the browser executed was a GET request for the new resource.

Here's a great article on PRG that provides an implementation of the pattern for MVC. It's important to note that you only want to resort to a redirect when an non-idempotent action is performed on the server. In other words, if you have a valid model and have actually persisted the data in some way, then it's important to ensure the request isn't accidentally submitted twice. But if the model is invalid, the current page and model should be returned so the user can make any necessary modifications.

Here's an example Controller:

[HttpGet]
public ActionResult Edit(int id) {
    var model = new EditModel();
    //...
    return View(model);
}

[HttpPost]
public ActionResult Edit(EditModel model) {
    if (ModelState.IsValid) {
        product = repository.SaveOrUpdate(model);
        return RedirectToAction("Details", new { id = product.Id });
    }
    return View(model);
}

[HttpGet]
public ActionResult Details(int id) {
    var model = new DetailModel();
    //...
    return View(model);
}

Upvotes: 17

Ramin Bateni
Ramin Bateni

Reputation: 17425

If you doesn't like redirect the user to other page, then by using my way you dose not need Post/Redirect/Get (PRG) Pattern and the user remain on the current page without fear of the negative effects of re-submitting of the form!

I use a TempData item and a Hidden field (a property in the ViewModel of the form) to keep a same Guid in both sides (Server/Client) and it is my sign to detect if the form is Resubmitting by refresh or not.

Final face of the codes looks like very short and simple:

Action:

[HttpPost]
public virtual ActionResult Order(OrderViewModel vModel)
{
     if (this.IsResubmit(vModel)) //  << Check Resubmit
     {
         ViewBag.ErrorMsg = "Form is Resubmitting";
     }
     else
     {
        // .... Post codes here without any changes...
     }

     this.PreventResubmit(vModel);// << Fill TempData & ViewModel PreventResubmit Property

     return View(vModel)
 }

In View:

@if (!string.IsNullOrEmpty(ViewBag.ErrorMsg))
{
    <div>ViewBag.ErrorMsg</div>
}

@using (Html.BeginForm(...)){

    @Html.HiddenFor(x=>x.PreventResubmit) // << Put this Hidden Field in the form

    // Others codes of the form without any changes
}

In View Model:

public class OrderViewModel: NoResubmitAbstract // << Inherit from NoResubmitAbstract 
{
     // Without any changes!
}

What do you think?


I make it simple by writing 2 class:

  • NoResubmitAbstract abstract class
  • ControllerExtentions static class (An Extension class for System.Web.Mvc.ControllerBase)

ControllerExtentions:

public static class ControllerExtentions
{
    [NonAction]
    public static bool IsResubmit (this System.Web.Mvc.ControllerBase controller, NoResubmitAbstract vModel)
    {
        return (Guid)controller.TempData["PreventResubmit"]!= vModel.PreventResubmit;
    }

    [NonAction]
    public static void PreventResubmit(this System.Web.Mvc.ControllerBase controller, params NoResubmitAbstract[] vModels)
    {
        var preventResubmitGuid = Guid.NewGuid();
        controller.TempData["PreventResubmit"] = preventResubmitGuid ;
        foreach (var vm in vModels)
        {
            vm.SetPreventResubmit(preventResubmitGuid);
        }
    }
}

NoResubmitAbstract:

public abstract class NoResubmitAbstract
{
    public Guid PreventResubmit { get; set; }

    public void SetPreventResubmit(Guid prs)
    {
        PreventResubmit = prs;
    }
}

Just put them in your MVC project and run it... ;)

Upvotes: 0

CShark
CShark

Reputation: 2221

Note that the PRG pattern does not completely guard against multiple form submissions, as multiple post requests can be fired off even before a single redirect has taken place - this can lead to your form submissions not being idempotent.

Do take note of the answer that has been provided here, which provides a workaround to this issue, which I quote here for convenience:

If you make use of a hidden anti-forgery token in your form (as you should), you can cache the anti-forgery token on first submit and remove the token from cache if required, or expire the cached entry after set amount of time.

You will then be able to check with each request against the cache whether the specific form has been submitted and reject it if it has.

You don't need to generate your own GUID as this is already being done when generating the anti-forgery token.

Upvotes: 2

Nasser
Nasser

Reputation: 663

Kazi Manzur Rashid wrote about this (together with other asp.net mvc best-practices). He suggests using two filters to handle data transfer between the POST and the follwing GET using TempData.

Upvotes: -1

Klaus Byskov Pedersen
Klaus Byskov Pedersen

Reputation: 121057

Simply do a redirect from the page that does all the nasty stuff to the "Thank you for your order" page. Having done that, the user can hit refresh as many times as he likes.

Upvotes: 0

D&#39;Arcy Rittich
D&#39;Arcy Rittich

Reputation: 171559

The standard solution to this is the POST/REDIRECT/GET pattern. This pattern can be implemented using pretty much any web development platform. You would typically:

  • Validate submission after POST
  • if it fails re-render the original entry form with validation errors displayed
  • if it succeeds, REDIRECT to a confirmation page, or page where you re-display the input - this is the GET part
  • since the last action was a GET, if the user refreshes at this point, there is no form re-submission to occur.

Upvotes: 21

Wim
Wim

Reputation: 12092

Off the top of my head, generate a System.Guid in a hidden field on the GET request of the page and associate it with your checkout/payment. Simply check for it and display a message saying 'Payment already processed.' or such.

Upvotes: -1

Macha
Macha

Reputation: 14664

Give each visitor's form a unique ID when the page is first loaded. Note the ID when the form is submitted. Once a form has been submitted with that ID, don't allow any further requests using it. If they click refresh, the same ID will be sent.

Upvotes: 1

Sands
Sands

Reputation: 547

While serving up the order confirmation page you can set a token that you also store in the DB/Cache. At the first instance of order confirmation, check for this token's existence and clear the token. If implemented with thread safety, you will not be able to submit the order twice.

This is just one of the many approaches possible.

Upvotes: 2

Related Questions