HTTP request and response duplication for multiple use

Hi,

I am auditing http request and response by dumping both into a flat file which works at the moment. However, I have a feeling this can be improved because as far as I see, resource usage is high (RAM etc.).

Do you think there is better way or can we improve this code? Particularly interested in AUDIT REQUEST and AUDIT RESPONSE blocks.

Thanks

Bench results below reflects 100 concurrent users sending requests for 10 seconds.

Alloc = 12 MiB	TotalAlloc = 534 MiB	Sys = 27 MiB	NumGC = 190
Alloc =  2 MiB	TotalAlloc = 309 MiB	Sys = 15 MiB	NumGC = 148
Alloc =  4 MiB	TotalAlloc = 348 MiB	Sys = 11 MiB	NumGC = 172
Alloc = 10 MiB	TotalAlloc = 223 MiB	Sys = 23 MiB	NumGC = 39
Alloc =  3 MiB	TotalAlloc = 167 MiB	Sys = 11 MiB	NumGC = 76
...
package xhttp

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	"os"
	"path/filepath"

	"github.com/google/uuid"
)

type Client struct {
	Client http.RoundTripper
}

func (c Client) Request(ctx context.Context, met, url string, bdy io.Reader, hdrs map[string]string) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, met, url, bdy)
	if err != nil {
		return nil, err
	}

	for k, v := range hdrs {
		req.Header.Add(k, v)
	}

	id := uuid.NewString()

	// AUDIT REQUEST -----------------------------------------------------------
	reqCopy := req.Clone(req.Context())
	if req.Body != nil || req.Body != http.NoBody {
		var buff bytes.Buffer
		if _, err := io.Copy(&buff, req.Body); err == nil {
			req.Body = io.NopCloser(bytes.NewReader(buff.Bytes()))
			reqCopy.Body = io.NopCloser(bytes.NewReader(buff.Bytes()))
		}
	}
	go LogRequest(reqCopy, id)
	// -------------------------------------------------------------------------

	res, err := c.Client.RoundTrip(req)
	if err != nil {
		return nil, err
	}

	// AUDIT RESPONSE ----------------------------------------------------------
	resCopy := *res
	if res.Body != nil || res.Body != http.NoBody {
		var buff bytes.Buffer
		if _, err := io.Copy(&buff, res.Body); err == nil {
			res.Body = io.NopCloser(bytes.NewReader(buff.Bytes()))
			resCopy.Body = io.NopCloser(bytes.NewReader(buff.Bytes()))
		}
	}
	go LogResponse(&resCopy, req, id)
	// -------------------------------------------------------------------------

	return res, nil
}

func LogRequest(req *http.Request, id string) {
	dump, err := httputil.DumpRequest(req, true)
	if err != nil {
		fmt.Println("dump request", err)
		return
	}

	path := id + "_request.log"

	if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
		fmt.Println("mkdir all:", err)
		return
	}

	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
	if err != nil {
		fmt.Println("open file:", err)
		return
	}
	defer file.Close()

	if _, err := file.Write(dump); err != nil {
		fmt.Println("file write:", err)
		return
	}
}

func LogResponse(res *http.Response, req *http.Request, id string) {
	dump, err := httputil.DumpResponse(res, true)
	if err != nil {
		fmt.Println("dump response", err)
		return
	}
	defer res.Body.Close()

	method := http.MethodGet
	if req.Method != "" {
		method = req.Method
	}

	uri := req.RequestURI
	if uri == "" {
		uri = req.URL.RequestURI()
	}

	dump = append(
		[]byte(fmt.Sprintf("%s %s HTTP/%d.%d\nHost: %s\n", method, uri, req.ProtoMajor, req.ProtoMinor, req.URL.Host)),
		dump...,
	)

	path := id + "_response.log"

	if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
		fmt.Println("mkdir all:", err)
		return
	}

	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0600))
	if err != nil {
		fmt.Println("open file:", err)
		return
	}
	defer file.Close()

	if _, err := file.Write(dump); err != nil {
		fmt.Println("file write:", err)
		return
	}
}

I did notice that you’re manually cloning your request and response, and it looks like all for the sake of preserving the body. But the documentation for DumpRequest (and by extension DumpResponse) state that those methods preserve the body for you:

If body is true, DumpRequest also returns the body. To do so, it consumes req.Body and then replaces it with a new io.ReadCloser that yields the same bytes.

So, do you still need to manually clone the objects? If not, might that reduce some memory usage?

Looking back at the previous post you made about this subject, peakedshout’s suggestion looks to be complete and efficient from a bytes-copying perspective. How did that solution work for you?