siyuan-note/siyuan · error

invalid plugin JSONP response

Error message

invalid plugin JSONP response

What it means

For endpoints declared with PluginServiceJSONP, ValidatePluginServiceResponse requires the payload to be a JSONP envelope: either a bare JSON value or a JavaScript-style call like callback({...}); (a trailing ');' is tolerated). The validator scans for a '(' and checks that everything after it parses as JSON; if neither form is valid it returns "invalid plugin JSONP response". This enforces that the JSONP contract is met so browsers can execute the wrapper as a function call.

Solutions

  1. Marshal the inner payload to valid JSON first, then wrap it as callbackName(<json>) (optionally with a trailing semicolon)
  2. Validate the inner JSON with json.Valid before wrapping it in the callback
  3. Check that the response is not an error page or empty body being validated as JSONP
  4. Return the endpoint in a JSON mode instead of JSONP if no client-side callback wrapper is actually needed

Example fix

// before
fmt.Fprintf(w, "%s(%s)", callback, rawBody)
// after
inner, _ := json.Marshal(data)
fmt.Fprintf(w, "%s(%s);", callback, inner)
Defensive patterns

Strategy: validation

Validate before calling

func isValidJSONP(payload []byte) bool {
  s := string(payload)
  if json.Valid(payload) { return true }
  if tail, ok := strings.CutSuffix(s, ");"); ok {
    for i, c := range tail { if c == '(' && json.Valid([]byte(tail[i+1:])) { return true } }
  }
  return false
}

Try / catch

if err := bundle.ValidatePluginServiceResponse(method, path, PluginServiceJSONP, status, ct, payload); err != nil { if strings.Contains(err.Error(), "invalid plugin JSONP response") { rewrapAsJSONPAndRetry(); return }; return err }

Prevention

When it happens

Trigger: Calling Bundle.ValidatePluginServiceResponse with mode PluginServiceJSONP and a payload that is neither valid JSON nor of the form prefix(json)[;] — e.g. an empty string, plain text, or a callback wrapper whose inner payload is malformed JSON.

Common situations: The plugin handler wraps a non-JSON body in the callback name; the inner JSON was truncated or produced by string concatenation; the response is plain HTML from an error page; the callback name itself contains characters that break the parenthesis scan.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — 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/7e3a6007e63da937. Report an issue: GitHub.

Appendix: source

Thrown at kernel/apicontract/plugin_service_protocol.go:222

		if len(payload) != 0 {
			return fmt.Errorf("empty plugin response contains a body")
		}
	case PluginServiceJSON, PluginServiceASCIIJSON, PluginServiceIndentedJSON, PluginServicePureJSON:
		if !json.Valid(payload) {
			return fmt.Errorf("invalid plugin JSON response")
		}
	case PluginServiceJSONP:
		valid := json.Valid(payload)
		if tail, ok := strings.CutSuffix(string(payload), ");"); ok {
			for index, char := range tail {
				if char == '(' && json.Valid([]byte(tail[index+1:])) {
					valid = true
					break
				}
			}
		}
		if !valid {
			return fmt.Errorf("invalid plugin JSONP response")
		}
	case PluginServiceSecureJSON:
		if !json.Valid(payload) && !json.Valid([]byte(strings.TrimPrefix(string(payload), "while(1);"))) {
			return fmt.Errorf("invalid plugin secure JSON response")
		}
	case PluginServiceWebSocket:
		if status == 101 && len(payload) != 0 {
			return fmt.Errorf("WebSocket handshake contains a body")
		}
	case PluginServiceXML:
		decoder := xml.NewDecoder(strings.NewReader(string(payload)))
		for {
			if _, err := decoder.Token(); err != nil {
				if err == io.EOF {
					break
				}
				return err
			}

View on GitHub (pinned to 9f775e8a12)