Skip to content

Middleware ServeHTTP has to return caddyhttp.HandlerError since 2.9 #6829

@ninavdl

Description

@ninavdl

Since version 2.9.0, returning an error in a caddy middleware's ServeHTTP function causes caddy to fail silently. Only with the debug option, the cause is visible.

Minimal caddy middleware example

package caddy_error_handling

import (
	"errors"
	"net/http"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func init() {
	caddy.RegisterModule(Example{})
	httpcaddyfile.RegisterHandlerDirective("example", parseCaddyfile)
}

type Example struct {
}

func (Example) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.handlers.example",
		New: func() caddy.Module { return new(Example) },
	}
}

func (m Example) UnmarshalCaddyfile(_ *caddyfile.Dispenser) error { return nil }

func (m Example) ServeHTTP(w http.ResponseWriter, _ *http.Request, _ caddyhttp.Handler) error {
	w.WriteHeader(http.StatusBadRequest)
	w.Write([]byte("error"))
	return errors.New("example error")
}

func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
	var m Example
	err := m.UnmarshalCaddyfile(h.Dispenser)
	return m, err
}

Caddyfile:

{
	debug
	order example before respond
}

http:// {
	example
	respond 200 {
		body ok
		close
	}
}

When executing a HTTP request that uses the middleware, there is no response:

❯ curl -v http://localhost
* Host localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:80...
* Connected to localhost (::1) port 80
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
* Empty reply from server
* Closing connection
curl: (52) Empty reply from server

In debug mode, caddy logs:

2025/02/06 16:48:54.648	DEBUG	http.stdlib	http: panic serving [::1]:56190: runtime error: invalid memory address or nil pointer dereference
goroutine 51 [running]:
net/http.(*conn).serve.func1()
	net/http/server.go:1947 +0xb0
panic({0x1041657e0?, 0x104f7f390?})
	runtime/panic.go:785 +0x124
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Server).ServeHTTP(0x140003aaa88, {0x1043b8478, 0x1400057e0e0}, 0x140003b6140)
	github.com/caddyserver/caddy/v2@v2.9.0/modules/caddyhttp/server.go:445 +0xf00
net/http.serverHandler.ServeHTTP({0x1400037a420?}, {0x1043b8478?, 0x1400057e0e0?}, 0x6?)
	net/http/server.go:3210 +0xbc
net/http.(*conn).serve(0x140004be2d0, {0x1043bba40, 0x14000148540})
	net/http/server.go:2092 +0x4fc
created by net/http.(*Server).Serve in goroutine 71
	net/http/server.go:3360 +0x3dc

When running caddy 2.8, there is no error, and caddy responds with HTTP 400 and "error", as it should be.

To mitigate this, can return a caddyhttp.HandlerError instead of a plain error in the ServeHTTP function:

func (m Example) ServeHTTP(w http.ResponseWriter, _ *http.Request, _ caddyhttp.Handler) error {
	w.WriteHeader(http.StatusBadRequest)
	w.Write([]byte("error"))
	return caddyhttp.HandlerError{
		StatusCode: http.StatusBadRequest,
		Err:        errors.New("example error"),
	}
}

If it is expected that a HandlerError is returned, then this should be documented in the httphandler.Handler or httphandler.MiddlewareHandler interfaces.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions