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

  1. Validate the offending JSON printed in the %s slot with a linter or `jq -e .` before passing it to ParseConfig.
  2. 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.
  3. Construct the config from a typed Go struct and re-marshal it with json.Marshal to guarantee validity before handing it to the provider.
  4. 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

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


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