htmx hx-on fails: unsafe-eval is not an allowed source of script
Under a Content-Security-Policy with script-src 'self', htmx compiles hx-on attributes into a function at runtime and the browser refuses to run it. Moving the same code into a script file as an event listener fixes it.
- Errors or symptoms
Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: script-src 'self'a form submits fine but does not clear or reset afterwards- Affects
- htmx 2.0.7 · Content Security Policy
- Checked
- with htmx 2.0.7, Chromium 151.0.7922.10
- Depth
- one layer down
- Tags
- htmx · csp · javascript · security
What broke
The contact form on my site clears itself two seconds after a successful send. It used to do that with an hx-on::after-request attribute:
hx-on::after-request="if(event.detail.successful)
{setTimeout(()=>this.reset(),2000)}"
The site's Content-Security-Policy has script-src 'self', and under it that attribute never ran. Every message went through and the server answered 200, but the form kept whatever the visitor had typed. The only sign was an error in the browser console.
The error the browser threw
I reproduced it on a plain test page. It loaded the site's own copy of htmx 2.0.7 under script-src 'self' and had a form with the same kind of hx-on::after-request, posting to a stub endpoint. Clicking send in headless Chromium 151 gave this error, word for word:
Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: script-src 'self'".
Why hx-on needs eval
The browser never runs an hx-on attribute itself. htmx reads the attribute text and turns it into a function with new Function("event", n) the first time the event fires. That call is in the minified htmx the site serves.
MDN's page on script-src says a policy without 'unsafe-eval' blocks the Function() constructor as well as eval(). script-src 'self' has no 'unsafe-eval', so the browser refuses the function htmx builds.
htmx's docs put hx-on behind the htmx.config.allowEval setting, along with event filters and js: values in hx-vals or hx-headers. The setting is on by default. Turning it off does not help. htmx then fires an htmx:evalDisallowedError event instead of calling Function, so the console error goes away and the form still never resets.
The fix
I moved the reset out of the HTML attribute and into the site's own script file, which the policy allows because the site serves it. Nothing gets built from a string, so the CSP has nothing to block. It started as an htmx:afterRequest listener in src/js/app.js. The client code has since moved to TypeScript, and the same listener now lives in src/ts/app.ts:
document.body.addEventListener('htmx:afterRequest', (event) => {
const { elt: form, successful } = (event as CustomEvent<{ elt: Element; successful: boolean }>).detail;
if (successful && form instanceof HTMLFormElement && form.matches('[hx-post="/api/contact/submit"]')) {
setTimeout(() => form.reset(), 2000);
}
});
htmx:afterRequest fires after every htmx request, so the listener checks that the request succeeded and that it came from the contact form. Then it resets the form two seconds later, as before.
Other attributes that break the same way
Every hx-on attribute fails like this under a strict script-src, whatever event it listens for. So do js: values in hx-vals and hx-headers. Each one has the same fix, a listener in a script file the policy already allows.