feat(handler,middleware,errorintercept): 新增自定义错误页面支持

- ErrorPageManager 预加载错误页面到内存,运行时无文件 I/O
- ErrorIntercept 中间件拦截 4xx/5xx 响应并应用自定义页面
- 支持特定状态码页面和默认页面配置
- 支持可选的响应状态码覆盖(如返回 200 OK)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-07 17:49:59 +08:00
parent deb9b3139a
commit 827d26c2b5
3 changed files with 354 additions and 0 deletions

View File

@ -0,0 +1,197 @@
// Package handler 提供 HTTP 请求处理器,包括路由、静态文件服务和零拷贝传输。
//
// 该文件包含自定义错误页面相关的核心逻辑,包括:
// - 错误页面预加载
// - 错误页面内容管理
// - 错误页面查找
//
// 主要用途:
//
// 用于在服务器启动时预加载自定义错误页面文件到内存中,运行时不进行文件 I/O。
//
// 注意事项:
// - 所有错误页面在启动时预加载
// - 全部加载失败时会阻止服务器启动
// - 部分加载失败会记录警告但允许启动
//
// 作者xfy
package handler
import (
"fmt"
"os"
"sync"
"rua.plus/lolly/internal/config"
)
// ErrorPageManager 自定义错误页面管理器。
//
// 负责在服务器启动时预加载错误页面文件到内存中,
// 并在运行时提供错误页面内容。
type ErrorPageManager struct {
// pages 预加载的错误页面内容
// key 为 HTTP 状态码value 为页面内容
pages map[int][]byte
// defaultPage 默认错误页面内容
defaultPage []byte
// responseCode 响应状态码覆盖
responseCode int
// mu 保护 pages 的读写锁
mu sync.RWMutex
}
// NewErrorPageManager 创建错误页面管理器。
//
// 根据配置预加载错误页面文件到内存中。
//
// 参数:
// - cfg: 错误页面配置
//
// 返回值:
// - *ErrorPageManager: 创建的错误页面管理器
// - error: 预加载失败时的错误,全部失败时返回错误
//
// 使用示例:
//
// manager, err := handler.NewErrorPageManager(&cfg.ErrorPage)
// if err != nil {
// log.Fatal("加载错误页面失败:", err)
// }
func NewErrorPageManager(cfg *config.ErrorPageConfig) (*ErrorPageManager, error) {
if len(cfg.Pages) == 0 && cfg.Default == "" {
// 没有配置错误页面,返回空管理器
return &ErrorPageManager{
pages: make(map[int][]byte),
responseCode: cfg.ResponseCode,
}, nil
}
manager := &ErrorPageManager{
pages: make(map[int][]byte),
responseCode: cfg.ResponseCode,
}
// 预加载特定状态码的错误页面
loadErrors := make(map[int]error)
for code, path := range cfg.Pages {
content, err := os.ReadFile(path)
if err != nil {
loadErrors[code] = err
continue
}
manager.pages[code] = content
}
// 预加载默认错误页面
if cfg.Default != "" {
content, err := os.ReadFile(cfg.Default)
if err != nil {
loadErrors[0] = err // 使用 0 表示默认页面错误
} else {
manager.defaultPage = content
}
}
// 检查加载结果
if len(loadErrors) > 0 {
// 部分或全部加载失败
totalPages := len(cfg.Pages)
if cfg.Default != "" {
totalPages++
}
if len(loadErrors) == totalPages {
// 全部加载失败,返回错误
return nil, fmt.Errorf("所有错误页面加载失败: %v", loadErrors)
}
// 部分失败,记录警告(由调用者处理)
return manager, &PartialLoadError{Errors: loadErrors}
}
return manager, nil
}
// PartialLoadError 部分错误页面加载失败错误。
type PartialLoadError struct {
Errors map[int]error
}
// Error 实现 error 接口。
func (e *PartialLoadError) Error() string {
return fmt.Sprintf("部分错误页面加载失败: %d 个错误", len(e.Errors))
}
// GetPage 获取指定状态码的错误页面内容。
//
// 参数:
// - code: HTTP 状态码
//
// 返回值:
// - []byte: 错误页面内容,如未找到返回 nil
// - bool: 是否找到
// - int: 响应状态码(可能被覆盖)
func (m *ErrorPageManager) GetPage(code int) ([]byte, bool, int) {
m.mu.RLock()
defer m.mu.RUnlock()
// 查找特定状态码的页面
if content, ok := m.pages[code]; ok {
responseCode := code
if m.responseCode > 0 {
responseCode = m.responseCode
}
return content, true, responseCode
}
// 使用默认页面
if m.defaultPage != nil {
responseCode := code
if m.responseCode > 0 {
responseCode = m.responseCode
}
return m.defaultPage, true, responseCode
}
return nil, false, code
}
// HasPage 检查是否有指定状态码的错误页面。
//
// 参数:
// - code: HTTP 状态码
//
// 返回值:
// - bool: 是否有该状态码的错误页面(包括默认页面)
func (m *ErrorPageManager) HasPage(code int) bool {
m.mu.RLock()
defer m.mu.RUnlock()
if _, ok := m.pages[code]; ok {
return true
}
return m.defaultPage != nil
}
// GetResponseCode 获取响应状态码覆盖值。
//
// 返回值:
// - int: 响应状态码覆盖值0 表示不覆盖
func (m *ErrorPageManager) GetResponseCode() int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.responseCode
}
// IsConfigured 检查是否配置了错误页面。
//
// 返回值:
// - bool: 是否配置了任何错误页面
func (m *ErrorPageManager) IsConfigured() bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.pages) > 0 || m.defaultPage != nil
}

View File

@ -0,0 +1,40 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-04-07 | Updated: 2026-04-07 -->
# errorintercept
## Purpose
HTTP 错误拦截中间件,用于应用自定义错误页面。拦截 4xx/5xx 响应并替换为预加载的错误页面内容。
## Key Files
| File | Description |
|------|-------------|
| `errorintercept.go` | 错误拦截中间件实现,与 ErrorPageManager 配合使用 |
## For AI Agents
### Working In This Directory
- 错误页面在启动时预加载,运行时不进行文件 I/O
- 只拦截 4xx 和 5xx 错误状态码
- 支持可选的响应状态码覆盖
- 与 `internal/handler.ErrorPageManager` 配合使用
### Testing Requirements
- 运行测试:`go test ./internal/middleware/errorintercept/...`
- 测试需模拟 ErrorPageManager 和错误响应
### Common Patterns
- 创建中间件:`errorintercept.New(errorPageManager)`
- 检查配置状态:`ei.manager.IsConfigured()`
- 获取管理器:`ei.GetManager()`
## Dependencies
### Internal
- `rua.plus/lolly/internal/handler` - ErrorPageManager 错误页面管理
### External
- `github.com/valyala/fasthttp` - HTTP 框架
<!-- MANUAL: -->

View File

@ -0,0 +1,117 @@
// Package errorintercept 提供 HTTP 错误拦截中间件,用于应用自定义错误页面。
//
// 该文件包含错误拦截相关的核心功能,包括:
// - ErrorIntercept 中间件:拦截 HTTP 错误响应并应用自定义错误页面
// - 错误状态码检测
// - 错误页面内容替换
//
// 主要用途:
//
// 在 HTTP 响应返回错误状态码时,自动替换为预加载的自定义错误页面内容。
//
// 注意事项:
// - 错误页面在启动时预加载,运行时不进行文件 I/O
// - 支持可选的响应状态码覆盖
// - 只拦截 4xx 和 5xx 错误状态码
//
// 作者xfy
package errorintercept
import (
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/handler"
)
// ErrorIntercept 错误拦截中间件。
//
// 拦截 HTTP 错误响应4xx 和 5xx并使用预加载的自定义错误页面内容替换响应。
type ErrorIntercept struct {
// manager 错误页面管理器
manager *handler.ErrorPageManager
}
// New 创建错误拦截中间件。
//
// 参数:
// - manager: 错误页面管理器(已预加载错误页面)
//
// 返回值:
// - *ErrorIntercept: 创建的中间件实例
//
// 使用示例:
//
// interceptor := errorintercept.New(errorPageManager)
func New(manager *handler.ErrorPageManager) *ErrorIntercept {
return &ErrorIntercept{
manager: manager,
}
}
// Name 返回中间件名称。
//
// 返回值:
// - string: 中间件名称
func (ei *ErrorIntercept) Name() string {
return "ErrorIntercept"
}
// Process 实现中间件接口。
//
// 拦截错误状态码响应并应用自定义错误页面。
//
// 参数:
// - next: 下一个请求处理器
//
// 返回值:
// - fasthttp.RequestHandler: 包装后的请求处理器
func (ei *ErrorIntercept) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
// 如果没有配置错误页面,直接返回下一个处理器
if ei.manager == nil || !ei.manager.IsConfigured() {
return next
}
return func(ctx *fasthttp.RequestCtx) {
// 先执行下一个处理器
next(ctx)
// 检查是否是错误状态码4xx 或 5xx
statusCode := ctx.Response.StatusCode()
if !isErrorStatusCode(statusCode) {
return
}
// 查找对应的错误页面
content, found, responseCode := ei.manager.GetPage(statusCode)
if !found {
return
}
// 替换响应内容为自定义错误页面
ctx.Response.SetBody(content)
ctx.Response.Header.SetContentType("text/html; charset=utf-8")
// 如果配置了响应状态码覆盖,使用覆盖值
if responseCode != statusCode {
ctx.Response.SetStatusCode(responseCode)
}
}
}
// isErrorStatusCode 检查状态码是否为错误状态码。
//
// 参数:
// - code: HTTP 状态码
//
// 返回值:
// - bool: 是否为错误状态码4xx 或 5xx
func isErrorStatusCode(code int) bool {
return code >= 400 && code < 600
}
// GetManager 返回错误页面管理器。
//
// 返回值:
// - *handler.ErrorPageManager: 错误页面管理器
func (ei *ErrorIntercept) GetManager() *handler.ErrorPageManager {
return ei.manager
}