siyuan-note/siyuan · error

plugin WebSocket control frame exceeds 125 bytes

Error message

plugin WebSocket control frame exceeds 125 bytes

What it means

WebSocket control frames (opcodes 8 close, 9 ping, 10 pong) are limited by RFC 6455 to a payload of 125 bytes because they must fit in a single unfragmented frame. Bundle.ValidatePluginServiceFrame enforces this: when frameType >= 8 and len(payload) > 125 it returns "plugin WebSocket control frame exceeds 125 bytes". Oversized control frames break interoperability and are commonly rejected by other WebSocket implementations.

Solutions

  1. Truncate or drop the close-frame reason text so the control payload is at most 125 bytes (2 status bytes + reason)
  2. Send the data through text/binary frames (opcodes 1/2) instead of piggybacking on ping/pong
  3. Trim the payload to 125 bytes before emitting any control frame
  4. Add a size assertion in plugin tests for all close/ping/pong payloads

Example fix

// before
conn.WriteControl(8, append(statusBytes, longReason...), deadline)
// after
reason := longReason
if len(reason) > 123 { reason = reason[:123] }
conn.WriteControl(8, append(statusBytes, reason...), deadline)
Defensive patterns

Strategy: validation

Validate before calling

func controlFrameFits(t int, payload []byte) bool { return t < 8 || len(payload) <= 125 }

Try / catch

if err := bundle.ValidatePluginServiceFrame(method, path, frameType, payload); err != nil { if strings.Contains(err.Error(), "control frame exceeds 125 bytes") { truncateControlPayload(); return }; return err }

Prevention

When it happens

Trigger: Calling Bundle.ValidatePluginServiceFrame with frameType 8, 9, or 10 and a payload longer than 125 bytes — e.g. a close frame carrying a long reason string, or a ping used to transport application data.

Common situations: A close status/reason string longer than 125 bytes is packed into a close frame; an application misuses ping/pong as a data channel; a plugin echoes back an upstream payload as a pong without truncating it; automated tests generate large control payloads.

Understand the failure class

Background: payload too large / request exceeds maximum size: why libraries cap bytes and how to fix oversize payloads — this error's family across 50 libraries.

Related errors


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

Appendix: source

Thrown at kernel/apicontract/plugin_service_protocol.go:271

		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, "$")
		}
	}
	return fmt.Errorf("unregistered plugin service: %s %s", method, path)

View on GitHub (pinned to 9f775e8a12)