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
- Format refresh_interval as a quoted proto-JSON Duration string such as "600s" or "10.5s" (seconds-based, fractional seconds allowed).
- Drop the refresh_interval field entirely to fall back to the 10-minute default (defaultRefreshInterval).
- 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
- Format durations as proto-JSON strings like "600s", never Go-style "10m" or bare numbers.
- Omit refresh_interval to accept the 10-minute default.
- Document the expected format next to the config field in your template.
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
- pemfile: json.Unmarshal(%s) failed: %v
- pemfile: at least one credential file needs to be specified
- pemfile: private key file and identity cert file should be b
- pemfile: certificate and key file must be in the same direct
- rls: failed to parse lookup_service_timeout in route lookup
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/8972a6a89b56a745.
Report an issue: GitHub.