Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Context handling, better errors (#4)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Janos Pasztor authored Dec 15, 2020
1 parent d60119e commit 34024f8
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 14 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 48 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package http

import (
"context"
"fmt"
"time"
)

Expand All @@ -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
Expand All @@ -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)
}
40 changes: 34 additions & 6 deletions client_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
Expand All @@ -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,
Expand All @@ -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{},
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion handler_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
5 changes: 3 additions & 2 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -377,6 +377,7 @@ func runRequest(
}()
<-ready
if responseStatus, err = client.Post(
context.Background(),
"",
&Request{Message: message},
&response,
Expand Down

0 comments on commit 34024f8

Please sign in to comment.