Last active
October 16, 2019 07:51
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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