Categorygithub.com/joelhill/go-rest-http-blaster
modulepackage
1.0.2
Repository: https://github.com/joelhill/go-rest-http-blaster.git
Documentation: pkg.go.dev

# README

CBAPIClient

CBAPIClient is an opinionated Go HTTP client built on top of fasthttp that also provides built-in support for the following common InVision design patterns:

  • Circuit Breakers
  • New Relic Transactions in Context
  • Logrus logger in Context

Fasthttp has been benchmarked at up to 10 times faster than net/http.
Much of this boost comes from connection pooling and resource reuse. You can take advantage of those features by using recycling in your code.

Usage

Go straight to the examples

Setting Defaults

It is recommended that you initialize cbapiclient with package-level defaults when your application bootstraps.
To do this, use the Defaults struct in the SetDefaults function. The functions assigned in Defaults match the function signatures provided by jelly, but any function that returns the requested item with a boolean will work. In our examples, we use the jelly functions.

type Defaults struct {
	// ServiceName is the name of the calling service
	ServiceName string

	// NewRelicTransactionProviderFunc is a function that
	// provides the New Relic transaction to be used in the
	// HTTP Request.  If this function is not set, the client
	// will create a new New Relic transaction
	NewRelicTransactionProviderFunc func(ctx context.Context) (newrelic.Transaction, bool)

	// ContextLoggerProviderFunc is a function that provides
	// a logger from the current context.  If this function
	// is not set, the client will create a new logger for
	// the Request.
	ContextLoggerProviderFunc func(ctx context.Context) (*logrus.Entry, bool)

	// RequestIDProviderFunc is a function that provides the
	// parent Request id used in tracing the caller's Request.
	// If this function is not set, the client will generate
	// a new UUID for the Request id.
	RequestIDProviderFunc func(ctx context.Context) (string, bool)
}

It is strongly recommended that you provide these defaults. However, if they are not provided, cbapiclient will provide its own defaults:

  • ServiceName - if the service name is not provided, cbapiclient will look for an environment variable called SERVICE_NAME. If that variable doesnt exist, cbapiclient will fall back to HOSTNAME
  • NewRelicTransactionProviderFunc - if this function is not provided, cbapiclient will start a new New Relic transaction
  • ContextLoggerProviderFunc - if this function is not provided, cbapiclient will created a new logrus.Entry to use for the request
  • RequestIDProviderFunc - if this function is not provided, cbapiclient will generate a new UUID to use for the request

Typical usage, with jelly functions:

package main

import (
	"fmt"
	
	cbapi "github.com/InVisionApp/cbapiclient"
	"github.com/InVisionApp/jelly"
)

const serviceName = "my-service"

func main() {
	cbapi.SetDefaults(&cbapi.Defaults{
		ServiceName: serviceName,
		NewRelicTransactionProviderFunc: jelly.GetNewRelicTransaction,
		ContextLoggerProviderFunc: jelly.GetContextLogger,
		RequestIDProviderFunc: jelly.GetRequestID,
	})
}

Making Requests

Using Circuit Breakers

cbapiclient does not force you to use a circuit breaker, or, if you do, which library to use. cbapiclient is built to implicitly support the Sony GoBreaker library. The circuit breaker implementation must conform to the following interface:

type CircuitBreakerPrototype interface {
	Execute(func() (interface{}, error)) (interface{}, error)
}

To set the circuit breaker, use the SetCircuitBreaker function.

Content Type

By default, cbapiclient sets the Content-Type header to application/json. You may override this header if you are sending a different content type by using the SetContentType function.

Response Payloads

There are two ways to access the response payload from cbapiclient. If you want to access the raw bytes returned in the response, or if you are not expecting a JSON response payload, you can use the RawResponse function to access the response bytes.

The more common way to access the payload is to use one of the Saturate functions. You provide cbapiclient with an empty struct pointer, and it will be saturated when the response returns. Each works slightly differently depending on what you're trying to do:

  • WillSaturate
    • Use this function to saturate the struct you expect when the request succeeds. A request is considered successful if the response code is between 200 and 299, inclusive.
  • WillSaturateOnError
    • Use this function to saturate the struct you expect when the request fails. A request is considered a failure if the response code is below 200 or above 299. Also note that this struct will only be saturated if the actual response is an error. cbapiclient will not saturate any response if the error originated at the caller.
  • WillSaturateWithStatusCode
    • Use this function to saturate the struct you expect when a specific status code is returned.
      Structs provided here take precedence over WillSaturate and WillSaturateOnError. For example, if a specific response is expected for a 401 error, and a struct is provided for 401, then that struct will be saturated when the response is 401, regardless what was provided in WillSaturateOnError

Convenience Functions

cbapiclient provides the following functions for convenience:

  • Get - perform an HTTP GET request with no outgoing payload
  • Post - perform an HTTP POST request with an outgoing payload
  • Put - perform an HTTP PUT request with an outgoing payload
  • Patch - perform an HTTP PATCH request with an outgoing payload
  • Delete - perform an HTTP DELETE request with no outgoing payload

Fluent Interface

For convenience, some functions can be chained to provide a fluent interface:

var cb *CircuitBreaker

// circuit breaker is created ...

client, err := cbapiclient.NewClient("https://path.to.my.endpoint/foo/bar")
if err != nil {
	log.Fatalln(err.Error())
}

// create the empty struct to be saturated
response := &MyStruct{}

statusCode, err := client.SetCircuitBreaker(cb).WillSaturate(response).Get()

Request/Response Customization

Since cbapiclient is built atop fasthttp, you have access to the Request and Response structs, which are of type fasthttp.Request and fasthttp.Response, respectively.

Caution

The fasthttp request and response structs are unavailable after a call to Recycle (see below)

Headers

The following headers are set for every request:

  • Request-ID
  • Content-Type
  • User-Agent - the user agent is in the form {{service name}}-{{namespace}}-{{tenancy}}
  • Calling-Service

Additionally, the Content-Length header is set for all requests with an outgoing payload (POST, PUT, PATCH)

You can set additional headers by accessing the Request property:

c.Request.Header.Set("X-Forwarded-For", "127.0.0.1")

Performance

fasthttp's http client is much faster than Go's standard http client, partly because of how it allows for resource pooling, which reduces pressure on the garbage collector and overall memory consumption. To take advantage of this, you can defer the cbapiclient's Recycle function:

c, err := cbapiclient.NewClient("https://path.to.my.endpoint/foo/bar")
if err != nil {
	log.Fatalln(err.Error())
}

defer c.Recycle()

... // process your request

Examples

Get with known payload

package main

import (
	"context"
	"log"

	cbapi "github.com/InVisionApp/cbapiclient"
)

type ResponsePayload struct {
	Foo  string `json:"foo"`
	Fizz string `json:"fizz"`
}

func main() {
	ctx := context.Background()

	// make you a client
	c, err := cbapi.NewClient("http://localhost:8080/foo/bar")
	if err != nil {
		log.Fatalln(err.Error())
	}

	// make empty struct pointer
	payload := &ResponsePayload{}
	
	// we will saturate the response with this GET
	statusCode, err := c.WillSaturate(payload).Get(ctx)
	
	// recycle the request/response
	c.Recycle()

	log.Println(statusCode)
	log.Printf("%+v", payload)
}

Get with raw bytes

package main

import (
	"context"
	"log"

	cbapi "github.com/InVisionApp/cbapiclient"
)

func main() {
	ctx := context.Background()

	// make you a client
	c, err := cbapi.NewClient("http://localhost:8080/foo/bar/xml")
	if err != nil {
		log.Fatalln(err.Error())
	}
	
	// xml? it could happen ;)
	c.SetContentType("application/xml")

	// run the request 
	statusCode, err := c.Get(ctx)
	if err != nil {
		log.Fatalln(err.Error())
	}
	
	// get the raw byte slice 
	data := c.RawResponse()
	
	// recycle the request and response
	c.Recycle()

	log.Println(statusCode)
	log.Printf("%+v", data)
}

Post with known payload

package main

import (
	"context"
	"log"

	cbapi "github.com/InVisionApp/cbapiclient"
)

type Payload struct {
	Foo  string `json:"foo"`
	Fizz string `json:"fizz"`
}

type ResponsePayload struct {
	Bar string `json:"bar"`
	Baz string `json:"baz"`
}

func main() {
	payload := &Payload{
		Foo: "this is foo",
		Fizz: "this is fizz",
	}
	ctx := context.Background()

	// make you a client
	c, err := cbapi.NewClient("http://localhost:8080/foo/bar")
	if err != nil {
		log.Fatalln(err.Error())
	}
	// you can defer the recycle if you need to hang on to the request and response
	defer c.Recycle()

	// make the empty struct pointer
	response := &ResponsePayload{}
	
	// we will saturate the response with this post
	statusCode, err := c.WillSaturate(response).Post(ctx, payload)
	if err != nil {
		log.Fatalln(err.Error())
	}

	log.Println(statusCode)
	log.Printf("%+v", response)
}

Get with differing responses

package main

import (
	"context"
	"log"

	cbapi "github.com/InVisionApp/cbapiclient"
)

type SuccessPayload struct {
	Foo  string `json:"foo"`
	Fizz string `json:"fizz"`
}

type ErrorPayload struct {
	Reason  string `json:"reason"`
	Code int `json:"code"`
}

type ForbiddenPayload struct {
	Domain  string `json:"domain"`
	UserID string `json:"userID"`
}

func main() {
	ctx := context.Background()

	// make you a client
	c, err := cbapi.NewClient("http://localhost:8080/foo/bar")
	if err != nil {
		log.Fatalln(err.Error())
	}

	// make empty struct pointers
	payload := &SuccessPayload{}
	errorPayload := &ErrorPayload{}
	forbiddenPayload := &ForbiddenPayload{}
	
	// this gets saturated on error
	c.WillSaturateOnError(errorPayload)
	
	// this gets saturated on 403
	c.WillSaturateWithStatusCode(403, forbiddenPayload)
	
	// we will saturate the response with this GET
	statusCode, err := c.WillSaturate(payload).Get(ctx)
	
	// recycle the request/response
	c.Recycle()

	log.Println(statusCode)
	
	if statusCode == 403 {
		log.Printf("%+v", forbiddenPayload)
	} else if c.StatusCodeIsError() {
		log.Printf("%+v", errorPayload)
	} else {
		log.Printf("%+v", payload)
	}
}

Future enhancements

  • Add a statsd provider using the same convention established in Defaults

# Packages

Code generated by counterfeiter.

# Functions

NewClient will initialize and return a new client with a fasthttp request and endpoint.
SetDefaults will apply package-level default values to be used on all requests.

# Structs

Client encapsulates the http Request functionality.
Defaults is a container for setting package level values.

# Interfaces

CircuitBreakerPrototype defines the circuit breaker Execute function signature.
No description provided by the author