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  }