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)
pull/24109/head
wxiaoguang 2 years ago committed by GitHub
parent 5768bafeb2
commit 1c8bc4081a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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
if len(groups) != 4 {
return "", nil
} }
templateName, lineNumberStr, message := groups[1], groups[2], groups[3] var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
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 handleNotDefinedPanicError(err error) (string, []interface{}) { func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
groups := notDefinedError.FindStringSubmatch(err.Error()) 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, functionName := groups[1], groups[2], groups[3] var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
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 (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]
func handleUnexpected(err error) (string, []interface{}) { funcName, _ = strconv.Unquote(`"` + funcName + `"`)
groups := unexpectedError.FindStringSubmatch(err.Error()) return p.makeDetailedError(message, tmplName, lineStr, -1, funcName)
if len(groups) != 4 {
return "", nil
} }
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
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 (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
} }
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
func handleExpectedEnd(err error) (string, []interface{}) { unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
groups := expectedEndError.FindStringSubmatch(err.Error()) return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
if len(groups) != 4 {
return "", nil
} }
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
lineNumber, _ := strconv.Atoi(lineNumberStr)
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) handleExpectedEndError(err error) string {
groups := reExpectedEndError.FindStringSubmatch(err.Error())
if len(groups) != 5 {
return ""
} }
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
const dashSeparator = "----------------------------------------------------------------------\n" return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
// GetLineFromTemplate returns a line from a template with some context
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{} var (
reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
// Write the header reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
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 func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
if end < 0 { if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
end = len(bs) 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)
} else if execErr, ok := err.(texttemplate.ExecError); ok {
layerName := p.assets.GetFileLayerName(execErr.Name + ".tmpl")
return fmt.Sprintf("asset from: %s, %s", layerName, err.Error())
} else { } else {
end += start return err.Error()
}
} }
// set lineBs to the current line []byte func HandleTemplateRenderingError(err error) string {
lineBs = bs[start:end] p := &templateErrorPrettier{assets: AssetFS()}
return p.handleTemplateRenderingError(err)
}
// move start to after the current new line position const dashSeparator = "----------------------------------------------------------------------"
start = end + 1
// Write 2 preceding lines + the target line func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string {
if targetLineNum-currentLineNum < 3 { code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl")
_, _ = sb.Write(lineBs) if err != nil {
_ = sb.WriteByte('\n') return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName)
} }
line, err := util.ToInt64(lineNum)
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)
// FIXME: this algorithm could provide incorrect results and mislead the developers. var msg string
// For example: Undefined function "file" in template ..... if pos >= 0 {
// {{Func .file.Addition file.Deletion .file.Addition}} msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg)
// ^^^^ ^(the real error is here) } else {
// The pointer is added to the first one, but the second one is the real incorrect one. msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg)
//
// 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] = ' '
} }
return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
} }
// write the preceding "space" func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
_, _ = sb.Write(lineBs[:position]) b := bufio.NewReader(bytes.NewReader(code))
var line []byte
// Now write the ^^ pointer var err error
targetLen := len(target) for i := 0; i < lineNum; i++ {
if targetLen == 0 { if line, err = b.ReadBytes('\n'); err != nil {
targetLen = 1 if i == lineNum-1 && errors.Is(err, io.EOF) {
err = nil
} }
_, _ = sb.WriteString(strings.Repeat("^", targetLen)) break
_ = sb.WriteByte('\n')
} }
}
// Finally write the footer if err != nil {
sb.WriteString(dashSeparator) return fmt.Sprintf("unable to find target line %d", lineNum)
}
return sb.String()
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")
} }

@ -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))
}

@ -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

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

@ -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" .}}

@ -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>

@ -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" .}}

@ -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; }

@ -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

Loading…
Cancel
Save