package bunny import ( "fmt" "net/http" ) type errorKind int const ( // errorKindNetwork: the request never got a response (DNS, connection, TLS, // context cancellation surfaced as a transport error). errorKindNetwork errorKind = iota // errorKindAuth: the access key was rejected (401/403). errorKindAuth // errorKindRateLimited: Bunny is throttling (429, in any retry-exhausted form). errorKindRateLimited // errorKindServer: Bunny-side failure (5xx). errorKindServer // errorKindInternal: an unexpected client error (non-auth, non-rate-limit 4xx). // These almost always mean hopper built a bad request: a likely bug. errorKindInternal // errorKindTooLarge: Bunny rejected the asset as too large (413). This is the // operator's payload, not a hopper bug, so it gets a distinct message. errorKindTooLarge ) func errorKindFromStatus(status int) errorKind { switch { case status == http.StatusUnauthorized || status == http.StatusForbidden: return errorKindAuth case status == http.StatusRequestEntityTooLarge: return errorKindTooLarge case status >= 500: return errorKindServer default: return errorKindInternal } } // apiError is the single error type returned by all API operations. Error() // renders a friendly, kind-based message for the user; the technical detail // (method, path, status, wrapped cause) is exposed separately via // ErrorVerbose() and surfaced only under --verbose by the presentation layer. // method and path are derived from the request itself (HTTP verb and // req.URL.Path). type apiError struct { method string // HTTP verb, e.g. "PUT" path string // req.URL.Path, e.g. "/zone/a/b.js" status int // HTTP status code; 0 for transport-level (KindNetwork) errors kind errorKind cause error // wrapped technical cause, exposed via Unwrap (NOT via Error) } func newAPIError(kind errorKind, err error, req *http.Request, statusCode int) *apiError { return &apiError{ method: req.Method, path: req.URL.Path, status: statusCode, kind: kind, cause: err, } } func (e *apiError) Unwrap() error { return e.cause } // ErrorVerbose returns the technical description (verb, path, status, cause) // surfaced only under --verbose. It is the "re-run with --verbose for details" // payload; the user-facing message stays in Error(). Any error type may // implement this interface to contribute extra detail to failure reports. func (e *apiError) ErrorVerbose() string { if e.status != 0 { return fmt.Sprintf("%s %s -> %d: %v", e.method, e.path, e.status, e.cause) } return fmt.Sprintf("%s %s -> %v", e.method, e.path, e.cause) } func (e *apiError) Error() string { switch e.kind { case errorKindAuth: return "The access key was rejected. Check your Bunny storage credentials." case errorKindRateLimited: return "Bunny is rate-limiting requests. Please try again shortly." case errorKindServer: return "Bunny had an internal server error. Please try again later." case errorKindNetwork: return "Couldn't reach Bunny. Check your network connection and endpoint." case errorKindTooLarge: return "The asset was rejected by Bunny as too large." case errorKindInternal: return "Unexpected response from Bunny. This is likely a bug in hopper. Please report it (re-run with --verbose for details)." default: return "An unknown Bunny error occurred (re-run with --verbose for details)." } }