-
Notifications
You must be signed in to change notification settings - Fork 18k
net/http/httptest: using Recorder.HeaderMap contains headers that were written after http.ResponseWriter.WriteHeader is called #37650
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
Comments
Workaround, use:
Having two ways for accessing the header feels very confusing to me. |
Change https://golang.org/cl/222117 mentions this issue: |
/cc @bradfitz |
Hello @elgohr, thank you for filing this issue! Could you please explain what the problem is and provide sample code instead of perhaps a link to a comment in a bug that was fixed? Did you mean net/http/httptest.ResponseRecorder or net/http/httptest.Server? I can't seem to reproduce this problem and also we have functioning tests that ensure that this doesn't happen. However, if you declared a Trailer before invoking w.WriteHeader, that trailer will be sent along, Given that you didn't mention where you are getting the problem but I can allude to the usage of net/http/httptest.ResponseRecorder, I shall provide you with 2 different scenarios, and please tweak the code to see if it reproduces your issue. 1. net/http/httptest.ResponseRecorderpackage main
import (
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Declare this Trailer that will be permissible after
// after w.WriteHeader is invoked.
w.Header().Set("Trailer", "AB")
w.WriteHeader(200)
w.Write([]byte("Aloha"))
// Throw in this stray header that was not declared as a Trailer.
w.Header().Set("Stray", "Bar")
w.Header().Set("AB", "CD")
}
func TestHeaderWriteInResponseRecorder(t *testing.T) {
req := httptest.NewRequest("GET", "https://example.com/", nil)
rec := httptest.NewRecorder()
handler(rec, req)
resp := rec.Result()
defer resp.Body.Close()
blob, _ := httputil.DumpResponse(resp, true)
want := "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nAloha"
if g, w := string(blob), want; g != w {
t.Fatalf("Response mismatch\nGot:\n%q\n\nWant:\n%q", g, w)
}
if g, w := resp.Trailer.Get("AB"), "CD"; g != w {
t.Fatalf("Trailer mismatch\nGot: %q\nWant: %q", g, w)
}
if g := resp.Header["Stray"]; g != nil {
t.Fatalf("Stray header unfortunately sent after w.WriteHeader(...), got: %#v", g)
}
} $ go test -v
=== RUN TestHeaderWriteInResponseRecorder
--- PASS: TestHeaderWriteInResponseRecorder (0.00s)
PASS
ok github.com/odeke-em/bugs/golang/37650 0.030s 2. net/http/httptest.ServerCould you perhaps please try tweaking this code to see if it reproduces your issue? package main
import (
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Declare this Trailer that will be permissible after
// after w.WriteHeader is invoked.
w.Header().Set("Trailer", "AB")
w.WriteHeader(200)
w.Write([]byte("Aloha"))
// Throw in this stray header that was not declared as a Trailer.
w.Header().Set("Stray", "Bar")
w.Header().Set("AB", "CD")
}
func TestHeaderWrite_h1(t *testing.T) {
testHeaderWrite(t, false)
}
func TestHeaderWrite_h2(t *testing.T) {
testHeaderWrite(t, true)
}
func testHeaderWrite(t *testing.T, isHTTP2 bool) {
cst := httptest.NewUnstartedServer(http.HandlerFunc(handler))
cst.EnableHTTP2 = isHTTP2
cst.StartTLS()
defer cst.Close()
req, _ := http.NewRequest("GET", cst.URL, nil)
resp, err := cst.Client().Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// firstly strip out ephemeral header: "Date"
delete(resp.Header, "Date")
blob, _ := httputil.DumpResponse(resp, true)
want := "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nTrailer: Ab\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n5\r\nAloha\r\n0\r\nAb: CD\r\n\r\n"
if isHTTP2 {
want = "HTTP/2.0 200 OK\r\nContent-Length: 5\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nAloha"
}
if g, w := string(blob), want; g != w {
t.Fatalf("Response mismatch\nGot:\n%q\n\nWant:\n%q", g, w)
}
if g, w := resp.Trailer.Get("AB"), "CD"; g != w {
t.Fatalf("Trailer mismatch\nGot: %q\nWant: %q", g, w)
}
if g := resp.Header["Stray"]; g != nil {
t.Fatalf("Stray header unfortunately sent after w.WriteHeader(...), got: %#v", g)
}
} and currently it passes with $ go test -v
=== RUN TestHeaderWrite_h1
--- PASS: TestHeaderWrite_h1 (0.00s)
=== RUN TestHeaderWrite_h2
--- PASS: TestHeaderWrite_h2 (0.00s)
PASS
ok github.com/odeke-em/bugs/golang/37650 0.019s
I don't follow what you mean, please elaborate. Thank you! |
Hey @odeke-em did you see the test in the PR? In this way you could reproduce and see the issue. |
@elgohr please use recorder.Result() to compare apples to apples otherwise you are using an attribute HeaderMap that was deprecated since Go1.11, and is meant to be an internal attribute As I demonstrated in the tests in #37650 (comment), if you use ResponseRecorder().Result() to retrieve the Response and then compare against that, that'll give you the correct results. |
So why is |
.Header() is a method that allows Recorder to satisfy the
http.ResponseWriter interface.
…On Sat, Mar 28, 2020 at 11:17 PM Lars ***@***.***> wrote:
So why is .Header() still exported and non-deprecated?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#37650 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABFL3VZSQCAN3DCRKZ5DPJTRJ3RYVANCNFSM4LBBEPOQ>
.
|
So this should probably be the default method for checking the "right" headers? Otherwise it's very confusing, as it's even returning the "wrong" headers. |
I wouldn't go with #38149 . |
The point of Recorder is to capture what traditionally be relayed on the
wire, so that you don’t have to spin up a server, and then you can see the
response which you get from .Result()
We show an example of how to correctly use Recorder() in
https://golang.org/pkg/net/http/httptest/#example_ResponseRecorder
…On Sun, Mar 29, 2020 at 12:44 AM Lars ***@***.***> wrote:
So this should probably be the default method for checking the "right"
headers? Otherwise it's very confusing, as it's even returning the "wrong"
headers.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#37650 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABFL3V66W64JB4R7M63TVLTRJ336PANCNFSM4LBBEPOQ>
.
|
Sorry, big thumb on mobile app closed this issue. So reopen |
If you want the Headers from a response you need to use
ResponseRecorder.Result().Header, it is basically the correct equivalent
of doing http.Client.Do(req) the way to retrieve a Response.
ResponseRecorder.Header() should not be used for comparing Response headers
unless you want undefined behaviour like you are seeing currently. Please
take a look at the example I posted.
…On Sun, Mar 29, 2020 at 12:53 AM Lars ***@***.***> wrote:
Reopened #37650 <#37650>.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#37650 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABFL3V3P75SHL353J45XAIDRJ346DANCNFSM4LBBEPOQ>
.
|
I see your point. I'm just saying that the current behavior is very unintuitive to me. A method name is like a promise. If I would have to look up the documentation for every promise, we could also call the method |
Please take a look at the docs of those methods. They literally say
With an advisory that you should use .Result().Header and so does the
example show.
Unfortunately, a misuse can’t be expected to change meaning, and regardless
of style, that method exists to satisfy a specific purpose so that we can
use ResponseRecorder.
…On Sun, Mar 29, 2020 at 1:05 AM Lars ***@***.***> wrote:
I see your point. I'm just saying that the current behavior is very
unintuitive to me. A method name is like a promise. If I would have to look
up the documentation for every promise, we could also call the method x, y
or z (see https://www.lysator.liu.se/c/pikestyle.html - "function names
should reflect what they return")
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#37650 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABFL3VYNQUFSRI6QUFEGMC3RJ36MHANCNFSM4LBBEPOQ>
.
|
So why can't In 2f33e25#diff-ee0abc83acc8d7e468b0773ae2dd9ea5R288 I implemented the test the other way around by accident. It reflects that fields, which will never be returned by the server (as set too late) are shown as set. This is simply wrong. |
.Headers() is a method that's intended for use inside the server aka http.Handler surface, and not in the client API surface. There isn't a way that you can use an item to satisfy an interface i.e. http.ResponseWriter without implementing its methods.
That is not using the right APIs, perhaps this list can be helpful: Using methods meant for the http.ResponseWriter is out of scope for when you expect to operate on an *http.Response. In the future, if you'd like to ask questions, please check out these resources at https://github.com/golang/go/wiki/Questions, or inlined below:
I am going to close this issue as it is working as intended and conflation of APIs. |
#14914 (comment)
Reproducible in go1.14.
The text was updated successfully, but these errors were encountered: