resources: Add more details to .Err
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 24 Mar 2022 07:12:51 +0000 (08:12 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 25 Mar 2022 15:40:36 +0000 (16:40 +0100)
This commit adds a .Data object (a map with `Body`, `StatusCode` etc.) to the .Err returned from `resources.GetRemote`, which means you can now do:

```
{{ with .Err }}
{{ range $k, $v := .Data }}
{{ end }}
{{ end }}
```

Fixes #9708

hugolib/page.go
hugolib/resource_chain_test.go
resources/errorResource.go
resources/page/page_nop.go
resources/page/testhelpers_test.go
resources/resource.go
resources/resource/resourcetypes.go
resources/resource_factories/create/remote.go
resources/transform.go
tpl/resources/resources.go

index 2fddaa299dbb92cca4bab9338444f9cd04e2a935..77165c072921ad0d1b4e182671197a2e35a8f6d7 100644 (file)
@@ -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
 }
 
index cc8f5511990316cd4a9d1de5532cdba75aa480f6..471ea54e82e84ce61c1b8f94cc40d3737511eb4a 100644 (file)
@@ -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{})))
index 70f05d3f7095ab405b1d64bb333db6c6baca6f88..50f0be371e2e7530ecfba894f9303220ffa0ad41 100644 (file)
@@ -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)
 }
index 67b8b8d4b09fa102aad110290586f4d5e8e158f9..cdc5fd8b160a3d08f1a5ffca6e4ea302e4f69e20 100644 (file)
@@ -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
 }
 
index 6758667996e4acb294bd5dec34f57f703781ac3e..3f6accdacf0ea999c25db625bac2ee9c84ab4b65 100644 (file)
@@ -120,7 +120,7 @@ type testPage struct {
        sectionEntries []string
 }
 
-func (p *testPage) Err() error {
+func (p *testPage) Err() resource.ResourceError {
        return nil
 }
 
index dd3a4730ada9e6bc50457497843c926e7bec29fe..77cc11ddecbd824e88784762437c433866417775 100644 (file)
@@ -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
 }
 
index 259706e2db80d097f16edd86c56bcf234e823c03..ae076ed9a01bf162d19dca2101098d9d6b1750eb 100644 (file)
@@ -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.
index 0beb87195d47114dc528432822648b9fce0475ad..56bd99cb16f2b0f1630eee1a46dd3182cb3a80a8 100644 (file)
@@ -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
                }
 
+               httpResponse, err := httputil.DumpResponse(res, true)
+               if err != nil {
+                       return nil, toHTTPError(err, res)
+               }
+
                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))
-                       }
-               }
+                               return nil, toHTTPError(errors.Errorf("failed to fetch remote resource: %s", http.StatusText(res.StatusCode)), res)
 
-               httpResponse, err := httputil.DumpResponse(res, true)
-               if err != nil {
-                       return nil, err
+                       }
                }
 
                return hugio.ToReadCloser(bytes.NewReader(httpResponse)), nil
index cbf77363c459be61d17fcff647eca8bf2f4558a7..9b69ee37a1244eefefd8f3812ba232a697ab653f 100644 (file)
@@ -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
 }
 
index f4b5b07196565e48b1fc8accc8048b0950e0f8f5..7e137c6615ee9f9b0e7b63b3d4ad1d12bc6e4926 100644 (file)
@@ -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