gohugoio/hugo · error
failed to decode options for resource
Error message
failed to decode options for resource %s: %w
What it means
Wrapped error from `Client.FromRemote` (remote.go:229-233) when `decodeRemoteOptions` fails. That helper uses `mapstructure.WeakDecode` (remote.go:448-460) to coerce the options map into the `fromRemoteOptions` struct (`Method`, `Headers`, `Body`, `ResponseHeaders`). Failure means a field has a type that cannot be weakly coerced -- e.g. `headers` set to a string instead of a map, `body` set to a struct, or `method` set to a list.
Solutions
- Provide `headers` as a map of string-to-string/list: `(dict "headers" (dict "Authorization" "Bearer x"))`.
- Provide `body` as a string or bytes (use `jsonify` to marshal).
- Provide `responseHeaders` as a list of header names to capture.
- Validate each option field against `fromRemoteOptions` field types before calling.
Example fix
// template -- before
{{ $r := resources.GetRemote $url (dict "headers" "Authorization: Bearer x") }}
// after
{{ $r := resources.GetRemote $url (dict "headers" (dict "Authorization" "Bearer x")) }} Defensive patterns
Strategy: validation
Validate before calling
// Validate option map shape before calling FromRemote.
func validateRemoteOpts(m map[string]any) error {
if h, ok := m["headers"]; ok {
if _, ok := h.(map[string]any); !ok {
return fmt.Errorf("headers must be a map, got %T", h)
}
}
if b, ok := m["body"]; ok {
switch b.(type) {
case string, []byte:
default:
return fmt.Errorf("body must be string or bytes, got %T", b)
}
}
return nil
} Prevention
- `headers` -> map[string]string/list; `body` -> string/bytes; `responseHeaders` -> []string.
- Marshal JSON bodies with `jsonify` before passing.
- Cross-check against `fromRemoteOptions` field types.
When it happens
Trigger: Calling `resources.GetRemote` with `(dict "headers" "foo: bar")` (string instead of map), `(dict "body" (slice 1 2 3))` (list where bytes/string expected), `(dict "responseHeaders" true)` (bool where list expected), or `method` as a number/map.
Common situations: Copy-pasting curl-style header strings into the `headers` dict; passing JSON body as a Go map rather than marshalled bytes; misreading docs and providing scalar where collection is required; version drift where option field shapes changed.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- invalid timeout 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/8c6fc630f4a5613a.
Report an issue: GitHub.
Appendix: source
Thrown at resources/resource_factories/create/remote.go:232
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)
}
if err := c.validateFromRemoteArgs(uri, options); err != nil {
return nil, err
}
getRes := func() (*http.Response, context.CancelFunc, error) {
ctx := context.Background()
var cancel context.CancelFunc
if perRequestTimeout > 0 {
ctx, cancel = context.WithTimeout(ctx, perRequestTimeout)
}
ctx = c.resourceIDDispatcher.Set(ctx, filecacheKey)
req, err := options.NewRequest(uri)
if err != nil {
if cancel != nil {
cancel()View on GitHub (pinned to 52c9bd7908)