Categorygithub.com/devlibx/gox-http/v4
modulepackage
4.0.15
Repository: https://github.com/devlibx/gox-http.git
Documentation: pkg.go.dev

# README

Gox Http

Gox Http provides utility to call a http endpoint. It provides following:

  1. Define all endpoint and api config in configuration file
  2. Circuit breaker using Hystrix
  3. Set concurrency for each api - this ensures that if we go beyond "concurrency" no of parallel requests then hystrix will reject the requests
  4. Set timeout for each api - the call will timeout if this request takes time > timeout defined
  5. acceptable_codes - list of "," separated status codes which are acceptable. These status codes will not be counted as errors and will not open hystrix circuit

How to use

Given below is a example on how to use this liberary

package main

import (
	"context"
	"fmt"
	"github.com/devlibx/gox-base"
	"github.com/devlibx/gox-base/serialization"
	goxHttpApi "github.com/devlibx/gox-http/api"
	"github.com/devlibx/gox-http/command"
	"log"
)

// Here you can define your own configuration
// We have used "jsonplaceholder" as a test server. A api "getPosts" is defined which uses "server=jsonplaceholder"
var httpConfig = `
servers:
  jsonplaceholder:
    host: jsonplaceholder.typicode.com
    port: 443
    https: true
    connect_timeout: 1000
    connection_request_timeout: 1000
  testServer:
    host: localhost
    port: 9123

apis:
  getPosts:
    method: GET
    path: /posts/{id}
    server: jsonplaceholder
    timeout: 1000
    acceptable_codes: 200,201
  delay_timeout_10:
    path: /delay
    server: testServer
    timeout: 10
    concurrency: 3 
`

func main() {

	cf := gox.NewCrossFunction()

	// Read config and
	config := command.Config{}
	err := serialization.ReadYamlFromString(httpConfig, &config)
	if err != nil {
		log.Println("got error in reading config", err)
		return
	}

	// Setup goHttp context
	goxHttpCtx, err := goxHttpApi.NewGoxHttpContext(cf, &config)
	if err != nil {
		log.Println("got error in creating gox http context config", err)
		return
	}

	// Make a http call and get the result
	// 	ResponseBuilder - this is used to convert json response to your custom object
	//
	//  The following interface can be implemented to convert from bytes to the desired output.
	//  response.Response will hold the object which is returned from  ResponseBuilder
	//
	//	type ResponseBuilder interface {
	//		Response(data []byte) (interface{}, error)
	//	}
	request := command.NewGoxRequestBuilder("getPosts").
		WithContentTypeJson().
		WithPathParam("id", 1).
		WithResponseBuilder(command.NewJsonToObjectResponseBuilder(&gox.StringObjectMap{})).
		Build()
	response, err := goxHttpCtx.Execute(context.Background(), "getPosts", request)
	if err != nil {

		// Error details can be extracted from *command.GoxHttpError
		if goxError, ok := err.(*command.GoxHttpError); ok {
			if goxError.Is5xx() {
				fmt.Println("got 5xx error")
			} else if goxError.Is4xx() {
				fmt.Println("got 5xx error")
			} else if goxError.IsBadRequest() {
				fmt.Println("got bad request error")
			} else if goxError.IsHystrixCircuitOpenError() {
				fmt.Println("hystrix circuit is open due to many errors")
			} else if goxError.IsHystrixTimeoutError() {
				fmt.Println("hystrix timeout because http call took longer then configured")
			} else if goxError.IsHystrixRejectedError() {
				fmt.Println("hystrix rejected the request because too many concurrent request are made")
			} else if goxError.IsHystrixError() {
				fmt.Println("hystrix error - timeout/circuit open/rejected")
			}

		} else {
			fmt.Println("got unknown error")
		}

	} else {
		fmt.Println(serialization.Stringify(response.Response))
		// {some json response ...}
	}
}

Retry Handling

You can specify following properties in a API to enable a retry.

  1. retry_count - how many times you want to retry
  2. retry_initial_wait_time_ms - a delay before making a retry
  3. NOTE - the total Hystrix timeout will be set to (timeout + (retry_count * timeout) + retry_initial_wait_time_ms)
    Timeout is the time taken by a single call. So the total time is adjusted to cover retries
  4. If response from a server is an acceptable code then retry will not be done e.g. in this case status=404 will not trigger a retry.
apis:
  getPosts:
    method: GET
    path: /posts/{id}
    server: jsonplaceholder
    timeout: 1000
    acceptable_codes: 200,201,404
    retry_count: 3
    retry_initial_wait_time_ms: 10

Environment Specific Configs Support

You can setup all properties with env specific values

  1. env = name of the env (default=prod). This is used to find the values for all properties
  2. add "env: " in front of all values to make it configurable
  3. setup env specific configs
  4. Note - You must use serialization.ReadParameterizedYaml() method if you uses parameterized yaml
host: "env:string: prod=localhost.prod; dev=localhost.dev; stage=localhost.stage"

Here host value will be based on the "env" you have provided in a config. For example host will be "localhost.prod" if env=prod, or host="localhost.stage" if env=stage" 4. Default: You can sprcify "default" - if no value match this will be used
e.g. port: "env:int: prod=443; default=8080" dev/stage/any other will pick port=8080. Only prod will use 443

env: dev

servers:
  jsonplaceholder:
    host: "env:string: prod=jsonplaceholder.typicode.com; stage=localhost.stage; default=localhost.dev"
    port: "env:int: prod=443; default=8080"
    https: true
    connect_timeout: "env:int: prod=10; default=1000"
    connection_request_timeout: "env:int: prod=11; default=1001"
  testServer:
    host: "env:string: prod=localhost.prod; dev=localhost.dev; stage=localhost.stage"
    port: 9123
    https: "env:int: prod=true; dev=false; stage=false"

apis:
  delay_timeout_10:
    path: /delay/delay_timeout_10
    server: testServer
    timeout: "env:int: prod=10; default=1000"
    concurrency: "env:int: prod=10; default=300"
  delay_timeout_10_POST:
    path: /delay/delay_timeout_10_POST
    method: POST
    server: testServer
    timeout: "env:int: prod=100; default=1001"
    concurrency: "env:int: prod=11; default=200"

How to add or update a new API dynamically

NOTE - we only support adding new API (new server has not be done manually)

// Load config from test_config_real_server.yaml example file
config := command.Config{}
err := serialization.ReadYamlFromString(testhelper.TestConfigWithRealServer, &config)
	
// Suppose a API "delay_timeout_10" was using a path "/delay", and you want to chnage it to point to "/delay_new"
// Update the config and reload API
config.Apis["delay_timeout_10"].Path = "/delay_new"
err = goxHttpCtx.ReloadApi("delay_timeout_10")


// Add a new API inimically
config.Apis["new_api"] = &command.Api{
    Name:        "new_api",
    Method:      "GET",
    Path:        "/bad_new",
    Server:      "testServer",
    Timeout:     100,
    Concurrency: 10,
    QueueSize:   10,
}
err = goxHttpCtx.ReloadApi("new_api")

request = command.NewGoxRequestBuilder("new_api").
                WithContentTypeJson().
               WithPathParam("id", 1).
               WithResponseBuilder(command.NewJsonToObjectResponseBuilder(&gox.StringObjectMap{})).
               Build()
response, err = goxHttpCtx.Execute(ctx, "new_api", request)
assert.NoError(t, err)
assert.Equal(t, "ok", response.AsStringObjectMapOrEmpty().StringOrEmpty("status"))
assert.Equal(t, "/bad_new", response.AsStringObjectMapOrEmpty().StringOrEmpty("url"))

Enable HmacSha256 validation

  1. interceptor_config.hmac_config => set this up to use HMAC SHA256
  2. key => secret key to use for HMAC
  3. hash_header_key => Hash will be calculated and will be passed with this header key
  4. timestamp_header_key => timestamp will be passed with this header key
  5. headers_to_include_in_signature => list of headers which will be included in signature calculation
  6. convert_header_keys_to_lower_case => if true then all header keys will be converted to lower case before calculating signature
servers:
  jsonplaceholder:
    host: jsonplaceholder.typicode.com
    port: 443
    https: true
    interceptor_config:
       hmac_config:
          key: <some secret>
          hash_header_key: X-Hash-code-sha
          timestamp_header_key: X-Time
          headers_to_include_in_signature: [x-header-1, x-header-2]
          convert_header_keys_to_lower_case: true

Error Handling

You can register RequestValidatorErrorHandlingMiddleware in your router to get common error handler for bad request.

type User struct {
    Name       string   `json:"name" binding:"required,max=10"`
    Email      string   `json:"email" binding:"required,email"`
    Address    Address  `json:"address" binding:"required"`
    AddressPtr *Address `json:"address_ptr" binding:"required"`
}

type Address struct {
    FlatNo int `json:"flat_no" binding:"required"`
}

// Register middleware to handle bad request
r := gin.Default()
r.Use(RequestValidatorErrorHandlingMiddleware(DoNotSkipRequestValidationFunc, nil))

// Use this middleware to handle error 
var user User
if err := c.ShouldBindJSON(&user); err != nil {
    _ = c.AbortWithError(http.StatusBadRequest, err)
}
{
    "status": 400,
    "error_mapping_info": {
        "User.Address.FlatNo": "Error:Field validation for 'FlatNo' failed on the 'required' tag",
        "User.AddressPtr": "Error:Field validation for 'AddressPtr' failed on the 'required' tag",
        "User.Email": "Error:Field validation for 'Email' failed on the 'required' tag"
    }
}

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author