htmx icon indicating copy to clipboard operation
htmx copied to clipboard

loading history from server with authentication

Open jbockle opened this issue 1 year ago • 4 comments

using events htmx:confirm and htmx:configRequest, I'm adding an authorization header to the request and is working well.

However, when used with hx-push-url and hx-history="false", I noticed the XMLHttpRequest history restoration does not utilize the confirm/configRequest behavior, resulting in missing authorization header.

https://github.com/bigskysoftware/htmx/blob/7415f395b2c31d2f5f76597d425c9fbf403c9022/src/htmx.js#L3176-L3204

Is there another way to provide this authorization header in this scenario? If not, would you be open to a contribution to handle this?

jbockle avatar Nov 22 '24 19:11 jbockle

Hey, would binding a listener to the existing htmx:historyCacheMiss event solve your usecase here? As it provides a reference to the XMLHTTPRequest in its details (see xhr property in the snippet you linked), I would expect you to be able to add the headers from there. Could totally be wrong though, I never used this one. Hope this helps!

Telroshan avatar Nov 23 '24 07:11 Telroshan

thanks for the suggestion - would that result in a race condition as the event listener may be called after the request is sent?

jbockle avatar Nov 24 '24 02:11 jbockle

@jbockle events fired by htmx run synchronously, so your listener callback will complete before htmx proceeds with its next line. (Unless you explicitly declare an async listener of course, that htmx unfortunately doesn't support for now anyway)

Telroshan avatar Nov 24 '24 08:11 Telroshan

Hopefully That callback works well. Just a note that ideally when using htmx you should prefer to use cookies for authentication instead of an authorization header. Normally in htmx the requests are not cross origin and here cookies are the ideal authentication method and have many advantages in ease of use and security compared to authorization headers. Authorization headers are normally used for accessing data api's from remote origins where cookies are not as easy to setup and use.

With htmx cookie based auth just works with no additional htmx changes required. Good security guide to understand how to use cookies well with htmx is https://htmx.org/essays/web-security-basics-with-htmx/

The loadHistoryFromServer() function you mention hitting is really almost equivalent to a full page reload when the history is missed even though it is handled by htmx in some situations. One issue is that all full page reloads of any url will not include the auth header so the user will be forced back to your login screen most likely. So while you can update the url with hx-push-url and maybe get it to handle sending auth header on some back actions using htmx:historyCacheMiss event listener you will not be able to handle the user doing a manual refresh without a lot of extra complexity. This is where moving to auth cookies can make things simpler.

MichaelWest22 avatar Nov 24 '24 10:11 MichaelWest22

This is related to #2486.

Even though we receive xhr in htmx:historyCacheMiss event, we cannot set headers using xhr.setRequestHeader() because htmx:historyCacheMiss is triggered before xhr.open()

function loadHistoryFromServer(path) {
    const request = new XMLHttpRequest()
    const details = { path, xhr: request }
    
    // --  event is triggered before xhr.open --
    triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
    request.open('GET', path, true)
    // --  --
    
    request.setRequestHeader('HX-Request', 'true')

If htmx:historyCacheMiss event triggered after xhr.open() something like below, then we can set headers in htmx:historyCacheMiss.

function loadHistoryFromServer(path) {
    const request = new XMLHttpRequest()
    const details = { path, xhr: request }
    
    // -- changes --
    request.open('GET', path, true)
    triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
    // -- changes --
    
    request.setRequestHeader('HX-Request', 'true')

This will be a small change and will not break anything.

/cc @1cg

manoharank avatar Apr 02 '25 09:04 manoharank

Workaround to set custom headers in history navigation

document.body.addEventListener('htmx:historyCacheMiss', (event) => {
    const xhr = event.detail.xhr;
    xhr.addEventListener("readystatechange", function () {
        if (xhr.readyState === XMLHttpRequest.OPENED) {
            xhr.setRequestHeader("HEADER_NAME", "HEADER_VALUE");
        }
    });
});

manoharank avatar Apr 14 '25 12:04 manoharank

yeah interesting workaround!

I think it would be great to move the trigger event right to the bottom just before it calls .send so then anyone consuming this event can easily change anything they need. And at the same time change it so the returning false with event.preventDefault() in the listener will stop the send happening. You can then modify the xhr with changes or do another .open which aborts it and starts it again maybe with an updated path if needed or you can preventDefault and handle the history restore in your own way. Also with the trigger event after xhr.onload set you can replace or wrap onload to perform your own response handling code if needed. I don't think HTMX will be making any more breaking changes or adding new major feature changes but supporting better extensibility via events or extensions or plugin points would still be great.

MichaelWest22 avatar Apr 15 '25 00:04 MichaelWest22

History funciton now re-written in 2.0.5 and it now supports more events and easier override as @manoharank suggested.

MichaelWest22 avatar Jun 23 '25 00:06 MichaelWest22