diff --git a/hugolib/page.go b/hugolib/page.go index 2fddaa299..77165c072 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -133,7 +133,7 @@ func (p *pageState) reusePageOutputContent() bool { return p.pageOutputTemplateVariationsState.Load() == 1 } -func (p *pageState) Err() error { +func (p *pageState) Err() resource.ResourceError { return nil } diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index cc8f55119..471ea54e8 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -35,7 +35,19 @@ import ( ) func TestResourceChainBasic(t *testing.T) { - ts := httptest.NewServer(http.FileServer(http.Dir("testdata/"))) + failIfHandler := func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/fail.jpg" { + http.Error(w, "{ msg: failed }", 500) + return + } + h.ServeHTTP(w, r) + + }) + } + ts := httptest.NewServer( + failIfHandler(http.FileServer(http.Dir("testdata/"))), + ) t.Cleanup(func() { ts.Close() }) @@ -58,6 +70,7 @@ 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" }} {{ $rimg := resources.GetRemote "%[1]s/sunset.jpg" }} {{ $remotenotfound := resources.GetRemote "%[1]s/notfound.jpg" }} {{ $localnotfound := resources.Get "images/notfound.jpg" }} @@ -71,7 +84,8 @@ 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 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 }}| `, ts.URL)) fs := b.Fs.Source @@ -103,8 +117,9 @@ SUNSET REMOTE: sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d0 FIT REMOTE: sunset_%[1]s.jpg|/sunset_%[1]s_hu59e56ffff1bc1d8d122b1403d34e039f_0_200x200_fit_q75_box.jpg|200 REMOTE NOT FOUND: OK LOCAL NOT FOUND: OK -PRINT PROTOCOL ERROR1: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher" -PRINT PROTOCOL ERROR2: error calling resources.GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher" +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: Internal Server Error|Body: { msg: failed } +|StatusCode: 500|ContentLength: 16|ContentType: text/plain; charset=utf-8| `, helpers.HashString(ts.URL+"/sunset.jpg", map[string]any{}))) diff --git a/resources/errorResource.go b/resources/errorResource.go index 70f05d3f7..50f0be371 100644 --- a/resources/errorResource.go +++ b/resources/errorResource.go @@ -19,9 +19,7 @@ import ( "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resources/images/exif" - "github.com/gohugoio/hugo/resources/resource" ) @@ -40,94 +38,94 @@ var ( ) // NewErrorResource wraps err in a Resource where all but the Err method will panic. -func NewErrorResource(err error) resource.Resource { - return &errorResource{error: err} +func NewErrorResource(err resource.ResourceError) resource.Resource { + return &errorResource{ResourceError: err} } type errorResource struct { - error + resource.ResourceError } -func (e *errorResource) Err() error { - return e.error +func (e *errorResource) Err() resource.ResourceError { + return e.ResourceError } func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Content() (any, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) ResourceType() string { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) MediaType() media.Type { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Permalink() string { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) RelPermalink() string { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Name() string { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Title() string { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Params() maps.Params { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Data() any { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Height() int { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Width() int { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Crop(spec string) (resource.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Fill(spec string) (resource.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Fit(spec string) (resource.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Resize(spec string) (resource.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Filter(filters ...any) (resource.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Exif() *exif.Exif { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) DecodeImage() (image.Image, error) { - panic(e.error) + panic(e.ResourceError) } func (e *errorResource) Transform(...ResourceTransformation) (ResourceTransformer, error) { - panic(e.error) + panic(e.ResourceError) } diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index 67b8b8d4b..cdc5fd8b1 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -48,7 +48,7 @@ var ( // PageNop implements Page, but does nothing. type nopPage int -func (p *nopPage) Err() error { +func (p *nopPage) Err() resource.ResourceError { return nil } diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 675866799..3f6accdac 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -120,7 +120,7 @@ type testPage struct { sectionEntries []string } -func (p *testPage) Err() error { +func (p *testPage) Err() resource.ResourceError { return nil } diff --git a/resources/resource.go b/resources/resource.go index dd3a4730a..77cc11dde 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -233,7 +233,7 @@ func (l *genericResource) Content() (any, error) { return l.content, nil } -func (r *genericResource) Err() error { +func (r *genericResource) Err() resource.ResourceError { return nil } diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go index 259706e2d..ae076ed9a 100644 --- a/resources/resource/resourcetypes.go +++ b/resources/resource/resourcetypes.go @@ -24,6 +24,11 @@ import ( "github.com/gohugoio/hugo/common/hugio" ) +var ( + _ ResourceDataProvider = (*resourceError)(nil) + _ ResourceError = (*resourceError)(nil) +) + // Cloner is an internal template and not meant for use in the templates. It // may change without notice. type Cloner interface { @@ -37,9 +42,33 @@ type OriginProvider interface { GetFieldString(pattern string) (string, bool) } +// NewResourceError creates a new ResourceError. +func NewResourceError(err error, data any) ResourceError { + return &resourceError{ + error: err, + data: data, + } +} + +type resourceError struct { + error + data any +} + +// The data associated with this error. +func (e *resourceError) Data() any { + return e.data +} + +// ResourceError is the error return from .Err in Resource in error situations. +type ResourceError interface { + error + ResourceDataProvider +} + // ErrProvider provides an Err. type ErrProvider interface { - Err() error + Err() ResourceError } // Resource represents a linkable resource, i.e. a content page, image etc. diff --git a/resources/resource_factories/create/remote.go b/resources/resource_factories/create/remote.go index 0beb87195..56bd99cb1 100644 --- a/resources/resource_factories/create/remote.go +++ b/resources/resource_factories/create/remote.go @@ -36,6 +36,41 @@ import ( "github.com/pkg/errors" ) +type HTTPError struct { + error + Data map[string]any + + StatusCode int + Body string +} + +func toHTTPError(err error, res *http.Response) *HTTPError { + if err == nil { + panic("err is nil") + } + if res == nil { + return &HTTPError{ + error: err, + Data: map[string]any{}, + } + } + + var body []byte + body, _ = ioutil.ReadAll(res.Body) + + return &HTTPError{ + error: err, + Data: map[string]any{ + "StatusCode": res.StatusCode, + "Status": res.Status, + "Body": string(body), + "TransferEncoding": res.TransferEncoding, + "ContentLength": res.ContentLength, + "ContentType": res.Header.Get("Content-Type"), + }, + } +} + // FromRemote expects one or n-parts of a URL to a resource // If you provide multiple parts they will be joined together to the final URL. func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resource, error) { @@ -70,15 +105,16 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou return nil, err } - if res.StatusCode != http.StatusNotFound { - if res.StatusCode < 200 || res.StatusCode > 299 { - return nil, errors.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)) - } - } - httpResponse, err := httputil.DumpResponse(res, true) if err != nil { - return nil, err + return nil, toHTTPError(err, res) + } + + if res.StatusCode != http.StatusNotFound { + if res.StatusCode < 200 || res.StatusCode > 299 { + return nil, toHTTPError(errors.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res) + + } } return hugio.ToReadCloser(bytes.NewReader(httpResponse)), nil diff --git a/resources/transform.go b/resources/transform.go index cbf77363c..9b69ee37a 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -167,7 +167,7 @@ func (r *resourceAdapter) Content() (any, error) { return r.target.Content() } -func (r *resourceAdapter) Err() error { +func (r *resourceAdapter) Err() resource.ResourceError { return nil } diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index f4b5b0719..7e137c661 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -151,8 +151,13 @@ func (ns *Namespace) GetRemote(args ...any) resource.Resource { r, err := get(args...) if err != nil { - // This allows the client to reason about the .Err in the template. - return resources.NewErrorResource(errors.Wrap(err, "error calling resources.GetRemote")) + switch v := err.(type) { + case *create.HTTPError: + return resources.NewErrorResource(resource.NewResourceError(v, v.Data)) + default: + return resources.NewErrorResource(resource.NewResourceError(errors.Wrap(err, "error calling resources.GetRemote"), make(map[string]any))) + } + } return r