Show friendly 500 error page to users and developers (#24110)

Close #24104

This also introduces many tests to cover many complex error handling
functions.

### Before

The details are never shown in production.

<details>

![image](https://user-images.githubusercontent.com/2114189/231805004-13214579-4fbe-465a-821c-be75c2749097.png)

</details>

### After

The details could be shown to site admin users. It is safe.

![image](https://user-images.githubusercontent.com/2114189/231803912-d5660994-416f-4b27-a4f1-a4cc962091d4.png)
This commit is contained in:
wxiaoguang 2023-04-14 13:19:11 +08:00 committed by GitHub
parent 5768bafeb2
commit 1c8bc4081a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 313 additions and 180 deletions

View file

@ -16,10 +16,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
texttemplate "text/template"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -216,7 +214,7 @@ func (ctx *Context) RedirectToFirst(location ...string) {
ctx.Redirect(setting.AppSubURL + "/") ctx.Redirect(setting.AppSubURL + "/")
} }
var templateExecutingErr = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): executing (?:"(.*)" at <(.*)>: )?`) const tplStatus500 base.TplName = "status/500"
// HTML calls Context.HTML and renders the template to HTTP response // HTML calls Context.HTML and renders the template to HTTP response
func (ctx *Context) HTML(status int, name base.TplName) { func (ctx *Context) HTML(status int, name base.TplName) {
@ -229,34 +227,11 @@ func (ctx *Context) HTML(status int, name base.TplName) {
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms" return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
} }
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil { if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
if status == http.StatusInternalServerError && name == base.TplName("status/500") { if status == http.StatusInternalServerError && name == tplStatus500 {
ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.") ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.")
return return
} }
if execErr, ok := err.(texttemplate.ExecError); ok { err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
if groups := templateExecutingErr.FindStringSubmatch(err.Error()); len(groups) > 0 {
errorTemplateName, lineStr, posStr := groups[1], groups[2], groups[3]
target := ""
if len(groups) == 6 {
target = groups[5]
}
line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]*
pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]*
assetLayerName := templates.AssetFS().GetFileLayerName(errorTemplateName + ".tmpl")
filename := fmt.Sprintf("(%s) %s", assetLayerName, errorTemplateName)
if errorTemplateName != string(name) {
filename += " (subtemplate of " + string(name) + ")"
}
err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
} else {
assetLayerName := templates.AssetFS().GetFileLayerName(execErr.Name + ".tmpl")
filename := fmt.Sprintf("(%s) %s", assetLayerName, execErr.Name)
if execErr.Name != string(name) {
filename += " (subtemplate of " + string(name) + ")"
}
err = fmt.Errorf("failed to render %s, error: %w", filename, err)
}
}
ctx.ServerError("Render failed", err) ctx.ServerError("Render failed", err)
} }
} }
@ -324,24 +299,25 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
return return
} }
if !setting.IsProd { // it's safe to show internal error to admin users, and it helps
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
ctx.Data["ErrorMsg"] = logErr ctx.Data["ErrorMsg"] = logErr
} }
} }
ctx.Data["Title"] = "Internal Server Error" ctx.Data["Title"] = "Internal Server Error"
ctx.HTML(http.StatusInternalServerError, base.TplName("status/500")) ctx.HTML(http.StatusInternalServerError, tplStatus500)
} }
// NotFoundOrServerError use error check function to determine if the error // NotFoundOrServerError use error check function to determine if the error
// is about not found. It responds with 404 status code for not found error, // is about not found. It responds with 404 status code for not found error,
// or error context description for logging purpose of 500 server error. // or error context description for logging purpose of 500 server error.
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, err error) { func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
if errCheck(err) { if errCheck(logErr) {
ctx.notFoundInternal(logMsg, err) ctx.notFoundInternal(logMsg, logErr)
return return
} }
ctx.serverErrorInternal(logMsg, err) ctx.serverErrorInternal(logMsg, logErr)
} }
// PlainTextBytes renders bytes as plain text // PlainTextBytes renders bytes as plain text

View file

@ -4,6 +4,7 @@
package templates package templates
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"errors" "errors"
@ -18,19 +19,13 @@ import (
"sync/atomic" "sync/atomic"
texttemplate "text/template" texttemplate "text/template"
"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
var ( var rendererKey interface{} = "templatesHtmlRenderer"
rendererKey interface{} = "templatesHtmlRenderer"
templateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
notDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
unexpectedError = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
)
type HTMLRender struct { type HTMLRender struct {
templates atomic.Pointer[template.Template] templates atomic.Pointer[template.Template]
@ -107,11 +102,12 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
renderer := &HTMLRender{} renderer := &HTMLRender{}
if err := renderer.CompileTemplates(); err != nil { if err := renderer.CompileTemplates(); err != nil {
wrapFatal(handleNotDefinedPanicError(err)) p := &templateErrorPrettier{assets: AssetFS()}
wrapFatal(handleUnexpected(err)) wrapFatal(p.handleFuncNotDefinedError(err))
wrapFatal(handleExpectedEnd(err)) wrapFatal(p.handleUnexpectedOperandError(err))
wrapFatal(handleGenericTemplateError(err)) wrapFatal(p.handleExpectedEndError(err))
log.Fatal("HTMLRenderer error: %v", err) wrapFatal(p.handleGenericTemplateError(err))
log.Fatal("HTMLRenderer CompileTemplates error: %v", err)
} }
if !setting.IsProd { if !setting.IsProd {
go AssetFS().WatchLocalChanges(ctx, func() { go AssetFS().WatchLocalChanges(ctx, func() {
@ -123,148 +119,153 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
return context.WithValue(ctx, rendererKey, renderer), renderer return context.WithValue(ctx, rendererKey, renderer), renderer
} }
func wrapFatal(format string, args []interface{}) { func wrapFatal(msg string) {
if format == "" { if msg == "" {
return return
} }
log.FatalWithSkip(1, format, args...) log.FatalWithSkip(1, "Unable to compile templates, %s", msg)
} }
func handleGenericTemplateError(err error) (string, []interface{}) { type templateErrorPrettier struct {
groups := templateError.FindStringSubmatch(err.Error()) assets *assetfs.LayeredFS
}
var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
groups := reGenericTemplateError.FindStringSubmatch(err.Error())
if len(groups) != 4 { if len(groups) != 4 {
return "", nil return ""
}
tmplName, lineStr, message := groups[1], groups[2], groups[3]
return p.makeDetailedError(message, tmplName, lineStr, -1, "")
} }
templateName, lineNumberStr, message := groups[1], groups[2], groups[3] var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
lineNumber, _ := strconv.Atoi(lineNumberStr)
line := GetLineFromTemplate(templateName, lineNumber, "", -1)
return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)} func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
groups := reFuncNotDefinedError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
}
tmplName, lineStr, message, funcName := groups[1], groups[2], groups[3], groups[4]
funcName, _ = strconv.Unquote(`"` + funcName + `"`)
return p.makeDetailedError(message, tmplName, lineStr, -1, funcName)
} }
func handleNotDefinedPanicError(err error) (string, []interface{}) { var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
groups := notDefinedError.FindStringSubmatch(err.Error())
if len(groups) != 4 { func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
return "", nil groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
} }
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
functionName, _ = strconv.Unquote(`"` + functionName + `"`)
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
lineNumber, _ := strconv.Atoi(lineNumberStr)
line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
}
func handleUnexpected(err error) (string, []interface{}) {
groups := unexpectedError.FindStringSubmatch(err.Error())
if len(groups) != 4 {
return "", nil
}
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`) unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl")) return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
lineNumber, _ := strconv.Atoi(lineNumberStr)
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
} }
func handleExpectedEnd(err error) (string, []interface{}) { var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
groups := expectedEndError.FindStringSubmatch(err.Error())
if len(groups) != 4 { func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
return "", nil groups := reExpectedEndError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
}
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
} }
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] var (
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl")) reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
lineNumber, _ := strconv.Atoi(lineNumberStr) reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1) )
return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)} func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
tmplName, lineStr, posStr, msgPart := groups[1], groups[2], groups[3], groups[4]
target := ""
if groups = reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
target = groups[2]
} }
return p.makeDetailedError(msgPart, tmplName, lineStr, posStr, target)
const dashSeparator = "----------------------------------------------------------------------\n" } else if execErr, ok := err.(texttemplate.ExecError); ok {
layerName := p.assets.GetFileLayerName(execErr.Name + ".tmpl")
// GetLineFromTemplate returns a line from a template with some context return fmt.Sprintf("asset from: %s, %s", layerName, err.Error())
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
bs, err := AssetFS().ReadFile(templateName + ".tmpl")
if err != nil {
return fmt.Sprintf("(unable to read template file: %v)", err)
}
sb := &strings.Builder{}
// Write the header
sb.WriteString(dashSeparator)
var lineBs []byte
// Iterate through the lines from the asset file to find the target line
for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
// Find the next new line
end := bytes.IndexByte(bs[start:], '\n')
// adjust the end to be a direct pointer in to []byte
if end < 0 {
end = len(bs)
} else { } else {
end += start return err.Error()
}
// set lineBs to the current line []byte
lineBs = bs[start:end]
// move start to after the current new line position
start = end + 1
// Write 2 preceding lines + the target line
if targetLineNum-currentLineNum < 3 {
_, _ = sb.Write(lineBs)
_ = sb.WriteByte('\n')
} }
} }
// FIXME: this algorithm could provide incorrect results and mislead the developers. func HandleTemplateRenderingError(err error) string {
// For example: Undefined function "file" in template ..... p := &templateErrorPrettier{assets: AssetFS()}
// {{Func .file.Addition file.Deletion .file.Addition}} return p.handleTemplateRenderingError(err)
// ^^^^ ^(the real error is here)
// The pointer is added to the first one, but the second one is the real incorrect one.
//
// If there is a provided target to look for in the line add a pointer to it
// e.g. ^^^^^^^
if target != "" {
targetPos := bytes.Index(lineBs, []byte(target))
if targetPos >= 0 {
position = targetPos
}
}
if position >= 0 {
// take the current line and replace preceding text with whitespace (except for tab)
for i := range lineBs[:position] {
if lineBs[i] != '\t' {
lineBs[i] = ' '
}
} }
// write the preceding "space" const dashSeparator = "----------------------------------------------------------------------"
_, _ = sb.Write(lineBs[:position])
// Now write the ^^ pointer func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string {
targetLen := len(target) code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl")
if targetLen == 0 { if err != nil {
targetLen = 1 return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName)
} }
_, _ = sb.WriteString(strings.Repeat("^", targetLen)) line, err := util.ToInt64(lineNum)
_ = sb.WriteByte('\n') if err != nil {
return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum)
}
pos, err := util.ToInt64(posNum)
if err != nil {
return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum)
}
detail := extractErrorLine(code, int(line), int(pos), target)
var msg string
if pos >= 0 {
msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg)
} else {
msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg)
}
return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
} }
// Finally write the footer func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
sb.WriteString(dashSeparator) b := bufio.NewReader(bytes.NewReader(code))
var line []byte
return sb.String() var err error
for i := 0; i < lineNum; i++ {
if line, err = b.ReadBytes('\n'); err != nil {
if i == lineNum-1 && errors.Is(err, io.EOF) {
err = nil
}
break
}
}
if err != nil {
return fmt.Sprintf("unable to find target line %d", lineNum)
}
line = bytes.TrimRight(line, "\r\n")
var indicatorLine []byte
targetBytes := []byte(target)
targetLen := len(targetBytes)
for i := 0; i < len(line); {
if posNum == -1 && target != "" && bytes.HasPrefix(line[i:], targetBytes) {
for j := 0; j < targetLen && i < len(line); j++ {
indicatorLine = append(indicatorLine, '^')
i++
}
} else if i == posNum {
indicatorLine = append(indicatorLine, '^')
i++
} else {
if line[i] == '\t' {
indicatorLine = append(indicatorLine, '\t')
} else {
indicatorLine = append(indicatorLine, ' ')
}
i++
}
}
// if the indicatorLine only contains spaces, trim it together
return strings.TrimRight(string(line)+"\n"+string(indicatorLine), " \t\r\n")
} }

View file

@ -0,0 +1,106 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"errors"
"html/template"
"os"
"strings"
"testing"
"code.gitea.io/gitea/modules/assetfs"
"github.com/stretchr/testify/assert"
)
func TestExtractErrorLine(t *testing.T) {
cases := []struct {
code string
line int
pos int
target string
expect string
}{
{"hello world\nfoo bar foo bar\ntest", 2, -1, "bar", `
foo bar foo bar
^^^ ^^^
`},
{"hello world\nfoo bar foo bar\ntest", 2, 4, "bar", `
foo bar foo bar
^
`},
{
"hello world\nfoo bar foo bar\ntest", 2, 4, "",
`
foo bar foo bar
^
`,
},
{
"hello world\nfoo bar foo bar\ntest", 5, 0, "",
`unable to find target line 5`,
},
}
for _, c := range cases {
actual := extractErrorLine([]byte(c.code), c.line, c.pos, c.target)
assert.Equal(t, strings.TrimSpace(c.expect), strings.TrimSpace(actual))
}
}
func TestHandleError(t *testing.T) {
dir := t.TempDir()
p := &templateErrorPrettier{assets: assetfs.Layered(assetfs.Local("tmp", dir))}
test := func(s string, h func(error) string, expect string) {
err := os.WriteFile(dir+"/test.tmpl", []byte(s), 0o644)
assert.NoError(t, err)
tmpl := template.New("test")
_, err = tmpl.Parse(s)
assert.Error(t, err)
msg := h(err)
assert.EqualValues(t, strings.TrimSpace(expect), strings.TrimSpace(msg))
}
test("{{", p.handleGenericTemplateError, `
template error: tmp:test:1 : unclosed action
----------------------------------------------------------------------
{{
----------------------------------------------------------------------
`)
test("{{Func}}", p.handleFuncNotDefinedError, `
template error: tmp:test:1 : function "Func" not defined
----------------------------------------------------------------------
{{Func}}
^^^^
----------------------------------------------------------------------
`)
test("{{'x'3}}", p.handleUnexpectedOperandError, `
template error: tmp:test:1 : unexpected "3" in operand
----------------------------------------------------------------------
{{'x'3}}
^
----------------------------------------------------------------------
`)
// no idea about how to trigger such strange error, so mock an error to test it
err := os.WriteFile(dir+"/test.tmpl", []byte("god knows XXX"), 0o644)
assert.NoError(t, err)
expectedMsg := `
template error: tmp:test:1 : expected end; found XXX
----------------------------------------------------------------------
god knows XXX
^^^
----------------------------------------------------------------------
`
actualMsg := p.handleExpectedEndError(errors.New("template: test:1: expected end; found XXX"))
assert.EqualValues(t, strings.TrimSpace(expectedMsg), strings.TrimSpace(actualMsg))
}

View file

@ -6,6 +6,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
<script> <script>
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);}); window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
window.config = { window.config = {
initCount: (window.config?.initCount ?? 0) + 1,
appUrl: '{{AppUrl}}', appUrl: '{{AppUrl}}',
appSubUrl: '{{AppSubUrl}}', appSubUrl: '{{AppSubUrl}}',
assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly

View file

@ -0,0 +1,3 @@
sub template triggers an executing error
{{.locale.NoSuch "asdf"}}

View file

@ -0,0 +1,12 @@
{{template "base/head" .}}
<div class="page-content devtest">
<div class="gt-df">
<div style="width: 80%; ">
hello hello hello hello hello hello hello hello hello hello
</div>
<div style="width: 20%;">
{{template "devtest/tmplerr-sub" .}}
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -1,5 +1,5 @@
{{template "base/head" .}} {{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content ui container center gt-full-screen-width {{if .IsRepo}}repository{{end}}"> <div role="main" aria-label="{{.Title}}" class="page-content ui container center gt-w-screen {{if .IsRepo}}repository{{end}}">
{{if .IsRepo}}{{template "repo/header" .}}{{end}} {{if .IsRepo}}{{template "repo/header" .}}{{end}}
<div class="ui container center"> <div class="ui container center">
<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p> <p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p>

View file

@ -1,13 +1,36 @@
{{template "base/head" .}} {{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content ui container gt-full-screen-width center"> <div role="main" aria-label="{{.Title}}" class="page-content gt-w-screen status-page-500">
<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/500.png" alt="500"></p> <p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
<div class="ui divider"></div> <div class="ui divider"></div>
<br>
<div class="ui container gt-mt-5">
{{if .ErrorMsg}} {{if .ErrorMsg}}
<p>{{.locale.Tr "error.occurred"}}:</p> <p>{{.locale.Tr "error.occurred"}}:</p>
<pre style="text-align: left">{{.ErrorMsg}}</pre> <pre class="gt-whitespace-pre-wrap">{{.ErrorMsg}}</pre>
{{end}} {{end}}
<div class="center gt-mt-5">
{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}} {{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
{{if .IsAdmin}}<p>{{.locale.Tr "error.report_message" | Safe}}</p>{{end}} {{if .IsAdmin}}<p>{{.locale.Tr "error.report_message" | Safe}}</p>{{end}}
</div> </div>
</div>
</div>
{{/* when a sub-template triggers an 500 error, its parent template has been partially rendered,
then the 500 page will be rendered after that partially rendered page, the HTML/JS are totally broken.
so use this inline script to try to move it to main viewport */}}
<script type="module">
const embedded = document.querySelector('.page-content .page-content.status-page-500');
if (embedded) {
// move footer to main view
const footer = document.querySelector('footer');
if (footer) document.querySelector('body').append(footer);
// move the 500 error page content to main view
const embeddedParent = embedded.parentNode;
let main = document.querySelector('.page-content');
main = main ?? document.querySelector('body');
main.prepend(document.createElement('hr'));
main.prepend(embedded);
embeddedParent.remove(); // remove the unrelated 500-page elements (eg: the duplicate nav bar)
}
</script>
{{template "base/footer" .}} {{template "base/footer" .}}

View file

@ -46,8 +46,8 @@
text-overflow: ellipsis !important; text-overflow: ellipsis !important;
} }
.gt-full-screen-width { width: 100vw !important; } .gt-w-screen { width: 100vw !important; }
.gt-full-screen-height { height: 100vh !important; } .gt-h-screen { height: 100vh !important; }
.gt-rounded { border-radius: var(--border-radius) !important; } .gt-rounded { border-radius: var(--border-radius) !important; }
.gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; } .gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; }
@ -202,6 +202,7 @@
.gt-shrink-0 { flex-shrink: 0 !important; } .gt-shrink-0 { flex-shrink: 0 !important; }
.gt-whitespace-nowrap { white-space: nowrap !important; } .gt-whitespace-nowrap { white-space: nowrap !important; }
.gt-whitespace-pre-wrap { white-space: pre-wrap !important; }
@media (max-width: 767px) { @media (max-width: 767px) {
.gt-db-small { display: block !important; } .gt-db-small { display: block !important; }

View file

@ -20,6 +20,10 @@ export function showGlobalErrorMessage(msg) {
* @param {ErrorEvent} e * @param {ErrorEvent} e
*/ */
function processWindowErrorEvent(e) { function processWindowErrorEvent(e) {
if (window.config.initCount > 1) {
// the page content has been loaded many times, the HTML/JS are totally broken, don't need to show error message
return;
}
if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) { if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
// At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240 // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
// If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
@ -33,7 +37,13 @@ function initGlobalErrorHandler() {
if (!window.config) { if (!window.config) {
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
} }
if (window.config.initCount > 1) {
// when a sub-templates triggers an 500 error, its parent template has been partially rendered,
// then the 500 page will be rendered after that partially rendered page, which will cause the initCount > 1
// in this case, the page is totally broken, so do not do any further error handling
console.error('initGlobalErrorHandler: Gitea global config system has already been initialized, there must be something else wrong');
return;
}
// we added an event handler for window error at the very beginning of <script> of page head // we added an event handler for window error at the very beginning of <script> of page head
// the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init // the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init
// then in this init, we can collect all error events and show them // then in this init, we can collect all error events and show them