bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
37k stars 1.25k forks source link

hx-boost or hx-push-url : reloading browser session does not reload full page if html fragment returned #497

Open danjac opened 3 years ago

danjac commented 3 years ago

Note: I'm not sure if this is necessarily fixable with htmx due to the way the browsers behave, but it might be something that needs to be highlighted in the documentation.

Use case: I'm using hx-boost or hx-get+hx-push-url to return an HTML fragment rather than a whole page. The stripped down whole page looks like this:

<html>
<head>
...styling etc tags...
</head>
<body hx-boost="true" hx-select="#content" hx-swap="outerHTML">
..header content here
<div id="content"></div>
....footer content here
</body>
</html>

A typical URL returns an HTML fragment. It uses Django templates, but the result is that if the request has the HX-Header, the result looks something like this:


<div id="content">
   my page
</div>

and if the request does not have the HX-header it returns the above content inside the full page - for example if reloading the page manually or opening the URL in the browser instead of navigating to the page inside the site. That ensures that we have very efficient page swaps, only returning the content we need.

Unfortunately there appears to be a problem when the browser preferences are set to restore the last tabs from cache when a browser is closed and re-opened. What appears to happen is that the browser just pulls the last GET response for that specific URL in the tab, whether it is a xhr response or not. The result is that if the last request was an htmx request returning a

fragment, then that fragment is restored instead of the full page, resulting in a broken page (as all surrounding CSS, JS, header/footer etc will be missing).

Setting htmx settings such as historyCacheSize or refreshOnHistoryMiss make no difference. Setting cache-control to no-cache in either the meta tags or HTTP responses make no difference. The browser appears to ignore all cache settings.

The only two workarounds I can find are:

1) Avoid returning html fragments in hx-boost and hx-push-url calls and return the entire body, using hx-select if you need to pull out a specific fragment from the response. This is workable but obviously far less efficient than being able to return html fragments, as you have to build parts of the page in the server you never need.

2) Add the following script to the start of your html fragments:

<script>
if (!window.doNotReload) {
   location.reload();
}
</script>

and somewhere in your initial HTML page:


<script>
window.doNotReload = true;
</script>

This ensures that if the fragment is loaded from cache it will force a reload. It's a bit janky and may have other issues, but at least the user is not forced to manually reload.

This behaviour has been observed in Firefox and Chrome and in multiple devices. To replicate ensure you have browser settings to something like "reload last session" instead of some home page or new tab, navigate to a page with hx-boost or hx-push-url, close the browser and re-open.

Again, this might not be something necessarily fixable with htmx.That said, I did try this approach when returning the HTML fragment:

<html>
<head>
  <meta name="htmx-config"
        content='{"historyCacheSize": 0, "refreshOnHistoryMiss": true}'>
  <meta name="Cache-Control"
        content="no-cache">
 <script src="https://unpkg.com/htmx.org@1.3.3/dist/htmx.min.js" defer="" integrity="sha384-QrlPmoLqMVfnV4lzjmvamY0Sv/Am8ca1W7veO++Sp6PiIGixqkD+0xZ955Nc03qO" crossorigin="anonymous"></script>
</head>
<body>
<div id="content">
   my page
</div>
</body>
</html>

Thus returning a truncated page but with required meta tags.

So in theory, refreshOnHistoryMiss should force a reload, but this does not happen.

danjac commented 3 years ago

Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:


class CacheControlMiddleware:
    def __call__(self, request):
        response = self.get_response(request)
        if "HX-Request" in request.headers:
            response["Cache-Control"] = "no-store, max-age=0"
        return response

This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.

perplexErik commented 3 years ago

I am experiencing the same behaviour as described by @danjac with Google Chrome Version 90.0.4430.212 But i also noticed similar behaviour in another scenario: When navigating between a list and detailpage.

I have a newslisting page with some pagination below the listed items. The pagination links are wrapped like this: <div class="c-pagination__wrapper" hx-target="closest .js-partialcontainer" hx-swap="innerHTML" hx-boost="true" hx-push-url="true">[implementation omitted]</div>

reproduction steps:

  1. When i click a pagination hyperlink, a xhr request gets fired, and the listing on the page gets updated. All seems fine.
  2. Then i click a newsitem hyperlink and get redirected to a page containing the item details. (this is a normal redirect, no htmx involved).
  3. Now when i click the back button of the browser, it only displays the html fragment that was sent by the server in step 1.

In step 3 The browser Network tab shows 1 request: Request URL: https://domainname.eu/news?page=2 Request Method: GET Status Code: 200 (from disk cache) Referrer Policy: strict-origin-when-cross-origin

Hope this helps.

CaptainPaella commented 3 years ago

Update on the issue reported by perplexErik (which is my other GH account). After applying the "Cache-Control: no-store" header the browser started behaving better.

But wat weird is that de browser back button works correct when i click it once. But when i click the back button for a second time it does something weird. The URL gets updated, but the content stays the same.

Repro path using pagination example <div class="c-pagination__wrapper" hx-target="closest .js-partialcontainer" hx-swap="innerHTML" hx-boost="true" hx-push-url="true"><a href="?page=1">1</a><a href="?page=2">2</a><a href="?page=3">3</a></div>

  1. Click page "1"
  2. Click page "2"
  3. Click page "3"
  4. Resultlist page 3 is shown --> Click a result item in the listing
  5. Detailpage of result item is shown --> click the browser back button
  6. Resultlist page 3 is shown --> click the browser back button again
  7. URL changed to "?page=2", but the resultlist of ?page=3 is shown.

Expected behaviour in step 7:

  • URL changed to "?page=2"
  • The resultlist of "?page=3" is shown.
gabrielmotaa commented 2 years ago

Hey guys! Looking through the docs I found this, under the History Support section:

When a user hits the back button, htmx will retrieve the old content from storage and swap it back into the target, simulating "going back" to the previous state. If the location is not found in the cache, htmx will make an ajax request to the given URL, with the header HX-History-Restore-Request set to true, and expects back the HTML needed for the entire page. Alternatively, if the htmx.config.refreshOnHistoryMiss config variable is set to true, it will issue a hard browser refresh.

The django-htmx extension recognizes the HX-History-Restore-Resquest using request.htmx.history_restore_request. Maybe using this logic when generating the views will help:

def myview(request):
    template_name = 'myapp/mytemplate.html'
    if request.htmx and not request.htmx.history_restore_request:
        template_name = 'myapp/mytemplate_partial.html'
    ...
danjac commented 2 years ago

My solution was to just set

  <meta name="htmx-config"
        content='{"historyCacheSize": 0, "refreshOnHistoryMiss": false}'>

This also fixed an issue with client DOM changes (e.g. modals opened using AlpineJS) from being triggered on cache reload.

jamesf408 commented 2 years ago

Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:


class CacheControlMiddleware:
    def __call__(self, request):
        response = self.get_response(request)
        if "HX-Request" in request.headers:
            response["Cache-Control"] = "no-store, max-age=0"
        return response

This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.

A variation of this solution worked for me. I use Wagtail CMS and HTMX on the Homepage to deal with a lot of different filters. Once you go off that page from the results delivered via HTMX and press the back button in a Chromium browser, only the HTMX part of the page is delivered, leaving the site header and footer and body content blank.

This solution was not added to a middleware, but I suppose it still can be. I just needed HTMX to act nicely on my home page, so this went into the home page model

Here is my solution:

        if "HX-Request" in request.headers:
            headers = {"Cache-Control": "no-store, max-age=0"}
        else:
            headers = {}

        return TemplateResponse(request, html_template, context, headers=headers)
RmaxTwice commented 1 year ago

Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:


class CacheControlMiddleware:
    def __call__(self, request):
        response = self.get_response(request)
        if "HX-Request" in request.headers:
            response["Cache-Control"] = "no-store, max-age=0"
        return response

This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.

This actually was the most straightforward solution when using django and django-htmx. Very much appreciated!

jonsmith1982 commented 1 year ago

Would setting the Vary header not be a suitable fix for this?

foggy54 commented 8 months ago

My solution was to just set

  <meta name="htmx-config"
        content='{"historyCacheSize": 0, "refreshOnHistoryMiss": false}'>

This also fixed an issue with client DOM changes (e.g. modals opened using AlpineJS) from being triggered on cache reload.

The problem was solved for me by following settings:

  <meta name="htmx-config"
        content='{"historyCacheSize": 0, "refreshOnHistoryMiss": true}'>

According to documentation: "refreshOnHistoryMiss defaults to false, if set to true htmx will issue a full page refresh on history misses rather than use an AJAX request"

brablc commented 6 months ago

Reflecting the discussion on https://www.reddit.com/r/htmx/comments/1aw5iqa/page_fragment_after_page_tombstones/ the proper backend solution seem to be setting response header: Vary: HX-Request (see https://htmx.org/docs/#caching).