Preventing Form Resubmission Warnings in WordPress with the Post/Redirect/Get Pattern

Post/Redirect/Get pattern WordPress

Today’s article is on an advanced topic in WordPress development, and is by a distinguished guest and a truly outstanding WordPress developer and educator: Carl Alexander.

CCarl Alexander PHP developerarl is a PHP developer from Montréal, Canada, who shares his passion for advanced programming topics on his website, carlalexander.ca, where he publishes articles on a regular basis.

Carl is a regular WordCamp Speaker, has been a WordPress Montréal organizer since 2010, and helps organize other WordPress events during the year. He’s also the author of Learn Object-Oriented Programming Using WordPress. You can find him on Twitter and GitHub.

We’ve all had this happen to us at one point or another. We’re filling a form in a web application. We go to submit, and that’s when we see it, straight from a web usability expert’s nightmare: the infamous “Confirm Form Resubmission” warning box!

Everyone hates seeing this warning. Why is there? Why aren’t you just submitting my form, web browser? Whyyyyyyy?

It’s also common to see the “Confirm Form Resubmission” warning in a lot of plugin admin pages. That’s because most WordPress developers don’t use the Settings API. It’s one of the least (if not the least!) popular APIs in WordPress, and few developers use it. But it does prevent this issue.

So how can you build a WordPress admin page without causing this dreadful warning to appear? By using the Post/Redirect/Get pattern. This is also a pattern that you can use with WordPress. Let’s look at how you can do it!

Why do you get a “Confirm Form Resubmission” warning?

First, let’s investigate why this warning appears. A form resubmission warning exists for a reason, after all! Why is your web browser giving you such a hard time?

Let’s imagine that you’re on the checkout page on an e-commerce site. You enter all your personal information and then click the “Buy” button. Your web browser will then send the form to the e-commerce platform, and it’ll reply with a success page.

POST request / HTML response pattern WordPress

Above is a diagram of the communication between your web browser and the e-commerce platform. Your web browser sends an HTTP POST request, and the e-commerce platform replies with the content of the success page.

The problem with doing things this way is what happens if you refresh the page in your web browser: refreshing a page causes the browser to resend the last request that it sent. For us, that would mean resending POST request with the form that we used to buy from the e-commerce site.

If the developers of the e-commerce website didn’t do their jobs well, this can cause issues—the most obvious being that you’ll buy everything a second time. (Which isn’t what you had in mind when you refreshed the page!) That’s why web browsers added this warning when you try to refresh a page after doing a POST request.

How does the Post/Redirect/Get pattern prevent this?

The Post/Redirect/Get pattern is a way that developers have found to prevent this problem. It ensures that the last request from your web browser isn’t a POST request. It does that by using a specific series of HTTP requests and responses.

Post/Redirect/Get pattern WordPress

Here’s a diagram to show what’s going on when you use the Post/Redirect/Get pattern. Your web browser still sends an HTTP POST request like it did earlier. The difference is the response from the e-commerce platform.

The e-commerce doesn’t respond with the content of the success page like it did before. Instead, it responds with a redirect status code. (To be more specific, it should respond with a 303 status code.) This tells the browser where to go get the content of the success page.

Your browser will then do a GET request based on the location header in redirection response. This will fetch the content of the success page. But now, since the last request was a GET request, you can refresh the page without getting a warning. (Huzzah!)

Our initial admin page

Let’s imagine that we already have a WordPress admin page for a plugin that we’re working on. It has a form that we submit to update its settings. Here’s what it looks like:

/**
 * Register plugin admin page.
 */
function myplugin_create_admin_page() {
    add_menu_page('My Plugin Admin Page', 'My Plugin Admin Page', 'edit_posts', 'myplugin_admin_page', 'myplugin_display_admin_page');
}
add_action('admin_menu', 'myplugin_create_admin_page');

/**
 * Display plugin admin page.
 */
function myplugin_display_admin_page() {
    if (isset($_POST['myplugin_option'])) {
        update_option('myplugin_option', $_POST['myplugin_option']);
    }

    $option = get_option('myplugin_option', '');
    ?>
    <h1>My Plugin Admin Page</h1>

    <form method="POST">
        <p>
            <label for="myplugin_option">Option:</label>
            <input type="text" name="myplugin_option" id="myplugin_option" value="<?php echo $option; ?>">
        </p>
        <input type="submit" value="Save" class="button button-primary button-large">
    </form>
    <?php
}

There are two functions in the code sample above. The first one is the myplugin_create_admin_page function. It registers our plugin admin page using the add_menu_page function.

The myplugin_display_admin_page function is in charge of displaying our plugin’s admin page. It starts by checking if we submitted the form by looking for myplugin_option key in the $_POST superglobal. If there’s a value set in the $_POST array, it saves the value as the new plugin option.

After that, we can fetch the option using get_option and assign it to the option variable. We’re also going to use an empty string as the default value. We’ll then output the HTML of our form with the options value as the value of the text box.

As you can see, this follows the bad pattern that we saw earlier. When we submit this form, it’ll display the form again after saving the value. But, if you try to refresh, you’ll get the “Confirm Form Resubmission” error, warning you that you’re about to submit the same form data a second time.

Using the Post/Redirect/Get pattern with WordPress

Like we mentioned earlier, if you use the Settings API, this is all done for you by default. But our form doesn’t use the Settings API. So how do we do we convert it to use the Post/Redirect/Get pattern?

Adding a redirect

Well, we could add the redirect right in the myplugin_display_admin_page function. Something like this:

/**
 * Display plugin admin page.
 */
function myplugin_display_admin_page() {
    if (isset($_POST['myplugin_option'])) {
        update_option('myplugin_option', $_POST['myplugin_option']);
        wp_redirect(menu_page_url('myplugin_admin_page', false), 303);
        exit;
    }

    // ...
}

We added a call to the wp_redirect function in our if statement. We feed it the return value from the menu_page_url function. This is a handy helper function for generating admin page URLs. By default, it echoes the URL so we have to pass it false as a second argument to prevent that.

With redirects, you also always need to make a call to the exit function. This terminates the script and sends the redirect headers back to the browser. This is what tells the browser to perform the redirect.

Now, there’s a big problem with trying to implement the Post/Redirect/Get pattern this way: except for some rare cases, it won’t work. (Yes, I’ve deceived you!) Instead of a redirect, what you’ll see is a PHP warning that the headers were already sent.

The admin_post hook

So what can you do instead? Well, there’s a little-known hook that we can use. We call it the admin_post hook.

It’s a hook that WordPress calls when you submit a request to the wp-admin/admin-post.php admin page. When the WordPress receives a request to this page, it’ll look for the action parameter. If there’s one, WordPress will then call the hook admin_post_$action. This might seem confusing, but it’ll be easier to understand once we update our example.

Using the admin_post hook

/**
 * Register plugin admin page.
 */
function myplugin_create_admin_page() {
    add_menu_page('My Plugin Admin Page', 'My Plugin Admin Page', 'edit_posts', 'myplugin_admin_page', 'myplugin_display_admin_page');
}
add_action('admin_menu', 'myplugin_create_admin_page');

/**
 * Updates our plugin options when we submit the form to admin-post.php.
 */
function myplugin_admin_update_options() {
    if (isset($_POST['myplugin_option'])) {
        update_option('myplugin_option', $_POST['myplugin_option']);
    }

    wp_redirect(admin_url('admin.php?page=myplugin_admin_page'), 303);
}
add_action('admin_post_myplugin_update_options', 'myplugin_admin_update_options');

function myplugin_display_admin_page() {
    $option = get_option('myplugin_option', '');
    ?>
    <h1>My Plugin Admin Page</h1>

    <form action="<?php echo admin_url('admin-post.php') ?>" method="POST">
        <p>
            <label for="myplugin_option">Option:</label>
            <input type="text" name="myplugin_option" id="myplugin_option" value="<?php echo $option; ?>">
        </p>
        <input type="hidden" name="action" value="myplugin_update_options">
        <input type="submit" value="Save" class="button button-primary button-large">
    </form>
    <?php
}

Here’s what our updated example looks like. We have a new function called myplugin_admin_update_options. We moved some of the code that we had in the myplugin_display_admin_page function into it.

The code in question is the if statement that we used to check if we submitted the form. We check if the myplugin_option key is in the $_POST superglobal. And we update the option if it is.

The myplugin_admin_update_options function uses the admin post hook that we just talked about. But why is the hook named admin_post_myplugin_update_options? That goes back to how we said the admin post hook worked.

It’s a combination of the admin_post_ prefix and the action parameter. For our form, the action parameter comes from a hidden field in our form. And it contains the myplugin_update_options value.

We also changed the form to send the form to admin-post.php. And, to do that, we used the admin_url function to generate admin-post.php URL. This is a method that you can use to generate URLs for the WordPress admin.

We also use the admin_url function inside the myplugin_admin_update_options function to generate the redirect URL. That’s because the menu_page_url function wouldn’t work with the admin post hook: we haven’t added our admin page at that time, so it would give us an error.

And that’s how you do it!

With this, we now have a form that uses the Post/Redirect/Get pattern. (Yay!) The key was using this admin post hook that a lot of us don’t know about. It’s really the tool that WordPress put at our disposal to do this.

It lets us implement the pattern without much of a hassle. Of course, it requires that we break out our code into more functions. But that’s okay!

One function is in charge of displaying the form. The other takes care of updating the option in WordPress. The result is that we made our code more focused and less prone to errors. These are always things that we should strive for!

Further Resources

This article is a follow-up post to:
https://wpshout.com/make-wordpress-admin-options-page-without-using-settings-api/

Learn more about the topic on Stack Overflow.

Our huge thanks to Carl for this article! Please check out his writing at carlalexander.ca—he’s one of the absolute best WordPress educators writing right now. And we strongly recommend his book, Learn Object-Oriented Programming Using WordPress. Till next time!

2 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Collins Agbonghama
April 19, 2017 3:00 pm

I have always gone with a custom settings page for all my plugins. After data is saved to the Database, i do a header redirect with a query string appended to the url. The query string is inspected and if found, a success message that settings has been updated is displayed. Slight difference with my approach is the absence of 303 status code in my wp_redirect().

Like Igor, i never hitherto knew it has a name. Thanks Carl for sharing.

Igor
April 19, 2017 1:55 am

I had encountered this when I was working on my own plugin for giveaways. I had a situation where I wanted to update the post status of my custom post type to a different one than ‘publish’.

Even though it does call the right function and it does what it should be doing, the post gets it status back to published. I am not sure why was that happening and I have even tried hooking the function at different priority but nothing changed.

I thought it did a double post or something similar which I have encountered previously with different browsers.

Finally, I created a redirection back to the post once I got the post status changed. Seems I did the post/redirect/get pattern just there 🙂

Did not know that had a name. As always, a great article by Carl. Thank you!