gohugoio/hugo · error
invalid timeout for resource
Error message
invalid timeout for resource %s: %w
What it means
Wrapped error from `Client.FromRemote` (remote.go:212-216) when the `timeout` option value cannot be parsed as a duration via `cast.ToDurationE`. The timeout is extracted from the options map before the request is built. Valid forms include Go duration strings (`"30s"`), or numeric values that cast can interpret; anything else triggers this.
Solutions
- Use a Go-style duration string: `(dict "timeout" "30s")` or `"500ms"`.
- Quote duration literals in TOML: `timeout = "30s"`.
- Confirm the frontmatter field holds a string or integer, not a list/map.
- Avoid ambiguous forms like `"30 seconds"` -- only `s`, `m`, `ms`, `h` suffixes parse.
Example fix
// template -- before
{{ $r := resources.GetRemote $url (dict "timeout" "30 seconds") }}
// after
{{ $r := resources.GetRemote $url (dict "timeout" "30s") }} Defensive patterns
Strategy: validation
Validate before calling
import (
"fmt"
"time"
"github.com/spf13/cast"
)
func validateDuration(v any) error {
if _, err := cast.ToDurationE(v); err != nil {
return fmt.Errorf("invalid duration %v: %w", v, err)
}
return nil
} Prevention
- Use Go duration strings ("30s", "500ms").
- Quote durations in TOML (`timeout = "30s"`).
- Avoid ambiguous forms like "30 seconds".
When it happens
Trigger: Calling `resources.GetRemote` with `(dict "timeout" <bad>)` where `<bad>` is e.g. a struct, a list, an unparseable string like `"30 sec"`, or a bool. `cast.ToDurationE` accepts things like `30000000000` (ns int) or `"5s"` but rejects free-form text.
Common situations: Typing `timeout = "30"` expecting seconds (cast reads it as 30 nanoseconds, not an error, but functionally wrong) vs `timeout = "30 seconds"` (parse error); passing a frontmatter value coerced to the wrong type; mixing YAML duration syntax (`timeout: 30s`) with TOML unquoted (`timeout = 30s` triggers parse error in some loaders).
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- failed to decode options for resource
- failed to create request for resource
- failed to parse URL for resource
- failed to resolve media type for remote resource
- missing 'src' in metadata for resource
AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09).
Data as JSON: /api/errors/ec51fc6fc8f0722e.
Report an issue: GitHub.
Appendix: source
Thrown at resources/resource_factories/create/remote.go:215
if err != nil {
return nil, fmt.Errorf("failed to parse URL for resource %s: %w", uri, err)
}
method := "GET"
if s, _, ok := hmaps.LookupEqualFold(optionsm, "method"); ok {
method = strings.ToUpper(s.(string))
}
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
// a way to control cache eviction,
// so make sure we use any user provided key as the file cache key,
// but the auto generated and more stable key for everything else.
filecacheKey := userKey
return c.rs.ResourceCache.CacheResourceRemote.GetOrCreate(optionsKey, func(key string) (resource.Resource, error) {
options, err := decodeRemoteOptions(optionsm)
if err != nil {
return nil, fmt.Errorf("failed to decode options for resource %s: %w", uri, err)
}View on GitHub (pinned to 52c9bd7908)