github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/api/undocumented/undocumented.go (about) 1 // Package undocumented provides abstractions for talking to undocumented Fastly 2 // API endpoints. 3 package undocumented 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httputil" 11 "net/url" 12 "strings" 13 "time" 14 15 "github.com/fastly/cli/pkg/api" 16 fsterr "github.com/fastly/cli/pkg/errors" 17 "github.com/fastly/cli/pkg/useragent" 18 ) 19 20 // EdgeComputeTrial is the API endpoint for activating a compute trial. 21 const EdgeComputeTrial = "/customer/%s/edge-compute-trial" 22 23 // RequestTimeout is the timeout for the API network request. 24 const RequestTimeout = 5 * time.Second 25 26 // APIError models a custom error for undocumented API calls. 27 type APIError struct { 28 Err error 29 StatusCode int 30 } 31 32 // Error implements the error interface. 33 func (e APIError) Error() string { 34 return e.Err.Error() 35 } 36 37 // NewError returns an APIError. 38 func NewError(err error, statusCode int) APIError { 39 return APIError{ 40 Err: err, 41 StatusCode: statusCode, 42 } 43 } 44 45 // HTTPHeader represents a HTTP request header. 46 type HTTPHeader struct { 47 Key string 48 Value string 49 } 50 51 // CallOptions is used as input to Call(). 52 type CallOptions struct { 53 APIEndpoint string 54 Body io.Reader 55 Debug bool 56 HTTPClient api.HTTPClient 57 HTTPHeaders []HTTPHeader 58 Method string 59 Path string 60 Token string 61 } 62 63 // Call calls the given API endpoint and returns its response data. 64 // 65 // WARNING: Loads entire response body into memory. 66 func Call(opts CallOptions) (data []byte, err error) { 67 host := strings.TrimSuffix(opts.APIEndpoint, "/") 68 endpoint := fmt.Sprintf("%s%s", host, opts.Path) 69 70 req, err := http.NewRequest(opts.Method, endpoint, opts.Body) 71 if err != nil { 72 return data, NewError(err, 0) 73 } 74 75 if opts.Token != "" { 76 req.Header.Set("Fastly-Key", opts.Token) 77 } 78 req.Header.Set("User-Agent", useragent.Name) 79 for _, header := range opts.HTTPHeaders { 80 req.Header.Set(header.Key, header.Value) 81 } 82 83 if opts.Debug { 84 rc := req.Clone(context.Background()) 85 rc.Header.Set("Fastly-Key", "REDACTED") 86 dump, _ := httputil.DumpRequest(rc, true) 87 fmt.Printf("undocumented.Call request dump:\n\n%#v\n\n", string(dump)) 88 } 89 90 res, err := opts.HTTPClient.Do(req) 91 92 if opts.Debug && res != nil { 93 dump, _ := httputil.DumpResponse(res, true) 94 fmt.Printf("undocumented.Call response dump:\n\n%#v\n\n", string(dump)) 95 } 96 97 if err != nil { 98 if urlErr, ok := err.(*url.Error); ok && urlErr.Timeout() { 99 return data, fsterr.RemediationError{ 100 Inner: err, 101 Remediation: fsterr.NetworkRemediation, 102 } 103 } 104 return data, NewError(err, 0) 105 } 106 defer res.Body.Close() // #nosec G307 107 108 data, err = io.ReadAll(res.Body) 109 if err != nil { 110 return []byte{}, NewError(err, res.StatusCode) 111 } 112 113 if res.StatusCode >= 400 { 114 return data, NewError(fmt.Errorf("error response: %q", data), res.StatusCode) 115 } 116 117 return data, nil 118 }