Why Gravity Forms File Uploads Fail on LiteSpeed Cached Pages (and How to Fix It)

Why Gravity Forms File Uploads Fail on LiteSpeed Cached Pages (and How to Fix It)

Your client submits a support ticket: "The file upload on our contact form stopped working." You test it yourself and get a cryptic 403 error or a "session expired" message. The form was working fine last week. Nothing changed. What happened?

If you're running Gravity Forms with LiteSpeed Cache on WordPress, there's a silent conflict between page caching and file upload security that can break uploads without warning. We recently discovered this affecting a client site and traced it to a fundamental mismatch between how long pages are cached and how long upload security tokens last. Here's exactly what causes it, why it's hard to catch, and how to fix it permanently.

The Problem: Nonces Expire, but Cached Pages Don't

WordPress uses security tokens called "nonces" to verify that form submissions are legitimate. When Gravity Forms renders a form with a file upload field, it generates unique nonces like gform_file_upload_nonce_68_11 (where 68 is the form ID and 11 is the field ID). These nonces are embedded directly in the page HTML.

Here's the conflict: WordPress nonces expire after 12-24 hours. But LiteSpeed Cache's default page TTL is 7 days. So a visitor loading a cached page on day 3 gets a form with nonces that expired two days ago. When they try to upload a file, Gravity Forms rejects the request because the nonce is no longer valid.

The result? A 403 "Forbidden" error or a "session expired" message on a form that looks perfectly normal.

Why This Is Hard to Catch

This bug is particularly insidious because of the conditions required to trigger it:

  • It only affects file uploads. Standard form fields (text, email, dropdowns) submit with the main form nonce, which Gravity Forms handles differently. File uploads use a separate AJAX upload mechanism with its own per-field nonces.
  • It only affects cached pages. If you're logged in as an admin, LiteSpeed typically serves uncached pages, so you'll never see the problem during testing.
  • It works fine right after a cache purge. Every time you purge the cache (during plugin updates, for example), fresh nonces get cached and the form works again - for another 12-24 hours.
  • It's intermittent. Visitors who happen to load the page shortly after a cache rebuild will have valid nonces. Visitors who load it later won't. This makes it nearly impossible to reproduce on demand.

On a site where file uploads are behind a login wall (like a client portal), this rarely surfaces because logged-in pages aren't cached. But the moment a client adds a public-facing form with a file upload - a job application, a support request form, an event registration - the clock starts ticking.

How We Discovered It

A client reported that applicants couldn't upload resumes through their public job application form. The form had been working for months (behind a login wall), but was recently moved to a public page. Testing as an admin showed everything working perfectly. Testing in an incognito browser window, however, consistently failed with a 403 on the file upload AJAX request.

Inspecting the network request revealed the issue: the gform_file_upload_nonce value in the cached HTML didn't match what WordPress expected. The nonce had expired, but the page cache was still happily serving it to every visitor.

The Fix: LiteSpeed ESI Nonces

LiteSpeed Cache has a feature called ESI (Edge Side Includes) that solves exactly this problem. ESI lets you mark specific parts of a cached page as "dynamic" - they get refreshed on every request even though the rest of the page is served from cache.

Important: ESI requires LiteSpeed Enterprise or LSWS. It is not available on OpenLiteSpeed (OLS). If you're running OpenLiteSpeed, you'll need to use cache exclusion rules for pages with file upload forms, or upgrade to LiteSpeed Enterprise. Most managed WordPress hosts that advertise LiteSpeed run Enterprise, but it's worth confirming with your provider.

LiteSpeed Cache for WordPress includes built-in ESI support for nonces. When you add a nonce name to the ESI nonce list, LiteSpeed replaces the static nonce value in the cached HTML with an ESI block that fetches a fresh nonce on every page load. The page stays cached, but the nonces are always current.

The fix requires two WordPress options to be set:

  1. Enable ESI (litespeed.conf.esi = 1) - Turns on the ESI engine.
  2. Add Gravity Forms nonce wildcards to the ESI nonce list:
    • gform_file_upload_* - Covers all file upload nonces across all forms and fields (the wildcard matches gform_file_upload_nonce_68_11, gform_file_upload_nonce_42_7, etc.)
    • gform_ajax_submission - Covers the AJAX form submission nonce

Applying the Fix via WP-CLI

You can configure this entirely from the command line without touching the WordPress admin:

# Enable ESI wp option update litespeed.conf.esi 1 # Add GF nonce wildcards (preserves any existing nonces) wp eval ' $nonces = get_option("litespeed.conf.esi-nonce"); if (!is_array($nonces)) $nonces = []; $to_add = ["gform_file_upload_*", "gform_ajax_submission"]; foreach ($to_add as $n) { if (!in_array($n, $nonces)) $nonces[] = $n; } update_option("litespeed.conf.esi-nonce", $nonces); echo json_encode($nonces) . "
"; ' # Purge the cache so pages rebuild with ESI blocks wp litespeed-purge all

Verifying the Fix

After applying the configuration and purging the cache:

# Confirm ESI is enabled wp option get litespeed.conf.esi # Should return: 1 # Confirm nonce wildcards are set wp option get litespeed.conf.esi-nonce --format=json # Should include: gform_file_upload_* and gform_ajax_submission

To verify it's actually working, visit the page with the form in an incognito window. View the page source and look for an ESI include tag where the nonce value used to be. Then try uploading a file - it should succeed even though the page is served from cache (check the X-LiteSpeed-Cache response header for hit).

Why Not Just Exclude the Page from Cache?

The obvious alternative is to exclude form pages from the LiteSpeed cache entirely. Don't do this. Here's why:

  • Performance penalty. Uncached pages hit PHP and the database on every request. For high-traffic pages (like a contact or application page), this adds unnecessary server load.
  • Moving target. Clients add forms to new pages all the time. You'd need to maintain an ever-growing exclusion list.
  • ESI is surgical. Only the nonce values become dynamic. The rest of the page (layout, content, styles, scripts) stays fully cached. The performance cost is negligible.

There's also a WordPress plugin called "Fresh Forms for Gravity" that attempts to solve this by excluding all Gravity Forms pages from cache. Same problem - it trades caching for correctness when ESI gives you both.

Is This Safe to Apply Proactively?

Yes. ESI nonce wildcards that don't match any nonce on a page are simply ignored. Adding gform_file_upload_* to a site that doesn't have Gravity Forms (or doesn't have file upload fields) has zero effect. There's no downside to applying this configuration to every WordPress site running LiteSpeed Cache as a preventive measure.

After discovering this issue on one client site, we rolled the fix out across our entire fleet of 100+ WordPress sites. The configuration change takes seconds per site via WP-CLI and prevents a class of support tickets that's otherwise very difficult to diagnose.

Does This Affect Other Form Plugins?

The nonce expiration problem isn't unique to Gravity Forms - any WordPress plugin that uses AJAX with nonces can be affected by page caching. However, the specific nonce names differ by plugin:

  • Gravity Forms: gform_file_upload_*, gform_ajax_submission
  • WPForms: Uses wpforms-setting-nonce (check their documentation for current nonce names)
  • Contact Form 7: Uses _wpcf7_nonce but handles caching differently

The principle is the same for any plugin: identify the nonce names used in AJAX requests and add them to the LiteSpeed ESI nonce list. LiteSpeed's wildcard support makes this straightforward.

Key Takeaways

  • Page caching and form nonces are fundamentally at odds. Cached pages serve stale nonces. ESI resolves this by making nonces dynamic within cached pages.
  • File uploads are the canary. Standard form submissions often work fine because they use different nonce handling. File upload AJAX is where the nonce expiration actually causes failures.
  • Test forms as a logged-out visitor on a page that's been cached for more than 24 hours. This is the only way to catch nonce-related issues.
  • Apply ESI nonce configuration proactively. Don't wait for a client to report the problem. The fix is safe, fast, and prevents an entire category of hard-to-diagnose issues.

Questions about caching, forms, or LiteSpeed configuration? Reach out to our team - we deal with these kinds of infrastructure-level WordPress issues every day.

The Author

Ryan Davis

Comments (0)

No comments yet. Be the first to comment!

Leave a Comment