fatedier/frp · error

visitor plugin type is empty

Error message

visitor plugin type is empty

What it means

Thrown by DecodeVisitorPluginOptionsJSON when decoding visitor plugin options JSON that is not null but whose top-level "type" is empty. Visitor plugins (used by stcp/sudp/xtcp visitors for things like TLS termination or secret-key handling) go through the same typedEnvelope mechanism as client plugins, dispatched via visitorPluginOptionsTypeMap; a missing type makes dispatch impossible. This is a strict decoding guard fired before the unknown-visitor-plugin-type check.

Source

Thrown at pkg/config/v1/decode.go:137

		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")
	}

	v, ok := visitorPluginOptionsTypeMap[env.Type]
	if !ok {
		return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type)
	}
	optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)
	if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
		return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
	}
	return TypedVisitorPluginOptions{
		Type:                 env.Type,
		VisitorPluginOptions: optionsStruct,
	}, nil
}

func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
	type rawClientConfig struct {

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Add a valid non-empty "type" to the visitor plugin options JSON if a visitor plugin is intended.
  2. If no visitor plugin options are needed, remove the options object or set it to JSON null — the decoder returns an empty TypedVisitorPluginOptions without error.
  3. Verify the exact key spelling "type" (case-sensitive) and that it is at the top level of the options object, not nested.

Example fix

# before
[[visitors]]
name = "stcpv"
type = "stcp"
secretKey = "s"
bindAddr = "127.0.0.1"
bindPort = 9000
[visitors.plugin]
# type missing

# after
[visitors.plugin]
type = "https2http"
localAddr = ":8443"
Defensive patterns

Strategy: validation

Validate before calling

// before calling DecodeVisitorPluginOptionsJSON
var env struct{ Type string `json:"type"` }
_ = jsonx.Unmarshal(b, &env)
if !isJSONNull(b) && env.Type == "" {
    return fmt.Errorf("visitor plugin options must declare a non-empty \"type\" or be null")
}

Type guard

func hasVisitorPluginType(b []byte) bool {
    if isJSONNull(b) {
        return true
    }
    var env struct{ Type string `json:"type"` }
    if err := jsonx.Unmarshal(b, &env); err != nil {
        return false
    }
    return env.Type != ""
}

Try / catch

if _, err := v1.DecodeVisitorPluginOptionsJSON(raw, v1.DecodeOptions{}); err != nil {
    if strings.Contains(err.Error(), "visitor plugin type is empty") {
        // fix the config: add "type" to visitor plugin options or set them to null
    }
    return err
}

Prevention

When it happens

Trigger: Calling DecodeVisitorPluginOptionsJSON(b, options) where the JSON object lacks a "type" key or has type: "" — e.g. a [[visitors]] section in frpc config whose plugin options table was written without a type, or where null was expected but an empty object {} was supplied.

Common situations: Writing an stcp/xtcp visitor entry and omitting or blanking the plugin type, migrating configs from a format that represented 'no plugin' as {} instead of null, or programmatic generation that always emits an options object even when there is no visitor plugin.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/67a33f2abd4fd801. Report an issue: GitHub.