Skip to content

Handle a refused event

A format's Decode returns an Event and an error, and on every error path the Event carries every attribute that could be read before the refusal, extensions included. So does envelope.Read once a format has claimed the message.

The exception is a message nothing claimed: Read returns an empty event with ErrUnrecognised or ErrAmbiguous, because nothing was decoded. A header map with no CloudEvents attributes at all, given straight to Decode, gives back an event with no ID or Source, because a message that is not a CloudEvent has no id to report.

That exists for one reason: core NATS has no dead letter. A handler that returns on error has destroyed the message, so if the log line says "a message failed to parse" the event is gone and unidentifiable.

The shape to copy

e, _, err := format.Decode(envelope.Wire{Headers: msg.Header, Body: msg.Data})
if err != nil {
    refused.Add(1)
    logger.Error("refused a CloudEvent",
        "id", e.ID,
        "source", e.Source,
        "type", e.Type,
        "err", err,
    )

    return
}

e.ID and e.Source together identify the occurrence, which is what makes the line actionable. Somebody can go and look at the producer.

Count the kind, not just the failure

Every refusal is a distinct sentinel, so a counter with a label costs nothing and tells you whether you have one broken producer or a version skew:

switch {
case errors.Is(err, envelope.ErrUnsupportedSpecVersion):
    refused.Add(1, metric.WithAttributes(attribute.String("reason", "specversion")))
case errors.Is(err, envelope.ErrMissingAttribute):
    refused.Add(1, metric.WithAttributes(attribute.String("reason", "missing")))
default:
    refused.Add(1, metric.WithAttributes(attribute.String("reason", "other")))
}

The validation and reading sentinels are envelope.*; the three only CloudEvents' formats return are cloudevents.*. The full list is in the errors reference.

Do not retry

Every refusal is deterministic: the same bytes will be refused the same way for ever. Redelivering a malformed event only moves the failure later. Fix the producer, and if the event mattered, replay it from wherever the producer keeps its own record.