Skip to content

Instantly share code, notes, and snippets.

@dpordomingo
Last active October 16, 2019 07:51
Show Gist options
  • Save dpordomingo/0f1ef5d371e2862e47e59aad56be8134 to your computer and use it in GitHub Desktop.
Save dpordomingo/0f1ef5d371e2862e47e59aad56be8134 to your computer and use it in GitHub Desktop.
Reproduce how request and response can be ruined if readed along the chain.
package main
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)
func main() {
client := oauth2.NewClient(context.TODO(), oauth2.StaticTokenSource(
&oauth2.Token{
AccessToken: os.Getenv("GITHUB_TOKEN"),
},
))
client.Transport = &RetryTransport{
Transport: &LogTransport{ // Deeper Transport
// <-------- Beyond this level, it's Go internals, and network stuf
Transport: &FakerTransport{ // Simulates a network error couple of times before succeeding
Transport: client.Transport,
},
},
}
gh4Client := githubv4.NewClient(client)
var query struct {
Viewer struct {
Login string
}
}
err := gh4Client.Query(context.TODO(), &query, nil)
if err != nil {
fmt.Println("[USER] got an error:", err)
}
fmt.Println("[USER] result:", query)
}
// maxFake number of times that the network call will fail
const maxFake = 2
// FakerTransport simulates a network error couple of times before succeeding
type FakerTransport struct {
Transport http.RoundTripper
iterFake int
}
func (t *FakerTransport) RoundTrip(request *http.Request) (*http.Response, error) {
t.iterFake++
response, err := t.Transport.RoundTrip(request)
if t.iterFake <= maxFake {
err := fmt.Errorf("network error")
fmt.Println("[HTTP]", err)
return nil, err
}
return response, err
}
type LogTransport struct {
Transport http.RoundTripper
}
const backupRequestAfterLeavingLastTransport = true
const backupRequestAfterReadingIt = true
const backupResponseAfterReadingIt = true
func (t *LogTransport) RoundTrip(request *http.Request) (*http.Response, error) {
var response *http.Response
var err error
var responseBodyContent []byte
var requestBodyContent []byte
if backupRequestAfterLeavingLastTransport {
requestBody := BackupRequestBody(request)
if err != nil {
fmt.Println("could not get a copy of the Request.Body")
}
response, err = t.Transport.RoundTrip(request)
request.Body = requestBody
} else {
response, err = t.Transport.RoundTrip(request)
}
// the LogTransport will try to read and log the Request if it exist
// otherwise it'll log the error
if err == nil {
if backupResponseAfterReadingIt {
responseBodyContent, _ = ReadResponseAndRestore(response)
} else {
responseBodyContent, _ = ioutil.ReadAll(response.Body)
}
fmt.Println("[TRANSPORT.LOG] response=", string(responseBodyContent)) // this will consume the Response, avoiding others in the Transport chain reading it"
} else {
fmt.Println("[TRANSPORT.LOG] got an error:", err)
}
// the LogTransport will read and log the Request
if backupRequestAfterReadingIt {
requestBodyContent, _ = ReadRequestAndRestore(request)
} else {
requestBodyContent, _ = ioutil.ReadAll(request.Body)
}
fmt.Println("[TRANSPORT.LOG] request=", string(requestBodyContent)) // this is not working because the Request was already read by "t.Transport.RoundTrip(request)"
return response, err
}
type RetryTransport struct {
Transport http.RoundTripper
}
func (t *RetryTransport) RoundTrip(request *http.Request) (*http.Response, error) {
for {
response, err := t.Transport.RoundTrip(request)
if err == nil {
return response, err
}
fmt.Println("[TRANSPORT.retry] retrying after getting error:", err)
time.Sleep(time.Second)
}
}
// BackupRequestBody returns a backup of the Request.Body, leaving it untouched
// so the passed Request.Body will be still readable.
func BackupRequestBody(req *http.Request) io.ReadCloser {
content, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(content))
return ioutil.NopCloser(bytes.NewBuffer(content))
}
// ReadResponseAndRestore reads the content of the passed Response.Body,
// and restores it to allow its reuse (e.g. when read from a nested Transport)
func ReadResponseAndRestore(resp *http.Response) ([]byte, error) {
reader, content, err := readAndGetNew(resp.Body)
if err != nil {
return nil, err
}
resp.Body = reader
return content, nil
}
// ReadRequestAndRestore reads the content of the Response.Body, and restores it
// to allow its reuse (e.g. when read from a nested Transport)
func ReadRequestAndRestore(req *http.Request) ([]byte, error) {
reader, content, err := readAndGetNew(req.Body)
if err != nil {
return nil, err
}
req.Body = reader
return content, nil
}
func readAndGetNew(reader io.Reader) (io.ReadCloser, []byte, error) {
content, err := ioutil.ReadAll(reader)
if err != nil {
return nil, nil, err
}
return ioutil.NopCloser(bytes.NewReader(content)), content, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment