Caching FormsImproved in 5.0+
Forms and pages can be cached in a variety of ways, but you need to ensure that your CSRF token is being refreshed in order for the form to continue working.
Here are some solutions, depending on your caching approach:
Craft Caching
When using simple Twig-based Craft Caching, you'll need to make sure that you are refreshing the CSRF token.
There are two approaches you can take:
- Craft's `asyncCsrfInputs` setting
- Manually Refresh CSRF token
The simplest approach you can take is enabling Craft's asyncCsrfInputs
setting. It will automatically take care of the CSRF token for you and you don't need to do anything else.Recommended
Template Code
{% cache %}
{{ freeform.form("myFormHandle").render }}
{% endcache %}
Craft Config
- Static Config
- Environment Override
->asyncCsrfInputs(true)
CRAFT_ASYNC_CSRF_INPUTS=true
If you need wish to refresh the CSRF token manually instead, this can be done with a bit of javascript in the template:
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{# Cached form comes here #}
{% cache %}
{{ form.render }}
{% endcache %}
{# Script for updating the form's CSRF token #}
<script>
// Find the corresponding Form
var form = document.querySelector('form');
// Locate and update the CSRF input
var csrfInput = form.querySelector('input[name={{ craft.app.config.general.csrfTokenName|e('js') }}]');
csrfInput.value = '{{ craft.app.request.csrfToken|e('js') }}';
</script>
Static Caching / Blitz
When using full static page caching with services like CloudFlare, Craft Cloud or the Blitz Craft plugin, you'll need to make sure that you are refreshing the CSRF token.
There are a few different approaches you can take:
- Single-Page forms
- Single & Multi-Page forms
- Blitz `csrfInput()`
If your form is a single-page form, you can use Craft's asyncCsrfInputs
setting. It will automatically handle the CSRF token for you, and you won't need to do anything else.Recommended
Craft Config
- Static Config
- Environment Override
->asyncCsrfInputs(true)
CRAFT_ASYNC_CSRF_INPUTS=true
Craft's asyncCsrfInputs
setting will not work correctly on multi-page forms at this time. Instead, you'll need to manually refresh the CSRF token in your template.
Your forms must have AJAX enabled for the form in order for this to work correctly.
Create a separate Twig template to handle loading the refreshed tokens with the following code:
{{
{
csrf: {
name: craft.app.config.general.csrfTokenName,
value: craft.app.request.csrfToken
}
}
|json_encode|raw
}}
Make sure routing to that template works correctly. Best practice is to dedicate a directory inside the Craft templates directory named something like dynamic
and place files in that directory that should never be cached. The above template might be in a directory called dynamic
with a filename of index.twig
.
Set up a rule in Blitz, CloudFlare or your preferred CDN that makes sure any URLs starting with dynamic
are not cached. You can then aggressively cache all of your other site URLs.
Add the following Javascript snippet to your main .js
file or to the bottom of all your web pages.
It uses AJAX to fetch the contents of https://yourwebsite.com/dynamic
, which just returns a fresh CSRF token. This is then replaced in the CSRF token field of all forms on the page.
- Plain JS
- jQuery
document.addEventListener("DOMContentLoaded", function () {
// Find the corresponding form
var form = document.querySelector('form');
if (!form) return;
// Fetch the new CSRF token
fetch('https://yourwebsite.com/dynamic')
.then(response => response.json())
.then(data => {
if (data.csrf && data.csrf.name && data.csrf.value) {
var csrfInput = form.querySelector('input[name="' + data.csrf.name + '"]');
if (csrfInput) {
csrfInput.value = data.csrf.value;
}
}
})
.catch(error => console.error("CSRF update error:", error
));
});
$(function () {
// Find the corresponding Form
var form = document.querySelector('form');
$.ajax({
// Specify the form handle in the GET parameters
// ! Make sure to change the `myFormHandle` to your specific form handle.
url: 'https://yourwebsite.com/dynamic',
type: 'get',
dataType: 'json',
success: function (response) {
// Locate and update the CSRF input
var csrf = response.csrf;
form.querySelector('input[name=' + csrf.name + ']').value = csrf.value;
},
});
});
Note that this snippet assumes you're already running jQuery.
Your final template code might look something like this:
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{{ form.render }}
{# Load the jQuery library, if using #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
{# The script for updating the form's CSRF token loads after #}
<script src="/assets/js/main.js"></script>
Another option is to use Blitz's CSRF Token Input tag. However, this method requires coding your form formatting inside your template rather than using a formatting template, which can be quite inconvenient.
{# Initialize the form #}
{% set form = freeform.form("myFormHandle") %}
{{ form.renderTag }}
{# Add Blitz's CSRF token input inside <form> tags #}
{{ craft.blitz.csrfInput() }}
{% if form.hasErrors %}
<div class="freeform-form-errors">
{{ "Error! Please review the form and try submitting again."|t('freeform') }}
</div>
{% endif %}
{% for row in form %}
<div class="field-row">
{% for field in row %}
<div class="field-container">
{{ field.render() }}
</div>
{% endfor %}
</div>
{% endfor %}
{{ form.renderClosingTag }}
Stripe Payment Forms
When using the built-in Stripe integration, Freeform will trigger Payment Intent requests automatically upon page load. These requests use the initial form data, meaning the original CSRF token.
The assumption is that payment forms should never be cached, so if your form has a Stripe integration enabled and you are using Blitz caching and refreshing your CSRF tokens, we suggest injecting your payment form into your template using Blitz dynamic content or Sprig.
There is then no need to refresh CSRF tokens as the dynamic content is injected after the page loads. Otherwise, you risk a 400 Bad Request - Unable to verify your data submission
error.