siyuan-note/siyuan · error

invalid plugin service protocol variants

Error message

invalid plugin service protocol variants

What it means

The PluginService protocol declaration attached to a plugin service endpoint must be byte-for-byte equal (via reflect.DeepEqual) to the canonical definition returned by PluginServiceOptions(). Because the variant list, admission statuses, WebSocket frame names, and SSE schema define the wire contract published in the bundle, arbitrary custom definitions are rejected so the declared protocol stays consistent across bundles.

Solutions

  1. Set definition.PluginService directly from PluginServiceOptions(): &PluginServiceOptions().PluginService
  2. Remove any hand edits (extra variants, reordered entries, changed statuses) so DeepEqual succeeds
  3. After a library upgrade, re-copy from PluginServiceOptions() rather than keeping a stored definition

Example fix

// before
svc := apicontract.PluginServiceOptions().PluginService
svc.Variants = svc.Variants[:3] // keep only JSON modes
definition.PluginService = &svc
// after
definition.PluginService = &apicontract.PluginServiceOptions().PluginService
Defensive patterns

Strategy: validation

Validate before calling

if definition.PluginService != nil &&
    !reflect.DeepEqual(*definition.PluginService, apicontract.PluginServiceOptions().PluginService) {
    return errors.New("plugin service definition must equal PluginServiceOptions() exactly")
}

Try / catch

if err := apicontract.BuildBundle(endpoints); err != nil {
    if strings.Contains(err.Error(), "protocol variants") { log.Fatalf("stale plugin service definition: %v", err) }
    return err
}

Prevention

When it happens

Trigger: BuildBundle validates a plugin-service endpoint whose PluginService field was hand-constructed or modified (extra/missing variant, changed AdmissionStatuses, altered SSEEvent schema) instead of copied from PluginServiceOptions().

Common situations: Trimming the variants list to 'only the modes I use', reordering variants (DeepEqual is order-sensitive for slices), bumping the canonical options in a library update while a stale cached definition is embedded, or hand-writing the definition from the JSON schema.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/34bd7720c254c346. Report an issue: GitHub.

Appendix: source

Thrown at kernel/apicontract/plugin_service_protocol.go:128

	}
	if err := validatePluginServiceStatus(response.pluginServiceMode, response.httpStatus); err != nil {
		panic(err)
	}
	return response.httpStatus
}

func validatePluginServiceDefinition(definition Definition) error {
	if (definition.Output == PluginServiceOutput) != (definition.PluginService != nil) {
		return fmt.Errorf("plugin service output requires a protocol declaration")
	}
	if definition.PluginService == nil {
		return nil
	}
	if definition.Data != reflect.TypeFor[PluginServiceContent]() || definition.SSE != nil || definition.Proxy != nil || definition.WebSocket != nil || definition.DataOnError || definition.ErrorStatus != 0 {
		return fmt.Errorf("invalid plugin service response options")
	}
	if !reflect.DeepEqual(definition.PluginService, PluginServiceOptions().PluginService) {
		return fmt.Errorf("invalid plugin service protocol variants")
	}
	return nil
}

func validatePluginServiceStatus(mode PluginServiceMode, status int) error {
	if status < 100 || status > 999 {
		return fmt.Errorf("invalid plugin service HTTP status: %d", status)
	}
	known := false
	for _, variant := range PluginServiceOptions().PluginService.Variants {
		if variant.Mode == mode {
			known = true
			break
		}
	}
	if !known {
		return fmt.Errorf("unknown plugin service mode: %s", mode)
	}

View on GitHub (pinned to 9f775e8a12)