HTMX stops the browser from redirecting `302` responses
Closed this issue · 12 comments
I tried with the htmx:afterRequest
event listener to capture the 302 responses and handle them myself in JavaScript but the 302 responses are not triggering a htmx:afterRequest
. Instead HTMX decides that it is a good idea to just send an AJAX request to the redirected location. If the location returns an entire HTML page then HTMX decides it is a good idea to swap the elements, instead of doing a proper page reload.
I need the browser to follow 302 responses as it normally would and HTMX is encumbering this process.
For example, if I were to use one of the HX-Redirect
headers, I also need to send a 200
response. This is deceiving.
For example, if the browser has not loaded HTMX JavaScript because the user is loading the page for the first time, getting a 200
response without the HTMX bundle will cause the browser to standstill and offer a blank page.
This is not great.
So instead of simply sending a status: 302 location: URI
from the server, if I want the browser to actually redirect, then I need to send all redirects as a status: 200
with htmx <script>
tag in the response body, and also a HX-Redirect
or HX-Refresh
header, on top of thinking about the swapping strategy. I just want to send a 302
! and I might want it to work for people that don't have JavaScript enabled as well.
I tried adding HX-Redirect
header to the 302
response, that would have been easy enough! Buuut HTMX does not look at that header in a 302
(even though it does look at location
to send an AJAX request).
I think allowing HTMX to look at HX-Redirect
/HX-Refersh
header in a 302
would be an acceptable fix for now.
Otherwise the other routes have to be aware of whether the user may or may not arrive by redirect and send the HX-Refresh
and similar, clumsy.
Tangentially related to: #230
If the server response is forcing a redirect using the 302 redirect status response code then it may also return an empty body
This is exactly it though
Hm so I guess for the work-around is to have two ways of redirecting the user...
If it is an interactive redirect, then I do an htmx-friendly redirect. Such as when a user clicks a button on a form:
{:status 200
:headers {"HX-Redirect" resource}})
If it is a non-interactive redirect, then I do a good old 302
. Such as when a user tries to access a route that is not available and the server should not return HTML nor JS to redirect the user:
{:status 302
:headers {"Location" resource}})
This seems to work well enough 🤔
Perhaps I don't need anything else to “fix” this.
The upside is that it makes it explicit whether a redirect is happening due to interactivity or due to another reason.
Yeah that's how I would go about this matter as well.
For example, if the browser has not loaded HTMX JavaScript because the user is loading the page for the first time, getting a 200 response without the HTMX bundle will cause the browser to standstill and offer a blank page.
In that case the request wouldn't come from a HTMX context do simply do a good ol' 302 redirect here indeed.
Htmx is doing partial requests, unlike standard forms for ex that are expected to bring you to a whole new other page on submit. In this context, I wouldn't expect htmx to do full page redirects as, in the situation where you're not targeting the body, then that initial request was only targeting a part of the document anyway.
Htmx provides everything you need to address this situation, but making htmx now fully redirect on 302 means we would have to add another feature to allow using the old behaviour, which doesn't sound ideal to me.
On the server side, you can rely on the HX-Request
header (that is set by htmx when sending its requests) to know whether this is an htmx request that you should redirect using the HX-Redirect
response header, or a htmx-less one that you can then redirect using a standard 30x redirect
If anybody else faces this issue I fixed it by redirecting conditionally after checking the "hx-request" request header, like this:
(defn redirect
"Redirect with htmx support"
[req next-url]
(if (get-in req [:headers "hx-request"])
{:status 200
:headers {"hx-redirect" next-url}}
{:status 302
:headers {"Location" next-url}}))
This is particularly useful where the redirect can trigger on any request (for example if a user's auth cookie expires).
If anybody else faces this issue I fixed it by redirecting conditionally after checking the "hx-request" request header, like this:
(defn redirect "Redirect with htmx support" [req next-url] (if (get-in req [:headers "hx-request"]) {:status 200 :headers {"hx-redirect" next-url}} {:status 302 :headers {"Location" next-url}}))
This is particularly useful where the redirect can trigger on any request (for example if a user's auth cookie expires).
Weird code, what language is that, how would one use that?
But what I did was this, when using Django and django_middleware_global_request
class HtmxResponsePermanentRedirect(HttpResponsePermanentRedirect):
def __init__(self, redirect_to, *args, **kwargs):
super().__init__(redirect_to, *args, **kwargs)
request: HttpRequest = get_request()
if request.headers.get("HX-Request"):
self["HX-Redirect"] = self["Location"]
self.status_code = 200
So that when there's a redirect, then it checks if there is the htmlx header, and will return 200 in that case. But I still dont' like this pattern, as other places not using this redirect class will still just return 302, which htmx can't work with nicely. This is good to not break all tests and such, as 302 is still a valid response if there's no htmx...
I've tried various method, but ideally I'd not want to have to add push-url to every form/htmx usage... very bad...
I would highly expect that the response header HX-Redirect
have the browser respect the redirect... I've tried changing target to "body"
but still whenever there's a 302 redirect htmx just removes the content it should swap... very sad.
def htmlx_redirect_middleware(get_response):
def middleware(request: HttpRequest) -> HttpResponse:
response: HttpResponse = get_response(request)
if response.status_code in [301, 302] and request.headers.get("HX-Request"):
response.headers["HX-Redirect"] = response.headers.get("HX-Redirect", response.headers.get("Location"))
response.status_code = 200
return response
return middleware
Update: as auth and other places use other view/middleware/etc use the normal redirect shortcuts and views etc, I wrote this middle ware, which switches normal redirect format (30x code) into htmx redirects (200 w/ hx-redirect header),
Per specification you cannot read headers via xhr when a response indicates a redirect. I don’t know about fetch and its handling of redirects, but I’m pretty sure it’s a requirement of the w3c.
I’m writing from mobile and cannot easily look up the specification (also I don’t want to :)) right now. But I’m 99% sure I remember it correctly.
We never see the 3xx response, that is handled internally by the browser. We only see the eventual 2xx, 4xx or 5xx responses.
So unfortunately we can't do much about this.
@1cg How about switching to fetch
with redirect: "manual"
? https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect
This works, but not nice;
htmx.defineExtension('redirectToResponseUrl', {
/*
Abuse `defineExtension` to redirect on a known 302 response.
Use in theme as `hx-ext="<name>"`
*/
transformResponse: function (text, xhr) {
globalThis.document.location = xhr.responseURL
return 'Redirecting...'
}
})
Then just
<button
hx-post="/article"
hx-vals='{"title": "New article", "content": "The story begins..."}'
hx-ext="redirectToResponseUrl"
hx-trigger="click"
>Create article</button>