gohugoio/hugo · critical

invalid cache config: %s

Error message

invalid cache config: %s

What it means

A panic raised in NewCache (cache/filecache/filecache.go:81) when FileCacheConfig.init() returns an error. Currently init() only errors when the Dir still contains ':' placeholders that were not resolved during DecodeConfig. Hugo panics because a half-resolved cache directory would silently corrupt builds.

Source

Thrown at cache/filecache/filecache.go:81

}

// Lock tracks the ids in use. We use this information to do garbage collection
// after a Hugo build.
func (l *lockTracker) Lock(id string) {
	l.seen.AddIfAbsent(id)
	l.Locker.Lock(id)
}

// ItemInfo contains info about a cached file.
type ItemInfo struct {
	// This is the file's name relative to the cache's filesystem.
	Name string
}

// NewCache creates a new file cache with the given filesystem and max age.
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
	if err := cfg.init(); err != nil {
		panic(fmt.Sprintf("invalid cache config: %s", err))
	}

	return &Cache{
		Fs:          fs,
		entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: maphelpers.NewConcurrentSet[string]()},
		cfg:         cfg,
	}
}

// lockedFile is a file with a lock that is released on Close.
type lockedFile struct {
	afero.File
	unlock func()
}

func (l *lockedFile) Close() error {
	defer l.unlock()
	return l.File.Close()

View on GitHub (pinned to 52c9bd7908)

Solutions

  1. Always run cache configs through filecache.DecodeConfig so placeholders are resolved before NewCache
  2. Avoid unknown ':' placeholders in cache dir configuration
  3. In tests, set DirCompiled directly rather than Dir with placeholders

Example fix

// before
c := NewCache(fs, FileCacheConfig{Dir: ":cacheDir/modules"})
// after
cfgs, _ := filecache.DecodeConfig(fs, bcfg, map[string]any{})
c := NewCache(fs, cfgs[filecache.CacheKeyModules])
Defensive patterns

Strategy: validation

Validate before calling

// Resolve placeholders via DecodeConfig before constructing a cache.
if err := cfg.init(); err != nil {
    log.Fatal(err)
}
// Or always go through:
// cfgs, err := filecache.DecodeConfig(fs, bcfg, userConfig)

Try / catch

// Panics cannot be caught cleanly in production; prefer validation.
// If unavoidable, isolate in a goroutine with recover:
func safeNewCache(fs afero.Fs, cfg FileCacheConfig) (c *Cache, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("NewCache panic: %v", r)
        }
    }()
    return NewCache(fs, cfg), nil
}

Prevention

When it happens

Trigger: Programmatically building a FileCacheConfig with a Dir containing an unresolved ':' placeholder and calling NewCache directly, bypassing DecodeConfig's placeholder resolution.

Common situations: Custom test setups that construct FileCacheConfig by hand; an unknown placeholder (not :cacheDir/:resourceDir/:project) slipping through resolution.

Related errors


AI-assisted analysis of gohugoio/hugo@52c9bd7908 (2026-08-09). Data as JSON: /api/errors/e011dde817c71042. Report an issue: GitHub.