diff --git a/common/herrors/errors.go b/common/herrors/errors.go index c7ee90dd0ec..a21b3ae84be 100644 --- a/common/herrors/errors.go +++ b/common/herrors/errors.go @@ -185,3 +185,9 @@ func improveIfNilPointerMsg(inErr error) string { s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field) return nilPointerErrRe.ReplaceAllString(inErr.Error(), s) } + +// ErrorWithCause is an error that also provides a Cause, which is the underlying error. +type ErrorWithCause interface { + error + Cause() error +} diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go index c8a48823e82..9709e9580e0 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -260,8 +260,16 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) { // Cause returns the underlying error or itself if it does not implement Unwrap. func Cause(err error) error { - if u := errors.Unwrap(err); u != nil { - return u + type unwrapper interface { + Unwrap() error + } + + for err != nil { + cause, ok := err.(unwrapper) + if !ok { + break + } + err = cause.Unwrap() } return err } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 02ecd57855d..60fb8ecc02f 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -343,6 +343,18 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error { siteRenderContext := &siteRenderContext{cfg: config, multihost: h.Configs.IsMultihost} + renderErr := func(err error) error { + if err == nil { + return nil + } + if strings.Contains(err.Error(), "can't evaluate field Err in type resource.Resource") { + // In Hugo 0.141.0 we replaced the special error handling for resources.GetRemote + // with the more general try. + return fmt.Errorf("%s: Resource.Err was removed in Hugo v0.141.0 and replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err) + } + return err + } + i := 0 for _, s := range h.Sites { segmentFilter := s.conf.C.SegmentFilter @@ -390,7 +402,7 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error { } } else { if err := s.render(siteRenderContext); err != nil { - return err + return renderErr(err) } } loggers.TimeTrackf(ll, start, nil, "") diff --git a/hugolib/page.go b/hugolib/page.go index 83f0c6e25c2..3653767379a 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -185,10 +185,6 @@ func (p *pageState) isContentNodeBranch() bool { return p.IsNode() } -func (p *pageState) Err() resource.ResourceError { - return nil -} - // Eq returns whether the current page equals the given page. // This is what's invoked when doing `{{ if eq $page $otherPage }}` func (p *pageState) Eq(other any) bool { diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 0b17a8db091..f8f8d7bf1af 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -67,11 +67,11 @@ FIT: {{ $fit.Name }}|{{ $fit.RelPermalink }}|{{ $fit.Width }} CSS integrity Data first: {{ $cssFingerprinted1.Data.Integrity }} {{ $cssFingerprinted1.RelPermalink }} CSS integrity Data last: {{ $cssFingerprinted2.RelPermalink }} {{ $cssFingerprinted2.Data.Integrity }} -{{ $failedImg := resources.GetRemote "%[1]s/fail.jpg" }} +{{ $failedImg := try (resources.GetRemote "%[1]s/fail.jpg") }} {{ $rimg := resources.GetRemote "%[1]s/sunset.jpg" }} {{ $remotenotfound := resources.GetRemote "%[1]s/notfound.jpg" }} {{ $localnotfound := resources.Get "images/notfound.jpg" }} -{{ $gopherprotocol := resources.GetRemote "gopher://example.org" }} +{{ $gopherprotocol := try (resources.GetRemote "gopher://example.org") }} {{ $rfit := $rimg.Fit "200x200" }} {{ $rfit2 := $rfit.Fit "100x200" }} {{ $rimg = $rimg | fingerprint }} @@ -79,10 +79,10 @@ SUNSET REMOTE: {{ $rimg.Name }}|{{ $rimg.RelPermalink }}|{{ $rimg.Width }}|{{ le FIT REMOTE: {{ $rfit.Name }}|{{ $rfit.RelPermalink }}|{{ $rfit.Width }} REMOTE NOT FOUND: {{ if $remotenotfound }}FAILED{{ else}}OK{{ end }} LOCAL NOT FOUND: {{ if $localnotfound }}FAILED{{ else}}OK{{ end }} -PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ . | safeHTML }}{{ end }} +PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ .Value | safeHTML }}{{ end }} PRINT PROTOCOL ERROR2: {{ with $gopherprotocol }}{{ .Err | safeHTML }}{{ end }} -PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}Err: {{ .Err | safeHTML }}{{ with .Err }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }} -FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg.Err }}|{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}| +PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}{{ with .Err }}Err: {{ . | safeHTML }}{{ with .Cause }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }}{{ end }} +FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg }}{{ with .Err }}{{ with .Cause }}{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}{{ end }}{{ end }}| `, ts.URL)) fs := b.Fs.Source @@ -114,8 +114,8 @@ SUNSET REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d FIT REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s_hu15210517121918042184.jpg|200 REMOTE NOT FOUND: OK LOCAL NOT FOUND: OK -PRINT PROTOCOL ERROR DETAILS: Err: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"|| -FAILED REMOTE ERROR DETAILS CONTENT: |failed to fetch remote resource from '%[2]s/fail.jpg': Not Implemented|Body: { msg: failed } +PRINT PROTOCOL ERROR DETAILS: Err: template: index.html:22:36: executing "index.html" at : error calling GetRemote: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher" +FAILED REMOTE ERROR DETAILS CONTENT: failed to fetch remote resource from '%[2]s/fail.jpg': Not Implemented|Body: { msg: failed } |StatusCode: 501|ContentLength: 16|ContentType: text/plain; charset=utf-8| diff --git a/resources/errorResource.go b/resources/errorResource.go deleted file mode 100644 index 582c54f6d15..00000000000 --- a/resources/errorResource.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2021 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. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package resources - -import ( - "context" - "image" - - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resources/images" - "github.com/gohugoio/hugo/resources/images/exif" - "github.com/gohugoio/hugo/resources/resource" -) - -var ( - _ error = (*errorResource)(nil) - // Image covers all current Resource implementations. - _ images.ImageResource = (*errorResource)(nil) - // The list of user facing and exported interfaces in resource.go - // Note that if we're missing some interface here, the user will still - // get an error, but not as pretty. - _ resource.ContentResource = (*errorResource)(nil) - _ resource.ReadSeekCloserResource = (*errorResource)(nil) - _ resource.ResourcesLanguageMerger = (*resource.Resources)(nil) - // Make sure it also fails when passed to a pipe function. - _ ResourceTransformer = (*errorResource)(nil) -) - -// NewErrorResource wraps err in a Resource where all but the Err method will panic. -func NewErrorResource(err resource.ResourceError) resource.Resource { - return &errorResource{ResourceError: err} -} - -type errorResource struct { - resource.ResourceError -} - -func (e *errorResource) Err() resource.ResourceError { - return e.ResourceError -} - -func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Content(context.Context) (any, error) { - panic(e.ResourceError) -} - -func (e *errorResource) ResourceType() string { - panic(e.ResourceError) -} - -func (e *errorResource) MediaType() media.Type { - panic(e.ResourceError) -} - -func (e *errorResource) Permalink() string { - panic(e.ResourceError) -} - -func (e *errorResource) RelPermalink() string { - panic(e.ResourceError) -} - -func (e *errorResource) Name() string { - panic(e.ResourceError) -} - -func (e *errorResource) Title() string { - panic(e.ResourceError) -} - -func (e *errorResource) Params() maps.Params { - panic(e.ResourceError) -} - -func (e *errorResource) Data() any { - panic(e.ResourceError) -} - -func (e *errorResource) Height() int { - panic(e.ResourceError) -} - -func (e *errorResource) Width() int { - panic(e.ResourceError) -} - -func (e *errorResource) Process(spec string) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Crop(spec string) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Fill(spec string) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Fit(spec string) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Resize(spec string) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Filter(filters ...any) (images.ImageResource, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Exif() *exif.ExifInfo { - panic(e.ResourceError) -} - -func (e *errorResource) Colors() ([]images.Color, error) { - panic(e.ResourceError) -} - -func (e *errorResource) DecodeImage() (image.Image, error) { - panic(e.ResourceError) -} - -func (e *errorResource) Transform(...ResourceTransformation) (ResourceTransformer, error) { - panic(e.ResourceError) -} - -func (e *errorResource) TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error) { - panic(e.ResourceError) -} diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index 5a03b19941c..af9f2682d37 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -61,10 +61,6 @@ type nopPage int var noOpPathInfo = media.DefaultPathParser.Parse(files.ComponentFolderContent, "no-op.md") -func (p *nopPage) Err() resource.ResourceError { - return nil -} - func (p *nopPage) Aliases() []string { return nil } diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 8a2d28e31fe..8e6dfb79ac2 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -111,10 +111,6 @@ type testPage struct { sectionEntries []string } -func (p *testPage) Err() resource.ResourceError { - return nil -} - func (p *testPage) Aliases() []string { panic("testpage: not implemented") } diff --git a/resources/resource.go b/resources/resource.go index 7ab10b0ae34..29b9e5ddd82 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -224,9 +224,6 @@ type resourceCopier interface { // Copy copies r to the targetPath given. func Copy(r resource.Resource, targetPath string) resource.Resource { - if r.Err() != nil { - panic(fmt.Sprintf("Resource has an .Err: %s", r.Err())) - } return r.(resourceCopier).cloneTo(targetPath) } @@ -439,10 +436,6 @@ func (l *genericResource) Content(context.Context) (any, error) { return hugio.ReadString(r) } -func (r *genericResource) Err() resource.ResourceError { - return nil -} - func (l *genericResource) Data() any { return l.sd.Data } diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go index b33750e8033..8c2318436cb 100644 --- a/resources/resource/resourcetypes.go +++ b/resources/resource/resourcetypes.go @@ -65,13 +65,6 @@ type ResourceError interface { ResourceDataProvider } -// ErrProvider provides an Err. -type ErrProvider interface { - // Err returns an error if this resource is in an error state. - // This will currently only be set for resources obtained from resources.GetRemote. - Err() ResourceError -} - // Resource represents a linkable resource, i.e. a content page, image etc. type Resource interface { ResourceWithoutMeta @@ -83,7 +76,6 @@ type ResourceWithoutMeta interface { MediaTypeProvider ResourceLinksProvider ResourceDataProvider - ErrProvider } type ResourceWrapper interface { diff --git a/resources/resource_factories/create/create_integration_test.go b/resources/resource_factories/create/create_integration_test.go index 17084574da9..5ed56bed56e 100644 --- a/resources/resource_factories/create/create_integration_test.go +++ b/resources/resource_factories/create/create_integration_test.go @@ -31,18 +31,17 @@ func TestGetRemoteHead(t *testing.T) { [security.http] methods = ['(?i)GET|POST|HEAD'] urls = ['.*gohugo\.io.*'] - -- layouts/index.html -- {{ $url := "https://gohugo.io/img/hugo.png" }} {{ $opts := dict "method" "head" }} -{{ with resources.GetRemote $url $opts }} +{{ with try (resources.GetRemote $url $opts) }} {{ with .Err }} {{ errorf "Unable to get remote resource: %s" . }} - {{ else }} + {{ else with .Value }} Head Content: {{ .Content }}. Head Data: {{ .Data }} - {{ end }} -{{ else }} + {{ else }} {{ errorf "Unable to get remote resource: %s" $url }} + {{ end }} {{ end }} ` @@ -90,14 +89,14 @@ mediaTypes = ['text/plain'] -- layouts/_default/single.html -- {{ $url := printf "%s%s" "URL" .RelPermalink}} {{ $opts := dict }} -{{ with resources.GetRemote $url $opts }} +{{ with try (resources.GetRemote $url $opts) }} {{ with .Err }} {{ errorf "Got Err: %s. Data: %v" . .Data }} - {{ else }} + {{ else with .Value }} Content: {{ .Content }} + {{ else }} + {{ errorf "Unable to get remote resource: %s" $url }} {{ end }} -{{ else }} - {{ errorf "Unable to get remote resource: %s" $url }} {{ end }} ` diff --git a/resources/transform.go b/resources/transform.go index c5d24066937..73f3b85d25d 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -192,10 +192,6 @@ func (r *resourceAdapter) Content(ctx context.Context) (any, error) { return r.target.Content(ctx) } -func (r *resourceAdapter) Err() resource.ResourceError { - return nil -} - func (r *resourceAdapter) GetIdentity() identity.Identity { return identity.FirstIdentity(r.target) } diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index 0dbee02f733..4f107bf99fa 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template.go @@ -19,6 +19,7 @@ import ( "io" "reflect" + "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hreflect" "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" @@ -261,23 +262,33 @@ type TryValue struct { // Value is the value returned by the function or method wrapped with "try". // This will always be nil if Err is set. Value any + // Err is the error returned by the function or method wrapped with "try". // This will always be nil if Value is set. - Err error + Err herrors.ErrorWithCause +} + +type causeError struct { + error +} + +func (c causeError) Cause() error { + return herrors.Cause(c.error) } // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] // as the function itself. func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) (val reflect.Value) { - // Added for Hugo. + // Added for Hugo.sssss if name == "try" { defer func() { if r := recover(); r != nil { + // Cause: herrors.Cause(err) if err, ok := r.(error); ok { - val = reflect.ValueOf(TryValue{nil, err}) + val = reflect.ValueOf(TryValue{Value: nil, Err: &causeError{err}}) } else { - val = reflect.ValueOf(TryValue{nil, fmt.Errorf("%v", r)}) + val = reflect.ValueOf(TryValue{Value: nil, Err: causeError{fmt.Errorf("%v", r)}}) } } }() @@ -396,7 +407,7 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node // Added for Hugo. if name == "try" { - return reflect.ValueOf(TryValue{vv.Interface(), nil}) + return reflect.ValueOf(TryValue{Value: vv.Interface()}) } return vv diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index beace14e6c8..66c3b7f0158 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -115,7 +115,7 @@ func (ns *Namespace) Get(filename any) resource.Resource { // // Note: This method does not return any error as a second return value, // for any error situations the error can be checked in .Err. -func (ns *Namespace) GetRemote(args ...any) resource.Resource { +func (ns *Namespace) GetRemote(args ...any) (resource.Resource, error) { get := func(args ...any) (resource.Resource, error) { if len(args) < 1 { return nil, errors.New("must provide an URL") @@ -146,12 +146,12 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource { if err != nil { switch v := err.(type) { case *create.HTTPError: - return resources.NewErrorResource(resource.NewResourceError(v, v.Data)) + return nil, resource.NewResourceError(v, v.Data) default: - return resources.NewErrorResource(resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any))) + return nil, resource.NewResourceError(fmt.Errorf("error calling resources.GetRemote: %w", err), make(map[string]any)) } } - return r + return r, nil } // GetMatch finds the first Resource matching the given pattern, or nil if none found.