// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package route import ( "fmt" "net/http" "runtime/debug" "strings" "github.com/flamego/flamego" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" "github.com/tamakyi/TamaBox/internal/i18n" "github.com/tamakyi/TamaBox/internal/shared" ) // recoveryMiddleware 替代 flamego.Recovery(),把 panic 渲染成 HTML 500 页。 // // 为什么不用框架自带的:flamego.Recovery() 直接写纯文本 "Internal Server Error", // 用户看到的是白底黑字的两行英文,地址栏还停在出错的 URL 上 —— // 既不知道发生了什么,也没有回去的入口,更没法把这次错误的编号报给我们。 func recoveryMiddleware() flamego.Handler { return func(ctx flamego.Context) { defer func() { if r := recover(); r != nil { // 带栈:没有栈的 panic 日志几乎无法定位。 logrus.WithContext(ctx.Request().Context()). Errorf("panic recovered: %v\n%s", r, debug.Stack()) if ctx.ResponseWriter().Written() { // 头已经发出去了,没法再改状态码,只能让连接断在这里。 return } writeErrorPage(ctx, http.StatusInternalServerError, "error.internalTitle", "error.internalDesc") } }() ctx.Next() } } // writeErrorPage 输出不依赖模板系统的完整错误页。 // // 为什么不用模板:这两个页面出现在两处「拿不到模板」的地方 —— // NotFound 处理器不在组级中间件链里(template.Templater 与 Contexter 都挂在组级, // 包括 session 与 CSRF),而 recovery 中间件挂在最外层,那时模板还没装配。 // 错误页恰恰是最不该依赖出错链路的页面,所以这里用内联 HTML + i18n.T。 func writeErrorPage(ctx flamego.Context, status int, titleKey, descKey string) { lang := pickErrorPageLang(ctx.Request().Request) title := i18n.T(lang, titleKey) desc := i18n.T(lang, descKey) home := i18n.T(lang, "deactivated.backHome") hint := i18n.T(lang, "error.traceHint") traceID := "" if span := trace.SpanFromContext(ctx.Request().Context()); span.SpanContext().TraceID().IsValid() { traceID = span.SpanContext().TraceID().String() } body := fmt.Sprintf(`
` + esc(hint) + ` ` + esc(traceID) + `