payloaderr

package
v0.64.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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

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

func NewDecode(cause error, summary string) *Body

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

func NewOpen(cause error) *Body

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

func NewValidate(cause error) *Body

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

func ValidateStruct(v any) *Body

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

func (b *Body) Fields() []string

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

func (b *Body) Message(prefix, stage, subject string) string

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

func (b *Body) Unwrap() error

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

func NewDecoder[T any](codec Codec) *Decoder[T]

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

func (d *Decoder[T]) Decode(data []byte, dst *T) *Body

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.

func (JSONCodec) Summarize

func (JSONCodec) Summarize(err error, fieldPathIsSchema bool) string

Summarize delegates to the shared safe-rendering seam; see saferender.JSONDecodeSummary for the rules and their rationale.

func (JSONCodec) Unmarshal

func (JSONCodec) Unmarshal(data []byte, v any) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL