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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 17:49:59 +08:00

198 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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