Skip to content
skyl

Error

A provider failure with enough context to act on.

*skyl.Error is what every adapter produces. It wraps a sentinel so errors.Is works, and carries the detail errors.As recovers.

Reference#

type Error struct
FieldTypeDescription
ProviderstringThe adapter that produced the failure.
StatusCodeintThe HTTP status, or 0 for transport-level failures.
Zero value: a transport failure — a dial timeout, a reset connection
MessagestringThe provider's explanation, when it gave one.
KinderrorThe sentinel this failure classifies as. Unwrap returns it.
RetryAftertime.DurationHow long the provider asked us to wait.
Zero value: the provider did not say
BodystringThe raw error payload, truncated at 2048 bytes. Useful when a provider reports something skyl does not model. An Error never contains credentials.

Methods#

func (e *Error) Error() string func (e *Error) Unwrap() []error func (e *Error) Retryable() bool func (e *Error) Cause() error func (e *Error) WithCause(err error) *Error

Caveats

  • It never contains credentials. Body is the provider's error payload truncated at 2048 bytes — enough to diagnose, small enough not to bloat logs, and never a header.
  • StatusCode == 0 means a transport-level failure — a dial timeout, a reset connection, a rejected certificate.
  • Unwrap returns a slice, so both the sentinel and the underlying cause match.
  • Kind may be nil for an unclassified failure; guard before using it.
  • Error() trims the sentinel's own "skyl: " prefix to avoid repeating it.

Usage#

Recovering the detail

goCompiles
var e *skyl.Error
if errors.As(err, &e) {
	log.Printf("%s returned %d: %s", e.Provider, e.StatusCode, e.Message)
	if e.RetryAfter > 0 {
		log.Printf("it asked for %s", e.RetryAfter)
	}
}
var e *skyl.Error
if errors.As(err, &e) {
	log.Printf("%s returned %d: %s", e.Provider, e.StatusCode, e.Message)
	if e.RetryAfter > 0 {
		log.Printf("it asked for %s", e.RetryAfter)
	}
}

Passing a Retry-After through to your own callers

goCompiles
var e *skyl.Error
if errors.As(err, &e) && e.RetryAfter > 0 {
	w.Header().Set("Retry-After", strconv.Itoa(int(e.RetryAfter.Seconds())))
}
http.Error(w, "upstream capacity exhausted", http.StatusTooManyRequests)
var e *skyl.Error
if errors.As(err, &e) && e.RetryAfter > 0 {
	w.Header().Set("Retry-After", strconv.Itoa(int(e.RetryAfter.Seconds())))
}
http.Error(w, "upstream capacity exhausted", http.StatusTooManyRequests)

Diagnosing an unclassified failure

goCompiles
var e *skyl.Error
if errors.As(err, &e) && e.Kind == nil {
	// Body is the provider's own words — the thing to paste into a ticket.
	log.Printf("unclassified: provider=%s status=%d body=%s", e.Provider, e.StatusCode, e.Body)
}
var e *skyl.Error
if errors.As(err, &e) && e.Kind == nil {
	// Body is the provider's own words — the thing to paste into a ticket.
	log.Printf("unclassified: provider=%s status=%d body=%s", e.Provider, e.StatusCode, e.Body)
}

Troubleshooting#

Body is empty

The provider sent no error payload, which is common for transport-level failures where StatusCode is also 0.

Body ends with “… (truncated)”

It exceeded 2048 bytes. That bound is deliberate — an unbounded error body in a log line is how a single bad request fills a disk.

Kind is nil

The failure could not be classified. Check StatusCode: 0 means transport, and anything else means the provider answered with something skyl did not recognise. Body will say what.

Edit this page on GitHub