1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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)."
}
}
|