skip to content
Stylized letter A in teal with pink sparkle accents Khalid Abuhakmeh

"Confirm Form Resubmission": Why It Appears and How to Fix It

/ 5 min read

Updated:

I was paying a bill today and after submitting the form, I saw a successful confirmation page. There on the confirmation page was a message displayed in retina-burning red.

Do Not Refresh The Page

This was an expensive payment. I assume that refreshing the page would cause a double billing issue. As a seasoned web developer, I have learned to solve this issue. Before we solve the issue, let us discuss how the problem occurs.

The “confirm form resubmission” dialog appears because you are refreshing the page after a POST action has occurred and a refresh is resubmitting the form. It is likely the developer of the site has not correctly developed the flow of the site. If you can, contact them and point them to this blog post.

Why the “Confirm Form Resubmission” Dialog Appears

As developers, we like to keep our code as “simple” as possible. This can cause us to violate the Single Responsibility Principle. Let’s take the simple use case of a contact form. We may violate the principle as seen in the diagram below.

flowchart LR
    client([Browser]) -->|GET| contact@{ shape: doc, label: "/contact" }
    contact -->|POST| contact

The diagram shows that a single action handles both the GET and POST HTTP verbs. A refresh of the page is ambiguous. Most browsers have defaulted to assuming a resubmit of the form.

The Fix: Post/Redirect/Get

The solution is simple. We need to break our contact form into the sum of its parts. Three parts to be exact:

  1. The action that displays the form via GET.
  2. The action that handles the form POST.
  3. A confirmation page that we Redirect to.

You can see the resulting flow in the next diagram.

flowchart LR
    client([Browser]) -->|GET| contact1@{ shape: doc, label: "/contact" }
    contact1 -->|POST| contact2@{ shape: doc, label: "/contact" }
    contact2 -->|"GET (redirect)"| confirm@{ shape: doc, label: "/confirm" }

In our diagram, the two /contact interactions are distinct actions differentiated by an HTTP verb. In our first figure, a single action could handle both GET and POST verbs. This will help in the event our users use the back button. By pressing back, they are redirected to our initial form. The confirmation page is the most important addition. If a user is redirected to a different URL, a refresh of the confirmation page will not resubmit the form. The key is to use Redirect correctly.

How Do I Implement Post/Redirect/Get in ASP.NET Core?

Here’s a minimal ASP.NET Core MVC controller that implements the pattern correctly. Notice there are two separate actions for /contact, one for each verb, plus a dedicated /contact/confirm destination.

public class ContactController : Controller
{
[HttpGet("contact")]
public IActionResult Index() => View();
[HttpPost("contact")]
public IActionResult Index(ContactForm form)
{
if (!ModelState.IsValid)
{
// redisplay the form with validation errors
// no redirect needed here, there's no state change yet
return View(form);
}
// do the actual work: save to a database, send an email, etc.
_repository.Save(form);
// carry a small amount of state across the redirect
TempData["ConfirmationMessage"] = $"Thanks, {form.Name}! We'll be in touch.";
// the browser issues a brand-new GET to /contact/confirm
return RedirectToAction(nameof(Confirm));
}
[HttpGet("contact/confirm")]
public IActionResult Confirm()
{
ViewBag.Message = TempData["ConfirmationMessage"] as string
?? "Thanks for your submission!";
return View();
}
}

TempData is the key ingredient here. It stores data server-side (backed by session or cookies, depending on configuration) just long enough to survive the redirect, then clears itself after it’s read. This lets the confirmation page display something meaningful, like an order number or a thank-you message, without needing the POST body to still be in flight. Because RedirectToAction issues an HTTP 302 followed by a fresh GET, refreshing the confirmation page only re-requests /contact/confirm, never resubmits the form.

Key Takeaways

Most of the time, the “Confirm Form Resubmission” problem is not a technological issue, but an implementation one. Some technologies like ASP.NET WebForms have this problem due to their implementation philosophy, mainly the PostBack model. If you are stuck on a platform like WebForms, you may want to look into utilizing better routing strategies. Following the solution above should solve most resubmission issues.

There are also technical solutions to this problem, but they are harder to execute. You could put a token on the page and is checked against previous submissions. This technical approach helps avoid duplicate requests being sent to the server. For most applications, it is not necessary. Although it may be a good addition depending on your circumstances.

FAQ

Does Post/Redirect/Get work with AJAX? Yes, and it’s less of a concern there since the browser’s address bar never changes with an AJAX POST. There’s no page to refresh, so the resubmission dialog simply doesn’t apply. The PRG pattern mainly matters for traditional full-page form submissions.

What about the browser back button? PRG actually improves the back-button experience. Since the confirmation page lives at its own URL (like /contact/confirm), pressing back takes the user to the original GET /contact form, not back into the POST. Without PRG, pressing back can re-trigger the browser’s “resubmit form” warning.

How do I prevent duplicate form submissions beyond PRG? PRG stops the refresh case, but a user can still double-click a submit button before the first request completes. Disable the submit button on click with a bit of JavaScript, and consider a server-side idempotency token (a hidden field with a unique value, checked against prior submissions) for critical operations like payments.

Related reading: more HTTP methods for ASP.NET Core HTML forms, Blazor HTML forms, submitting, and antiforgery tokens, and simple redirects with ASP.NET Core endpoint routing.

Comments

Related Posts