method
: (`string`) The action to perform on the requested resource, typically one of `GET`, `POST`, or `HEAD`.
+timeout
+: (`string`) Cancels the request if it does not complete within this duration (e.g. "30s").
+
responseHeaders
: {{< new-in 0.143.0 />}}
-: (`[]string`) The headers to extract from the server's response, accessible through the resource's [`Data.Headers`] method. Header name matching is case-insensitive.
-
-[`Data.Headers`]: /methods/resource/data/#headers
+: (`[]string`) The headers to extract from the server's response, accessible through the resource's [`Data.Headers`] method. Header name matching is case-insensitive.[`Data.Headers`]: /methods/resource/data/#headers
## Options examples
{{ $resource := resources.GetRemote $url $opts }}
```
+To set a per-request timeout (e.g. when fetching many feeds where a few slow ones should not stall the build):
+
+```go-html-template
+{{ $url := "https://example.org/feed.rss" }}
+{{ $opts := dict "timeout" "10s" }}
+{{ with try (resources.GetRemote $url $opts) }}
+ {{ with .Err }}
+ {{ warnf "Failed to fetch feed: %s" . }}
+ {{ else with .Value }}
+ {{ $data = . | transform.Unmarshal }}
+ {{ end }}
+{{ end }}
+```
+
## Remote data
When retrieving remote data, use the [`transform.Unmarshal`] function to [unmarshal](g) the response.
[`try`]: /functions/go-template/try
[configure file caches]: /configuration/caches/
+
[error handling]: #error-handling
"net/http/httptest"
"strings"
"testing"
+ "time"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
}
})
}
+
+func TestGetRemotePerRequestTimeout(t *testing.T) {
+ t.Parallel()
+ htesting.SkipSlowTestUnlessCI(t)
+
+ // A server that always sleeps longer than the per-request timeout.
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(2 * time.Second)
+ w.Header().Add("Content-Type", "text/plain")
+ w.Write([]byte("too late"))
+ }))
+ t.Cleanup(func() { srv.Close() })
+
+ files := `
+-- hugo.toml --
+timeout = "30s"
+[security]
+[security.http]
+urls = ['.*']
+mediaTypes = ['text/plain']
+-- layouts/home.html --
+{{ $url := "URL" }}
+{{ $opts := dict "timeout" "200ms" }}
+{{ with try (resources.GetRemote $url $opts) }}
+ {{ with .Err }}
+ Err: {{ . }}
+ {{ else with .Value }}
+ Content: {{ .Content }}
+ {{ end }}
+{{ end }}
+`
+ files = strings.ReplaceAll(files, "URL", srv.URL)
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ },
+ )
+ b.Build()
+
+ // The per-request timeout of 200ms should fire well before the global 30s timeout.
+ b.AssertFileContent("public/index.html", "Err:")
+}
"strings"
"time"
+ "github.com/spf13/cast"
+
"github.com/gohugoio/httpcache"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hmaps"
isHeadMethod := method == "HEAD"
optionsm = maps.Clone(optionsm)
+
+ // Extract timeout before computing cache keys: it only affects fetch behaviour,
+ // not the cached content, so it must not influence the cache key.
+ var perRequestTimeout time.Duration
+ if v, k, ok := hmaps.LookupEqualFold(optionsm, "timeout"); ok {
+ d, err := cast.ToDurationE(v)
+ if err != nil {
+ return nil, fmt.Errorf("invalid timeout for resource %s: %w", uri, err)
+ }
+ perRequestTimeout = d
+ delete(optionsm, k)
+ }
+
userKey, optionsKey := remoteResourceKeys(uri, optionsm)
// A common pattern is to use the key in the options map as
getRes := func() (*http.Response, error) {
ctx := context.Background()
+ if perRequestTimeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
+ defer cancel()
+ }
ctx = c.resourceIDDispatcher.Set(ctx, filecacheKey)
req, err := options.NewRequest(uri)