Skip to content
Snippets Groups Projects
Unverified Commit 3761c4ae authored by Will McCutchen's avatar Will McCutchen Committed by GitHub
Browse files

Add tests for correct handling of Expect: 100-continue (#138)

Issue #129 prompted me to look into how go-httpbin actually handles
incoming requests that specify an `Expect: 100-continue` header. Here we
add additional unit tests to explicitly codify the, uh, _expected_
behavior for those requests.

Note that the Go stdlib http client and server implementations have
automagical `Expect: 100-continue` handling, which means
these particular tests need to be written at a slightly lower level, by
directly writing to and reading from the underlying TCP connection.

Now, it may well be that the the moving parts (e.g. load balancers)
between a client and the instances of go-httpbin deployed at
https://httpbingo.org intervene and change this behavior, but these
tests give me some confidence that, at least when such requests are made
directly to go-httpbin itself, we are correctly handling them.
parent c9f4177d
No related branches found
No related tags found
No related merge requests found
......@@ -451,6 +451,7 @@ func testRequestWithBody(t *testing.T, verb, path string) {
testRequestWithBodyBinaryBody,
testRequestWithBodyBodyTooBig,
testRequestWithBodyEmptyBody,
testRequestWithBodyExpect100Continue,
testRequestWithBodyFormEncodedBody,
testRequestWithBodyFormEncodedBodyNoContentType,
testRequestWithBodyHTML,
......@@ -570,6 +571,172 @@ func testRequestWithBodyHTML(t *testing.T, verb, path string) {
assert.BodyContains(t, resp, data)
}
func testRequestWithBodyExpect100Continue(t *testing.T, verb, path string) {
// The stdlib http client automagically handles 100 Continue responses
// by continuing the request until a "final" 200 OK response is
// received, which prevents us from confirming that a 100 Continue
// response is sent when using the http client directly.
//
// So, here we instead manally write the request to the wire in two
// steps, confirming that we receive a 100 Continue response before
// sending the body and getting the normal expected response.
t.Run("non-zero content-length okay", func(t *testing.T) {
t.Parallel()
conn, err := net.Dial("tcp", srv.Listener.Addr().String())
assert.NilError(t, err)
defer conn.Close()
body := []byte("test body")
req := newTestRequestWithBody(t, verb, path, bytes.NewReader(body))
req.Header.Set("Expect", "100-continue")
req.Header.Set("Content-Type", "text/plain")
reqBytes, _ := httputil.DumpRequestOut(req, false)
t.Logf("raw request:\n%q", reqBytes)
if !strings.Contains(string(reqBytes), "Content-Length: 9") {
t.Fatalf("expected request to contain Content-Length header")
}
// first, we write the request line and headers -- but NOT the body --
// which should cause the server to respond with a 100 Continue
// response.
{
n, err := conn.Write(reqBytes)
assert.NilError(t, err)
assert.Equal(t, n, len(reqBytes), "incorrect number of bytes written")
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
assert.NilError(t, err)
assert.StatusCode(t, resp, http.StatusContinue)
}
// Once we've gotten the 100 Continue response, we write the body. After
// that, we should get a normal 200 OK response along with the expected
// result.
{
n, err := conn.Write(body)
assert.NilError(t, err)
assert.Equal(t, n, len(body), "incorrect number of bytes written")
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
assert.NilError(t, err)
assert.StatusCode(t, resp, http.StatusOK)
got := must.Unmarshal[bodyResponse](t, resp.Body)
assert.Equal(t, got.Data, string(body), "incorrect body")
}
})
t.Run("transfer-encoding:chunked okay", func(t *testing.T) {
t.Parallel()
conn, err := net.Dial("tcp", srv.Listener.Addr().String())
assert.NilError(t, err)
defer conn.Close()
body := []byte("test body")
reqParts := []string{
fmt.Sprintf("%s %s HTTP/1.1", verb, path),
"Host: test",
"Content-Type: text/plain",
"Expect: 100-continue",
"Transfer-Encoding: chunked",
}
reqBytes := []byte(strings.Join(reqParts, "\r\n") + "\r\n\r\n")
t.Logf("raw request:\n%q", reqBytes)
// first, we write the request line and headers -- but NOT the body --
// which should cause the server to respond with a 100 Continue
// response.
{
n, err := conn.Write(reqBytes)
assert.NilError(t, err)
assert.Equal(t, n, len(reqBytes), "incorrect number of bytes written")
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
assert.NilError(t, err)
assert.StatusCode(t, resp, http.StatusContinue)
}
// Once we've gotten the 100 Continue response, we write the body. After
// that, we should get a normal 200 OK response along with the expected
// result.
{
// write chunk size
_, err := conn.Write([]byte("9\r\n"))
assert.NilError(t, err)
// write chunk data
n, err := conn.Write(append(body, "\r\n"...))
assert.NilError(t, err)
assert.Equal(t, n, len(body)+2, "incorrect number of bytes written")
// write empty terminating chunk
_, err = conn.Write([]byte("0\r\n\r\n"))
assert.NilError(t, err)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
assert.NilError(t, err)
assert.StatusCode(t, resp, http.StatusOK)
got := must.Unmarshal[bodyResponse](t, resp.Body)
assert.Equal(t, got.Data, string(body), "incorrect body")
}
})
t.Run("zero content-length ignored", func(t *testing.T) {
// The Go stdlib's Expect:100-continue handling requires either a a)
// non-zero Content-Length header or b) Transfer-Encoding:chunked
// header to be present. Otherwise, the Expect header is ignored and
// the request is processed normally.
t.Parallel()
conn, err := net.Dial("tcp", srv.Listener.Addr().String())
assert.NilError(t, err)
defer conn.Close()
req := newTestRequest(t, verb, path)
req.Header.Set("Expect", "100-continue")
reqBytes, _ := httputil.DumpRequestOut(req, false)
t.Logf("raw request:\n%q", reqBytes)
// For GET and DELETE requests, it appears the Go stdlib does not
// include a Content-Length:0 header, so we ensure that the header is
// either missing or has a value of 0.
switch verb {
case "GET", "DELETE":
if strings.Contains(string(reqBytes), "Content-Length:") {
t.Fatalf("expected no Content-Length header for %s request", verb)
}
default:
if !strings.Contains(string(reqBytes), "Content-Length: 0") {
t.Fatalf("expected Content-Length:0 header for %s request", verb)
}
}
n, err := conn.Write(reqBytes)
assert.NilError(t, err)
assert.Equal(t, n, len(reqBytes), "incorrect number of bytes written")
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
assert.NilError(t, err)
// Note: the server should NOT send a 100 Continue response here,
// because we send a request without a Content-Length header or with a
// Content-Length: 0 header.
assert.StatusCode(t, resp, http.StatusOK)
got := must.Unmarshal[bodyResponse](t, resp.Body)
assert.Equal(t, got.Data, "", "incorrect body")
})
}
func testRequestWithBodyFormEncodedBodyNoContentType(t *testing.T, verb, path string) {
params := url.Values{}
params.Set("foo", "foo")
......@@ -1745,9 +1912,9 @@ func TestDrip(t *testing.T) {
assert.NilError(t, err)
defer conn.Close()
n, err := conn.Write(append(reqBytes, []byte("\r\n\r\n")...))
n, err := conn.Write(reqBytes)
assert.NilError(t, err)
assert.Equal(t, n, len(reqBytes)+4, "incorrect number of bytes written")
assert.Equal(t, n, len(reqBytes), "incorrect number of bytes written")
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
assert.NilError(t, err)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment