Coder Social home page Coder Social logo

h2non / gentleman Goto Github PK

View Code? Open in Web Editor NEW
1.1K 18.0 53.0 262 KB

Plugin-driven, extensible HTTP client toolkit for Go

Home Page: https://pkg.go.dev/github.com/h2non/gentleman?tab=doc

License: MIT License

Go 100.00%
http-client golang middleware pluggable modular retry consul http client sdk

gentleman's Introduction

gentleman GitHub release GoDoc Coverage Status Go Report Card Go Version

Full-featured, plugin-driven, middleware-oriented toolkit to easily create rich, versatile and composable HTTP clients in Go.

gentleman embraces extensibility and composition principles in order to provide a flexible way to easily create featured HTTP client layers based on built-in or third-party plugins that you can register and reuse across HTTP clients.

As an example, you can easily provide retry policy capabilities or dynamic server discovery in your HTTP clients simply attaching the retry or consul plugins.

Take a look to the examples, list of supported plugins, HTTP entities or middleware layer to get started.

For testing purposes, see baloo, an utility library for expressive end-to-end HTTP API testing, built on top of gentleman toolkit. For HTTP mocking, see gentleman-mock, which uses gock under the hood for easy and expressive HTTP client request mocking.

Versions

  • v2 - Latest version. Stable. Recommended.
  • v1 - First version. Stable. Actively maintained.

Features

  • Plugin driven architecture.
  • Simple, expressive, fluent API.
  • Idiomatic built on top of net/http package.
  • Context-aware hierarchical middleware layer supporting all the HTTP life cycle.
  • Built-in multiplexer for easy composition capabilities.
  • Easy to extend via plugins/middleware.
  • Ability to easily intercept and modify HTTP traffic on-the-fly.
  • Convenient helpers and abstractions over Go's HTTP primitives.
  • URL template path params.
  • Built-in JSON, XML and multipart bodies serialization and parsing.
  • Easy to test via HTTP mocking (e.g: gentleman-mock).
  • Supports data passing across plugins/middleware via its built-in context.
  • Fits good while building domain-specific HTTP API clients.
  • Easy to hack.
  • Dependency free.

Installation

go get -u gopkg.in/h2non/gentleman.v2

Requirements

  • Go 1.9+

Plugins

Name Docs Status Description
url Easily declare URL, base URL and path values in HTTP requests
auth Declare authorization headers in your requests
body Easily define bodies based on JSON, XML, strings, buffers or streams
bodytype Define body MIME type by alias
cookies Declare and store HTTP cookies easily
compression Helpers to define enable/disable HTTP compression
headers Manage HTTP headers easily
multipart Create multipart forms easily. Supports files and text fields
proxy Configure HTTP proxy servers
query Easily manage query params
redirect Easily configure a custom redirect policy
timeout Easily configure the HTTP timeouts (request, dial, TLS...)
transport Define a custom HTTP transport easily
tls Configure the TLS options used by the HTTP transport
retry Provide retry policy capabilities to your HTTP clients
mock Easy HTTP mocking using gock
consul Consul based server discovery with configurable retry/backoff policy

Community plugins

Name Docs Status Description
logger Easily log requests and responses

Send a PR to add your plugin to the list.

Creating plugins

You can create your own plugins for a wide variety of purposes, such as server discovery, custom HTTP tranport, modify any request/response param, intercept traffic, authentication and so on.

Plugins are essentially a set of middleware function handlers for one or multiple HTTP life cycle phases exposing a concrete interface consumed by gentleman middleware layer.

For more details about plugins see the plugin package and examples.

Also you can take a look to a plugin implementation example.

HTTP entities

gentleman provides two HTTP high level entities: Client and Request.

Each of these entities provides a common API and are both middleware capable, giving you the ability to plug in custom components with own logic into any of them.

gentleman was designed to provide strong reusability capabilities. This is mostly achieved via its built-in hierarchical, inheritance-based middleware layer.

The following list describes how inheritance hierarchy works and is used across gentleman's entities.

  • Client entity can inherit from other Client entity.
  • Request entity can inherit from a Client entity.
  • Client entity is mostly designed for reusability.
  • Client entity can create multiple Request entities who implicitly inherits from Client entity itself.
  • Request entity is designed to have specific HTTP request logic that is not typically reused.
  • Both Client and Request entities are full middleware capable interfaces.
  • Both Client and Request entities can be cloned in order to produce a copy but side-effects free new entity.

You can see an inheritance usage example here.

Middleware

gentleman is completely based on a hierarchical middleware layer based on plugins that executes one or multiple function handlers (aka plugin interface) providing a simple way to plug in intermediate custom logic in your HTTP client.

It supports multiple phases which represents the full HTTP request/response life cycle, giving you the ability to perform actions before and after an HTTP transaction happen, even intercepting and stopping it.

The middleware stack chain is executed in FIFO order designed for single thread model. Plugins can support goroutines, but plugins implementors should prevent data race issues due to concurrency in multithreading programming.

For more implementation details about the middleware layer, see the middleware package and examples.

Middleware phases

Supported middleware phases triggered by gentleman HTTP dispatcher:

  • request - Executed before a request is sent over the network.
  • response - Executed when the client receives the response, even if it failed.
  • error - Executed in case that an error ocurrs, support both injected or native error.
  • stop - Executed in case that the request has been manually stopped via middleware (e.g: after interception).
  • intercept - Executed in case that the request has been intercepted before network dialing.
  • before dial - Executed before a request is sent over the network.
  • after dial - Executed after the request dialing was done and the response has been received.

Note that the middleware layer has been designed for easy extensibility, therefore new phases may be added in the future and/or the developer could be able to trigger custom middleware phases if needed.

Feel free to fill an issue to discuss this capabilities in detail.

API

See godoc reference for detailed API documentation.

Subpackages

  • plugin - godoc - Plugin layer for gentleman.
  • mux - godoc - HTTP client multiplexer with built-in matchers.
  • middleware - godoc - Middleware layer used by gentleman.
  • context - godoc - HTTP context implementation for gentleman's middleware.
  • utils - godoc - HTTP utilities internally used.

Examples

See examples directory for featured examples.

Simple request

package main

import (
  "fmt"

  "gopkg.in/h2non/gentleman.v2"
)

func main() {
  // Create a new client
  cli := gentleman.New()

  // Define base URL
  cli.URL("http://httpbin.org")

  // Create a new request based on the current client
  req := cli.Request()

  // Define the URL path at request level
  req.Path("/headers")

  // Set a new header field
  req.SetHeader("Client", "gentleman")

  // Perform the request
  res, err := req.Send()
  if err != nil {
    fmt.Printf("Request error: %s\n", err)
    return
  }
  if !res.Ok {
    fmt.Printf("Invalid server response: %d\n", res.StatusCode)
    return
  }

  // Reads the whole body and returns it as string
  fmt.Printf("Body: %s", res.String())
}

Send JSON body

package main

import (
  "fmt"

  "gopkg.in/h2non/gentleman.v2"
  "gopkg.in/h2non/gentleman.v2/plugins/body"
)

func main() {
  // Create a new client
  cli := gentleman.New()

  // Define the Base URL
  cli.URL("http://httpbin.org/post")

  // Create a new request based on the current client
  req := cli.Request()

  // Method to be used
  req.Method("POST")

  // Define the JSON payload via body plugin
  data := map[string]string{"foo": "bar"}
  req.Use(body.JSON(data))

  // Perform the request
  res, err := req.Send()
  if err != nil {
    fmt.Printf("Request error: %s\n", err)
    return
  }
  if !res.Ok {
    fmt.Printf("Invalid server response: %d\n", res.StatusCode)
    return
  }

  fmt.Printf("Status: %d\n", res.StatusCode)
  fmt.Printf("Body: %s", res.String())
}

Composition via multiplexer

package main

import (
  "fmt"

  "gopkg.in/h2non/gentleman.v2"
  "gopkg.in/h2non/gentleman.v2/mux"
  "gopkg.in/h2non/gentleman.v2/plugins/url"
)

func main() {
  // Create a new client
  cli := gentleman.New()

  // Define the server url (must be first)
  cli.Use(url.URL("http://httpbin.org"))

  // Create a new multiplexer based on multiple matchers
  mx := mux.If(mux.Method("GET"), mux.Host("httpbin.org"))

  // Attach a custom plugin on the multiplexer that will be executed if the matchers passes
  mx.Use(url.Path("/headers"))

  // Attach the multiplexer on the main client
  cli.Use(mx)

  // Perform the request
  res, err := cli.Request().Send()
  if err != nil {
    fmt.Printf("Request error: %s\n", err)
    return
  }
  if !res.Ok {
    fmt.Printf("Invalid server response: %d\n", res.StatusCode)
    return
  }

  fmt.Printf("Status: %d\n", res.StatusCode)
  fmt.Printf("Body: %s", res.String())
}

License

MIT - Tomas Aparicio

gentleman's People

Contributors

00arthur00 avatar djui avatar dpastoor avatar h2non avatar jonasagx avatar kevinjcross avatar rodorgas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gentleman's Issues

the "net/http/httputil.DumpRequest" will destory the context value storage.

package main

import (
	"fmt"
	"net/http/httputil"

	"gopkg.in/h2non/gentleman.v1"
	"gopkg.in/h2non/gentleman.v1/context"
)

func main() {
	req := gentleman.NewRequest()
	req.Method("GET")
	req.URL("httpbin.org/post")
	req.BodyString("Body Hell!")
	req.UseHandler("before dial", func(ctx *context.Context, h context.Handler) {
		fmt.Printf("Before setting context:%T - %v\n", ctx.Request.Body, ctx.Request.Body)
		ctx.Set("Foo", "Bar")
		fmt.Printf("After setting context:%T - %v\n", ctx.Request.Body, ctx.Request.Body)
		httputil.DumpRequest(ctx.Request, true)
		fmt.Printf("After DumpRequest:%T - %v\n", ctx.Request.Body, ctx.Request.Body)
		h.Next(ctx)
	})
	req.Do()
}

Output:

Before setting context:*context.contextReadCloser - &{{0xc042068d80} map[$phase:before dial]}
After setting context:*context.contextReadCloser - &{{0xc042068d80} map[Foo:Bar $phase:before dial]}
After DumpRequest:ioutil.nopCloser - {Body Hell!}

With so many issues caused by wrapping context storage in http.Request.Body , I think we urgently need a solution for context storing.

How to disable redirect

I just want to get the redirected information but not follow to the location.
I tried cli.Use(redirect.Limit(-1)) and cli.Use(redirect.Limit(0)), all sucks.
Is there anyway to disable redirect totally?

Support standard rfc6570 URL Template syntax and replacement

Currently, gentleman supports a non-standard :<name> notation for defining replacement path tokens in a URI, excluding the query parameters.

Level 1 of the proposed standard: rfc6570 specifies the use of {<name>} notation for replacement tokens in both the path and the query parameters.

In order to support the use of URIs obtained from HATEOAS links which may follow the rfc6570 standard, gentleman should support the standard notation. Additionally, it should support replacement of tokens in the query parameters in addition to the path.

Balancer plugin

Balance traffic across multiple URLs. Those URLs could be provided via context or static configuration.

Consolidate interface for reusable HTTP client middlewares

This thread is intended to provide a design discussion frame in order to consolidate a homogeneous and versatile HTTP client middleware interface that can be used in gentleman, as well as in other packages.

Ideally, the conclusion of this proposal will be introduced in gentleman@v2.

Proposal MVP:

  • context.Context capable.
  • Minimal, idiomatic interface, inspired by http.Handler.
  • Exclusive net/http types and interfaces coupling (third-party package agnostic, even gentleman).

Proposals and ideas are very welcome.

See also related issues:

Error middlewares don't run if layer has parent

Steps to reproduce:

  1. create a client
  2. create a request from the client
  3. register middleware for error phase for the request (not the client!)
  4. trigger an error (eg: call a random url: localhost:12345)

Registered middleware won't run.

In middleware.go, Layer.Run method:

	ctx = s.parent.Run(phase, ctx)
	if ctx.Error != nil || ctx.Stopped {
		return ctx
	}

This will cause middlewares registered for error phase not to run if there is an error.

Possible solution:

	ctx = s.parent.Run(phase, ctx)
	if (phase != "error" && ctx.Error != nil) || ctx.Stopped {
		return ctx
	}

but I'm not sure what that would break.

log plugin

First of all, thanks for creating and sharing a nice software!

I would like to have log plugin which will log requests and responses using github.com/go-kit/kig/log.Logger interface.

I tried to create such a plugin myself, but I'm stuck now.

Here is an example usage.

package main

import (
        "log"
        "os"

        kitlog "github.com/go-kit/kit/log"
        "github.com/h2non/gentleman"
        "github.com/h2non/gentleman/plugins/auth"
        genlog "github.com/hnakamur/gentleman-log"
)

func main() {
        logger := kitlog.NewLogfmtLogger(os.Stdout)
        host := "http://localhost:8529"
        user := "root"
        password := "root"
        cli := gentleman.New()
        cli.Use(genlog.Logger(logger))
        cli.URL(host)
        cli.Use(auth.Basic(user, password))
        resp, err := cli.Request().Path("/_db/_system/_api/version").Send()
        if err != nil {
                log.Printf("got error. err=%v", err)
        }
        if !resp.Ok {
                log.Printf("bad status. status=%v", resp.StatusCode)
        }
}

Here is my plugin code.

package log

import (
        kitlog "github.com/go-kit/kit/log"
        c "gopkg.in/h2non/gentleman.v1/context"
        p "gopkg.in/h2non/gentleman.v1/plugin"
)
func Logger(logger kitlog.Logger) p.Plugin {
        return p.NewResponsePlugin(func(ctx *c.Context, h c.Handler) {
                h.Next(ctx)
                req := ctx.Request
                res := ctx.Response
                // payload := ... // request body
                // body := ... // response body
                logger.Log("method", req.Method, "url", req.URL, "payload", payload, "status", res.StatusCode, "body", body)
        })
}

Could you tell me how to retrieve the request body and the response body?

Tracking what is done by a stack of plugins is hard

I am tracking in which step the body of a request is consumed, but the way plugins are stacked makes it hard to check what is running and when.

The library should make transparent what it runs and when it does it.

Using body plugin will destroy the context.

You wrap the context storage in *http.Request.Body.

When using body plugin, the plugin replace the *http.Request.Body directly.

Here is a example to reproduce the problem.

package main

import (
	"fmt"

	"gopkg.in/h2non/gentleman.v1"
)

func main() {
	req := gentleman.NewRequest()
	req.Method("GET")
	req.URL("http://httpbin.org/headers")
	// Define the URL path at request level
	//req.Path("/headers")
	// Set a new header field
	req.SetHeader("Client", "gentleman")
	req.Context.Set("Foo", "Bar")
	req.BodyString("Body Hell!")
	// Perform the request
	fmt.Println(req.Context.GetAll(), req.Context.Stopped)
	res, err := req.Do()
	fmt.Println(req.Context.GetAll(), req.Context.Stopped)
	fmt.Println(res.Context.GetAll(), res.Context.Stopped)
	if err != nil {
		fmt.Printf("Request error: %s\n", err)
		return
	}
	if !res.Ok {
		fmt.Printf("Invalid server response: %d\n", res.StatusCode)
		return
	}
	// Reads the whole body and returns it as string
	fmt.Printf("Body: %s", res.String())
}

Output:

map[Foo:Bar] false
map[$phase:response] false
map[$phase:response] false
Body: {
  "headers": {
    "Accept-Encoding": "gzip", 
    "Client": "gentleman", 
    "Content-Length": "14", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "gentleman/1.0.2"
  }
}

HTTP method is overridden

I wanted to try gentleman doing a simple:

func (r *Repository) Send(authPayload, method, url string, data interface{}) ([]byte, error) {
    req := r.c.Request()
    req.AddHeader("Auth-Server-Payload", authPayload)
    req.Method(method)
    req.URL(url)
    req.JSON(data)
    res, err := req.Send()
    if err != nil {
        return nil, err
    }
        return res.Bytes(), nil
}

But when I use it, the HTTP method is overridden and the client always send a POST.

When I write fmt.Printf(req.Context.Request.Method) just before the Send and it prints a GET, I can see that a fmt.Printf(ctx.Request.Method) in the doDial method of the dispatcher prints a POST.

Am I doing something wrong ?

Segmented response for GET request with transfer-encoding: chunked

Whenever attempting to GET a resource, on the response assembly, gentlemen invokes EnsureResponseFinalized, which works perfectly for the majority of use cases.

However, when the response uses a transfer-encoding: chunked strategy, the response should only be finalized once all chunks have been processed.

Data race in middlewares

When running go test -race in one of my projects I detected a few data races in gentleman's middleware code. Just to make sure, I ran go run -race _examples/goroutines/goroutines.go and got the output below.

I can't have a closer look right now, but I will as soon as I can. Meanwhile, if someone knows anything bout this, please let me know. Thanks :)

==================
WARNING: DATA RACE
Read at 0x00c420016f30 by goroutine 7:
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x76
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420016f30 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0xc8
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 7 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420104180 by goroutine 7:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420104180 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func2()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:30 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 7 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420016f30 by goroutine 8:
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x76
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420016f30 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0xc8
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 8 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420104180 by goroutine 9:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420104180 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func2()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:30 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 9 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420011050 by goroutine 7:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func2()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:30 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420011050 by goroutine 9:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 7 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 9 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420124090 by goroutine 8:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420124090 by goroutine 7:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runBefore()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:78 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func1()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:27 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 8 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 7 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
Path: /headers => Response: 200
==================
WARNING: DATA RACE
Read at 0x00c420104680 by goroutine 8:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func4()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:36 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420104680 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func5()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:39 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 8 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (finished) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
==================
WARNING: DATA RACE
Read at 0x00c420104680 by goroutine 9:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:138 +0x9a
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func4()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:36 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c420104680 by goroutine 6:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func5()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:39 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 9 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 6 (finished) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
Path: /get => Response: 200
==================
WARNING: DATA RACE
Read at 0x00c4200114a0 by goroutine 9:
  gopkg.in/h2non/gentleman.v2/middleware.trigger()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:166 +0x1c5
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:133 +0x148
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func4()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:36 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Previous write at 0x00c4200114a0 by goroutine 8:
  gopkg.in/h2non/gentleman.v2/middleware.filter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:140 +0x144
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:132 +0x99
  gopkg.in/h2non/gentleman.v2/middleware.(*Layer).Run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/middleware/middleware.go:126 +0x1d4
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).run()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:142 +0x96
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).runAfter()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:95 +0x5a
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch.func5()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:39 +0x64
  gopkg.in/h2non/gentleman%2ev2.(*Dispatcher).Dispatch()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/dispatcher.go:49 +0x37d
  gopkg.in/h2non/gentleman%2ev2.(*Request).Do()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:271 +0x13c
  gopkg.in/h2non/gentleman%2ev2.(*Request).Send()
      /home/rdumont/go/src/gopkg.in/h2non/gentleman.v2/request.go:261 +0x38
  main.fetch()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:40 +0xb3

Goroutine 9 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e

Goroutine 8 (running) created at:
  main.main()
      /home/rdumont/go/src/github.com/h2non/gentleman/_examples/goroutines/goroutines.go:30 +0x15e
==================
Path: /ip => Response: 200
Path: /delay/1 => Response: 200
Done!
Found 9 data race(s)
exit status 66

Issue pulling as a Go Module

When running go get -u gopkg.in/h2non/gentleman.v2 I get the following error:

> go get -u gopkg.in/h2non/gentleman.v2
go: downloading gopkg.in/h2non/gentleman.v2 v2.0.4
go get gopkg.in/h2non/gentleman.v2: gopkg.in/h2non/[email protected]: verifying module: checksum mismatch
        downloaded: h1:Qq4Sk2jY7GoYBu8C5rZF/+RU9GdcnzPN9v3z5aBBGg8=
        sum.golang.org: h1:9R3K6CFYd/RdXDLi0pGXwaPnRx/pn5EZlrN3VkNygWc=

SECURITY ERROR
This download does NOT match the one reported by the checksum server.
The bits may have been replaced on the origin server, or an attacker may
have intercepted the download attempt.

For more information, see 'go help module-auth'.
Exception: go exited with 1
[tty 27], line 1: go get -u gopkg.in/h2non/gentleman.v2

I'm able to pull down other modules. Any ideas?
Using Go version 1.14.

Allow client setting methods reusability

I may be wrong but for now, it seems that the setting client methods such as func (c *Client) BaseURL(uri string) *Client only allows one time uses.

You can't really do:

cli := gentleman.New()
cli.BaseURL("foo.com")
cli.BaseURL("bar.com")

Because a new middleware is added each time.

The thing is I really need that kind of feature to use gentleman in a database driver because I need to be able to switch the DB url at any time, without touching the other middlewares.

I'm pretty sure there must be a workaround to do that right now. What would be the best way ?

GetBody() the Method in Golang is not define in gentleman.

When creating a new request, the GetBody property of the request is set according to the body parameter.
This attribute will affect the client's behavior after receiving the 307, 308 response code. If it is empty, it is likely that it will not follow redirect.

if body != nil {
		switch v := body.(type) {
		case *bytes.Buffer:
			req.ContentLength = int64(v.Len())
			buf := v.Bytes()
			req.GetBody = func() (io.ReadCloser, error) {
				r := bytes.NewReader(buf)
				return io.NopCloser(r), nil
			}
}
if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
	// We had a request body, and 307/308 require
	// re-sending it, but GetBody is not defined. So just
	// return this response to the user instead of an
	// error, like we did in Go 1.7 and earlier.
	shouldRedirect = false
}

But in gentleman the 'body.go ' and the createRequest the GetBody๏ผˆ๏ผ‰ Method is not define.

// String defines the HTTP request body based on the given string.
func String(data string) p.Plugin {
	return p.NewRequestPlugin(func(ctx *c.Context, h c.Handler) {
		ctx.Request.Method = getMethod(ctx)
		ctx.Request.Body = utils.StringReader(data)
		ctx.Request.ContentLength = int64(bytes.NewBufferString(data).Len())
		h.Next(ctx)
	})
}


// JSON defines a JSON body in the outgoing request.
// Supports strings, array of bytes or buffer.
func JSON(data interface{}) p.Plugin {
	return p.NewRequestPlugin(func(ctx *c.Context, h c.Handler) {
		buf := &bytes.Buffer{}
	switch data.(type) {
	case string:
		buf.WriteString(data.(string))
	case []byte:
		buf.Write(data.([]byte))
	default:
		if err := json.NewEncoder(buf).Encode(data); err != nil {
			h.Error(ctx, err)
			return
		}
	}

	ctx.Request.Method = getMethod(ctx)
	ctx.Request.Body = ioutil.NopCloser(buf)
	ctx.Request.ContentLength = int64(buf.Len())
	ctx.Request.Header.Set("Content-Type", "application/json")
	h.Next(ctx)
})
..........
}
// createRequest creates a default http.Request instance.
func createRequest() *http.Request {
	// Create HTTP request
	req := &http.Request{
		Method:     "GET",
		URL:        &url.URL{},
		Host:       "",
		ProtoMajor: 1,
		ProtoMinor: 1,
		Proto:      "HTTP/1.1",
		Header:     make(http.Header),
		Body:       utils.NopCloser(),
	}
	// Return shallow copy of Request with the new context
	return req.WithContext(emptyContext())
}

Default gentleman use doesn't seem to participate in TLS session resumption

We use gentleman as a client library between services in our product. We began noticing particular services receiving a lot of restart events due to CPU limit violations. When we enabled pprof for those services and took a look at the data, there was a lot of time being spent in addTLS - https://github.com/golang/go/blob/master/src/net/http/transport.go#L1508. This was true even across multiple calls to the same URL and service. This also matched with our observation that the restarts were more frequent with TLS across the full environment instead of it being terminated at the reverse proxy.

We were able to replicate this in a small test outside of the larger application. I've pushed that test to http://github.com/caseyhadden/gentle-http-test. This test runs 10 identical GET calls against httpbin.org using both HTTPS and HTTP. The output below is fairly representative of the type of difference we see. We ran the test cases against our internal server so latency wouldn't have as much of a chance to affect results, but this is what I came up with as something showing the issue and could be shared. The regular "gentleman" usage is consistently around 2x as slow in this microbenchmark. We stuck the grequests library in as another "not bare net/http" representative.

*** HTTPS
2021/02/22 12:49:32 nethttp: 757.704072ms
2021/02/22 12:49:34 gentleman: 1.266130582s
2021/02/22 12:49:35 gentleman transport: 899.396886ms
2021/02/22 12:49:35 grequests: 461.614291ms
*** HTTP
2021/02/22 12:49:36 nethttp: 968.82515ms
2021/02/22 12:49:37 gentleman: 843.205109ms
2021/02/22 12:49:38 gentleman transport: 724.24357ms
2021/02/22 12:49:38 grequests: 769.325955ms

When you check the test, you can see that the difference between "gentleman" and "gentleman transport" instances is that the latter calls cli.Use(transport.Set(http.DefaultTransport)) in order to reuse the HTTP transport instance across the client.

We used a basic load testing tool against one part of the application with such a change. That showed a marked improvement in both throughput and memory usage.

I guess I have a few questions related to this issue:

  1. Do you expect to see this difference for TLS and non-TLS connections seemingly related to TLS resumption protocols?
  2. Should the default gentleman client have a better setup for this?
  3. If not, is there some better way than setting the transport that you're expecting folks to have TLS connections function more performantly?

Data Race with Clone

Hey all - I suspect I'm missing something, but I'm wondering why this causes a data race within the middleware chain every time I call send several times in a row. I would have thought Clone would have made a complete copy of the request.

type Sender struct {
	Request *gentleman.Request
}

// New instantiates a new Notifier
func New(url string, key string) (*Sender, error) {
	req := gentleman.NewRequest()
	req.Method("POST")
	req.URL(url)
	req.AddHeader("Content-Type", "application/json")
	req.Use(timeout.Request(2 * time.Second))
	req.Use(timeout.Dial(5*time.Second, 30*time.Second))

	return &Sender{
		Request: req,
	}, nil
}

func (n *Sender) Send() {
	body, _ := "foo"

	go func(req *gentleman.Request, body string) {
		req.BodyString(body).Send()
	}(n.Request.Clone(), body)
}

net/context and httptrace support

First of all, great library. I really like design decisions and usability of it.

I do have some things that I miss and that I can not write plugin for. Although gentleman.context.Context is useful, I would really like to have support for net/context, especially now that it is part of standard library and net.Request has support for it.

Also, if context is added, following part should be easy, but I would also like to have some kind of support for httptrace for debugging purposes (also part of standard library since golang 1.7).

Do you have any plans of adding support for these things? And if yes, in which direction were you thinking of going?

I have experimented a bit locally and I have had some progress, but changes that I have made are not backward compatible, so I am not sure if PR would be accepted (not that I have something worthy or PR, it was just experiment).

Thanks.

client.URL or client.BaseURL undefined behavior

client.URL or client.BaseURL with empty host but port will send request to localhost

for example:

    // Create a new client
    cli := gentleman.New()

    // Define the Base URL
    // when host is empty, but port, there will cause an undefined behavior
    // cli.BaseURL(fmt.Sprintf("http://%s:%s", host, port))
    cli.BaseURL("http://:8889")  // or cli.URL("http://:8889")
    // fmt.Println(cli.Context.Request.URL.String())

    // Create a new request based on the current client
    req := cli.Request()

    // Method to be used
    req.Method("POST")

    // Define the JSON payload via body plugin
    data := map[string]string{"foo": "bar"}
    req.Use(body.JSON(data))

    // Perform the request, then it will send request to localhost
    res, err := req.Send()

http/1.1 and http/2

Supposedly, the newer version GO (I'm using GO 1.14.9) would use http/2 with TLS by default on the client-side when the server-side supports http/2. I expect to see HTTP/2 getting logged on the server-side when I use the following code (gentleman.V2 2.0.4)

cli := gentleman.New()
cli.URL("https://mytestserver.com")
cli.Request().Send()

However, I got this logged on the server-side,

[12/Oct/2020:15:43:03 +0000] "GET / HTTP/1.1" 200 14195 "-" "gentleman/2.0.4"

If I do this,

cli := gentleman.New()
cli.Use(transport.Set(http.DefaultTransport))
cli.URL("https://mytestserver.com")
cli.Request().Send()

I got this logged on the server-side,

[12/Oct/2020:15:58:12 +0000] "GET / HTTP/2.0" 200 14175 "-" "gentleman/2.0.4"

Of course, a simple call from the http package like below also got HTTP/2.0 logged on the server-side,

http.Get("https://mytestserver.com")

I've not found the gentleman documentation about this behavior (probably, I'm not thorough enough).

Any comments?

Issue with escaping parameters

Hello.

I am having an issue with the way Gentleman escape the parameters, I can't get it to work in my case,

I am building a request with a dynamic parameter that contains a "/" that need to be escaped as "%2F",

I tried multiple ways but could not make it work so far.

Either it doesn't escape anything when I set the parameter like this (so sends the "/" as this) :

request.Param("targetTag", targetTag)

Or it double escape when I do this way (and send "%252F") :

request.Param("targetTag", url.PathEscape(targetTag))

Any idea of how to do this ?

Thanks

While using mutlipart form-data I am not able to set fieldName diff from fileName

I am creating formdata like below : Server requires form-data as : name=file ; filename=
The value of name is expected to "file" on sever and filename is the actual filename
Using the current mutipart I can only generate something like :
1> Content-Disposition;form-data; name=file ; filename=file
2> Content-Disposition;form-data; name=a.txt ; filename=a.txt

Cannot get something like :
3>Content-Disposition;form-data; name=file ; filename=a.txt

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: application/octet**

... contents of file goes here ...

------WebKitFormBoundaryePkpFF7tjBAqx29L--

Here is the snippet I use and then pass it to gentelman's client.Use(multipart.Data(dt))

fields := map[string]common.Values{"attachment": {"SomeValue"}}
dt := common.FormData{
Data: fields,
Files: []common.FormFile{{Name: , Reader: r}},
}

It seems that when I pass "Name" in Formfile the fieldname also switches to "Name" .
It seems in the file :plugins/multipart/multipart.go (in function writeFile this change happens before passing these values to multipartWriter.CreateFormFile(fileName, file.Name)).

post form is complex to use

u := "http://httpbin.org/post"
// Using net/http
v := make(url.Values) // url.Values has methods for value manipulating
v.Set("k1", "v1")
http.PostForm(u, v)
// or
http.PostForm(u, url.Values{"k1": []string{"v1"}})
// Using gentleman
r := gentleman.NewRequest()
// multipart.DataFields is actually same as url.Values, but without methods
d := make(multipart.DataFields)
d["k1"] = multipart.Values{"v1"} // multipart.Values is useless, why not just use []string?
r.Form(multipart.FormData{Data: d})
// or
r.Use(multipart.Fields(d))
r.Send()

Suggestions:

  1. remove multipart.Values, multipart.DataFields, using url.Values
  2. add a shortcut gentleman.Request.Fields

Finalizers are used incorrectly?

Some of our tests are run with --race to check for race conditions and I have traced an issue with a failing test that seem to come from gentelmen's use of finalizer.
There are a few uses of runtime.SetFinalizer() which I believe are incorrect.
See the following code:

func SetTransportFinalizer(transport *http.Transport) {
	runtime.SetFinalizer(&transport, func(t **http.Transport) {
		(*t).CloseIdleConnections()
	})
}

You get a pointer to an http.Transport and you're setting a finalizer for the pointer to that pointer. This means that the finalizer will fire when the local variable is GCed and not when the value itself.
Suggestions

  • The following solves the issue for us:
func SetTransportFinalizer(transport *http.Transport) {
	runtime.SetFinalizer(transport, func(t *http.Transport) {
		t.CloseIdleConnections()
	})
}
  • this probably needs to be changed also in response.EnsureResponseFinalized too

  • But.. I think you should reconsider the usage of the finalizer at all. I know it as been copied from Hashicorp's go-cleanhttp package but note that they have removed that from their package.

  • Also note that in the same file you have an almost identical func EnsureTransporterFinalized() which doesn't seem to be used anywhere.

Add a String() method to Request

Response has an useful method to show its raw content: String(). It would be helpful to have an equivalent method for Resquest.

The checksum on sum.golang.org is incorrect

$ go mod tidy
gopkg.in/h2non/[email protected]: verifying module: checksum mismatch
	downloaded: h1:Qq4Sk2jY7GoYBu8C5rZF/+RU9GdcnzPN9v3z5aBBGg8=
	sum.golang.org: h1:9R3K6CFYd/RdXDLi0pGXwaPnRx/pn5EZlrN3VkNygWc=

SECURITY ERROR
This download does NOT match the one reported by the checksum server.
The bits may have been replaced on the origin server, or an attacker may
have intercepted the download attempt.

For more information, see 'go help module-auth'.

gentleman/proxy error in example and code

cli.Use(proxy.Set([]string{"http://proxy:3128", "http://proxy2:3128"}))
This is from example but in code proxy.Set accept map[string]string with parameters by url schema, and same proxy.
It's all nothing - but there is one nuance, map take a schema by request schema. It's not good, but you can adapt.
But map can only contain unique keys. So we can use only 1 proxy per 1 scheme. This is not good.

context.createRequest didn't set "GetBody" properly.

The context package wrap the http.Request.Body with a internal type utils.nopCloser in createRequest and replace body with a new type context.contextReadCloser when we try to update values in context.

Unfortunately, the GetBody method for http.Request didn't been set.

The std lib net/http set GetBody for us when we use http.NewRequest and this solution covered most cases. L788-L830 in Go1.8

	if body != nil {
		switch v := body.(type) {
		case *bytes.Buffer:
			req.ContentLength = int64(v.Len())
			buf := v.Bytes()
			req.GetBody = func() (io.ReadCloser, error) {
				r := bytes.NewReader(buf)
				return ioutil.NopCloser(r), nil
			}
		case *bytes.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
		case *strings.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
		default:
			// This is where we'd set it to -1 (at least
			// if body != NoBody) to mean unknown, but
			// that broke people during the Go 1.8 testing
			// period. People depend on it being 0 I
			// guess. Maybe retry later. See Issue 18117.
		}
		// For client requests, Request.ContentLength of 0
		// means either actually 0, or unknown. The only way
		// to explicitly say that the ContentLength is zero is
		// to set the Body to nil. But turns out too much code
		// depends on NewRequest returning a non-nil Body,
		// so we use a well-known ReadCloser variable instead
		// and have the http package also treat that sentinel
		// variable to mean explicitly zero.
		if req.GetBody != nil && req.ContentLength == 0 {
			req.Body = NoBody
			req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
		}
	}

That's mean, even we sent our request, we still can get the request body if we want.

For gentleman, the request was built when dispatching, we can't get the request body before we sent it compared with http.NewRequest(although this behavior is useless ).

More importantly, we also can't get the request body !

That's mean, it's tricky if some jobs base on the request we sent, like storing the response by request fingerprint.

Here is a example to reproduce the problem.

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"

	"gopkg.in/h2non/gentleman.v1"
)

func main() {
	body := bytes.NewBufferString("Body Hell!")
	request, _ := http.NewRequest("POST", "http://httpbin.org/post", body)
	res, _ := http.DefaultClient.Do(request)
	// get request body after sent
	if body, _ := ioutil.ReadAll(res.Request.Body); body != nil {
		fmt.Println("the origin Body is empty:", string(body[:]))
	}
	bodyClone, _ := res.Request.GetBody()
	if body, _ := ioutil.ReadAll(bodyClone); body != nil {
		fmt.Println("Body still can be gotten by GetBody:", string(body[:]))
	}
	req := gentleman.NewRequest()
	req.Method("POST")
	req.URL("http://httpbin.org/post")
	req.BodyString("Body Hell!")
	fmt.Println("the req.Context.Request.GetBody is nil because it didn't be set in `createRequest`:", req.Context.Request.GetBody == nil)
	gres, _ := req.Do()
	if body, _ := ioutil.ReadAll(gres.Context.Request.Body); body != nil {
		fmt.Println("the Body is empty and we can'y get cached body by GetBody:", string(body[:]))
	}

Output:

the origin Body is empty: 
Body still can be gotten by GetBody: Body Hell!
the req.Context.Request.GetBody is nil because it didn't be set in `createRequest`: true
the Body is empty and we can'y get a cache for GetBody: 

Multiplexer combinators

// Every conditional
mux.If(mux.Method("GET"), mux.Status(200))

// Some conditional
mux.Or(mux.Method("GET"), mux.Status(200))

Thread Safety of Gentleman instance

I am wondering if one instance of Gentleman can be shared across multiple go-routines Or every go-routine should create it's own instance every time a request comes?

Enhance redirect plugin to allow for trusted hosts

We're using the gentleman client to make calls to 1st party services that can result in redirects. Initially, we were using the 'Trusted' field of the plugin to ensure that any headers were forwarded. For security purposes, we wanted to add a capability and limit header forwarding to only our 1st party services so any potential redirection to a 3rd party service wouldn't end up exposing sensitive information. In order to accomplish this, we created a fork of the redirect plugin that includes a field for a list of trusted host suffixes before doing the header copying.

It would be great to have that feature as part of the upstream gentleman, and I'd be happy to put together a PR for it if there is interest. But I wanted to open an issue and check on feasibility before just dropping a PR on your doorstep.

Thanks!

On Go 1.9 redirect plugin produce duplicate of headers.

Same code compiled with go 1.7 and 1.9 produce different requests.

On go 1.7:

DEBU[0001] http request
GET /api-v2.1/ HTTP/0.0
Host: test.sdn:8765
Content-Type: application/json
Referer: http://test.sdn:8765/api-v2.1
User-Agent: gentleman/2.0.2
X-Auth-Token: censored

Go 1.9:

DEBU[0001] http request
GET /api-v2.1/ HTTP/0.0
Host: test.sdn:8765
Content-Type: application/json
Content-Type: application/json
Referer: http://test.sdn:8765/api-v2.1
User-Agent: gentleman/2.0.2
User-Agent: gentleman/2.0.2
X-Auth-Token: censored
X-Auth-Token: censored

I workaround that by making custom plugin:

func NewRedirectPlugin() plugin.Plugin {
	return plugin.NewRequestPlugin(func(ctx *context.Context, h context.Handler) {
		ctx.Client.CheckRedirect = func(req *http.Request, pool []*http.Request) error {
			if len(pool) >= redirect.RedirectLimit {
				return redirect.ErrRedirectLimitExceeded
			}

			// redirect.Config() copy Headers from pool[0].Headers to req.Headers using req.Headers.Add()
			// That works on Go 1.7 (and seems required), but on Go 1.9 that produce header duplicates.
			// E.g. two X-Auth-Token instead of one. That broke auth.
			// Seems that in Go 1.9 net/http do headers copy

			return nil
		}
		h.Next(ctx)
	})
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.