I maintain an ASP.NET Core enhancement library focused on integrating HTMX naturally into your web development workflow. It works great, but I must admit that HTMX is doing much of the heavy lifting regarding developer experience; it’s incredible! That said, my library (HTMX.NET) helps smooth out some of the edges that come with integrating the two parts of the experience: ASP.NET Core and the HTML UI.

In this post, I’ll discuss how to work with anti-forgery tokens and some techniques I’ve taken to help mitigate the issues you might run into when working with HTMX boosts and ASP.NET Core security measures.

What is HTMX and What’s a Boost?

HTMX is a client-side library that takes a hypermedia approach to manage user interaction between the client and the server. In simpler terms, HTMX works with HTML fragments and replaces DOM elements based on an HTML server response. While seemingly simple, this approach is robust and used by organizations like GitHub and the Ruby on Rails community.

HTMX also supports the concept of Boost. In practice, boosting requires no changes to the server-side implementation but can increase perceived user performance on the client. HTMX can intercept any anchor-based web request and then process the response, replacing the page’s existing <body> element with the response’s version. Boosting can limit client-side parsing to the elements in the <body> tag while eliminating the need for requesting and reprocessing the <head> element and all its resources, such as scripts and cascading stylesheets.

While boosting has benefits, there are drawbacks you should be aware of:

  • Any changes in the head are not automatically processed, except for the page’s title.
  • Additional scripts in the body will be reprocessed, leading to rerunning scripts (and potential re-registration of event handlers).

Following the previous best practices of placing scripts just before the <body> tag ends, these drawbacks become apparent. Also, when working with ASP.NET Core anti-forgery tokens, as currently, the HTMX.NET library adds a global token to the <head> tag to be used generally by POST requests. Finally, for your information, it’s best practice to use <form> elements, as ASP.NET Core gives forms a new anti-forgery token to use on submissions.

The goal is to update anti-forgery tokens and stop event handlers from being re-registered. So how did I accomplish that?

Scripts Within The Body Tag

Since HTMX boosts only replace the contents of the <body> tag, not the <body> tag itself, I can add additional metadata to the DOM element that survives while the page session is still active. For example, if the script executes at the end of the <body> tag, we can wrap all scripts with the following check.

if (!document.body.attributes.__htmx_antiforgery) {
    // register HTMX event handlers here
    document.body.attributes.__htmx_antiforgery = true;
}

That solves the multiple registrations of event handlers issue, and now we can handle boosts. In the latest release of HTMX (1.9.2), we have more metadata to determine if a response resulted from a boosted request. So after a response is loaded but yet to be processed, we can glean the latest anti-forgery token from the <head> element of the response.

document.addEventListener("htmx:afterOnLoad", evt => {
    if (evt.detail.boosted) {
        const parser = new DOMParser();
        const html = parser.parseFromString(evt.detail.xhr.responseText, 'text/html');
        const selector = 'meta[name=htmx-config]';
        const config = html.querySelector(selector);
        if (config) {
            const current = document.querySelector(selector);
            // only change the anti-forgery token
            const key = 'antiForgery';
            htmx.config[key] = JSON.parse(config.attributes['content'].value)[key];
            // update DOM, probably not necessary, but for sanity's sake
            current.replaceWith(config);
        }
    }
});

This JavaScript event handler will keep our anti-forgery tokens up to date on each full-page boost. Neat!

While this approach works in the <body> tag, I think there’s a better way.

Add Script to Head and Defer

The JavaScript I’ve written for HTMX.NET works excellently, but we can use the registration differently to avoid some of the abovementioned issues.

By changing how the client can access the script, we can take advantage of some newer concepts in resource management. The first step is to register an endpoint that can serve our initial script.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace Htmx.TagHelpers;

public static class HtmxAntiforgeryScriptEndpoints
{
    /// <summary>
    /// The path to the anti-forgery script that is used from HTML
    /// </summary>
    public static string Path { get; private set; } = "_htmx/antiforgery.js";
    
    /// <summary>
    /// Register an endpoint that responds with the HTMX anti-forgery script.<br/>
    /// IMPORTANT: Remember to add the following script tag to your _Layout.cshtml or Razor view:
    /// <![CDATA[
    /// <script src="@HtmxAntiforgeryScriptEndpoints.Path" defer></script>
    /// ]]>
    /// </summary>
    /// <param name="builder">Endpoint builder</param>
    /// <param name="path">The path to the anti-forgery script</param>
    /// <returns>The registered endpoint (Use <seealso cref="Path"/> to reference endpoint)</returns>
    public static IEndpointConventionBuilder MapHtmxAntiforgeryScript(
        this IEndpointRouteBuilder builder,
        string? path = null)
    {
        // set Path globally for access
        Path = path ?? Path;
        
        return builder.MapGet(Path, async ctx =>
        {
            ctx.Response.ContentType = "text/javascript";
            await ctx.Response.WriteAsync(HtmxSnippets.AntiforgeryJavaScript);
        });
    }
}

Now we can register our endpoint with our ASP.NET Core application.

app.MapHtmxAntiforgeryScript();

The added benefit here is we can now add response caching and other endpoint filters to alter how this script is delivered.

Finally, with the keyword’ defer’, we can add the script to our _Layout.cshtml file in the <head> element.

<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta
        name="htmx-config"
        historyCacheSize="20"
        indicatorClass="htmx-indicator"
        includeAspNetAntiforgeryToken="true"/>
    <title>@ViewData["Title"] - Htmx.Sample</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
    <script src="~/lib/jquery/dist/jquery.min.js" defer></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" defer></script>
    <script src="https://unpkg.com/htmx.org@@1.9.2" defer></script>
    <script src="@HtmxAntiforgeryScriptEndpoints.Path" defer></script>
</head>

The defer keyword will download the script parallel to parsing the page and only execute it after the client has finished parsing. The keyword keeps our scripts from blocking the critical rendering path.

While our script contains the if block, it is unnecessary now since HTMX boosts will not reload any of the <head> elements. Neat!

Conclusion

HTMX has many great features out-of-the-box, but it’s still necessary to fuse the elements of HTMX to your technology stack of choice. In my case, and likely yours, that’s ASP.NET Core. So you should expect this update in the current feature set of HTMX.NET. If you use HTMX.NET, please let me know what you think and where I can improve it.

As always, thanks for reading and sharing my posts. Cheers :)