Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fixes EOF error when having HTTP POST requests #342

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions internal/pkg/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package http

import (
"bytes"
"crypto/tls"
"fmt"
"io"
Expand Down Expand Up @@ -73,17 +74,22 @@ func NewClient(host string, insecure bool, timeoutMilliseconds int, protocol Pro
}

// SendRequest sends a request to the HTTP server and wraps useful information into a Response object.
func (c Client) SendRequest(method, path string, headers map[string]string, body io.Reader) response.Response {
func (c Client) SendRequest(method, path string, headers map[string]string, requestBody *string) response.Response {
const respType = "http"

var body io.Reader
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-introducing the i/o reader per request

if requestBody != nil {
body = bytes.NewBufferString(*requestBody)
}

url := fmt.Sprintf("%s/%s", c.host, strings.TrimLeft(path, "/"))
req, err := http.NewRequest(method, url, body)

if err != nil {
log.Printf("Failed to create request: %s %s: %v", method, url, err)
return response.Response{Duration: time.Duration(0), Err: err, Type: respType}
}

if req.Body != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Body must be closed when there is no error and when a body is present. In case of GET there is no Body.

defer req.Body.Close()
}
for k, v := range headers {
if strings.EqualFold(k, "Host") {
req.Host = v
Expand All @@ -94,14 +100,8 @@ func (c Client) SendRequest(method, path string, headers map[string]string, body

req.Header.Add(k, interpolatedHeaderValue)
}
if err != nil {
defer req.Body.Close()
}
startTime := time.Now()
resp, err := c.httpClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
endTime := time.Now()
if err != nil {
return response.Response{Duration: endTime.Sub(startTime), Err: err, Type: respType}
Expand Down
19 changes: 6 additions & 13 deletions internal/pkg/http/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"fmt"
"mittens/fixture"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -40,50 +39,44 @@ func TestMain(m *testing.M) {
func TestRequestSuccessHTTP1(t *testing.T) {
c := NewClient(serverUrl, false, 10000, HTTP1)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", WorkingPath, make(map[string]string), reader)
resp := c.SendRequest("GET", WorkingPath, make(map[string]string), &reqBody)
assert.Nil(t, resp.Err)
}

func TestRequestSuccessH2C(t *testing.T) {
c := NewClient(serverUrl, false, 10000, H2C)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", WorkingPath, make(map[string]string), reader)
resp := c.SendRequest("GET", WorkingPath, make(map[string]string), &reqBody)
assert.Nil(t, resp.Err)
}

func TestHttpErrorHTTP1(t *testing.T) {
c := NewClient(serverUrl, false, 10000, HTTP1)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", "/", make(map[string]string), reader)
resp := c.SendRequest("GET", "/", make(map[string]string), &reqBody)
assert.Nil(t, resp.Err)
assert.Equal(t, resp.StatusCode, 404)
}

func TestHttpErrorH2C(t *testing.T) {
c := NewClient(serverUrl, false, 10000, H2C)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", "/", make(map[string]string), reader)
resp := c.SendRequest("GET", "/", make(map[string]string), &reqBody)
assert.Nil(t, resp.Err)
assert.Equal(t, resp.StatusCode, 404)
}

func TestConnectionErrorHTTP1(t *testing.T) {
c := NewClient("http://localhost:9999", false, 10000, HTTP1)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", "/potato", make(map[string]string), reader)
resp := c.SendRequest("GET", "/potato", make(map[string]string), &reqBody)
assert.NotNil(t, resp.Err)
}

func TestConnectionErrorH2C(t *testing.T) {
c := NewClient("http://localhost:9999", false, 10000, H2C)
reqBody := ""
reader := strings.NewReader(reqBody)
resp := c.SendRequest("GET", "/potato", make(map[string]string), reader)
resp := c.SendRequest("GET", "/potato", make(map[string]string), &reqBody)
assert.NotNil(t, resp.Err)
}

Expand Down
61 changes: 39 additions & 22 deletions internal/pkg/http/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Request struct {
Method string
Headers map[string]string
Path string
Body io.Reader
Body *string
}

type CompressionType string
Expand Down Expand Up @@ -90,15 +90,23 @@ func ToHTTPRequest(requestString string, compression CompressionType) (Request,
var reader io.Reader
switch compression {
case COMPRESSION_GZIP:
reader = compressGzip([]byte(body))
reader, err = compressGzip([]byte(body))
case COMPRESSION_BROTLI:
reader = compressBrotli([]byte(body))
reader, err = compressBrotli([]byte(body))
case COMPRESSION_DEFLATE:
reader = compressFlate([]byte(body))
reader, err = compressFlate([]byte(body))
default:
reader = bytes.NewBufferString(body)
}

if err != nil {
return Request{}, fmt.Errorf("unable to compress body for request: %s", parts[2])
}

buf := new(bytes.Buffer)
buf.ReadFrom(reader)
compressedBody := buf.String()

headers := make(map[string]string)
if compression != COMPRESSION_NONE {
encoding := ""
Expand All @@ -117,33 +125,42 @@ func ToHTTPRequest(requestString string, compression CompressionType) (Request,
Method: method,
Headers: headers,
Path: path,
Body: reader,
Body: &compressedBody,
}, nil
}

func compressGzip(data []byte) io.Reader {
pr, pw := io.Pipe()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization not required as mittens requests are re-used in the channel. So compression is only done once for each request element.

go func() {
gz := gzip.NewWriter(pw)
_, err := gz.Write(data)
gz.Close()
pw.CloseWithError(err)
}()
return pr
func compressGzip(data []byte) (io.Reader, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligned implementation of all compression functions

var b bytes.Buffer
w := gzip.NewWriter(&b)
_, err := w.Write(data); if err != nil {
return nil, err
}
err = w.Close(); if err != nil {
return nil, err
}
return &b, nil
}

func compressFlate(data []byte) *bytes.Buffer {
func compressFlate(data []byte) (io.Reader, error) {
var b bytes.Buffer
w, _ := flate.NewWriter(&b, 9)
w.Write(data)
w.Close()
return &b
_, err := w.Write(data); if err != nil {
return nil, err
}
err = w.Close(); if err != nil {
return nil, err
}
return &b, nil
}

func compressBrotli(data []byte) *bytes.Buffer {
func compressBrotli(data []byte) (io.Reader, error) {
var b bytes.Buffer
w := brotli.NewWriterLevel(&b, brotli.BestCompression)
w.Write(data)
w.Close()
return &b
_, err := w.Write(data); if err != nil {
return nil, err
}
err = w.Close(); if err != nil {
return nil, err
}
return &b, nil
}
43 changes: 14 additions & 29 deletions internal/pkg/http/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package http

import (
"bytes"
"net/http"
"os"
"regexp"
Expand All @@ -34,9 +33,7 @@ func TestHttp_FlagToHttpRequest(t *testing.T) {

assert.Equal(t, http.MethodPost, request.Method)
assert.Equal(t, "/db", request.Path)
body := new(bytes.Buffer)
body.ReadFrom(request.Body)
assert.Equal(t, `{"db": "true"}`, body.String())
assert.Equal(t, `{"db": "true"}`, *request.Body)
}

func TestHttp_CompressGzip(t *testing.T) {
Expand All @@ -49,11 +46,9 @@ func TestHttp_CompressGzip(t *testing.T) {

assert.Equal(t, map[string]string{"Content-Encoding": "gzip"}, request.Headers)

body := new(bytes.Buffer)
body.ReadFrom(request.Body)
expected := &bytes.Buffer{}
expected.Write([]byte{0x1f, 0x8b, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xaa, 0x56, 0x4a, 0x49, 0x52, 0xb2, 0x52, 0x50, 0x2a, 0x29, 0x2a, 0x4d, 0x55, 0xaa, 0x5, 0x4, 0x0, 0x0, 0xff, 0xff, 0xa1, 0x4a, 0x9b, 0x5d, 0xe, 0x0, 0x0, 0x0})
assert.Equal(t, expected, body)
expected := []byte{0x1f, 0x8b, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xaa, 0x56, 0x4a, 0x49, 0x52, 0xb2, 0x52, 0x50, 0x2a, 0x29, 0x2a, 0x4d, 0x55, 0xaa, 0x5, 0x4, 0x0, 0x0, 0xff, 0xff, 0xa1, 0x4a, 0x9b, 0x5d, 0xe, 0x0, 0x0, 0x0}
actual := []byte(*request.Body)
assert.Equal(t, expected, actual)
}

func TestHttp_CompressBrotli(t *testing.T) {
Expand All @@ -66,11 +61,9 @@ func TestHttp_CompressBrotli(t *testing.T) {

assert.Equal(t, map[string]string{"Content-Encoding": "br"}, request.Headers)

body := new(bytes.Buffer)
body.ReadFrom(request.Body)
expected := &bytes.Buffer{}
expected.Write([]byte{0x8b, 0x6, 0x80, 0x7b, 0x22, 0x64, 0x62, 0x22, 0x3a, 0x20, 0x22, 0x74, 0x72, 0x75, 0x65, 0x22, 0x7d, 0x3})
assert.Equal(t, expected, body)
expected := []byte{0x8b, 0x6, 0x80, 0x7b, 0x22, 0x64, 0x62, 0x22, 0x3a, 0x20, 0x22, 0x74, 0x72, 0x75, 0x65, 0x22, 0x7d, 0x3}
actual := []byte(*request.Body)
assert.Equal(t, expected, actual)
}

func TestHttp_CompressDeflate(t *testing.T) {
Expand All @@ -81,11 +74,9 @@ func TestHttp_CompressDeflate(t *testing.T) {
assert.Equal(t, http.MethodPost, request.Method)
assert.Equal(t, "/db", request.Path)
assert.Equal(t, map[string]string{"Content-Encoding": "deflate"}, request.Headers)
body := new(bytes.Buffer)
body.ReadFrom(request.Body)
expected := &bytes.Buffer{}
expected.Write([]byte{0xaa, 0x56, 0x4a, 0x49, 0x52, 0xb2, 0x52, 0x50, 0x2a, 0x29, 0x2a, 0x4d, 0x55, 0xaa, 0x5, 0x4, 0x0, 0x0, 0xff, 0xff})
assert.Equal(t, expected, body)
expected := []byte{0xaa, 0x56, 0x4a, 0x49, 0x52, 0xb2, 0x52, 0x50, 0x2a, 0x29, 0x2a, 0x4d, 0x55, 0xaa, 0x5, 0x4, 0x0, 0x0, 0xff, 0xff}
actual := []byte(*request.Body)
assert.Equal(t, expected, actual)
}

func TestBodyFromFile(t *testing.T) {
Expand All @@ -100,9 +91,7 @@ func TestBodyFromFile(t *testing.T) {

assert.Equal(t, http.MethodPost, request.Method)
assert.Equal(t, "/db", request.Path)
buf := new(bytes.Buffer)
buf.ReadFrom(request.Body)
assert.Equal(t, `{"foo": "bar"}`, buf.String())
assert.Equal(t, `{"foo": "bar"}`, *request.Body)
}

func TestHttp_FlagWithoutBodyToHttpRequest(t *testing.T) {
Expand Down Expand Up @@ -130,14 +119,12 @@ func TestHttp_TimestampInterpolation(t *testing.T) {

var numbersRegex = regexp.MustCompile("\\d+")
matchPath := numbersRegex.MatchString(request.Path)
body := new(bytes.Buffer)
body.ReadFrom(request.Body)
matchBody := numbersRegex.MatchString(body.String())
matchBody := numbersRegex.MatchString(*request.Body)

assert.True(t, matchPath)
assert.True(t, matchBody)
assert.Equal(t, len(request.Path), 19) // "path_ + 13 numbers for timestamp
assert.Equal(t, len(body.String()), 25) // { "body": 13 numbers for timestamp
assert.Equal(t, len(*request.Body), 25) // { "body": 13 numbers for timestamp
}

func TestHttp_Interpolation(t *testing.T) {
Expand All @@ -151,9 +138,7 @@ func TestHttp_Interpolation(t *testing.T) {
matchPath := pathRegex.MatchString(request.Path)

var bodyRegex = regexp.MustCompile("{\"body\": \"(foo|bar) \\d\"}")
body := new(bytes.Buffer)
body.ReadFrom(request.Body)
matchBody := bodyRegex.MatchString(body.String())
matchBody := bodyRegex.MatchString(*request.Body)

assert.True(t, matchPath)
assert.True(t, matchBody)
Expand Down
Loading
Loading