HTMX Middleware and Headers

djxi ships an optional but recommended middleware that attaches HTMX header helpers to every request and response.

Enabling the middleware

Add DjxiHeadersMiddleware to MIDDLEWARE, after Django’s session and message middleware:

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "djxi.middleware.DjxiHeadersMiddleware",
    # ...
]

Reading request headers (request.htmx)

Once the middleware is active, every HttpRequest has a request.htmx attribute. Its class depends on DX_HTMX_VERSION:

  • DX_HTMX_VERSION = "4"Htmx4RequestHeaders

  • DX_HTMX_VERSION = "2"Htmx2RequestHeaders

The object evaluates as a boolean — True when the request was made by HTMX (i.e. the HX-Request: true header is present):

@dx_get("items/")
def items(self, request):
    if request.htmx:
        # Partial render for HTMX swap
        return self.render_section(request, "item-list", context)
    # Full page render for direct navigation
    return render(request, "items.html", context)

HTMX 4 request header properties:

Property

Description

is_hx_request

True when HX-Request: true

request_type

Value of HX-Request-Type (None if absent)

current_url

Browser URL at the time of the request (HX-Current-URL)

source

CSS selector of the element that triggered the request (HX-Source)

target

CSS selector of the target element (HX-Target)

is_boosted

True when the request was made via hx-boost

is_history_nav

True for history-restore requests

last_event_id

Value of Last-Event-ID (SSE resumption)

HTMX 2 request header properties:

is_hx_request, is_boosted, is_history_nav, current_url, prompt, target, trigger_id (alias trigger), trigger_name.

Setting response headers (response.htmx)

After calling render_section() (or any other response builder), the returned HttpResponse has a response.htmx helper attached.

All setter methods return self so they can be chained:

@dx_put("item/<int:pk>/move", name="item-move")
def move(self, request, pk):
    item = Item.objects.get(pk=pk)
    context = {"item": item}
    response = self.render_section(request, "item-row", context)
    # Chain multiple header mutations in a single expression
    response.htmx.set_trigger("itemMoved").set_retarget(f"#row-{pk}").set_reswap("outerHTML")
    return response

Available setters (HTMX 4 and 2):

Method

Effect

set_trigger(payload)

HX-Trigger — fire a client-side event

set_location(json_payload)

HX-Location — client-side AJAX redirect

set_redirect(url)

HX-Redirect — full browser redirect

set_refresh()

HX-Refresh: true — full page refresh

set_retarget(selector)

HX-Retarget — override swap target

set_reswap(swap_spec)

HX-Reswap — override swap method

set_reselect(selector)

HX-Reselect — select a subset of the response to swap

set_replace_url(url)

HX-Replace-Url — silently update the browser URL

set_push_url(url)

HX-Push-Url — push a new entry onto the browser history

HTMX 2 also provides set_trigger_after_settle(payload) and set_trigger_after_swap(payload).

HTTP method override

DjxiHeadersMiddleware supports two mechanisms for overriding the HTTP method, useful for PUT/PATCH/DELETE requests:

  1. Header (HTMX / JavaScript): X-HTTP-Method-Override: DELETE

  2. POST field (plain HTML forms, no JavaScript required): <input type="hidden" name="_method" value="DELETE">

The header takes precedence when both are present. The field is only checked on POST requests.

<!-- Plain HTML form that issues a DELETE without JavaScript -->
<form method="post" action="{% url 'todo:delete' item.pk %}">
    {% csrf_token %}
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete</button>
</form>

Without middleware

If you do not enable the middleware, request.htmx will not be attached automatically. You can still create header helpers manually:

from djxi.headers import Htmx4RequestHeaders, Htmx4ResponseHeaders

def my_view(request):
    htmx = Htmx4RequestHeaders(request)
    if htmx.is_hx_request:
        response = HttpResponse("<p>partial</p>")
        Htmx4ResponseHeaders(response).set_trigger("done")
        return response
    return render(request, "full.html")