Debugging Random HTTPS Redirects to `localhost` in Safari During Local Development

· 3 min read

While working on a local Express.js project, I ran into a strange problem: occasionally, clicking a link like /about would open https://localhost:3002/about instead of http://localhost:3002/about. Since nothing was running over TLS locally, Safari would display:

Safarfi displaying error page, cannot open https://localhost:3002/about

Inspecting the HTML confirmed there were no https:// URLs, just relative links. Yet sometimes they worked, sometimes they didn’t. Even more oddly, my server logs showed no incoming request when it failed. If I manually changed https:// to http:// in the address bar, the page loaded fine.


The Cause

This wasn’t a server bug, it was the browser.

Safari (and other browsers) can force HTTPS in certain situations, even for localhost:

  1. HSTS caching
    If a site ever sends a Strict-Transport-Security (HSTS) header, browsers remember it and will always try HTTPS for that host, even if you later serve it over HTTP. Once set, HSTS applies to all future requests until it expires or is manually cleared.

  2. HTTPS-Only / Auto-upgrade
    Some browsers offer an “upgrade to HTTPS” mode. If it’s on, any HTTP link may be rewritten to HTTPS automatically.

  3. Other scheme-forcers
    A <base href="https://...">, a service worker redirect, or cached state from another app using the same hostname/port.


Why It Happened in My Case

Looking at my app.js, I was using helmet for security headers:

const helmetOptions = {};
if (process.env.NODE_ENV === "production") {
  helmetOptions.contentSecurityPolicy = {
    /* ... */
  };
} else {
  helmetOptions.contentSecurityPolicy = false;
}
app.use(helmet(helmetOptions));

By default, helmet sends an HSTS header unless you explicitly disable it. So even in development, Safari may have cached HSTS for localhost, causing it to auto-upgrade requests.


The Fix

Here’s what I did:

1. Disable HSTS in development

if (process.env.NODE_ENV === "production") {
  helmetOptions.contentSecurityPolicy = {
    /* strict CSP */
  };
} else {
  helmetOptions.contentSecurityPolicy = false;
  helmetOptions.hsts = false; // Prevents HTTPS auto-upgrade in dev
}
app.use(helmet(helmetOptions));

2. Clear HTTPS state for localhost in Safari

Safari → SettingsPrivacyManage Website Data…
Search for localhost and 127.0.0.1 → Remove.

3. Check for other causes

  • Make sure there’s no <base href="https://localhost:3002/"> in your HTML templates.
  • Disable “Require HTTPS” or “Automatically upgrade to HTTPS” in Safari Advanced settings while debugging.
  • In the DevTools console, unregister any service workers:
    navigator.serviceWorker
      .getRegistrations()
      .then(regs => regs.forEach(r => r.unregister()));
    
  • Disable cache in DevTools during testing.

The Result

After:

  • Disabling HSTS in dev via helmetOptions.hsts = false
  • Clearing Safari’s website data for localhost

… the random HTTPS redirects disappeared completely.


Tip: If you ever run a local dev server behind a reverse proxy (like Caddy) that uses HTTPS for localhost, browsers may cache that as HSTS too. In that case, either use a different dev hostname (e.g., http://dev.local:3002) or clear the cached state before switching back to plain HTTP.

← Back to all posts