multica-ai/multica · warning

body must be a JSON object or array

Error message

body must be a JSON object or array

What it means

Returned by normalizeWebhookPayload when the body is valid JSON but not an object or array — e.g. a bare string, number, boolean, or null. Per the source comment, a scalar like "hello" parses fine but has no useful interpretation as a webhook payload and would land in the agent prompt as a bare primitive, so it is rejected after the early shape check (invalid JSON is a separate error).

Source

Thrown at server/internal/handler/autopilot_webhook.go:131

func normalizeWebhookPayload(body []byte, headers http.Header) (WebhookEnvelope, error) {
	body = stripBOM(body)
	if len(body) == 0 {
		return WebhookEnvelope{}, errors.New("empty body")
	}

	// First, validate JSON shape (object or array). Reject scalars early —
	// `"hello"` is technically valid JSON but has no useful interpretation
	// as a webhook payload and would land in the agent prompt as a bare
	// string.
	var asAny any
	if err := json.Unmarshal(body, &asAny); err != nil {
		return WebhookEnvelope{}, fmt.Errorf("invalid json: %w", err)
	}
	switch asAny.(type) {
	case map[string]any, []any:
		// ok
	default:
		return WebhookEnvelope{}, errors.New("body must be a JSON object or array")
	}

	now := time.Now().UTC().Format(time.RFC3339)
	contentType := headers.Get("Content-Type")
	if i := strings.Index(contentType, ";"); i >= 0 {
		contentType = strings.TrimSpace(contentType[:i])
	}

	env := WebhookEnvelope{
		Request: WebhookRequest{
			ReceivedAt:  now,
			ContentType: contentType,
		},
	}

	// 1. Caller-provided envelope.
	if obj, ok := asAny.(map[string]any); ok {
		if eventStr, ok := obj["event"].(string); ok && eventStr != "" {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Wrap the payload in an object: send {"value": 42} or {"message": "hello"} instead of a bare scalar.
  2. Check for double serialization in the sender — the top-level value must be {...} or [...].
  3. Validate the payload shape client-side before posting if you control the sender.

Example fix

# before
curl -X POST https://host/api/webhooks/autopilot -d '"deploy finished"'

# after
curl -X POST https://host/api/webhooks/autopilot \
  -H 'Content-Type: application/json' \
  -d '{"event":"deploy","message":"deploy finished"}'
Defensive patterns

Strategy: type-guard

Validate before calling

// Sender side, before POST
function isEnvelopeShape(v: unknown): boolean {
  return typeof v === "object" && v !== null;
}
if (!isEnvelopeShape(payload)) {
  throw new Error("webhook payload must be a JSON object or array");
}

Type guard

function isWebhookEnvelopeShape(v: unknown): v is Record<string, unknown> | unknown[] {
  return typeof v === "object" && v !== null;
}

Prevention

When it happens

Trigger: POSTing a JSON scalar: body of "42", "true", "null", or a double-quoted string; webhook senders emitting a bare token; clients double-encoding so the top level becomes a string.

Common situations: Hand-rolled integrations posting a plain status string; test payloads written as JSON strings; encoding bugs where JSON.stringify is applied twice (the body becomes '"{\"event\":...}"').

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/0168b5fddbce73b9. Report an issue: GitHub.