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

  1. Provide `headers` as a map of string-to-string/list: `(dict "headers" (dict "Authorization" "Bearer x"))`.
  2. Provide `body` as a string or bytes (use `jsonify` to marshal).
  3. Provide `responseHeaders` as a list of header names to capture.
  4. 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

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

Related errors


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)