Fix three doc-vs-implementation gaps found during audit

1. DB migration: add CHECK(mode IN ('l4', 'l7')) constraint on the
   routes.mode column. ARCHITECTURE.md documented this constraint but
   migration v2 omitted it. Enforces mode validity at the database
   level in addition to application-level validation.

2. L7 reverse proxy: distinguish timeout errors from connection errors
   in the ErrorHandler. Backend timeouts now return HTTP 504 Gateway
   Timeout instead of 502. Uses errors.Is(context.DeadlineExceeded)
   and net.Error.Timeout() detection. Added isTimeoutError unit tests.

3. Config validation: warn when L4 routes have tls_cert or tls_key set
   (they are silently ignored). ARCHITECTURE.md documented this warning
   but config.validate() did not emit it. Uses slog.Warn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 14:25:41 -07:00
parent b6c96ad42f
commit 5bc8f4fc8e
4 changed files with 52 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ package l7
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
@@ -132,7 +133,11 @@ func newReverseProxy(route RouteConfig, logger *slog.Logger) (*httputil.ReverseP
Transport: transport,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("reverse proxy error", "backend", route.Backend, "error", err)
w.WriteHeader(http.StatusBadGateway)
if isTimeoutError(err) {
w.WriteHeader(http.StatusGatewayTimeout)
} else {
w.WriteHeader(http.StatusBadGateway)
}
},
}
@@ -199,6 +204,19 @@ func setForwardingHeaders(r *http.Request, clientAddr netip.AddrPort) {
r.Header.Set("X-Real-IP", clientIP)
}
// isTimeoutError returns true if the error is a timeout (context deadline
// exceeded or net.Error timeout).
func isTimeoutError(err error) bool {
if errors.Is(err, context.DeadlineExceeded) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
return false
}
// singleConnListener is a net.Listener that returns a single connection once,
// then blocks until closed. Used to serve HTTP/1.1 on a single TLS connection.
type singleConnListener struct {