commands: Show server error info in browser

The main item in this commit is showing of errors with a file context when running `hugo server`.

This can be turned off: `hugo server --disableBrowserError` (can also be set in `config.toml`).

But to get there, the error handling in Hugo needed a revision. There are some items left TODO for commits soon to follow, most notable errors in content and config files.

Fixes #5284
Fixes #5290
See #5325
See #5324
This commit is contained in:
Bjørn Erik Pedersen
2018-10-03 14:58:09 +02:00
parent 3a3089121b
commit 35fbfb19a1
73 changed files with 1914 additions and 668 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,16 +14,26 @@
package tpl
import (
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"time"
"text/template/parse"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"html/template"
texttemplate "text/template"
"text/template/parse"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/metrics"
"github.com/pkg/errors"
)
var (
@@ -35,8 +45,7 @@ type TemplateHandler interface {
TemplateFinder
AddTemplate(name, tpl string) error
AddLateTemplate(name, tpl string) error
LoadTemplates(prefix string)
PrintErrors()
LoadTemplates(prefix string) error
NewTextTemplate() TemplateParseFinder
@@ -82,16 +91,122 @@ type TemplateDebugger interface {
type TemplateAdapter struct {
Template
Metrics metrics.Provider
// The filesystem where the templates are stored.
Fs afero.Fs
// Maps to base template if relevant.
NameBaseTemplateName map[string]string
}
var baseOfRe = regexp.MustCompile("template: (.*?):")
func extractBaseOf(err string) string {
m := baseOfRe.FindStringSubmatch(err)
if len(m) == 2 {
return m[1]
}
return ""
}
// Execute executes the current template. The actual execution is performed
// by the embedded text or html template, but we add an implementation here so
// we can add a timer for some metrics.
func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) error {
func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) {
defer func() {
// Panics in templates are a little bit too common (nil pointers etc.)
if r := recover(); r != nil {
execErr = t.addFileContext(t.Name(), fmt.Errorf("panic in Execute: %s", r))
}
}()
if t.Metrics != nil {
defer t.Metrics.MeasureSince(t.Name(), time.Now())
}
return t.Template.Execute(w, data)
execErr = t.Template.Execute(w, data)
if execErr != nil {
execErr = t.addFileContext(t.Name(), execErr)
}
return
}
var identifiersRe = regexp.MustCompile("at \\<(.*?)\\>:")
func (t *TemplateAdapter) extractIdentifiers(line string) []string {
m := identifiersRe.FindAllStringSubmatch(line, -1)
identifiers := make([]string, len(m))
for i := 0; i < len(m); i++ {
identifiers[i] = m[i][1]
}
return identifiers
}
func (t *TemplateAdapter) addFileContext(name string, inerr error) error {
f, realFilename, err := t.fileAndFilename(t.Name())
if err != nil {
return err
}
defer f.Close()
master, hasMaster := t.NameBaseTemplateName[name]
ferr := errors.Wrapf(inerr, "execute of template %q failed", realFilename)
// Since this can be a composite of multiple template files (single.html + baseof.html etc.)
// we potentially need to look in both -- and cannot rely on line number alone.
lineMatcher := func(le herrors.FileError, lineNumber int, line string) bool {
if le.LineNumber() != lineNumber {
return false
}
if !hasMaster {
return true
}
identifiers := t.extractIdentifiers(le.Error())
for _, id := range identifiers {
if strings.Contains(line, id) {
return true
}
}
return false
}
// TODO(bep) 2errors text vs HTML
fe, ok := herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
if ok || !hasMaster {
return fe
}
// Try the base template if relevant
f, realFilename, err = t.fileAndFilename(master)
if err != nil {
return err
}
defer f.Close()
ferr = errors.Wrapf(inerr, "execute of template %q failed", realFilename)
fe, _ = herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
return fe
}
func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) {
fs := t.Fs
filename := filepath.FromSlash(name)
fi, err := fs.Stat(filename)
if err != nil {
return nil, "", errors.Wrapf(err, "failed to Stat %q", filename)
}
f, err := fs.Open(filename)
if err != nil {
return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename)
}
return f, fi.(hugofs.RealFilenameInfo).RealFilename(), nil
}
// ExecuteToString executes the current template and returns the result as a