Documentation
¶
Overview ¶
Package payloaderr holds the payload-error core both messaging lanes build their typed consumers on: the decode and struct-validation steps, and a failure rendering that never echoes the body that caused it.
It exists here rather than in either lane because the two are deliberately decoupled — messaging/streams must not import messaging — so what they share travels through messaging/internal/*. Each lane exports a thin error type over Body and supplies its own prefix, subject and sentinels; the rules that keep payload bytes out of a rendering are stated once, here.
Index ¶
Constants ¶
const UnauditedDecoderSummary = "cause withheld (unaudited decoder); use errors.Unwrap for the raw error"
UnauditedDecoderSummary is the fail-closed rendering for a decode error whose shape has not been audited for payload content.
Variables ¶
var Validator = sync.OnceValue(validation.New)
Validator is the one validator instance every typed handler on either lane shares. validator caches struct metadata by reflect.Type, so per-message construction would throw that cache away on every delivery; the instance is safe for concurrent use, which is what lets one adapter serve every worker.
Functions ¶
This section is empty.
Types ¶
type Body ¶
type Body struct {
// Stage is where the failure happened. A lane exports it to label logs and
// metrics; for control flow a lane maps it onto its own sentinels.
Stage Stage
// contains filtered or unexported fields
}
Body is the lane-agnostic state of a payload failure: what stage failed, the payload-free rendering of why, and the cause itself.
SECURITY: message bodies are partner PII/PCI on both lanes, so the framework's own rendering must stay free of them. Message() and Fields() are safe to log; Unwrap() is not. Message() composes its text from schema facts only — it never renders the wrapped cause verbatim, because every producer in reach echoes payload bytes in at least one shape:
- json.UnmarshalTypeError.Value carries the raw literal ("number 1234.56") and, for integer-keyed maps, the raw key; its Field is schema-only for a map-free destination and carries the input key for a map one — or for a field decoding itself — which is why the summary's field path is gated on the payload type.
- json.SyntaxError quotes the offending payload byte.
- json.Decoder.DisallowUnknownFields reports the partner-supplied key verbatim.
- validator namespaces interpolate map keys verbatim ("Limits[4111...]"), which is why the namespace list is unexported and redacted on read.
The decode rendering itself lives on the codec seam (Codec.Summarize), so a new codec (issue #346) must supply its own audited phrasing; until it does, NewDecode substitutes the fail-closed phrase and the cause is never rendered.
func NewDecode ¶
NewDecode wraps a decode failure. The cause survives for Unwrap only; Message() prints summary instead, which the codec produced.
SECURITY: an empty summary means the codec did not audit this error shape, so the fail-closed phrase substitutes here rather than at the call site — no caller can render an unaudited cause by forgetting the fallback.
func NewOpen ¶ added in v0.63.0
NewOpen wraps an opener refusal. The cause renders itself as its code and its presence/length details only — the opener seam guarantees no wire value is in that text — so its own rendering is the summary Message() prints.
func NewValidate ¶
NewValidate wraps a validation failure and records the validator's own field namespaces verbatim. Redaction is Fields()' job, not the constructor's, so no assembly path can produce a Body whose namespace list reads back unsanitized.
func ValidateStruct ¶ added in v0.63.0
ValidateStruct runs the shared validator over an already-decoded value — the second half of Decode, exposed for a lane that decoded by other means (the sealed opener splices the plaintext back itself) and still owes validation.
func (*Body) Fields ¶
Fields returns the validator field namespaces that failed, e.g. ["CreateReq.Amount"]. It is empty for decode failures and for a nil receiver.
SECURITY: the bracketed span is redacted to [*] on the way out, and the result is a fresh slice, so the redaction survives whatever the caller does with it. This is the only read path onto the namespaces.
func (*Body) Message ¶
Message composes the payload-free text a lane's Error() returns. prefix is the lane's package name, stage is the lane's own rendering of the stage, and subject names what the body was routed to, already quoted by the lane — `event "OrderCreated"` on the AMQP lane, `consumer "order-projector"` on the streams lane.
The stage is the caller's rather than this Body's, so a lane error assembled without a Body — which only a lane's own tests can now do — still renders its stage instead of collapsing to a nil rendering.
func (*Body) Unwrap ¶
Unwrap exposes the underlying decode or validation error so errors.As can reach the cause.
SECURITY: the returned error MAY carry payload-derived text — a rejected numeric literal, an offending byte, an unknown key, a map key. It is the deliberate escape hatch for a caller that needs the raw diagnostic; logging it is opt-in and on the caller.
type Codec ¶
type Codec interface {
Unmarshal(data []byte, v any) error
// Summarize renders a decode failure with NO payload bytes in it. Returning
// "" means the shape was not audited; NewDecode substitutes the fail-closed
// phrase, so a codec never spells it out.
//
// fieldPathIsSchema tells the codec whether a field path the decoder reports
// can be trusted as schema-only. The caller decides it from the destination
// type, once per registration; a codec must never infer it from the error.
Summarize(err error, fieldPathIsSchema bool) string
}
Codec decodes a raw payload into a destination. It is a seam rather than a hardcoded call because schema negotiation and non-JSON payloads (issue #346) widen it without an API break on either lane.
type Decoder ¶
type Decoder[T any] struct { // contains filtered or unexported fields }
Decoder turns a message body into a T. Every field is decided once at construction and read-only afterwards, so one Decoder is shared by every worker goroutine and every tenant replaying the same declarations.
func NewDecoder ¶
NewDecoder is the single construction point, so the field-path gate cannot be forgotten on one of a lane's entry points. A nil codec is JSONCodec.
func (*Decoder[T]) Decode ¶
Decode fills dst from data and validates it, returning nil on success and the failure's Body otherwise. dst is written only on a successful decode; a caller that reuses it across messages would still see a partial value, so every lane passes a fresh one per delivery.
A non-struct T reaches validation with a *validator.InvalidValidationError, which yields no fields and still carries StageValidate — failing closed on the first delivery rather than silently skipping validation forever.
type JSONCodec ¶
type JSONCodec struct{}
JSONCodec is the only codec today: message bodies are JSON on every path the framework publishes, on both lanes.
type Stage ¶
type Stage string
Stage names the half of the typed-payload pipeline that failed. A lane exports its own string type over these two values.
const ( StageDecode Stage = "decode" StageValidate Stage = "validate" // StageOpen is the sealed-message opener refusing a body before decode: the // signature, the key families, the signed slots or the decrypt (ADR-097). StageOpen Stage = "open" )
The stages a Body can carry. A Body whose Stage is none of these is not one this package produced.