Doubts about how to implement 103 Early Hints

Hi, first of all thank you for the amazing work on the Caddy server, I have just started using it recently and I’m loving it.

1. The problem I’m having:

I’m trying to add Early Hints to the new site I’m building, I’m using vite and react-router v7 and I have a script that checks the manifest generated by vite for each route and adds the Link header with the assets I’d like to fetch with the 103 Early Hints. I have tried adding the respond 103 to my Caddyfile pretty much everywhere, and I also tried moving the script from my entry.server.jsx directly to a early-hints.caddy file that I was then importing inside my Caddyfile but nothing seemed to work.

The documentation doesn’t provide enough information on how to set this up so I don’t even know what I’m doing wrong here, some guidance would be very appreciated.

2. Error messages and/or full log output:

There’s no error messages, I just can’t seem to get the 103 requests to work.

3. Caddy version:

caddy --version
v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=

4. How I installed and ran Caddy:

I have installed Caddy via xcaddy (v0.4.5_linux_amd64) with the following command:

xcaddy build --with github.com/darkweak/souin/plugins/caddy --with github.com/ueffel/caddy-brotli --with github.com/darkweak/storages/redis/caddy

a. System environment:

Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-87-generic x86_64)

Running two Docker containers inside, one for WordPress and the other for the React app.

b. Command:

caddy start --config /etc/caddy/Caddyfile

c. Service/unit/compose file:

d. My complete Caddy config:

# Global options
{
	# Email for Let's Encrypt
	email dev.team@carney.co

	# Souin Cache Configuration
	order cache before rewrite

	cache {
		# Cache storage backend - Redis for production
		redis {
			url localhost:6379
		}

		ttl 1h
		stale 1h

		# Cache key configuration
		key {
			disable_body
			disable_host
			disable_method
		}

		log_level INFO

		allowed_http_verbs GET HEAD

		# Cache management API
		api {
			basepath /api/cache
			souin
		}
	}
}

# WordPress Admin Server (wpadmin.carney.co)
wpadmin.carney.co {
	# Increase upload size limit
	request_body {
		max_size 5120MB
	}

	# Redirect root to /wp-admin
	@root path /
	redir @root /wp-admin 301

	# Proxy all requests to WordPress
	reverse_proxy localhost:8080
}

# Main Site (beta.carney.co)
beta.carney.co {
	# Enable compression
	encode br gzip
	respond 103

	# Static assets - serve from local filesystem
	@static_assets {
		path_regexp static ^/(assets|lottie)/.*\.(js|css|json)$
	}

	handle @static_assets {
		root * /var/www/static-assets

		# Add caching headers
		header Cache-Control "public, max-age=31536000"
		header Vary "Accept-Encoding"

		respond 103
		file_server {
			precompressed br gzip
		}
	}

	# Daily carnage routes - new posts daily
	handle /daily-carnage* {
		# Cache for 24 hours (refreshes daily with new content)
		cache {
			ttl 24h
			stale 48h # Serve stale for 48h if backend is down
		}

		respond 103
		reverse_proxy localhost:3060
	}

	# Cache eviction endpoint for Apollo
	handle /api/cache/evict {
		reverse_proxy localhost:3060
	}

	handle {
		# Aggressive caching since content rarely changes
		cache {
			ttl 7d # Cache for 7 days (pages almost never change)
			stale 14d # Serve stale for up to 14 days if backend is down

			# Respect cache headers from upstream
			# If Remix sets Cache-Control: no-cache, it won't be cached
			# Use /api/cache/evict during deployments to clear cache
		}

		respond 103
		reverse_proxy localhost:3060
	}
}

5. Links to relevant resources:

Remove respond from your config. There’s nothing special to do if your upstream app has 103 support already, reverse_proxy will handle it.

The respond directive is for when you want to manually write a response back to the client, you wouldn’t ever use it at the same time as reverse_proxy.

Does your app write a 103 response with Link headers before its main response? It needs to do that.

3 Likes

This makes sense, at the moment I’m serving my application through React Router and they don’t have it implemented yet. I’m going to write a proposal for it on their repo. Thanks!

You can match the request and set hints and/or push from Caddy independent of upstream if you want. I use the following with a mediawiki site:

@hint {
	protocol http/2+
	query title=*
	path /mediawiki/index.php*
	path /w/*
}

route {
    header @cache { 
		Cache-Control "max-age=31536000, s-maxage=31536000, public"
		Strict-Transport-Security "max-age=31968000;  preload"
		X-Frame-Options DENY
    }
    header {
		Strict-Transport-Security "max-age=31968000;  preload"
		X-Frame-Options DENY
	}

	route @hint {
		header Strict-Transport-Security "max-age=31968000;  preload"
		header X-Frame-Options DENY
		header +Link "</mediawiki/load.php?lang=en-gb&modules=ext.visualEditor.desktopArticleTarget.noscript%7Cskins.timeless&only=styles&skin=timeless>; rel=preload; as=style"
		header +Link "</mediawiki/load.php?lang=en-gb&modules=site.styles&only=styles&skin=timeless>; rel=preload; as=style"
		header +Link "</mediawiki/load.php?lang=en-gb&modules=startup&only=scripts&raw=1&skin=timeless>; rel=preload; as=script"
		respond 103
		push
	}
}

root * /var/www/domains/mydom.com/htdocs
php_fastcgi unix//var/run/php-fpm/fpm-wiki.socket
file_server {
	precompressed br zstd gzip
}
3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.