mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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>  </details> ### After The details could be shown to site admin users. It is safe. 
This commit is contained in:
		| @@ -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] |  | ||||||
| 	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{}) { | var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`) | ||||||
| 	groups := notDefinedError.FindStringSubmatch(err.Error()) |  | ||||||
|  | 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] | ||||||
| 	templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3] | 	return p.makeDetailedError(message, tmplName, lineStr, -1, "") | ||||||
| 	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{}) { | var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`) | ||||||
| 	groups := unexpectedError.FindStringSubmatch(err.Error()) |  | ||||||
| 	if len(groups) != 4 { |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] | 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) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`) | ||||||
|  |  | ||||||
|  | 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] | ||||||
| 	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] | ||||||
| 	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] | 	return p.makeDetailedError(message, tmplName, lineStr, -1, 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!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const dashSeparator = "----------------------------------------------------------------------\n" | var ( | ||||||
|  | 	reTemplateExecutingError    = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`) | ||||||
|  | 	reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `) | ||||||
|  | ) | ||||||
|  |  | ||||||
| // GetLineFromTemplate returns a line from a template with some context | func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string { | ||||||
| func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string { | 	if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 { | ||||||
| 	bs, err := AssetFS().ReadFile(templateName + ".tmpl") | 		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 { | ||||||
|  | 		return err.Error() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func HandleTemplateRenderingError(err error) string { | ||||||
|  | 	p := &templateErrorPrettier{assets: AssetFS()} | ||||||
|  | 	return p.handleTemplateRenderingError(err) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const dashSeparator = "----------------------------------------------------------------------" | ||||||
|  |  | ||||||
|  | func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string { | ||||||
|  | 	code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Sprintf("(unable to read template file: %v)", err) | 		return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName) | ||||||
| 	} | 	} | ||||||
|  | 	line, err := util.ToInt64(lineNum) | ||||||
| 	sb := &strings.Builder{} | 	if err != nil { | ||||||
|  | 		return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum) | ||||||
| 	// 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 { |  | ||||||
| 			end += start |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// 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') |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  | 	pos, err := util.ToInt64(posNum) | ||||||
| 	// FIXME: this algorithm could provide incorrect results and mislead the developers. | 	if err != nil { | ||||||
| 	// For example: Undefined function "file" in template ..... | 		return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum) | ||||||
| 	//     {{Func .file.Addition file.Deletion .file.Addition}} |  | ||||||
| 	//             ^^^^          ^(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 { | 	detail := extractErrorLine(code, int(line), int(pos), target) | ||||||
| 		// 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" | 	var msg string | ||||||
| 		_, _ = sb.Write(lineBs[:position]) | 	if pos >= 0 { | ||||||
|  | 		msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg) | ||||||
| 		// Now write the ^^ pointer | 	} else { | ||||||
| 		targetLen := len(target) | 		msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg) | ||||||
| 		if targetLen == 0 { |  | ||||||
| 			targetLen = 1 |  | ||||||
| 		} |  | ||||||
| 		_, _ = sb.WriteString(strings.Repeat("^", targetLen)) |  | ||||||
| 		_ = sb.WriteByte('\n') |  | ||||||
| 	} | 	} | ||||||
|  | 	return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator | ||||||
| 	// Finally write the footer | } | ||||||
| 	sb.WriteString(dashSeparator) |  | ||||||
|  | func extractErrorLine(code []byte, lineNum, posNum int, target string) string { | ||||||
| 	return sb.String() | 	b := bufio.NewReader(bytes.NewReader(code)) | ||||||
|  | 	var line []byte | ||||||
|  | 	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") | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								modules/templates/htmlrenderer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								modules/templates/htmlrenderer_test.go
									
									
									
									
									
										Normal 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)) | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								templates/devtest/tmplerr-sub.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/devtest/tmplerr-sub.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | sub template triggers an executing error | ||||||
|  |  | ||||||
|  | 		{{.locale.NoSuch "asdf"}} | ||||||
							
								
								
									
										12
									
								
								templates/devtest/tmplerr.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								templates/devtest/tmplerr.tmpl
									
									
									
									
									
										Normal 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" .}} | ||||||
| @@ -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> |  | ||||||
| 	{{if .ErrorMsg}} | 	<div class="ui container gt-mt-5"> | ||||||
| 		<p>{{.locale.Tr "error.occurred"}}:</p> | 		{{if .ErrorMsg}} | ||||||
| 		<pre style="text-align: left">{{.ErrorMsg}}</pre> | 			<p>{{.locale.Tr "error.occurred"}}:</p> | ||||||
| 	{{end}} | 			<pre class="gt-whitespace-pre-wrap">{{.ErrorMsg}}</pre> | ||||||
| 	{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}} | 		{{end}} | ||||||
| 	{{if .IsAdmin}}<p>{{.locale.Tr "error.report_message"  | Safe}}</p>{{end}} |  | ||||||
|  | 		<div class="center gt-mt-5"> | ||||||
|  | 			{{if .ShowFooterVersion}}<p>{{.locale.Tr "admin.config.app_ver"}}: {{AppVer}}</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; } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								web_src/js/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								web_src/js/bootstrap.js
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user