siyuan-note/siyuan · error

plugin service response forbids a body

Error message

plugin service response forbids a body

What it means

HTTP rules forbid response bodies for HEAD requests, any status below 200 (1xx), 204 No Content, and 304 Not Modified. This error means the plugin service produced bytes for such a response, which would corrupt the HTTP framing (e.g. a body after 204 confuses clients and proxies).

Solutions

  1. Skip all body writes when the request method is HEAD or the status is <200, 204, or 304
  2. Set the body-less status only after ensuring no bytes were already written
  3. Return an empty payload ([]byte(nil)) for these responses in tests/validation

Example fix

// before
w.WriteHeader(http.StatusNoContent)
w.Write([]byte("{}"))
// after
w.WriteHeader(http.StatusNoContent) // no body written
Defensive patterns

Strategy: validation

Validate before calling

func bodyForbidden(method string, status int) bool {
	return method == http.MethodHead || status < 200 || status == http.StatusNoContent || status == http.StatusNotModified
}
if bodyForbidden(endpoint.Method, status) && len(payload) > 0 {
	payload = nil // drop body before validation/writing
}

Prevention

When it happens

Trigger: Writing payload bytes with http.ResponseWriter for a HEAD request, or replying with status 204/304 (or a 1xx) while still emitting a body in a plugin-service endpoint.

Common situations: A handler writes a JSON error body but sets the status to 204; a shared middleware writes a body before the handler decides on 304; framework helpers that always render a body regardless of method/status.

Related errors


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

Appendix: source

Thrown at kernel/apicontract/plugin_service_protocol.go:174

	case PluginServiceWebSocket:
		if status != 101 && status != 400 && status != 500 {
			return fmt.Errorf("invalid plugin WebSocket status")
		}
	case PluginServiceSSE:
		if status != 200 && status != 500 {
			return fmt.Errorf("invalid plugin SSE status")
		}
	}
	return nil
}

func (b *Bundle) validatePluginServiceHTTPResponse(endpoint EndpointSchema, status int, contentType string, payload []byte) error {
	if status < 100 || status > 999 {
		return fmt.Errorf("invalid plugin service HTTP status")
	}
	if endpoint.Method == "HEAD" || status < 200 || status == 204 || status == 304 {
		if len(payload) > 0 {
			return fmt.Errorf("plugin service response forbids a body")
		}
		return nil
	}
	// 原始文件、代理及插件自选媒体允许任意字节,具体分支由 ValidatePluginServiceResponse 校验。
	return nil
}

func (b *Bundle) ValidatePluginServiceResponse(method, path string, mode PluginServiceMode, status int, contentType string, payload []byte) error {
	var found bool
	for _, endpoint := range b.Endpoints {
		if endpoint.Method == method && endpoint.Path == path && endpoint.PluginService != nil {
			found = true
			break
		}
	}
	if !found {
		return fmt.Errorf("unregistered plugin service: %s %s", method, path)
	}

View on GitHub (pinned to 9f775e8a12)