refactor(utils): 提取 HTTP 错误响应辅助函数

新增 SendError 和 SendErrorWithDetail 函数,统一处理 HTTP 错误响应,减少重复代码。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-13 11:12:49 +08:00
parent 8a533ba0ca
commit e1df0ec205
5 changed files with 60 additions and 17 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)
}
}