diff --git a/internal/handler/errorpage.go b/internal/handler/errorpage.go new file mode 100644 index 0000000..86e8bb6 --- /dev/null +++ b/internal/handler/errorpage.go @@ -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 +} diff --git a/internal/middleware/errorintercept/AGENTS.md b/internal/middleware/errorintercept/AGENTS.md new file mode 100644 index 0000000..c7c45e5 --- /dev/null +++ b/internal/middleware/errorintercept/AGENTS.md @@ -0,0 +1,40 @@ + + + +# 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 框架 + + \ No newline at end of file diff --git a/internal/middleware/errorintercept/errorintercept.go b/internal/middleware/errorintercept/errorintercept.go new file mode 100644 index 0000000..afe9085 --- /dev/null +++ b/internal/middleware/errorintercept/errorintercept.go @@ -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 +}