siyuan-note/siyuan · error

undeclared plugin WebSocket frame

Error message

undeclared plugin WebSocket frame: %d

What it means

Bundle.ValidatePluginServiceFrame validates WebSocket frames sent on a plugin service endpoint. Only the data frames 1 (text) and 2 (binary) and control frames 8 (close), 9 (ping), and 10 (pong) are declared; any other opcode — including reserved ones (3-7, 11-15) and continuation frame 0 — is rejected with "undeclared plugin WebSocket frame: %d". The message includes the offending opcode so you can identify which frame type slipped through.

Solutions

  1. Restrict emitted frames to text (1), binary (2), close (8), ping (9), and pong (10); reassemble fragmented messages before forwarding
  2. Treat opcode 0 continuation frames as part of their initial data/binary frame rather than validating them separately
  3. Sanitize any frame type read from external input before passing it to the validator
  4. Log the offending opcode from the error message and map it to the sending code path

Example fix

// before
validateFrame(msg.Opcode, msg.Payload) // forwards opcode 0 continuations
// after
if msg.Opcode == 0 { bufferForAssembly(msg); return }
validateFrame(msg.Opcode, msg.Payload)
Defensive patterns

Strategy: validation

Validate before calling

func isDeclaredFrameType(t int) bool { return t == 1 || t == 2 || t == 8 || t == 9 || t == 10 }

Try / catch

if err := bundle.ValidatePluginServiceFrame(method, path, frameType, payload); err != nil { if strings.Contains(err.Error(), "undeclared plugin WebSocket frame") { reassembleFragmentedMessage(); return }; return err }

Prevention

When it happens

Trigger: Calling Bundle.ValidatePluginServiceFrame with frameType values outside {1,2,8,9,10}: opcode 0 (continuation frames of fragmented messages), opcodes 3-7 or 11-15 (reserved), or a raw integer from an untrusted source passed straight into the validator.

Common situations: A plugin relays raw frames from a client or upstream and forwards continuation frames (opcode 0) of fragmented messages individually; a custom protocol layer invents an opcode; tests feed arbitrary integers to check validator behavior; a non-WebSocket value like 80/100 is passed by mistake.

Related errors


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

Appendix: source

Thrown at kernel/apicontract/plugin_service_protocol.go:268

			return err
		}
	case PluginServiceProtoBuf:
		for len(payload) > 0 {
			_, _, size := protowire.ConsumeField(payload)
			if size < 0 {
				return protowire.ParseError(size)
			}
			payload = payload[size:]
		}
	}
	return nil
}

func (b *Bundle) ValidatePluginServiceFrame(method, path string, frameType int, payload []byte) error {
	for _, endpoint := range b.Endpoints {
		if endpoint.Method == method && endpoint.Path == path && endpoint.PluginService != nil {
			if frameType != 1 && frameType != 2 && frameType != 8 && frameType != 9 && frameType != 10 {
				return fmt.Errorf("undeclared plugin WebSocket frame: %d", frameType)
			}
			if frameType >= 8 && len(payload) > 125 {
				return fmt.Errorf("plugin WebSocket control frame exceeds 125 bytes")
			}
			return nil
		}
	}
	return fmt.Errorf("unregistered plugin service: %s %s", method, path)
}

func (b *Bundle) ValidatePluginServiceEvent(method, path string, payload []byte) error {
	for _, endpoint := range b.Endpoints {
		if endpoint.Method == method && endpoint.Path == path && endpoint.PluginService != nil {
			var event any
			if err := json.Unmarshal(payload, &event); err != nil {
				return err
			}
			return b.validate(endpoint.PluginService.SSEEvent, event, "$")

View on GitHub (pinned to 9f775e8a12)