Rewrite, templates and Content-Type

1. The problem I’m having:

I’m trying to process some template files, saved on disk as blah.gohtml

2. Error messages and/or full log output:

The files are served by Caddy literally, so the template functionality isn’t invoked

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

3. Caddy version:

caddy:2-alpine

4. How I installed and ran Caddy:

podman

a. System environment:

macOs something

b. Command:

podman run --rm -v ./htdocs:/usr/share/caddy -v ./Caddyfile.test:/etc/caddy/Caddyfile -p 8080:80 caddy:2-alpine

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

c. Service/unit/compose file:

PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

d. My complete Caddy config:

:80 {
  root /usr/share/caddy
  #templates *.gohtml

  templates {
    mime_types application/octet-stream
  }

  encode zstd gzip
  file_server

  @gohtmlgohtml path *.gohtml
  header @gohtml Content-Type text/html

  #rewrite /articles/* /article.gohtml
  rewrite /index.gohtml

  route {
    header *.gohtml Content-Type text/html
    rewrite /articles/* /article.gohtml
    rewrite /index.gohtml
    file_server
    templates
  }
}

# i’ve tried a lot of variants here
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.

5. Links to relevant resources:

I see the content-type matching is… basic


ct := header.Get(“Content-Type”)
  for _, mt := range t.MIMETypes {
    if strings.Contains(ct, mt) {
      return true
     }

so there’s no chance of matching “text/*” or even text/plain; charset="UTF=8"

which goat do i need to sacrifice to convince Caddy to process my files as templates

:goat: :dagger: :drop_of_blood: :pleading_face:

Here is a quick example you can adjust as needed:

test.gohtml

<!DOCTYPE html>
<html>
<body>

<pre>
Method         : {{ .Req.Method }}
URI            : {{ .Req.RequestURI }}
Protocol       : {{ .Req.Proto }}
Remote Address : {{ .Req.RemoteAddr }}
Server         : {{ .Req.Host }}
Connection     : {{ if .Req.TLS }}HTTPS{{ else }}HTTP{{ end }}
Timestamp      : {{ now | date "2006-01-02 15:04:05 UTC" }}
</pre>

</body>
</html>

Caddyfile

example.com {
	tls internal

	root * /var/www/html
	
	@gohtml path *.gohtml
	header @gohtml Content-Type text/html
	templates @gohtml {
		mime text/html
	}
	
	file_server
}

Test result:

$ curl https://example.com/test.gohtml -I
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/html
server: Caddy
vary: Accept-Encoding
content-length: 0
date: Fri, 06 Feb 2026 18:51:11 GMT
$ curl https://example.com/test.gohtml
<!DOCTYPE html>
<html>
<body>

<pre>
Method         : GET
URI            : /test.gohtml
Protocol       : HTTP/2.0
Remote Address : 127.0.0.1:65461
Server         : example.com
Connection     : HTTPS
Timestamp      : 2026-02-06 10:51:23 UTC
</pre>

</body>
</html>

In other words, things happen in this order.

  1. header sets the Content-Type for *.gohtml
  2. The response from *.gohtml is now text/html
  3. The templates handler sees a matching MIME type
  4. templates executes

The mime directive inside templates is not really needed here, since text/html is already the default. I included it only to highlight that templates runs based on the MIME type when the response is about to be sent to the client.

In your configuration, you were limiting templates to only process application/octet-stream:

  templates {
    mime_types application/octet-stream
  }

If that is the MIME type you actually want to send, then you need to adjust your config:

...
	@gohtml path *.gohtml
	header @gohtml Content-Type application/octet-stream
	templates @gohtml {
		mime application/octet-stream
	}
...

Test result:

$ curl https://example.com/test.gohtml -I
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: application/octet-stream
server: Caddy
vary: Accept-Encoding
content-length: 0
date: Fri, 06 Feb 2026 18:59:08 GMT
$ curl https://example.com/test.gohtml
<!DOCTYPE html>
<html>
<body>

<pre>
Method         : GET
URI            : /test.gohtml
Protocol       : HTTP/2.0
Remote Address : 127.0.0.1:50024
Server         : example.com
Connection     : HTTPS
Timestamp      : 2026-02-06 10:59:09 UTC
</pre>

</body>
</html>

Keep in mind that application/octet-stream will cause most browsers to download the file instead of displaying it.

2 Likes

Sorry, I missed this one:

	@gohtml path *.gohtml
	header @gohtml Content-Type "text/plain; charset=\"UTF=8\""
	templates @gohtml {
		mime text/html text/plain
	}

Again, mime text/html text/plain is not needed here, because that’s the default value.

Test result:

$ curl https://example.com/test.gohtml -I
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/plain; charset="UTF=8"
server: Caddy
vary: Accept-Encoding
content-length: 0
date: Fri, 06 Feb 2026 19:19:36 GMT
$ curl https://example.com/test.gohtml
<!DOCTYPE html>
<html>
<body>

<pre>
Method         : GET
URI            : /test.gohtml
Protocol       : HTTP/2.0
Remote Address : 127.0.0.1:53198
Server         : example.com
Connection     : HTTPS
Timestamp      : 2026-02-06 11:19:46 UTC
</pre>

</body>
</html>
2 Likes

There actually is :slight_smile: The important part here is Contains:

 if strings.Contains(ct, mt) {

This effectively reads as “if the Content Type string contains the MIME string”. Because of that, something like text/* can easily be achieved by matching mime on text/.

For example:

...
	@gohtml path *.gohtml
	header @gohtml Content-Type "text/foo; charset=\"UTF=8\""
	templates @gohtml {
		mime text/
	}
...

Test result:

$ curl https://example.com/test.gohtml -I
HTTP/2 200
alt-svc: h3=":443"; ma=2592000
content-type: text/foo; charset="UTF=8"
server: Caddy
vary: Accept-Encoding
content-length: 0
date: Fri, 06 Feb 2026 20:34:11 GMT
$ curl https://example.com/test.gohtml
<!DOCTYPE html>
<html>
<body>

<pre>
Method         : GET
URI            : /test.gohtml
Protocol       : HTTP/2.0
Remote Address : 127.0.0.1:65375
Server         : example.com
Connection     : HTTPS
Timestamp      : 2026-02-06 12:34:25 UTC
</pre>

</body>
</html>
2 Likes