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
- Restrict emitted frames to text (1), binary (2), close (8), ping (9), and pong (10); reassemble fragmented messages before forwarding
- Treat opcode 0 continuation frames as part of their initial data/binary frame rather than validating them separately
- Sanitize any frame type read from external input before passing it to the validator
- 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
- Reassemble fragmented messages (opcode 0) before validating or forwarding frames
- Map reserved opcodes to errors at the read boundary instead of passing them on
- Constrain frame types to an enum in plugin code
- Fuzz or table-test the frame validator with opcodes 0-15
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
- plugin WebSocket control frame exceeds 125 bytes
- WebSocket handshake contains a body
- event.data is required
- failed to convert request value to object
- invalid event object
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)