Cwicseolfren Writ

Making Caddy Sites Accessible to HTTP-Only Clients

Published November 6, 2026

The web server Caddy is known for its easy automatic HTTPS support. By default, Caddy will only serve a site over HTTPS, automatically redirecting HTTP connections. One downside of this, however, is that a Caddy site under this default configuration cannot be accessed by clients that do not support HTTPS, such as some web browsers on vintage computers. In my opinion, unless your site supports login or otherwise carries privileged information, it should be accessible over HTTP. There's little reason for a public static site like mine to be HTTPS-only.[1]

It is quite easy to modify Caddy's default behavior to allow HTTP-only clients to access your site while still automatically upgrading to HTTPS for any client that requests it. I'll use my site as an example.

hg.hunicke.net {
	root * /srv/sites/hg/public
	file_server
}

Adding my site's domain with http:// prefixed alongside the bare domain disables Caddy's default automatic-upgrade behavior, additionally serves the site over HTTP, and applies the settings below to the site when served over either HTTP or HTTPS (DRY).

hg.hunicke.net, http://hg.hunicke.net {
	root * /srv/sites/hg/public
	file_server
}

The site is now accessible over HTTP. It also may take visitors using browsers which support HTTPS to the HTTP version of the site if they use a URL that does not have the https: prefix, since automatic-upgrade is now disabled. This is undesirable behavior because (1) it is insecure unnecessarily, and (2) visitors may get a scary "Not Secure" message from their browser.

To address this, we can configure the site to automatically upgrade to HTTPS only if the client requests it. I'll define a snippet for conditional upgrade so I can reuse it on any site in my config.

(conditional-upgrade) {
	@upgrade {
		protocol http
		header Upgrade-Insecure-Requests 1
	}
	redir @upgrade https://{host}{uri} 307
	header +Vary Upgrade-Insecure-Requests
}

The @upgrade matcher matches for connections that are over HTTP and have the Upgrade-Insecure-Requests: 1 header. This targets HTTPS-capable clients which have connected to the site over HTTP and have indicated a preference for upgrading insecure requests to HTTPS. The redir directive redirects those clients to the HTTPS site. The Vary header tells HTTP caches that they must consider the value of the Upgrade-Insecure-Requests header when deciding if a cached response can be reused (because redirect to HTTPS only happens if the value is set to 1).

The conditional-upgrade snippet can be added to any site in my config with import conditional-upgrade. So now my Caddy config will look like this:

(conditional-upgrade) {
	@upgrade {
		protocol http
		header Upgrade-Insecure-Requests 1
	}
	redir @upgrade https://{host}{uri} 307
	header +Vary Upgrade-Insecure-Requests
}

hg.hunicke.net, http://hg.hunicke.net {
	import conditional-upgrade
	root * /srv/sites/hg/public
	file_server
}

With that, we're just about ready to roll, but there's one more issue I will address. If you have any logic in your site config that explicitly includes the scheme, you should replace it with the {scheme} placeholder,[2] e.g.:

hunicke.com, http://hunicke.com {
	import conditional-upgrade
	redir {scheme}://hg.hunicke.net/ 301
}

In this example, hunicke.com will now automatically redirect to either the HTTP or HTTPS version of hg.hunicke.net, depending on what scheme the current connection is on.[3] If I simply used redir https://hg.hunicke.net/ 301, that would try to redirect our HTTP-only clients to an HTTPS URL.

With that out of the way, I now have HTTP-only client support while automatically upgrading compatible clients to HTTPS on my Caddy site. Let's test it:


  1. There are actually many benefits to using HTTPS, even for static sites. As well as hiding private information, HTTPS protects the integrity of publicly available content against modification in transit. An attacker (or a sleazy internet service provider) could change the content of your pages or inject malicious javascript—or ads (but I repeat myself)—via a man-in-the-middle attack. This is why I configure my site to still upgrade to HTTPS for clients that request it. ↩︎

  2. You could also leave off the scheme, e.g. redir //hg.hunicke.net/ 301 (I think; I didn't test it), but you probably shouldn't do that. For instance, RFC 2616 (1999) states that the field value for Location is an absolute URI (i.e. full address including the scheme). This was relaxed later on—see RFC 7231 (2014)—but the whole point with this exercise is to support older browsers. ↩︎

  3. Eagle-eyed readers will note that if a client connects to hunicke.com over HTTP and requests to upgrade the connection, there will actually be two redirects under my current setup: first from http://hunicke.com to https://hunicke.com, then from https://hunicke.com to https://hg.hunicke.net. Ideally it should be one redirect http://hunicke.com → https://hg.hunicke.net, but I'm OK with it as-is. ↩︎