aboutsummaryrefslogtreecommitdiff
path: root/internal/bunny/error.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/bunny/error.go')
-rw-r--r--internal/bunny/error.go95
1 files changed, 95 insertions, 0 deletions
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)."
+ }
+}