JuliusBrussee/caveman · error

store is nil

Error message

store is nil

What it means

objectstore.Probe is a library-internal guard that rejects a nil Store before attempting its put/get/purge round-trip. The realistic path to a nil store is FromEnv's documented (nil, nil) return when S3_ENDPOINT is unset outside production — callers that skip the two-value nil check then pass the nil interface straight into Probe. It is a programming-error signal, not an infrastructure failure.

Source

Thrown at shared/platform/objectstore/objectstore.go:231

func New(cfg Config) (Store, error) {
	endpoint := strings.TrimPrefix(strings.TrimPrefix(cfg.Endpoint, "https://"), "http://")
	client, err := minio.New(endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
		Secure: cfg.UseSSL,
		Region: cfg.Region,
	})
	if err != nil {
		return nil, fmt.Errorf("objectstore: minio client: %w", err)
	}
	return &minioStore{client: client, bucket: cfg.Bucket}, nil
}

// Probe proves the operations production retention needs instead of accepting a
// syntactically valid but unusable bucket configuration. The random probe body
// contains no tenant data and every version is removed before success returns.
func Probe(ctx context.Context, store Store) error {
	if store == nil {
		return errors.New("store is nil")
	}
	probe := make([]byte, 32)
	if _, err := rand.Read(probe); err != nil {
		return fmt.Errorf("generate probe: %w", err)
	}
	key := "_cave_health/" + base64.RawURLEncoding.EncodeToString(probe)
	if err := store.Put(ctx, key, probe, "application/octet-stream"); err != nil {
		return err
	}
	cleaned := false
	defer func() {
		if !cleaned {
			// Keep health checks within their caller deadline. A failed probe may
			// leave one random, tenant-free canary for lifecycle cleanup; readiness
			// must never hang on an unbounded background delete.
			_ = PurgeObject(ctx, store, key)
		}
	}()

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Check both return values of FromEnv: outside production, nil store with nil error means object storage is intentionally disabled — skip the probe
  2. Make readiness handlers treat a disabled store as not-applicable rather than probing
  3. Grep call sites of Probe for direct FromEnv plumbing without an intermediate nil check

Example fix

// before
store, _ := objectstore.FromEnv()
if err := objectstore.Probe(ctx, store); err != nil { ... }

// after
store, err := objectstore.FromEnv()
if err != nil {
	return err
}
if store == nil {
	return nil // object storage disabled outside production
}
if err := objectstore.Probe(ctx, store); err != nil { ... }
Defensive patterns

Strategy: type-guard

Validate before calling

store, err := objectstore.FromEnv()
if err != nil {
	return err
}
if store == nil {
	return nil // outside production with S3_ENDPOINT unset, storage is disabled
}

Type guard

func storeEnabled(s objectstore.Store) bool {
	return s != nil
}

Try / catch

if err := objectstore.Probe(ctx, store); err != nil {
	if err.Error() == "store is nil" {
		// caller bug: FromEnv returned (nil, nil) for a disabled store; skip probing
	}
	return err
}

Prevention

When it happens

Trigger: Calling Probe(ctx, store) where store is nil — typically store, err := objectstore.FromEnv() outside production with S3_ENDPOINT unset, and the err == nil branch (correct here) proceeding to Probe without checking the first return value.

Common situations: Health-check endpoints that unconditionally probe storage; dev environments where FromEnv legitimately returns (nil, nil) but the readiness handler assumes a store always exists; refactors that moved the FromEnv call and dropped the nil guard.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/a85ae81c529a3e53. Report an issue: GitHub.