grpc/grpc-go · error
pemfile: json.Unmarshal(%s) failed: %v
Error message
pemfile: json.Unmarshal(%s) failed: %v
What it means
The pemfile (file_watcher) certificate provider plugin could not JSON-unmarshal the configuration blob passed to ParseConfig into its expected struct (certificate_file, private_key_file, ca_certificate_file, spiffe_trust_bundle_map_file, refresh_interval). The wrapper passes the raw bytes verbatim into the error so you can see exactly what failed to parse. It is thrown at config-parse time, before any provider is built, so it surfaces during grpc.Dial/Server startup when an xDS or programmatic config supplies malformed JSON.
Source
Thrown at credentials/tls/certprovider/pemfile/builder.go:74
}
func (p *pluginBuilder) Name() string {
return PluginName
}
func pluginConfigFromJSON(jd json.RawMessage) (Options, error) {
// The only difference between this anonymous struct and the Options struct
// is that the refresh_interval is represented here as a duration proto,
// while in the latter a time.Duration is used.
cfg := &struct {
CertificateFile string `json:"certificate_file,omitempty"`
PrivateKeyFile string `json:"private_key_file,omitempty"`
CACertificateFile string `json:"ca_certificate_file,omitempty"`
SPIFFETrustBundleMapFile string `json:"spiffe_trust_bundle_map_file,omitempty"`
RefreshInterval json.RawMessage `json:"refresh_interval,omitempty"`
}{}
if err := json.Unmarshal(jd, cfg); err != nil {
return Options{}, fmt.Errorf("pemfile: json.Unmarshal(%s) failed: %v", string(jd), err)
}
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{}View on GitHub (pinned to 03255a9237)
Solutions
- Validate the offending JSON printed in the %s slot with a linter or `jq -e .` before passing it to ParseConfig.
- Ensure the value passed to ParseConfig is a json.RawMessage whose contents are a complete JSON object; do not pass a Go struct, map, or empty slice.
- Construct the config from a typed Go struct and re-marshal it with json.Marshal to guarantee validity before handing it to the provider.
- Check that every field value matches its documented JSON type (all four *_file fields are strings; refresh_interval is a proto-JSON Duration).
Example fix
// before
cfg := []byte(`{ "certificate_file": "server.crt", "private_key_file": "server.key", }`) // trailing comma -> error
certprovider.ParseConfig("file_watcher", cfg)
// after
cfg := []byte(`{ "certificate_file": "server.crt", "private_key_file": "server.key" }`)
if _, err := certprovider.ParseConfig("file_watcher", cfg); err != nil { log.Fatal(err) } Defensive patterns
Strategy: validation
Validate before calling
func validFileWatcherJSON(b []byte) error {
var raw map[string]any
return json.Unmarshal(b, &raw) // nil == structurally valid JSON
}
// call before certprovider.ParseConfig("file_watcher", rawJSON) Type guard
func isRawMessage(v any) bool {
_, ok := v.(json.RawMessage)
return ok
} Try / catch
bc, err := certprovider.ParseConfig("file_watcher", rawJSON)
if err != nil {
return fmt.Errorf("invalid file_watcher config: %w", err)
} Prevention
- Always pass json.RawMessage (not a Go struct/map) into certprovider.ParseConfig.
- Generate config from typed structs via json.Marshal rather than hand-writing JSON strings.
- Lint config files with jq in CI before deploying.
When it happens
Trigger: Calling certprovider.ParseConfig("file_watcher", []byte(`{bad json`)) or providing a file_watcher config via xDS/bootstrap where a field has the wrong JSON type (e.g. "certificate_file": 123) or a structural error such as a trailing comma, unescaped quote, or unclosed brace.
Common situations: Hand-editing the GRPC_XDS_BOOTSTRAP file or a JSON config string and introducing a typo; feeding a Go map or struct into ParseConfig instead of json.RawMessage; copy-pasting a config that uses comments (//) which json.Unmarshal rejects.
Related errors
- pemfile: at least one credential file needs to be specified
- pemfile: certificate and key file must be in the same direct
- pemfile: protojson.Unmarshal(%+v) failed: %v
- pemfile: private key file and identity cert file should be b
- provider instance is closed
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/f57a0ca1762ecbdb.
Report an issue: GitHub.