Customers say they filled in the contact form, but nothing arrives and there is nothing under entries. Check today's access log to see what happens to the form POSTs.
"But I did fill in the form?" That question came in for a site with Gravity Forms and LiteSpeed Cache. There was no entry in WordPress, nothing in the spam folder and no email had been sent. The visitor had done nothing wrong. The cause was in the cache.
Why is there no trace anywhere?
Gravity Forms stores a submission that fails validation nowhere. No entry, no spam, no email. The visitor sees the form again, assumes it worked or gives up. The body of a POST is not in the access log, and Gravity Forms logging is usually off. If you then search the mail server or the spam filter, you are looking in the wrong place.
What causes it?
WordPress puts a nonce in forms: a code that proves the form really came from your site. A nonce is valid for 12 to 24 hours. In this case LiteSpeed Cache kept the page with the form for 7 days (cache-ttl_pub at 604800 seconds). So a visitor could get a page with nonces that had expired days earlier.
Two parts tripped over that:
- File uploads. The upload is a POST to
/?gf_page=<code>. It returned a 403 with a 93-byte response: Gravity Forms reporting that the session has expired. - reCAPTCHA v3. The Gravity Forms reCAPTCHA add-on also puts a nonce in the page. Once that has expired, the token is empty or invalid and the check rejects the whole submission.
How do you spot it in the access log?
Filter the day's access log on POSTs to the form page and on gf_page:
grep 'POST' access.log | grep -E 'gf_page|/contact'
grep 'gf_page=' access.log | awk '$9 == 403'
The pattern we saw:
- First POST from the cached page: 200, with a response larger than the normal page. That is the form again, with an error. The submission was rejected.
- Second POST, now from the fresh page that just came back: that one works.
- A successful submission that redirects to a thank-you page is a POST with a 302 to that page.
Watch the times. The access log is in local time, the entries in the database (wp_gf_entry.date_created) are in UTC. In summer in the Netherlands that is a two-hour difference.
How do you check the cache?
See whether the form page comes from the cache, and how long that cache lasts:
curl -sI https://www.yourdomain.com/contact/ | grep -i x-litespeed-cache
wp litespeed-option get cache-ttl_pub
x-litespeed-cache: hit means the page came from the cache. If the TTL is above 43200 seconds (12 hours), a visitor can get a page with a nonce that is no longer valid.
How do you fix it?
Set the public cache to less than 12 hours and purge the cache, so no old pages are left:
wp litespeed-option set cache-ttl_pub 36000
wp litespeed-purge all
36000 seconds is 10 hours. Every cached page is then younger than the shortest validity of a nonce.
If you want to keep the long cache, there are two other routes. LiteSpeed Cache can fill nonces separately from the page with ESI; the Gravity Forms and reCAPTCHA nonces then have to be in the plugin's nonce list. Or you exclude only the pages with a form from the cache.
Common mistakes
Searching the mail. No email was ever sent. The submission was rejected at validation.
Assuming the visitor made a mistake. The second attempt often works because the page is fresh by then. It looks like user error, but it is the cache.
Switching the cache off entirely. That makes your site slower for everyone. A TTL under 12 hours fixes it too.
Forgetting to purge. After changing the TTL, old pages stay in the cache until they expire. Purge the cache straight away.
Comparing times one to one. Access log in local time, database in UTC.