fatedier/frp · error
unmarshal ClientPluginOptions error: %v
Error message
unmarshal ClientPluginOptions error: %v
What it means
DecodeClientPluginOptionsJSON recognized the plugin type and reflect-constructed its options struct, but decodeJSONWithOptions failed to unmarshal the plugin JSON into that struct. The underlying error is wrapped as 'unmarshal ClientPluginOptions error'. It identifies a field-level mismatch inside the plugin options object, not a bad type.
Source
Thrown at pkg/config/v1/decode.go:119
if isJSONNull(b) {
return TypedClientPluginOptions{}, nil
}
var env typedEnvelope
if err := jsonx.Unmarshal(b, &env); err != nil {
return TypedClientPluginOptions{}, err
}
if env.Type == "" {
return TypedClientPluginOptions{}, errors.New("plugin type is empty")
}
v, ok := clientPluginOptionsTypeMap[env.Type]
if !ok {
return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
}
optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
}
return TypedClientPluginOptions{
Type: env.Type,
ClientPluginOptions: optionsStruct,
}, nil
}
func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
if isJSONNull(b) {
return TypedVisitorPluginOptions{}, nil
}
var env typedEnvelope
if err := jsonx.Unmarshal(b, &env); err != nil {
return TypedVisitorPluginOptions{}, err
}
if env.Type == "" {
return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")View on GitHub (pinned to 6c8a8d0a97)
Solutions
- Follow the wrapped error to the exact field; correct its value type or name for that plugin's schema.
- Use current option names (e.g. localPath for static_file) rather than legacy hyphenated keys.
- If strict mode is on and the field is obsolete, delete it instead of suppressing the error.
Example fix
// before
{"type": "static_file", "localPath": 8080}
// after
{"type": "static_file", "localPath": "/srv/files"} Defensive patterns
Strategy: try-catch
Validate before calling
// Optional schema lint for the most common mistake: string where number expected.
func lintPluginOptions(b []byte) error {
var probe map[string]any
if err := json.Unmarshal(b, &probe); err != nil {
return err
}
for _, numKey := range []string{"localPort", "remotePort", "bindPort", "httpsPort"} {
if v, ok := probe[numKey].(string); ok {
return fmt.Errorf("%s must be a number, got string %q", numKey, v)
}
}
return nil
} Try / catch
if _, err := v1.DecodeClientPluginOptionsJSON(b, opts); err != nil {
if strings.Contains(err.Error(), "unmarshal ClientPluginOptions error") {
// wrapped error names the field; fix type/name in the plugin options object
}
return err
} Prevention
- Use current camelCase option names (localPath, not plugin_local_path).
- Keep numbers as JSON numbers.
- When strict decoding is enabled, remove obsolete keys.
When it happens
Trigger: A plugin options object with a wrong-typed field (e.g. static_file's localPath given as a number, or an array where a string is expected), or unknown fields under strict DecodeOptions. Any DecodeClientPluginOptionsJSON call whose body doesn't match the struct for the named plugin.
Common situations: Plugin option renames across frp versions (INI-era hyphenated names like plugin_local_path vs TOML-era localPath); hand-written JSON with quoted numbers; strict decoding flagging legacy keys.
Related errors
- unmarshal VisitorPluginOptions error: %v
- unmarshal ProxyConfig error: %v
- unmarshal proxy plugin error: %v
- unmarshal VisitorConfig error: %v
- unmarshal visitor plugin error: %v
AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15).
Data as JSON: /api/errors/fd50003bc428479b.
Report an issue: GitHub.