Aditya Sharma

Contact Form 7 Keeps Spinning: Find the Failing REST Request

On this page, 8 sections

Contact Form 7 keeps spinning when the browser sent the form to WordPress and got back something the plugin’s script could not read as a successful reply.

The script sets the form to a “submitting” state before it sends anything, and in the code I read, the only path out of that state runs through a valid answer. No usable answer, no exit, so the spinner stays.

The fix is to find that one request in your browser’s Network tab and read its status code and response body. That tells you which of a handful of causes you have.

This is a different problem from the form refreshing the page after you press send. A refresh means the plugin’s script never took over the submit. A spinner means the script did run, and the request it made failed.

Why the spinner never stops

I read the plugin’s front-end source on the GitHub master branch on 11 October 2026. I did not record a version number, so check that your installed version matches before you rely on the details. Three pieces explain the behaviour.

First, submit.js calls setStatus(form, "submitting") and then awaits a call to apiFetch that posts to contact-forms/<id>/feedback. The same function returns early if the status is already “submitting”, so a second click does nothing.

Second, styles.css makes the spinner visible with one rule, form.submitting .wpcf7-spinner { visibility: visible; }. The spinner is not a separate loading indicator that times out. It is the visual for the “submitting” class on the form.

Third, api-fetch.js treats anything outside a 2xx status as a failure and throws. It also throws if a 2xx body cannot be parsed as JSON (the error it builds is named invalid_json), and it throws a fetch_error with the message “You are probably offline” if the network request itself fails.

In submit.js, I found no catch around that awaited call. When it throws, the status is never reset, the class stays on the form, and the spinner keeps going. I read that one file and did not trace every caller, so treat “nothing resets it” as what the code shows, not as a tested claim.

The practical reading: the spinner is a symptom of a failed or malformed response. The cause is wherever that response went wrong.

Step 1: Find the request

Open the page with the form in a desktop browser and do this.

  1. Open DevTools and go to the Network tab.
  2. Tick “Preserve log” and filter by feedback.
  3. Fill in the form and press send.
  4. Click the request that appears and note three things: the status code, the Content-Type of the response, and the first few lines of the response body.

The URL looks like /wp-json/contact-form-7/v1/contact-forms/123/feedback, where 123 is your form’s ID. On a site with plain permalinks it can instead be /?rest_route=/contact-form-7/v1/contact-forms/123/feedback.

If no request appears at all, skip to the “No request is sent” section below.

You can also replay it from a terminal, which takes the browser and its extensions out of the picture. It is a minimal request, so Contact Form 7 may reply with a validation status because fields are missing. That is still a healthy reply, because the route answered with JSON:

curl -i -X POST "https://example.com/wp-json/contact-form-7/v1/contact-forms/123/feedback" \
  -F "_wpcf7_unit_tag=wpcf7-f123-p1-o1" \
  -F "your-name=Test" -F "[email protected]" -F "your-message=Test"

Replace the domain, the form ID and the field names with your own. A healthy response is a 200 with Content-Type: application/json and a JSON body that has a status field. Anything else is your cause.

Step 2: Match the response to a cause

The status is 403

Something between the browser and WordPress refused the POST. The candidates are a security plugin, a web application firewall rule on your host or CDN, or a rule that blocks REST routes for visitors who are not logged in.

Check the security plugin’s log first, then the CDN’s firewall events. If you block REST access for anonymous users on purpose, add an exception for the contact-form-7/v1 namespace. I did not test any specific security plugin for this post, so I cannot name the exact setting in each.

The status is 404

The route did not resolve. Either the REST API is switched off, or permalinks are broken, or the request is hitting a different site than you think.

Open /wp-json/ on the site in a browser. If that does not return JSON, the REST API itself is the problem, and Contact Form 7 is only the first thing to notice. Re-save the permalinks under Settings, then Permalinks, to flush the rewrite rules. If a plugin that disables the REST API is active, that is your answer.

The status is 200 but the body is not JSON

This is the quiet one, and the source says why it matters: a 200 with a body that fails to parse still throws.

The body will tell you what is wrong. Look at the first characters.

  • A PHP warning or notice printed before the JSON. Something in your theme or a plugin is echoing output during the request. Set WP_DEBUG and WP_DEBUG_LOG to true, with WP_DEBUG_DISPLAY false, repeat the submit, and read wp-content/debug.log. The first line of the response is usually the same message.
  • HTML from a maintenance or coming-soon plugin, or a redirect to the login page. A plugin that gates the whole site may be gating the REST route too.
  • An HTML error page from a cache or proxy layer in front of WordPress.

If the body looks like JSON but has stray output before the opening brace, whatever printed it ran before the REST response was sent. The debug log usually names the file.

The status is 500

PHP crashed while handling the submit. This happens in code that runs on form submission, for example a custom hook on wpcf7_before_send_mail, a mail or SMTP plugin that throws, or an add-on for Contact Form 7. Read the PHP error log or debug.log at the time of the request. The fatal error names the file and line.

Cloudflare or another proxy returns its own page

If the response is an HTML challenge or block page, the request never reached WordPress. Look at the firewall events in the CDN dashboard for the same timestamp.

No request is sent

If the Network tab stays empty, the script did not start or the submit was stopped before the request.

index.js in the plugin starts with a set of guard checks and logs a message to the console when one fails. The messages I read in the source include “wpcf7 is not defined.”, “wpcf7.api is not defined.” and messages about the browser lacking window.fetch(), FormData, NodeList.forEach() or String.replaceAll(). Open the Console tab and look for them.

The two usual causes:

  • A script optimiser, defer or delay plugin, or minifier is stopping or reordering the plugin’s script. The wpcf7 object that the plugin prints into the page is missing or loads after the code that needs it. Exclude the Contact Form 7 scripts from delay, defer and combine settings, then clear the cache.
  • A JavaScript error from another script halts execution before Contact Form 7 initialises. The console shows it in red, with a file name.

A very old browser also fails the checks above, but that is rare enough to check last.

Check the caches after any fix

After you change a security rule, a script setting or a plugin, clear the page cache and purge the CDN, then test in a private window. A cached copy of the page can keep serving the old script setup, and you will think the fix failed. The Network tab settles it, because it shows the real status of the request each time.

Questions people ask

Why does Contact Form 7 show a spinner but never a message?

The message area is filled from the JSON reply. If the reply fails, no message is shown, and the spinner stays because the form never leaves its submitting state.

Is the spinner a plugin bug?

Not in the sense of a defect in the display. The spinner follows the form’s state, and the state follows the response. The thing to fix is the response. If you want the form to recover on failure, that would be a change to the plugin’s code, and I did not test a patch.

Does it matter whether mail sending works?

Sending mail happens on the server after the request arrives. If mail fails, the route still has to answer, and a failing mail plugin that crashes PHP produces the 500 case above. Checking the response status separates “mail failed” from “the request failed”.

Do this today

Open the page with the form, open DevTools, go to the Network tab, submit the form, and write down the status code and the first line of the response for the feedback request. That one note decides which section above applies to you.

Resources

Tell me where I am wrong

Your email is not published and I do not add it to any list. Corrections with a source are the ones I act on fastest.