github.com/rootless-containers/rootlesskit/v2@v2.3.4/pkg/httputil/httputil.go (about)

     1  package httputil
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  )
    13  
    14  // ErrorJSON is returned with "application/json" content type and non-2XX status code
    15  type ErrorJSON struct {
    16  	Message string `json:"message"`
    17  }
    18  
    19  func readAtMost(r io.Reader, maxBytes int) ([]byte, error) {
    20  	lr := &io.LimitedReader{
    21  		R: r,
    22  		N: int64(maxBytes),
    23  	}
    24  	b, err := io.ReadAll(lr)
    25  	if err != nil {
    26  		return b, err
    27  	}
    28  	if lr.N == 0 {
    29  		return b, fmt.Errorf("expected at most %d bytes, got more", maxBytes)
    30  	}
    31  	return b, nil
    32  }
    33  
    34  // HTTPStatusErrorBodyMaxLength specifies the maximum length of HTTPStatusError.Body
    35  const HTTPStatusErrorBodyMaxLength = 64 * 1024
    36  
    37  // HTTPStatusError is created from non-2XX HTTP response
    38  type HTTPStatusError struct {
    39  	// StatusCode is non-2XX status code
    40  	StatusCode int
    41  	// Body is at most HTTPStatusErrorBodyMaxLength
    42  	Body string
    43  }
    44  
    45  // Error implements error.
    46  // If e.Body is a marshalled string of api.ErrorJSON, Error returns ErrorJSON.Message .
    47  // Otherwise Error returns a human-readable string that contains e.StatusCode and e.Body.
    48  func (e *HTTPStatusError) Error() string {
    49  	if e.Body != "" && len(e.Body) < HTTPStatusErrorBodyMaxLength {
    50  		var ej ErrorJSON
    51  		if json.Unmarshal([]byte(e.Body), &ej) == nil {
    52  			return ej.Message
    53  		}
    54  	}
    55  	return fmt.Sprintf("unexpected HTTP status %s, body=%q", http.StatusText(e.StatusCode), e.Body)
    56  }
    57  
    58  // Successful returns an error if the status code is not 2xx.
    59  func Successful(resp *http.Response) error {
    60  	if resp == nil {
    61  		return errors.New("nil response")
    62  	}
    63  	if resp.StatusCode/100 != 2 {
    64  		b, _ := readAtMost(resp.Body, HTTPStatusErrorBodyMaxLength)
    65  		return &HTTPStatusError{
    66  			StatusCode: resp.StatusCode,
    67  			Body:       string(b),
    68  		}
    69  	}
    70  	return nil
    71  }
    72  
    73  func NewHTTPClient(socketPath string) (*http.Client, error) {
    74  	if _, err := os.Stat(socketPath); err != nil {
    75  		return nil, err
    76  	}
    77  	return &http.Client{
    78  		Transport: &http.Transport{
    79  			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
    80  				var d net.Dialer
    81  				return d.DialContext(ctx, "unix", socketPath)
    82  			},
    83  		},
    84  	}, nil
    85  }
    86  
    87  // WriteError writes an error.
    88  // WriteError sould not be used if an error may contain sensitive information and the client is not reliable.
    89  func WriteError(w http.ResponseWriter, r *http.Request, err error, ec int) {
    90  	w.WriteHeader(ec)
    91  	w.Header().Set("Content-Type", "application/json")
    92  	e := ErrorJSON{
    93  		Message: err.Error(),
    94  	}
    95  	_ = json.NewEncoder(w).Encode(e)
    96  }