From e1df0ec2050946156ae169c6419193ceb61ec246 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 13 Apr 2026 11:12:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor(utils):=20=E6=8F=90=E5=8F=96=20HTTP=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=93=8D=E5=BA=94=E8=BE=85=E5=8A=A9=E5=87=BD?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 SendError 和 SendErrorWithDetail 函数,统一处理 HTTP 错误响应,减少重复代码。 Co-Authored-By: Claude Opus 4.6 --- internal/handler/static.go | 15 +++++---- internal/middleware/security/ratelimit.go | 7 ++-- internal/proxy/proxy.go | 11 ++++--- internal/server/status.go | 5 +-- internal/utils/httperror.go | 39 +++++++++++++++++++++++ 5 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 internal/utils/httperror.go diff --git a/internal/handler/static.go b/internal/handler/static.go index 9643230..f38e9b9 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -28,6 +28,7 @@ import ( "github.com/valyala/fasthttp" "rua.plus/lolly/internal/cache" "rua.plus/lolly/internal/middleware/compression" + "rua.plus/lolly/internal/utils" ) // StaticHandler 静态文件处理器。 @@ -212,7 +213,7 @@ func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) { // 安全检查:防止目录遍历 if strings.Contains(reqPath, "..") { - ctx.Error("Forbidden", fasthttp.StatusForbidden) + utils.SendError(ctx, utils.ErrForbidden) return } @@ -290,7 +291,7 @@ func (h *StaticHandler) handleTryFiles(ctx *fasthttp.RequestCtx, reqPath string) } // 所有 try_files 都未找到 - ctx.Error("Not Found", fasthttp.StatusNotFound) + utils.SendError(ctx, utils.ErrNotFound) } // resolveTryFilePath 解析 try_files 中的占位符。 @@ -369,11 +370,11 @@ func (h *StaticHandler) handleInternalRedirect(ctx *fasthttp.RequestCtx, targetP } info, err := os.Stat(filePath) if err != nil { - ctx.Error("Not Found", fasthttp.StatusNotFound) + utils.SendError(ctx, utils.ErrNotFound) return } if info.IsDir() { - ctx.Error("Forbidden", fasthttp.StatusForbidden) + utils.SendError(ctx, utils.ErrForbidden) return } h.serveFile(ctx, filePath, info) @@ -419,7 +420,7 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string) // 检查文件/目录是否存在 info, err := os.Stat(filePath) if err != nil { - ctx.Error("Not Found", fasthttp.StatusNotFound) + utils.SendError(ctx, utils.ErrNotFound) return } @@ -432,7 +433,7 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string) return } } - ctx.Error("Forbidden", fasthttp.StatusForbidden) + utils.SendError(ctx, utils.ErrForbidden) return } @@ -492,7 +493,7 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf // 读取文件内容 data, err := os.ReadFile(filePath) if err != nil { - ctx.Error("Internal Server Error", fasthttp.StatusInternalServerError) + utils.SendError(ctx, utils.ErrInternalError) return } diff --git a/internal/middleware/security/ratelimit.go b/internal/middleware/security/ratelimit.go index d6c7132..06c1ac4 100644 --- a/internal/middleware/security/ratelimit.go +++ b/internal/middleware/security/ratelimit.go @@ -40,6 +40,7 @@ import ( "rua.plus/lolly/internal/config" "rua.plus/lolly/internal/middleware" "rua.plus/lolly/internal/netutil" + "rua.plus/lolly/internal/utils" ) const rateLimitHeader = "header" @@ -174,7 +175,7 @@ func (s *SlidingWindowLimiterWrapper) Process(next fasthttp.RequestHandler) fast key := s.keyFunc(ctx) if !s.limiter.Allow(key) { - ctx.Error("Too Many Requests", fasthttp.StatusTooManyRequests) + utils.SendError(ctx, utils.ErrTooManyRequests) return } @@ -208,7 +209,7 @@ func (rl *RateLimiter) Process(next fasthttp.RequestHandler) fasthttp.RequestHan // 计算重试等待时间 retryAfter := rl.getRetryAfter(key) ctx.Response.Header.Set("Retry-After", fmt.Sprintf("%d", retryAfter)) - ctx.Error("Too Many Requests", fasthttp.StatusTooManyRequests) + utils.SendError(ctx, utils.ErrTooManyRequests) return } @@ -615,7 +616,7 @@ func (m *connLimiterMiddleware) Name() string { func (m *connLimiterMiddleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { if !m.limiter.Acquire(ctx) { - ctx.Error("Service Unavailable: Connection limit exceeded", fasthttp.StatusServiceUnavailable) + utils.SendErrorWithDetail(ctx, utils.ErrServiceUnavailable, "Connection limit exceeded") return } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 683315b..89df25d 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -48,6 +48,7 @@ import ( "rua.plus/lolly/internal/logging" "rua.plus/lolly/internal/netutil" "rua.plus/lolly/internal/resolver" + "rua.plus/lolly/internal/utils" "rua.plus/lolly/internal/variable" ) @@ -337,7 +338,7 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { // 没有可用后端 upstreamAddr = "FAILED" upstreamStatus = 502 - ctx.Error("Bad Gateway: no healthy upstream", fasthttp.StatusBadGateway) + utils.SendErrorWithDetail(ctx, utils.ErrBadGateway, "no healthy upstream") return } // 没有更多可用目标,返回最后一次错误 @@ -522,18 +523,18 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { // 处理不同类型的错误 if errors.Is(lastErr, fasthttp.ErrTimeout) { upstreamStatus = 504 - ctx.Error("Gateway Timeout", fasthttp.StatusGatewayTimeout) + utils.SendError(ctx, utils.ErrGatewayTimeout) } else if errors.Is(lastErr, fasthttp.ErrConnectionClosed) { upstreamStatus = 502 - ctx.Error("Bad Gateway: upstream connection closed", fasthttp.StatusBadGateway) + utils.SendErrorWithDetail(ctx, utils.ErrBadGateway, "upstream connection closed") } else { upstreamStatus = 502 - ctx.Error("Bad Gateway", fasthttp.StatusBadGateway) + utils.SendError(ctx, utils.ErrBadGateway) } } else { upstreamAddr = "FAILED" upstreamStatus = 502 - ctx.Error("Bad Gateway: all upstreams failed", fasthttp.StatusBadGateway) + utils.SendErrorWithDetail(ctx, utils.ErrBadGateway, "all upstreams failed") } } diff --git a/internal/server/status.go b/internal/server/status.go index c2fa1b1..e7f30d2 100644 --- a/internal/server/status.go +++ b/internal/server/status.go @@ -22,6 +22,7 @@ import ( "github.com/valyala/fasthttp" "rua.plus/lolly/internal/config" "rua.plus/lolly/internal/netutil" + "rua.plus/lolly/internal/utils" ) // StatusHandler 状态监控处理器。 @@ -182,7 +183,7 @@ func (h *StatusHandler) Path() string { func (h *StatusHandler) ServeHTTP(ctx *fasthttp.RequestCtx) { // 步骤1: 检查 IP 访问权限 if !h.checkAccess(ctx) { - ctx.Error("Forbidden: Access denied", fasthttp.StatusForbidden) + utils.SendErrorWithDetail(ctx, utils.ErrForbidden, "Access denied") return } @@ -201,7 +202,7 @@ func (h *StatusHandler) ServeHTTP(ctx *fasthttp.RequestCtx) { data, err := json.MarshalIndent(status, "", " ") if err != nil { - ctx.Error("Internal Server Error", fasthttp.StatusInternalServerError) + utils.SendError(ctx, utils.ErrInternalError) return } diff --git a/internal/utils/httperror.go b/internal/utils/httperror.go new file mode 100644 index 0000000..51fd942 --- /dev/null +++ b/internal/utils/httperror.go @@ -0,0 +1,39 @@ +// Package utils provides utility functions for HTTP error handling. +// +// This file implements a unified HTTP error response helper to reduce +// the scattered pattern of ctx.Error throughout the codebase. +package utils + +import "github.com/valyala/fasthttp" + +// HTTPError represents an HTTP error with a message and status code. +type HTTPError struct { + Message string + StatusCode int +} + +// Predefined common HTTP errors. +var ( + ErrNotFound = HTTPError{Message: "Not Found", StatusCode: fasthttp.StatusNotFound} + ErrForbidden = HTTPError{Message: "Forbidden", StatusCode: fasthttp.StatusForbidden} + ErrUnauthorized = HTTPError{Message: "Unauthorized", StatusCode: fasthttp.StatusUnauthorized} + ErrBadGateway = HTTPError{Message: "Bad Gateway", StatusCode: fasthttp.StatusBadGateway} + ErrGatewayTimeout = HTTPError{Message: "Gateway Timeout", StatusCode: fasthttp.StatusGatewayTimeout} + ErrInternalError = HTTPError{Message: "Internal Server Error", StatusCode: fasthttp.StatusInternalServerError} + ErrTooManyRequests = HTTPError{Message: "Too Many Requests", StatusCode: fasthttp.StatusTooManyRequests} + ErrServiceUnavailable = HTTPError{Message: "Service Unavailable", StatusCode: fasthttp.StatusServiceUnavailable} +) + +// SendError sends an HTTP error response to the client. +func SendError(ctx *fasthttp.RequestCtx, err HTTPError) { + ctx.Error(err.Message, err.StatusCode) +} + +// SendErrorWithDetail sends an HTTP error response with additional detail. +func SendErrorWithDetail(ctx *fasthttp.RequestCtx, err HTTPError, detail string) { + if detail != "" { + ctx.Error(err.Message+": "+detail, err.StatusCode) + } else { + SendError(ctx, err) + } +}