Skip to content

Commit 1c8bc40

Browse files
authoredApr 14, 2023
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)
1 parent 5768baf commit 1c8bc40

File tree

10 files changed

+305
-172
lines changed

10 files changed

+305
-172
lines changed
 

‎modules/context/context.go

+10-34
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ import (
1616
"net/http"
1717
"net/url"
1818
"path"
19-
"regexp"
2019
"strconv"
2120
"strings"
22-
texttemplate "text/template"
2321
"time"
2422

2523
"code.gitea.io/gitea/models/db"
@@ -216,7 +214,7 @@ func (ctx *Context) RedirectToFirst(location ...string) {
216214
ctx.Redirect(setting.AppSubURL + "/")
217215
}
218216

219-
var templateExecutingErr = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): executing (?:"(.*)" at <(.*)>: )?`)
217+
const tplStatus500 base.TplName = "status/500"
220218

221219
// HTML calls Context.HTML and renders the template to HTTP response
222220
func (ctx *Context) HTML(status int, name base.TplName) {
@@ -229,34 +227,11 @@ func (ctx *Context) HTML(status int, name base.TplName) {
229227
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
230228
}
231229
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
232-
if status == http.StatusInternalServerError && name == base.TplName("status/500") {
230+
if status == http.StatusInternalServerError && name == tplStatus500 {
233231
ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.")
234232
return
235233
}
236-
if execErr, ok := err.(texttemplate.ExecError); ok {
237-
if groups := templateExecutingErr.FindStringSubmatch(err.Error()); len(groups) > 0 {
238-
errorTemplateName, lineStr, posStr := groups[1], groups[2], groups[3]
239-
target := ""
240-
if len(groups) == 6 {
241-
target = groups[5]
242-
}
243-
line, _ := strconv.Atoi(lineStr) // Cannot error out as groups[2] is [1-9][0-9]*
244-
pos, _ := strconv.Atoi(posStr) // Cannot error out as groups[3] is [1-9][0-9]*
245-
assetLayerName := templates.AssetFS().GetFileLayerName(errorTemplateName + ".tmpl")
246-
filename := fmt.Sprintf("(%s) %s", assetLayerName, errorTemplateName)
247-
if errorTemplateName != string(name) {
248-
filename += " (subtemplate of " + string(name) + ")"
249-
}
250-
err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
251-
} else {
252-
assetLayerName := templates.AssetFS().GetFileLayerName(execErr.Name + ".tmpl")
253-
filename := fmt.Sprintf("(%s) %s", assetLayerName, execErr.Name)
254-
if execErr.Name != string(name) {
255-
filename += " (subtemplate of " + string(name) + ")"
256-
}
257-
err = fmt.Errorf("failed to render %s, error: %w", filename, err)
258-
}
259-
}
234+
err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
260235
ctx.ServerError("Render failed", err)
261236
}
262237
}
@@ -324,24 +299,25 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
324299
return
325300
}
326301

327-
if !setting.IsProd {
302+
// it's safe to show internal error to admin users, and it helps
303+
if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
328304
ctx.Data["ErrorMsg"] = logErr
329305
}
330306
}
331307

332308
ctx.Data["Title"] = "Internal Server Error"
333-
ctx.HTML(http.StatusInternalServerError, base.TplName("status/500"))
309+
ctx.HTML(http.StatusInternalServerError, tplStatus500)
334310
}
335311

336312
// NotFoundOrServerError use error check function to determine if the error
337313
// is about not found. It responds with 404 status code for not found error,
338314
// or error context description for logging purpose of 500 server error.
339-
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, err error) {
340-
if errCheck(err) {
341-
ctx.notFoundInternal(logMsg, err)
315+
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
316+
if errCheck(logErr) {
317+
ctx.notFoundInternal(logMsg, logErr)
342318
return
343319
}
344-
ctx.serverErrorInternal(logMsg, err)
320+
ctx.serverErrorInternal(logMsg, logErr)
345321
}
346322

347323
// PlainTextBytes renders bytes as plain text

‎modules/templates/htmlrenderer.go

+126-125
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package templates
55

66
import (
7+
"bufio"
78
"bytes"
89
"context"
910
"errors"
@@ -18,19 +19,13 @@ import (
1819
"sync/atomic"
1920
texttemplate "text/template"
2021

22+
"code.gitea.io/gitea/modules/assetfs"
2123
"code.gitea.io/gitea/modules/log"
2224
"code.gitea.io/gitea/modules/setting"
2325
"code.gitea.io/gitea/modules/util"
2426
)
2527

26-
var (
27-
rendererKey interface{} = "templatesHtmlRenderer"
28-
29-
templateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
30-
notDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
31-
unexpectedError = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
32-
expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
33-
)
28+
var rendererKey interface{} = "templatesHtmlRenderer"
3429

3530
type HTMLRender struct {
3631
templates atomic.Pointer[template.Template]
@@ -107,11 +102,12 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
107102

108103
renderer := &HTMLRender{}
109104
if err := renderer.CompileTemplates(); err != nil {
110-
wrapFatal(handleNotDefinedPanicError(err))
111-
wrapFatal(handleUnexpected(err))
112-
wrapFatal(handleExpectedEnd(err))
113-
wrapFatal(handleGenericTemplateError(err))
114-
log.Fatal("HTMLRenderer error: %v", err)
105+
p := &templateErrorPrettier{assets: AssetFS()}
106+
wrapFatal(p.handleFuncNotDefinedError(err))
107+
wrapFatal(p.handleUnexpectedOperandError(err))
108+
wrapFatal(p.handleExpectedEndError(err))
109+
wrapFatal(p.handleGenericTemplateError(err))
110+
log.Fatal("HTMLRenderer CompileTemplates error: %v", err)
115111
}
116112
if !setting.IsProd {
117113
go AssetFS().WatchLocalChanges(ctx, func() {
@@ -123,148 +119,153 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
123119
return context.WithValue(ctx, rendererKey, renderer), renderer
124120
}
125121

126-
func wrapFatal(format string, args []interface{}) {
127-
if format == "" {
122+
func wrapFatal(msg string) {
123+
if msg == "" {
128124
return
129125
}
130-
log.FatalWithSkip(1, format, args...)
126+
log.FatalWithSkip(1, "Unable to compile templates, %s", msg)
131127
}
132128

133-
func handleGenericTemplateError(err error) (string, []interface{}) {
134-
groups := templateError.FindStringSubmatch(err.Error())
135-
if len(groups) != 4 {
136-
return "", nil
137-
}
138-
139-
templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
140-
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
141-
lineNumber, _ := strconv.Atoi(lineNumberStr)
142-
line := GetLineFromTemplate(templateName, lineNumber, "", -1)
143-
144-
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)}
129+
type templateErrorPrettier struct {
130+
assets *assetfs.LayeredFS
145131
}
146132

147-
func handleNotDefinedPanicError(err error) (string, []interface{}) {
148-
groups := notDefinedError.FindStringSubmatch(err.Error())
149-
if len(groups) != 4 {
150-
return "", nil
151-
}
152-
153-
templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
154-
functionName, _ = strconv.Unquote(`"` + functionName + `"`)
155-
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
156-
lineNumber, _ := strconv.Atoi(lineNumberStr)
157-
line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
133+
var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
158134

159-
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)}
160-
}
161-
162-
func handleUnexpected(err error) (string, []interface{}) {
163-
groups := unexpectedError.FindStringSubmatch(err.Error())
135+
func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
136+
groups := reGenericTemplateError.FindStringSubmatch(err.Error())
164137
if len(groups) != 4 {
165-
return "", nil
138+
return ""
166139
}
167-
168-
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
169-
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
170-
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
171-
lineNumber, _ := strconv.Atoi(lineNumberStr)
172-
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
173-
174-
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)}
140+
tmplName, lineStr, message := groups[1], groups[2], groups[3]
141+
return p.makeDetailedError(message, tmplName, lineStr, -1, "")
175142
}
176143

177-
func handleExpectedEnd(err error) (string, []interface{}) {
178-
groups := expectedEndError.FindStringSubmatch(err.Error())
179-
if len(groups) != 4 {
180-
return "", nil
181-
}
182-
183-
templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
184-
filename := fmt.Sprintf("%s (provided by %s)", templateName, AssetFS().GetFileLayerName(templateName+".tmpl"))
185-
lineNumber, _ := strconv.Atoi(lineNumberStr)
186-
line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
144+
var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
187145

188-
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)}
146+
func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
147+
groups := reFuncNotDefinedError.FindStringSubmatch(err.Error())
148+
if len(groups) != 5 {
149+
return ""
150+
}
151+
tmplName, lineStr, message, funcName := groups[1], groups[2], groups[3], groups[4]
152+
funcName, _ = strconv.Unquote(`"` + funcName + `"`)
153+
return p.makeDetailedError(message, tmplName, lineStr, -1, funcName)
189154
}
190155

191-
const dashSeparator = "----------------------------------------------------------------------\n"
156+
var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
192157

193-
// GetLineFromTemplate returns a line from a template with some context
194-
func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
195-
bs, err := AssetFS().ReadFile(templateName + ".tmpl")
196-
if err != nil {
197-
return fmt.Sprintf("(unable to read template file: %v)", err)
158+
func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
159+
groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
160+
if len(groups) != 5 {
161+
return ""
198162
}
163+
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
164+
unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
165+
return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
166+
}
199167

200-
sb := &strings.Builder{}
201-
202-
// Write the header
203-
sb.WriteString(dashSeparator)
168+
var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
204169

205-
var lineBs []byte
170+
func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
171+
groups := reExpectedEndError.FindStringSubmatch(err.Error())
172+
if len(groups) != 5 {
173+
return ""
174+
}
175+
tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
176+
return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
177+
}
206178

207-
// Iterate through the lines from the asset file to find the target line
208-
for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
209-
// Find the next new line
210-
end := bytes.IndexByte(bs[start:], '\n')
179+
var (
180+
reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
181+
reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
182+
)
211183

212-
// adjust the end to be a direct pointer in to []byte
213-
if end < 0 {
214-
end = len(bs)
215-
} else {
216-
end += start
184+
func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
185+
if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
186+
tmplName, lineStr, posStr, msgPart := groups[1], groups[2], groups[3], groups[4]
187+
target := ""
188+
if groups = reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
189+
target = groups[2]
217190
}
191+
return p.makeDetailedError(msgPart, tmplName, lineStr, posStr, target)
192+
} else if execErr, ok := err.(texttemplate.ExecError); ok {
193+
layerName := p.assets.GetFileLayerName(execErr.Name + ".tmpl")
194+
return fmt.Sprintf("asset from: %s, %s", layerName, err.Error())
195+
} else {
196+
return err.Error()
197+
}
198+
}
218199

219-
// set lineBs to the current line []byte
220-
lineBs = bs[start:end]
200+
func HandleTemplateRenderingError(err error) string {
201+
p := &templateErrorPrettier{assets: AssetFS()}
202+
return p.handleTemplateRenderingError(err)
203+
}
221204

222-
// move start to after the current new line position
223-
start = end + 1
205+
const dashSeparator = "----------------------------------------------------------------------"
224206

225-
// Write 2 preceding lines + the target line
226-
if targetLineNum-currentLineNum < 3 {
227-
_, _ = sb.Write(lineBs)
228-
_ = sb.WriteByte('\n')
229-
}
207+
func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string {
208+
code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl")
209+
if err != nil {
210+
return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName)
211+
}
212+
line, err := util.ToInt64(lineNum)
213+
if err != nil {
214+
return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum)
215+
}
216+
pos, err := util.ToInt64(posNum)
217+
if err != nil {
218+
return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum)
230219
}
220+
detail := extractErrorLine(code, int(line), int(pos), target)
231221

232-
// FIXME: this algorithm could provide incorrect results and mislead the developers.
233-
// For example: Undefined function "file" in template .....
234-
// {{Func .file.Addition file.Deletion .file.Addition}}
235-
// ^^^^ ^(the real error is here)
236-
// The pointer is added to the first one, but the second one is the real incorrect one.
237-
//
238-
// If there is a provided target to look for in the line add a pointer to it
239-
// e.g. ^^^^^^^
240-
if target != "" {
241-
targetPos := bytes.Index(lineBs, []byte(target))
242-
if targetPos >= 0 {
243-
position = targetPos
244-
}
222+
var msg string
223+
if pos >= 0 {
224+
msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg)
225+
} else {
226+
msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg)
245227
}
246-
if position >= 0 {
247-
// take the current line and replace preceding text with whitespace (except for tab)
248-
for i := range lineBs[:position] {
249-
if lineBs[i] != '\t' {
250-
lineBs[i] = ' '
228+
return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
229+
}
230+
231+
func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
232+
b := bufio.NewReader(bytes.NewReader(code))
233+
var line []byte
234+
var err error
235+
for i := 0; i < lineNum; i++ {
236+
if line, err = b.ReadBytes('\n'); err != nil {
237+
if i == lineNum-1 && errors.Is(err, io.EOF) {
238+
err = nil
251239
}
240+
break
252241
}
242+
}
243+
if err != nil {
244+
return fmt.Sprintf("unable to find target line %d", lineNum)
245+
}
253246

254-
// write the preceding "space"
255-
_, _ = sb.Write(lineBs[:position])
256-
257-
// Now write the ^^ pointer
258-
targetLen := len(target)
259-
if targetLen == 0 {
260-
targetLen = 1
247+
line = bytes.TrimRight(line, "\r\n")
248+
var indicatorLine []byte
249+
targetBytes := []byte(target)
250+
targetLen := len(targetBytes)
251+
for i := 0; i < len(line); {
252+
if posNum == -1 && target != "" && bytes.HasPrefix(line[i:], targetBytes) {
253+
for j := 0; j < targetLen && i < len(line); j++ {
254+
indicatorLine = append(indicatorLine, '^')
255+
i++
256+
}
257+
} else if i == posNum {
258+
indicatorLine = append(indicatorLine, '^')
259+
i++
260+
} else {
261+
if line[i] == '\t' {
262+
indicatorLine = append(indicatorLine, '\t')
263+
} else {
264+
indicatorLine = append(indicatorLine, ' ')
265+
}
266+
i++
261267
}
262-
_, _ = sb.WriteString(strings.Repeat("^", targetLen))
263-
_ = sb.WriteByte('\n')
264268
}
265-
266-
// Finally write the footer
267-
sb.WriteString(dashSeparator)
268-
269-
return sb.String()
269+
// if the indicatorLine only contains spaces, trim it together
270+
return strings.TrimRight(string(line)+"\n"+string(indicatorLine), " \t\r\n")
270271
}
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package templates
5+
6+
import (
7+
"errors"
8+
"html/template"
9+
"os"
10+
"strings"
11+
"testing"
12+
13+
"code.gitea.io/gitea/modules/assetfs"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestExtractErrorLine(t *testing.T) {
19+
cases := []struct {
20+
code string
21+
line int
22+
pos int
23+
target string
24+
expect string
25+
}{
26+
{"hello world\nfoo bar foo bar\ntest", 2, -1, "bar", `
27+
foo bar foo bar
28+
^^^ ^^^
29+
`},
30+
31+
{"hello world\nfoo bar foo bar\ntest", 2, 4, "bar", `
32+
foo bar foo bar
33+
^
34+
`},
35+
36+
{
37+
"hello world\nfoo bar foo bar\ntest", 2, 4, "",
38+
`
39+
foo bar foo bar
40+
^
41+
`,
42+
},
43+
44+
{
45+
"hello world\nfoo bar foo bar\ntest", 5, 0, "",
46+
`unable to find target line 5`,
47+
},
48+
}
49+
50+
for _, c := range cases {
51+
actual := extractErrorLine([]byte(c.code), c.line, c.pos, c.target)
52+
assert.Equal(t, strings.TrimSpace(c.expect), strings.TrimSpace(actual))
53+
}
54+
}
55+
56+
func TestHandleError(t *testing.T) {
57+
dir := t.TempDir()
58+
59+
p := &templateErrorPrettier{assets: assetfs.Layered(assetfs.Local("tmp", dir))}
60+
61+
test := func(s string, h func(error) string, expect string) {
62+
err := os.WriteFile(dir+"/test.tmpl", []byte(s), 0o644)
63+
assert.NoError(t, err)
64+
tmpl := template.New("test")
65+
_, err = tmpl.Parse(s)
66+
assert.Error(t, err)
67+
msg := h(err)
68+
assert.EqualValues(t, strings.TrimSpace(expect), strings.TrimSpace(msg))
69+
}
70+
71+
test("{{", p.handleGenericTemplateError, `
72+
template error: tmp:test:1 : unclosed action
73+
----------------------------------------------------------------------
74+
{{
75+
----------------------------------------------------------------------
76+
`)
77+
78+
test("{{Func}}", p.handleFuncNotDefinedError, `
79+
template error: tmp:test:1 : function "Func" not defined
80+
----------------------------------------------------------------------
81+
{{Func}}
82+
^^^^
83+
----------------------------------------------------------------------
84+
`)
85+
86+
test("{{'x'3}}", p.handleUnexpectedOperandError, `
87+
template error: tmp:test:1 : unexpected "3" in operand
88+
----------------------------------------------------------------------
89+
{{'x'3}}
90+
^
91+
----------------------------------------------------------------------
92+
`)
93+
94+
// no idea about how to trigger such strange error, so mock an error to test it
95+
err := os.WriteFile(dir+"/test.tmpl", []byte("god knows XXX"), 0o644)
96+
assert.NoError(t, err)
97+
expectedMsg := `
98+
template error: tmp:test:1 : expected end; found XXX
99+
----------------------------------------------------------------------
100+
god knows XXX
101+
^^^
102+
----------------------------------------------------------------------
103+
`
104+
actualMsg := p.handleExpectedEndError(errors.New("template: test:1: expected end; found XXX"))
105+
assert.EqualValues(t, strings.TrimSpace(expectedMsg), strings.TrimSpace(actualMsg))
106+
}

‎templates/base/head_script.tmpl

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

‎templates/devtest/tmplerr-sub.tmpl

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sub template triggers an executing error
2+
3+
{{.locale.NoSuch "asdf"}}

‎templates/devtest/tmplerr.tmpl

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{{template "base/head" .}}
2+
<div class="page-content devtest">
3+
<div class="gt-df">
4+
<div style="width: 80%; ">
5+
hello hello hello hello hello hello hello hello hello hello
6+
</div>
7+
<div style="width: 20%;">
8+
{{template "devtest/tmplerr-sub" .}}
9+
</div>
10+
</div>
11+
</div>
12+
{{template "base/footer" .}}

‎templates/status/404.tmpl

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

‎templates/status/500.tmpl

+32-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
{{template "base/head" .}}
2-
<div role="main" aria-label="{{.Title}}" class="page-content ui container gt-full-screen-width center">
3-
<p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/500.png" alt="500"></p>
2+
<div role="main" aria-label="{{.Title}}" class="page-content gt-w-screen status-page-500">
3+
<p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p>
44
<div class="ui divider"></div>
5-
<br>
6-
{{if .ErrorMsg}}
7-
<p>{{.locale.Tr "error.occurred"}}:</p>
8-
<pre style="text-align: left">{{.ErrorMsg}}</pre>
9-
{{end}}
10-
{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
11-
{{if .IsAdmin}}<p>{{.locale.Tr "error.report_message" | Safe}}</p>{{end}}
5+
6+
<div class="ui container gt-mt-5">
7+
{{if .ErrorMsg}}
8+
<p>{{.locale.Tr "error.occurred"}}:</p>
9+
<pre class="gt-whitespace-pre-wrap">{{.ErrorMsg}}</pre>
10+
{{end}}
11+
12+
<div class="center gt-mt-5">
13+
{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
14+
{{if .IsAdmin}}<p>{{.locale.Tr "error.report_message" | Safe}}</p>{{end}}
15+
</div>
16+
</div>
1217
</div>
18+
{{/* when a sub-template triggers an 500 error, its parent template has been partially rendered,
19+
then the 500 page will be rendered after that partially rendered page, the HTML/JS are totally broken.
20+
so use this inline script to try to move it to main viewport */}}
21+
<script type="module">
22+
const embedded = document.querySelector('.page-content .page-content.status-page-500');
23+
if (embedded) {
24+
// move footer to main view
25+
const footer = document.querySelector('footer');
26+
if (footer) document.querySelector('body').append(footer);
27+
// move the 500 error page content to main view
28+
const embeddedParent = embedded.parentNode;
29+
let main = document.querySelector('.page-content');
30+
main = main ?? document.querySelector('body');
31+
main.prepend(document.createElement('hr'));
32+
main.prepend(embedded);
33+
embeddedParent.remove(); // remove the unrelated 500-page elements (eg: the duplicate nav bar)
34+
}
35+
</script>
1336
{{template "base/footer" .}}

‎web_src/css/helpers.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
text-overflow: ellipsis !important;
4747
}
4848

49-
.gt-full-screen-width { width: 100vw !important; }
50-
.gt-full-screen-height { height: 100vh !important; }
49+
.gt-w-screen { width: 100vw !important; }
50+
.gt-h-screen { height: 100vh !important; }
5151

5252
.gt-rounded { border-radius: var(--border-radius) !important; }
5353
.gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; }
@@ -202,6 +202,7 @@
202202

203203
.gt-shrink-0 { flex-shrink: 0 !important; }
204204
.gt-whitespace-nowrap { white-space: nowrap !important; }
205+
.gt-whitespace-pre-wrap { white-space: pre-wrap !important; }
205206

206207
@media (max-width: 767px) {
207208
.gt-db-small { display: block !important; }

‎web_src/js/bootstrap.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function showGlobalErrorMessage(msg) {
2020
* @param {ErrorEvent} e
2121
*/
2222
function processWindowErrorEvent(e) {
23+
if (window.config.initCount > 1) {
24+
// the page content has been loaded many times, the HTML/JS are totally broken, don't need to show error message
25+
return;
26+
}
2327
if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
2428
// At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
2529
// 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() {
3337
if (!window.config) {
3438
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
3539
}
36-
40+
if (window.config.initCount > 1) {
41+
// when a sub-templates triggers an 500 error, its parent template has been partially rendered,
42+
// then the 500 page will be rendered after that partially rendered page, which will cause the initCount > 1
43+
// in this case, the page is totally broken, so do not do any further error handling
44+
console.error('initGlobalErrorHandler: Gitea global config system has already been initialized, there must be something else wrong');
45+
return;
46+
}
3747
// we added an event handler for window error at the very beginning of <script> of page head
3848
// the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init
3949
// then in this init, we can collect all error events and show them

0 commit comments

Comments
 (0)
Please sign in to comment.