Skip to content

Instantly share code, notes, and snippets.

@realgenekim
Created May 31, 2026 17:56
Show Gist options
  • Select an option

  • Save realgenekim/0ee6d6dcf761a43d36b17b4d272910ab to your computer and use it in GitHub Desktop.

Select an option

Save realgenekim/0ee6d6dcf761a43d36b17b4d272910ab to your computer and use it in GitHub Desktop.
Datastar + HTTP Basic Auth: making fetch()-based @get/@post work behind credentialed URLs (a fix found while migrating off HTMX)

Thank you so much for writing DataStar. It has been life-changing in reducing JavaScript client-side issues. Here is a fix for something I stumbled upon as I was migrating away from HTMX.

The remainder of this post is written by Claude Code.


TL;DR

fetch() refuses to use a URL that contains credentials (https://user:pass@host/…). Because Datastar's @get/@post are built on fetch(), opening a page with credentials in the URL — e.g. behind HTTP Basic Auth — makes every Datastar request fail. HTMX never hit this because it uses XMLHttpRequest, which tolerates credentialed URLs. The small script below restores parity, so Datastar "just works" behind Basic Auth.

The problem

When the page URL carries credentials, two browser behaviors break Datastar:

  1. new Request(url) throws before fetch() is ever called:
    TypeError: Failed to construct 'Request': Request cannot be constructed
    from a URL that includes credentials.
    
  2. Even past that, fetch() itself rejects the credentialed URL.

The Fetch standard disallows credentials in the request URL. XMLHttpRequest (HTMX's transport) does not — which is why the same app worked under HTMX and stops working under Datastar.

Why patching fetch alone isn't enough

The browser constructs the Request object before calling fetch(input, init). So a wrapper around window.fetch never even runs — the TypeError is thrown at Request construction. You have to patch the Request constructor too.

The fix

datastar-auth-fix.js (in this gist) patches both window.Request (via a Proxy, so instanceof and the prototype chain stay intact) and window.fetch, stripping the credentials from the URL before the native code sees them. It is a no-op unless the page URL actually contains user:pass@, and the browser keeps sending the cached Authorization header after the first auth challenge — so requests stay authenticated.

Load it before the Datastar module:

<script src="/js/datastar-auth-fix.js"></script>
<script type="module" src="/datastar.js"></script>

Could this live in Datastar itself?

Maybe worth considering: stripping credentials from the URL inside Datastar's own fetch path (before constructing the Request) would make @get/@post work behind Basic Auth out of the box, matching HTMX. The credentials can't travel in a fetch URL anyway, and the browser attaches the Authorization header automatically. Sharing the standalone fix here in case it's useful to others.

// datastar-auth-fix.js
// ----------------------------------------------------------------------------
// Make Datastar's fetch()-based @get/@post work when the page is opened with
// credentials in the URL (https://user:pass@host/...), e.g. behind HTTP Basic
// Auth.
//
// The Fetch standard forbids credentials in a request URL. The browser throws:
// TypeError: Failed to construct 'Request': Request cannot be constructed
// from a URL that includes credentials.
// ...and the Request is constructed BEFORE fetch() runs, so wrapping only fetch
// is too late. This patches BOTH:
// * window.Request (via Proxy, so `instanceof` and the prototype chain stay intact)
// * window.fetch (defense in depth)
// stripping credentials from the URL before the native code sees them.
//
// No-op unless the page URL actually contains "user:pass@". The browser caches
// the Basic Auth credentials after the first challenge and keeps sending the
// Authorization header, so stripping them from request URLs is safe.
//
// Load BEFORE the Datastar module:
// <script src="/js/datastar-auth-fix.js"></script>
// <script type="module" src="/datastar.js"></script>
// ----------------------------------------------------------------------------
(function () {
'use strict';
// Strip username:password from a URL string, resolving relative URLs against
// the current document. Returns the cleaned string (or the original on parse
// failure, e.g. opaque inputs we shouldn't touch).
function stripCreds(urlString) {
try {
var u = new URL(urlString, window.location.href);
if (u.username || u.password) {
u.username = '';
u.password = '';
return u.toString();
}
} catch (e) {
/* not a parseable URL -- leave untouched */
}
return urlString;
}
// Only patch when the page actually carries credentials. In the normal case
// fetch works fine and we stay out of the way entirely.
if (window.location.href.indexOf('@') === -1) return;
// Best-effort: scrub credentials from the visible URL so document.baseURI and
// future relative-URL resolution never reintroduce them.
try {
var clean = stripCreds(window.location.href);
if (clean !== window.location.href) {
window.history.replaceState(window.history.state, '', clean);
}
} catch (e) {
/* replaceState may be blocked; the Request/fetch patches below are the real fix */
}
// 1. Patch the Request constructor (the throw site).
if (typeof window.Request === 'function') {
var NativeRequest = window.Request;
window.Request = new Proxy(NativeRequest, {
construct: function (target, args) {
if (typeof args[0] === 'string') {
args[0] = stripCreds(args[0]);
} else if (args[0] && typeof args[0].url === 'string') {
// A Request was passed as input -- rebuild only if it carries creds.
var cleaned = stripCreds(args[0].url);
if (cleaned !== args[0].url) {
args[0] = new NativeRequest(cleaned, args[0]);
}
}
return Reflect.construct(target, args, this.newTarget || target);
}
});
}
// 2. Patch fetch (defense in depth + ensure the cached auth header is sent).
var nativeFetch = window.fetch;
window.fetch = function (input, init) {
init = init || {};
if (!('credentials' in init)) {
init.credentials = 'same-origin';
}
if (typeof input === 'string') {
input = stripCreds(input);
} else if (input && typeof input.url === 'string') {
var cleaned = stripCreds(input.url);
if (cleaned !== input.url) {
input = new Request(cleaned, input); // uses patched Request above
}
}
return nativeFetch.call(this, input, init);
};
})();
@yawaramin

yawaramin commented Jun 1, 2026

Copy link
Copy Markdown

Wouldn't it be possible to send the credentials in the Authorization header directly instead of in the URL? Then no need to patch anything, right? Eg @get('url', {headers: {Authorization: 'Basic ...'}})

EDIT: htmx 4 is built on Fetch API, so it would have the same issue if you had tried to upgrade.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment