What version of Go are you using (go version)?

$ go version
go version go1.13.5 linux/amd64

Does this issue reproduce with the latest release?

I am currently using the latest release afaik.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/nicolascouvrat/.cache/go-build"
GOENV="/home/nicolascouvrat/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/nicolascouvrat/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/nicolascouvrat/go/src/github.sie.com/SIE-Private/navscan/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build112066667=/tmp/go-build -gno-record-gcc-switches"
GOROOT/bin/go version: go version go1.13.5 linux/amd64
GOROOT/bin/go tool compile -V: compile version go1.13.5
uname -sr: Linux 5.0.0-37-generic
Distributor ID: Ubuntu
Description:    Ubuntu 18.04.3 LTS
Release:    18.04
Codename:   bionic
/lib/x86_64-linux-gnu/libc.so.6: GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
gdb --version: GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git

What did you do?

When the bug happens, I was playing with httputil.DumpRequest, but I managed to reproduce it with a shorter example:

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    data, err := json.Marshal(map[string]string{"key": "value"})
    if err != nil {
        fmt.Println("err")
    }

    c := &http.Client{}

    req, err := http.NewRequest("POST", "https://postman-echo.com/post", bytes.NewBuffer(data))
    if err != nil {
        fmt.Println("err")
    }

    clone := req.Clone(context.TODO())
    buf := make([]byte, 15)
    n, err := clone.Body.Read(buf)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(n, string(buf))

    _, err = c.Do(req)
    if err != nil {
        fmt.Println("ERROR:", err)
    }
}

What did you expect to see?

I expected Clone to return a deep copy, such as I can do whatever i want with the clone body without affecting the original request.

The documentation states:

Clone returns a deep copy of r with its context changed to ctx

What did you see instead?

The original request body is drained, and the code errors with

ERROR: Post https://postman-echo.com/post: http: ContentLength=15 with Body length 0

I think it comes from here where a shallow copy is done.

I feel like the reason why I want Clone to indeed be deep is not mainstream enough to warrant modifying Clone (I want to save the request object as it is in a Debug struct to inspect it later), and the cost could be big if the body is. However, I think it would be a good idea to update the documentation? I am happy to do a PR in this case.

EDIT: Doing:

clone.Body, err = req.GetBody()
if err != nil {
    //
}

solves my initial problem, which was that httputil.DumpRequestOut() essentially destroyed the Body of the argument. Maybe that function should be updated to use GetBody() instead? I would be happy to make a second issue and PR for that one too.

I still think the documentation problem for Clone stands though.

Comment From: nicolascouvrat

What I did to solve it is use the same approach as in httputil.DumpRequest:

// in Clone
*r2 = *r

var b bytes.Buffer
b.ReadFrom(r.Body)
r.Body = ioutil.NopCloser(&b)
r2.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes()))

To reiterate, I'm happy to submit a PR, whether it be documentation or code, but I would like to know which one first to avoid spending time on something not wanted! Let me know.

Comment From: odeke-em

Thank you for reporting this issue @nicolascouvrat and welcome to the Go project!

So, given that Request.Body is an io.ReadCloser, there aren't guarantees for being clonable or rewindable. For the longest time in Go, we didn't even have GetBody (introduced in Go1.8) and cloning a Request.Body was implicitly impossible.

What I think that we can do here is update the docs for Clone to indicate that Request.Body will not be cloned, but that GetBody can be used for this purpose.

Comment From: gopherbot

Change https://golang.org/cl/212408 mentions this issue: net/http: document non-clonability of Body and Response

Comment From: nicolascouvrat

Hi @odeke-em and thanks for the insight. I'll change the docs in the upcoming few days when I'll have time, I guess it will make for a good introduction/first PR for me :)

So, given that Request.Body is an io.ReadCloser, there aren't guarantees for being clonable or rewindable.

Could you elaborate a little? Sorry if this is obvious, but I am a little confused. Is cloning an io.ReadCloser not what we are doing in net.httputil?

From what I get of your comment (which makes a lot of sense, I've had a feeling that the httputil implementation of the cloning is clumsy), does that mean that the drainBody above should use GetBody() instead?

Thanks!

Comment From: talbspx

@nicolascouvrat i believe you are correct and the offending line should be - https://github.com/golang/go/blob/master/src/net/http/httputil/dump.go#L38

i think the solution is to return this instead return ioutil.NopCloser(bytes.NewReader(buf.Bytes())), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil

Comment From: J7mbo

What I did to solve it is use the same approach as in httputil.DumpRequest:

``` // in Clone r2 = r

var b bytes.Buffer b.ReadFrom(r.Body) r.Body = ioutil.NopCloser(&b) r2.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes())) ```

To reiterate, I'm happy to submit a PR, whether it be documentation or code, but I would like to know which one first to avoid spending time on something not wanted! Let me know.

Hi @nicolascouvrat. I am also trying to replace the context in a request. I tried your code in a middleware but it doesn't seem to copy the body:

        if body, err := ioutil.ReadAll(r.Body); err == nil {
        fmt.Println("BODY BEFORE: " + string(body)) // THIS PRINTS SOME JSON { .., }
    }

    r2 := r.Clone(ctx)
    *r2 = *r

    var b bytes.Buffer
    b.ReadFrom(r.Body)
    r.Body = ioutil.NopCloser(&b)
    r2.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes()))

    if body, err := ioutil.ReadAll(r.Body); err == nil {
        fmt.Println("AFTER: r2 BODY: " + string(body)) // DOES NOT PRINT ANY JSON
    }

    if body, err := ioutil.ReadAll(r2.Body); err == nil {
        fmt.Println("AFTER: r BODY: " + string(body)) // DOES NOT PRINT ANY JSON
    }

What am I missing?

Comment From: rhcarvalho

@J7mbo looks like your debugging code is the culprit. After you read the body the first time to print it, the second read reads nothing (all input was already consumed).

b.ReadFrom(r.Body)

^^ the body is empty after it has been read by ioutil.ReadAll.

Comment From: J7mbo

@rhcarvalho Thanks for finding that! That's a good few hours saved I reckon :) Sorry to hijack the thread!

Comment From: chennqqi

What I did to solve it is use the same approach as in httputil.DumpRequest:

``` // in Clone r2 = r

var b bytes.Buffer b.ReadFrom(r.Body) r.Body = ioutil.NopCloser(&b) r2.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes())) ```

To reiterate, I'm happy to submit a PR, whether it be documentation or code, but I would like to know which one first to avoid spending time on something not wanted! Let me know.

you forgot r.Body.Close()?

Comment From: 3052

just ran into this. I agree that either Clone should clone the body, or it should be documented that it does not clone the body. as others said workarounds are GetBody or DumpRequest

Comment From: arvenil

The Request can be not only client request, but also server request. In case of the later the GetBody() is not set. IMHO r.Clone() could also set the GetBody if it's not already set, same as NewRequest does?

Comment From: gopherbot

Change https://go.dev/cl/593175 mentions this issue: net/http: document that Request.Clone does not deep copy Body