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:
parent
deb9b3139a
commit
827d26c2b5
197
internal/handler/errorpage.go
Normal file
197
internal/handler/errorpage.go
Normal 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
|
||||
}
|
||||
40
internal/middleware/errorintercept/AGENTS.md
Normal file
40
internal/middleware/errorintercept/AGENTS.md
Normal 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: -->
|
||||
117
internal/middleware/errorintercept/errorintercept.go
Normal file
117
internal/middleware/errorintercept/errorintercept.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user