diff --git a/CHANGELOG.md b/CHANGELOG.md index 50393bb..81ef929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.9.3: Context handling, better errors + +This release includes two changes: + +1. The `Post()` method now accepts a context variable as its first parameter for timeout handling. +2. The `Post()` method now exclusively returns a `http.ClientError`, which includes the reason for failure. + ## 0.9.2: URL instead or Url This release changes the `Url` variable for the client to `URL`. It also bumps the [log dependency](https://github.com/containerssh/log) to the latest release. diff --git a/README.md b/README.md index 39dbe6c..e9bcf24 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,21 @@ if err != nil { request := yourRequestStruct{} response := yourResponseStruct{} -responseStatus := uint16(0) -if err := client.Post( +responseStatus, err := client.Post( + context.TODO(), "/relative/path/from/base/url", &request, - &responseStatus, &response, -); err != nil { +) +if err != nil { // Handle connection error + clientError := &http.ClientError{} + if errors.As(err, clientError) { + // Grab additional information here + } else { + // This should never happen + } } if responseStatus > 399 { diff --git a/client.go b/client.go index 6a69e8b..ba8194e 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,8 @@ package http import ( + "context" + "fmt" "time" ) @@ -9,7 +11,14 @@ import ( type Client interface { // Post queries the configured endpoint with the path, sending the requestBody and providing the // response in the responseBody structure. It returns the HTTP status code and any potential errors. - Post(path string, requestBody interface{}, responseBody interface{}) (statusCode int, err error) + // + // The returned error is always one of ClientError + Post( + ctx context.Context, + path string, + requestBody interface{}, + responseBody interface{}, + ) (statusCode int, err error) } // ClientConfiguration is the configuration structure for HTTP clients @@ -26,3 +35,41 @@ type ClientConfiguration struct { // ClientKey is a PEM containing a private key to use to connect the server or a file name containing the PEM. ClientKey string `json:"key" yaml:"key" comment:"Client key file in PEM format."` } + +// FailureReason describes the Reason why the request failed. +type FailureReason string + +const ( + // FailureReasonEncodeFailed indicates that JSON encoding the request failed. This is usually a bug. + FailureReasonEncodeFailed FailureReason = "encode_failed" + // FailureReasonConnectionFailed indicates a connection failure. + FailureReasonConnectionFailed FailureReason = "connection_failed" + // FailureReasonDecodeFailed indicates that decoding the JSON response has failed. The status code is set for this + // code. + FailureReasonDecodeFailed FailureReason = "decode_failed" +) + +// ClientError is the the description of the failure of the client request. +type ClientError struct { + // Reason is one of FailureReason describing the cause of the failure. + Reason FailureReason `json:"reason" yaml:"reason"` + // Cause is the original error that is responsible for the error + Cause error `json:"cause" yaml:"cause"` + // Message is the message that can be printed into a log. + Message string `json:"message" yaml:"message"` +} + +// Unwrap returns the original error. +func (c ClientError) Unwrap() error { + return c.Cause +} + +// Error returns the error string. +func (c ClientError) Error() string { + return c.Message +} + +// String returns a printable string +func (c ClientError) String() string { + return fmt.Sprintf("%s: %s (%v)", c.Reason, c.Message, c.Cause) +} diff --git a/client_impl.go b/client_impl.go index 4a260e8..b9df0ca 100644 --- a/client_impl.go +++ b/client_impl.go @@ -2,6 +2,7 @@ package http import ( "bytes" + "context" "crypto/tls" "encoding/json" "fmt" @@ -17,8 +18,17 @@ type client struct { httpClient *http.Client } -func (c *client) Post(path string, requestBody interface{}, responseBody interface{}) (int, error) { +func (c *client) Post( + ctx context.Context, + path string, + requestBody interface{}, + responseBody interface{}, +) ( + int, + error, +) { return c.request( + ctx, http.MethodPost, path, requestBody, @@ -27,6 +37,7 @@ func (c *client) Post(path string, requestBody interface{}, responseBody interfa } func (c *client) request( + ctx context.Context, method string, path string, requestBody interface{}, @@ -36,28 +47,45 @@ func (c *client) request( err := json.NewEncoder(buffer).Encode(requestBody) if err != nil { //This is a bug - return 0, err + return 0, ClientError{ + Reason: FailureReasonEncodeFailed, + Cause: err, + Message: "failed to encode request body", + } } - req, err := http.NewRequest( + req, err := http.NewRequestWithContext( + ctx, method, fmt.Sprintf("%s%s", c.config.URL, path), buffer, ) if err != nil { - return 0, err + return 0, &ClientError{ + Reason: FailureReasonEncodeFailed, + Cause: err, + Message: "failed to encode request body", + } } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { - return 0, err + return 0, ClientError{ + Reason: FailureReasonConnectionFailed, + Cause: err, + Message: "failed on HTTP request", + } } defer func() { _ = resp.Body.Close() }() decoder := json.NewDecoder(resp.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(responseBody); err != nil { - return 0, err + return resp.StatusCode, ClientError{ + Reason: FailureReasonDecodeFailed, + Cause: err, + Message: "failed to decode response", + } } return resp.StatusCode, nil } diff --git a/handler_impl.go b/handler_impl.go index 79c5706..ecdb73c 100644 --- a/handler_impl.go +++ b/handler_impl.go @@ -73,7 +73,7 @@ func (h *handler) ServeHTTP(goWriter goHttp.ResponseWriter, goRequest *goHttp.Re goWriter.WriteHeader(int(response.statusCode)) goWriter.Header().Add("Content-Type", "application/json") if _, err := goWriter.Write(bytes); err != nil { - h.logger.Infof("failed to write HTTP response") + h.logger.Infof("failed to write HTTP response (%v)", err) } } diff --git a/integration_test.go b/integration_test.go index a7e977f..320d670 100644 --- a/integration_test.go +++ b/integration_test.go @@ -23,12 +23,12 @@ import ( ) type Request struct { - Message string `json:"message"` + Message string `json:"Message"` } type Response struct { Error bool `json:"error"` - Message string `json:"message"` + Message string `json:"Message"` } type handler struct { @@ -377,6 +377,7 @@ func runRequest( }() <-ready if responseStatus, err = client.Post( + context.Background(), "", &Request{Message: message}, &response,