trying embedded caddy reverse proxy

This commit is contained in:
pascal
2026-01-14 17:16:42 +01:00
parent d9118eb239
commit 626e892e3b
30 changed files with 6978 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
package errors
import "fmt"
// Configuration errors
func NewConfigInvalid(message string) *AppError {
return New(CodeConfigInvalid, message)
}
func NewConfigNotFound(path string) *AppError {
return New(CodeConfigNotFound, fmt.Sprintf("configuration file not found: %s", path))
}
func WrapConfigParseFailed(err error, path string) *AppError {
return Wrap(CodeConfigParseFailed, fmt.Sprintf("failed to parse configuration file: %s", path), err)
}
// Server errors
func NewServerStartFailed(err error, reason string) *AppError {
return Wrap(CodeServerStartFailed, fmt.Sprintf("server start failed: %s", reason), err)
}
func NewServerStopFailed(err error) *AppError {
return Wrap(CodeServerStopFailed, "server shutdown failed", err)
}
func NewServerAlreadyRunning() *AppError {
return New(CodeServerAlreadyRunning, "server is already running")
}
func NewServerNotRunning() *AppError {
return New(CodeServerNotRunning, "server is not running")
}
// Proxy errors
func NewProxyBackendUnavailable(backend string, err error) *AppError {
return Wrap(CodeProxyBackendUnavailable, fmt.Sprintf("backend unavailable: %s", backend), err)
}
func NewProxyTimeout(backend string) *AppError {
return New(CodeProxyTimeout, fmt.Sprintf("request to backend timed out: %s", backend))
}
func NewProxyInvalidTarget(target string, err error) *AppError {
return Wrap(CodeProxyInvalidTarget, fmt.Sprintf("invalid proxy target: %s", target), err)
}
// Network errors
func NewNetworkTimeout(operation string) *AppError {
return New(CodeNetworkTimeout, fmt.Sprintf("network timeout: %s", operation))
}
func NewNetworkUnreachable(host string) *AppError {
return New(CodeNetworkUnreachable, fmt.Sprintf("network unreachable: %s", host))
}
func NewNetworkRefused(host string) *AppError {
return New(CodeNetworkRefused, fmt.Sprintf("connection refused: %s", host))
}
// Internal errors
func NewInternalError(message string) *AppError {
return New(CodeInternalError, message)
}
func WrapInternalError(err error, message string) *AppError {
return Wrap(CodeInternalError, message, err)
}
+138
View File
@@ -0,0 +1,138 @@
package errors
import (
"errors"
"fmt"
)
// Error codes for categorizing errors
type Code string
const (
// Configuration errors
CodeConfigInvalid Code = "CONFIG_INVALID"
CodeConfigNotFound Code = "CONFIG_NOT_FOUND"
CodeConfigParseFailed Code = "CONFIG_PARSE_FAILED"
// Server errors
CodeServerStartFailed Code = "SERVER_START_FAILED"
CodeServerStopFailed Code = "SERVER_STOP_FAILED"
CodeServerAlreadyRunning Code = "SERVER_ALREADY_RUNNING"
CodeServerNotRunning Code = "SERVER_NOT_RUNNING"
// Proxy errors
CodeProxyBackendUnavailable Code = "PROXY_BACKEND_UNAVAILABLE"
CodeProxyTimeout Code = "PROXY_TIMEOUT"
CodeProxyInvalidTarget Code = "PROXY_INVALID_TARGET"
// Network errors
CodeNetworkTimeout Code = "NETWORK_TIMEOUT"
CodeNetworkUnreachable Code = "NETWORK_UNREACHABLE"
CodeNetworkRefused Code = "NETWORK_REFUSED"
// Internal errors
CodeInternalError Code = "INTERNAL_ERROR"
CodeUnknownError Code = "UNKNOWN_ERROR"
)
// AppError represents a structured application error
type AppError struct {
Code Code // Error code for categorization
Message string // Human-readable error message
Cause error // Underlying error (if any)
}
// Error implements the error interface
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying error (for errors.Is and errors.As)
func (e *AppError) Unwrap() error {
return e.Cause
}
// Is checks if the error matches the target
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// New creates a new AppError
func New(code Code, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
// Wrap wraps an existing error with additional context
func Wrap(code Code, message string, cause error) *AppError {
return &AppError{
Code: code,
Message: message,
Cause: cause,
}
}
// Wrapf wraps an error with a formatted message
func Wrapf(code Code, cause error, format string, args ...interface{}) *AppError {
return &AppError{
Code: code,
Message: fmt.Sprintf(format, args...),
Cause: cause,
}
}
// GetCode extracts the error code from an error
func GetCode(err error) Code {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Code
}
return CodeUnknownError
}
// HasCode checks if an error has a specific code
func HasCode(err error, code Code) bool {
return GetCode(err) == code
}
// IsConfigError checks if an error is configuration-related
func IsConfigError(err error) bool {
code := GetCode(err)
return code == CodeConfigInvalid ||
code == CodeConfigNotFound ||
code == CodeConfigParseFailed
}
// IsServerError checks if an error is server-related
func IsServerError(err error) bool {
code := GetCode(err)
return code == CodeServerStartFailed ||
code == CodeServerStopFailed ||
code == CodeServerAlreadyRunning ||
code == CodeServerNotRunning
}
// IsProxyError checks if an error is proxy-related
func IsProxyError(err error) bool {
code := GetCode(err)
return code == CodeProxyBackendUnavailable ||
code == CodeProxyTimeout ||
code == CodeProxyInvalidTarget
}
// IsNetworkError checks if an error is network-related
func IsNetworkError(err error) bool {
code := GetCode(err)
return code == CodeNetworkTimeout ||
code == CodeNetworkUnreachable ||
code == CodeNetworkRefused
}
+160
View File
@@ -0,0 +1,160 @@
package errors
import (
"errors"
"testing"
)
func TestAppError_Error(t *testing.T) {
tests := []struct {
name string
err *AppError
expected string
}{
{
name: "error without cause",
err: New(CodeConfigInvalid, "invalid configuration"),
expected: "[CONFIG_INVALID] invalid configuration",
},
{
name: "error with cause",
err: Wrap(CodeServerStartFailed, "failed to bind port", errors.New("address already in use")),
expected: "[SERVER_START_FAILED] failed to bind port: address already in use",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.expected {
t.Errorf("Error() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetCode(t *testing.T) {
tests := []struct {
name string
err error
expected Code
}{
{
name: "app error",
err: New(CodeConfigInvalid, "test"),
expected: CodeConfigInvalid,
},
{
name: "wrapped app error",
err: Wrap(CodeServerStartFailed, "test", errors.New("cause")),
expected: CodeServerStartFailed,
},
{
name: "standard error",
err: errors.New("standard error"),
expected: CodeUnknownError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetCode(tt.err); got != tt.expected {
t.Errorf("GetCode() = %v, want %v", got, tt.expected)
}
})
}
}
func TestHasCode(t *testing.T) {
err := New(CodeConfigInvalid, "invalid config")
if !HasCode(err, CodeConfigInvalid) {
t.Error("HasCode() should return true for matching code")
}
if HasCode(err, CodeServerStartFailed) {
t.Error("HasCode() should return false for non-matching code")
}
}
func TestIsConfigError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "config invalid error",
err: New(CodeConfigInvalid, "test"),
expected: true,
},
{
name: "config not found error",
err: New(CodeConfigNotFound, "test"),
expected: true,
},
{
name: "server error",
err: New(CodeServerStartFailed, "test"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsConfigError(tt.err); got != tt.expected {
t.Errorf("IsConfigError() = %v, want %v", got, tt.expected)
}
})
}
}
func TestErrorUnwrap(t *testing.T) {
cause := errors.New("root cause")
err := Wrap(CodeInternalError, "wrapped error", cause)
unwrapped := errors.Unwrap(err)
if unwrapped != cause {
t.Errorf("Unwrap() = %v, want %v", unwrapped, cause)
}
}
func TestErrorIs(t *testing.T) {
err1 := New(CodeConfigInvalid, "test1")
err2 := New(CodeConfigInvalid, "test2")
err3 := New(CodeServerStartFailed, "test3")
if !errors.Is(err1, err2) {
t.Error("errors.Is() should return true for same error code")
}
if errors.Is(err1, err3) {
t.Error("errors.Is() should return false for different error codes")
}
}
func TestCommonConstructors(t *testing.T) {
t.Run("NewConfigNotFound", func(t *testing.T) {
err := NewConfigNotFound("/path/to/config")
if GetCode(err) != CodeConfigNotFound {
t.Error("NewConfigNotFound should create CONFIG_NOT_FOUND error")
}
})
t.Run("NewServerAlreadyRunning", func(t *testing.T) {
err := NewServerAlreadyRunning()
if GetCode(err) != CodeServerAlreadyRunning {
t.Error("NewServerAlreadyRunning should create SERVER_ALREADY_RUNNING error")
}
})
t.Run("NewProxyBackendUnavailable", func(t *testing.T) {
cause := errors.New("connection refused")
err := NewProxyBackendUnavailable("http://backend", cause)
if GetCode(err) != CodeProxyBackendUnavailable {
t.Error("NewProxyBackendUnavailable should create PROXY_BACKEND_UNAVAILABLE error")
}
if !errors.Is(err.Unwrap(), cause) {
t.Error("NewProxyBackendUnavailable should wrap the cause")
}
})
}