Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTMX stops the browser from redirecting 302 responses #2052

Closed
Sleepful opened this issue Nov 28, 2023 · 12 comments
Closed

HTMX stops the browser from redirecting 302 responses #2052

Sleepful opened this issue Nov 28, 2023 · 12 comments

Comments

@Sleepful
Copy link
Contributor

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.

@Sleepful
Copy link
Contributor Author

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.

@Sleepful
Copy link
Contributor Author

Sleepful commented Nov 28, 2023

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

@Sleepful
Copy link
Contributor Author

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}})

@Sleepful
Copy link
Contributor Author

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.

@Telroshan
Copy link
Collaborator

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

@amon22
Copy link

amon22 commented Mar 3, 2024

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).

@TonisPiip
Copy link

TonisPiip commented Mar 5, 2024

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.

@TonisPiip
Copy link

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),

@andryyy
Copy link

andryyy commented Mar 5, 2024

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.

@1cg
Copy link
Contributor

1cg commented Mar 5, 2024

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 1cg closed this as completed Mar 5, 2024
@yardenshoham
Copy link

@1cg How about switching to fetch with redirect: "manual"? https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect

@gnimmelf
Copy link

gnimmelf commented Apr 20, 2024

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants