grpc/grpc-go · error

pemfile: protojson.Unmarshal(%+v) failed: %v

Error message

pemfile: protojson.Unmarshal(%+v) failed: %v

What it means

The pemfile plugin parsed the top-level JSON successfully but failed to decode the refresh_interval field as a protobuf Duration (via protojson.Unmarshal into durationpb.Duration). The field is optional, so this only fires when refresh_interval is present yet not a valid proto-JSON Duration representation.

Source

Thrown at credentials/tls/certprovider/pemfile/builder.go:94

	if !envconfig.XDSSPIFFEEnabled {
		cfg.SPIFFETrustBundleMapFile = ""
	}

	opts := Options{
		CertFile:            cfg.CertificateFile,
		KeyFile:             cfg.PrivateKeyFile,
		RootFile:            cfg.CACertificateFile,
		SPIFFEBundleMapFile: cfg.SPIFFETrustBundleMapFile,
		// Refresh interval is the only field in the configuration for which we
		// support a default value. We cannot possibly have valid defaults for
		// file paths to watch. Also, it is valid to specify an empty path for
		// some of those fields if the user does not want to watch them.
		RefreshDuration: defaultRefreshInterval,
	}
	if cfg.RefreshInterval != nil {
		dur := &durationpb.Duration{}
		if err := protojson.Unmarshal(cfg.RefreshInterval, dur); err != nil {
			return Options{}, fmt.Errorf("pemfile: protojson.Unmarshal(%+v) failed: %v", cfg.RefreshInterval, err)
		}
		opts.RefreshDuration = dur.AsDuration()
	}

	if err := opts.validate(); err != nil {
		return Options{}, err
	}
	return opts, nil
}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Format refresh_interval as a quoted proto-JSON Duration string such as "600s" or "10.5s" (seconds-based, fractional seconds allowed).
  2. Drop the refresh_interval field entirely to fall back to the 10-minute default (defaultRefreshInterval).
  3. Validate with protoc or the google.protobuf.Duration JSON schema before deployment.

Example fix

// before
{ "ca_certificate_file": "ca.crt", "refresh_interval": 600 }

// after
{ "ca_certificate_file": "ca.crt", "refresh_interval": "600s" }
Defensive patterns

Strategy: validation

Validate before calling

import "google.golang.org/protobuf/types/known/durationpb"
import "google.golang.org/protobuf/encoding/protojson"

func validRefreshInterval(b []byte) error {
    if len(b) == 0 { return nil }
    d := &durationpb.Duration{}
    return protojson.Unmarshal(b, d)
}

Try / catch

opts, err := pemfile.BuildableConfigFromJSON(raw) // or ParseConfig
if err != nil { return fmt.Errorf("pemfile config: %w", err) }

Prevention

When it happens

Trigger: Supplying refresh_interval as a JSON string that is not a valid duration (e.g. "10" or "1h30m"), as a numeric value (protojson expects a string like "10s"), or as an object that does not match the well-known Duration schema ("seconds"/"nanos").

Common situations: Mixing Go time.Duration string syntax ("1h30m") with proto-JSON Duration syntax ("3600s"/"3600.5s"); pasting a number instead of a quoted string; field left over from a config meant for a different runtime.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/8972a6a89b56a745. Report an issue: GitHub.