From 84da43998f7cc42cc72652333ac6e3d293abb23e Mon Sep 17 00:00:00 2001 From: tsne Date: Mon, 6 Jul 2026 16:52:20 +0200 Subject: initial --- internal/bunny/error.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 internal/bunny/error.go (limited to 'internal/bunny/error.go') diff --git a/internal/bunny/error.go b/internal/bunny/error.go new file mode 100644 index 0000000..4c17bda --- /dev/null +++ b/internal/bunny/error.go @@ -0,0 +1,95 @@ +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)." + } +} -- cgit v1.3