multica-ai/multica · warning

empty body

Error message

empty body

What it means

Returned by normalizeWebhookPayload in server/internal/handler/autopilot_webhook.go when the incoming webhook body is empty after BOM stripping. The handler refuses to fabricate an envelope from nothing: with no bytes there is no event to infer (X-GitHub-Event, body.event, etc. all need a body) and no payload to hand the agent. It is a request-shape rejection at the trust boundary, before any JSON parsing.

Source

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

// normalizeWebhookPayload parses an incoming webhook body and returns a
// WebhookEnvelope. Rules:
//
//  1. Body must be a valid JSON object or array. Scalars / invalid JSON
//     return an error so the handler can respond 400.
//  2. If the body is an object containing a string `event` and any
//     `eventPayload`, those are preserved as-is.
//  3. Otherwise `event` is inferred from headers/body fields, and the entire
//     original body becomes `eventPayload`.
//  4. The default event is `webhook.received`.
//
// Inference order:
//
//	X-GitHub-Event (combined with body.action when present),
//	X-Gitlab-Event, X-Event-Type, body.event, body.type, body.action.
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)

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Send an actual JSON payload (object or array) in the request body.
  2. If testing, use curl with a data argument: curl -X POST -H 'Content-Type: application/json' -d '{"event":"ping"}' <url>.
  3. Point health checks at a dedicated health endpoint instead of the webhook URL.
  4. Check proxy/ingress config if legitimate provider payloads arrive empty.

Example fix

# before
curl -X POST https://host/api/webhooks/autopilot

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

Strategy: validation

Validate before calling

// Sender side, before POST
const body = JSON.stringify(payload ?? {});
if (!body || body === "null") {
  throw new Error("webhook payload is empty");
}
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body });

Prevention

When it happens

Trigger: POST to the autopilot webhook endpoint with a zero-length body (Content-Length: 0); a webhook sender issuing an empty POST for certain event types; a misconfigured proxy stripping the body; curl test without -d.

Common situations: Health-check probes hitting the webhook URL with empty POSTs; provider integrations that send notification-only pings with no body; reverse proxies misconfiguring request buffering.

Related errors


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