mirror of
https://github.com/golang/net.git
synced 2026-03-31 02:17:08 +09:00
Our HTTP/1 and HTTP/2 server implementations will panic when WriteHeader is called from a handler with a status code that is not 3 digits. For consistency, do this too for HTTP/3. For golang/go#70914 Change-Id: I65aca23ea186481ae06c7388db487e476a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/759300 Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1013 lines
30 KiB
Go
1013 lines
30 KiB
Go
// Copyright 2024 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package http3
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"golang.org/x/net/internal/quic/quicwire"
|
|
"golang.org/x/net/quic"
|
|
)
|
|
|
|
// requestHeader is a helper function to make sure that all required
|
|
// pseudo-headers exist in an http.Header used for a request. Per
|
|
// https://www.rfc-editor.org/rfc/rfc9114.html#name-request-pseudo-header-field:
|
|
// "All HTTP/3 requests MUST include exactly one value for the :method,
|
|
// :scheme, and :path pseudo-header fields, unless the request is a CONNECT
|
|
// request;"
|
|
func requestHeader(h http.Header) http.Header {
|
|
minimalHeader := http.Header{
|
|
":method": {"GET"},
|
|
":scheme": {"https"},
|
|
":path": {"/"},
|
|
}
|
|
maps.Copy(minimalHeader, h)
|
|
return minimalHeader
|
|
}
|
|
|
|
func TestServerReceivePushStream(t *testing.T) {
|
|
// "[...] if a server receives a client-initiated push stream,
|
|
// this MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR."
|
|
// https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.2-3
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, nil)
|
|
tc := ts.connect()
|
|
tc.newStream(streamTypePush)
|
|
tc.wantClosed("invalid client-created push stream", errH3StreamCreationError)
|
|
})
|
|
}
|
|
|
|
func TestServerCancelPushForUnsentPromise(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, nil)
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
const pushID = 100
|
|
tc.control.writeVarint(int64(frameTypeCancelPush))
|
|
tc.control.writeVarint(int64(quicwire.SizeVarint(pushID)))
|
|
tc.control.writeVarint(pushID)
|
|
tc.control.Flush()
|
|
|
|
tc.wantClosed("client canceled never-sent push ID", errH3IDError)
|
|
})
|
|
}
|
|
|
|
func TestServerHeader(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
header := w.Header()
|
|
for key, values := range r.Header {
|
|
for _, value := range values {
|
|
header.Add(key, value)
|
|
}
|
|
}
|
|
w.WriteHeader(204)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"header-from-client": {"that", "should", "be", "echoed"},
|
|
}))
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"204"},
|
|
"Header-From-Client": {"that", "should", "be", "echoed"},
|
|
})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerPseudoHeader(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Pseudo-headers from client request should populate a specific
|
|
// field in http.Request, and should not be part of http.Request.Header.
|
|
if len(r.Header) != 0 {
|
|
t.Errorf("got %v, want request header to be empty", r.Header)
|
|
}
|
|
if r.Method != "GET" {
|
|
t.Errorf("got %v, want GET method", r.Method)
|
|
}
|
|
if r.Host != "fake.tld:1234" {
|
|
t.Errorf("got %v, want fake.tld:1234", r.Host)
|
|
}
|
|
wantURL := &url.URL{
|
|
Path: "/some/path",
|
|
RawQuery: "query=value&query2=value2#fragment",
|
|
}
|
|
if !reflect.DeepEqual(r.URL, wantURL) {
|
|
t.Errorf("got %v, want URL to be %v", r.URL, wantURL)
|
|
}
|
|
|
|
// Conversely, server should not be able to set pseudo-headers by
|
|
// writing to the ResponseWriter's Header.
|
|
header := w.Header()
|
|
header.Add(":status", "123")
|
|
w.WriteHeader(321)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(http.Header{
|
|
":method": {"GET"},
|
|
":authority": {"fake.tld:1234"},
|
|
":scheme": {"https"},
|
|
":path": {"/some/path?query=value&query2=value2#fragment"},
|
|
})
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"321"}})
|
|
reqStream.wantClosed("request is complete")
|
|
|
|
reqStream = tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(http.Header{}) // Missing pseudo-header.
|
|
reqStream.wantError(quic.StreamErrorCode(errH3MessageError))
|
|
})
|
|
}
|
|
|
|
func TestServerInvalidHeader(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Add("valid-name", "valid value")
|
|
// Invalid headers are skipped.
|
|
w.Header().Add("invalid name with spaces", "some value")
|
|
w.Header().Add("some-name", "invalid value with \n")
|
|
w.Header().Add("valid-name-2", "valid value 2")
|
|
w.WriteHeader(200)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"200"},
|
|
"Valid-Name": {"valid value"},
|
|
"Valid-Name-2": {"valid value 2"},
|
|
})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerInvalidStatus(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
gotpanic := make(chan bool)
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
defer close(gotpanic)
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
got := fmt.Sprintf("%T, %v", e, e)
|
|
want := "string, invalid WriteHeader code 0"
|
|
if got != want {
|
|
t.Errorf("unexpected panic value:\n got: %v\nwant: %v\n", got, want)
|
|
}
|
|
gotpanic <- true
|
|
// Set an explicit 503. This also tests that the
|
|
// WriteHeader call panics before it recorded that an
|
|
// explicit value was set.
|
|
w.WriteHeader(503)
|
|
|
|
// Verify that writing invalid status will not panic if a
|
|
// status is already set anyways.
|
|
w.WriteHeader(0)
|
|
}
|
|
}()
|
|
w.WriteHeader(0) // Invalid. Will panic.
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
if !<-gotpanic {
|
|
t.Error("expected panic in handler")
|
|
}
|
|
synctest.Wait()
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"503"},
|
|
})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerBody(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write([]byte(r.URL.Path)) // Implicitly calls w.WriteHeader(200).
|
|
w.Write(body)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
bodyContent := []byte("some body content that should be echoed")
|
|
reqStream.writeData(bodyContent)
|
|
reqStream.stream.stream.CloseWrite()
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
// Small multiple calls to Write will be coalesced into one DATA frame.
|
|
reqStream.wantData(append([]byte("/"), bodyContent...))
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHeadResponseNoBody(t *testing.T) {
|
|
bodyContent := []byte("response body that will not be sent for HEAD requests")
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write(bodyContent)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantData(bodyContent)
|
|
reqStream.wantClosed("request is complete")
|
|
|
|
reqStream = tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{":method": {http.MethodHead}}))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerEmpty(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Empty handler should return a 200 OK
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerFlushing(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(time.Second)
|
|
w.Write([]byte("first"))
|
|
|
|
time.Sleep(time.Second)
|
|
w.Write([]byte("second"))
|
|
w.(http.Flusher).Flush()
|
|
|
|
time.Sleep(time.Second)
|
|
w.Write([]byte("third"))
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
respBody := make([]byte, 100)
|
|
|
|
time.Sleep(time.Second)
|
|
synctest.Wait()
|
|
if n, err := reqStream.Read(respBody); err == nil {
|
|
t.Errorf("got %v bytes read, want no message yet", n)
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
synctest.Wait()
|
|
if _, err := reqStream.Read(respBody); err != nil {
|
|
t.Errorf("failed to read partial response from server, got err: %v", err)
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
synctest.Wait()
|
|
if _, err := reqStream.Read(respBody); err != io.EOF {
|
|
t.Errorf("got err %v, want EOF", err)
|
|
}
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerStreaming(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
stream := make(chan string)
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Flushing when we have not written anything yet implicitly calls
|
|
// w.WriteHeader(200).
|
|
w.(http.Flusher).Flush()
|
|
for str := range stream {
|
|
w.Write([]byte(str))
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
|
|
for _, data := range []string{"a", "bunch", "of", "things", "to", "stream"} {
|
|
stream <- data
|
|
reqStream.wantData([]byte(data))
|
|
}
|
|
close(stream)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerTrimsContentBody(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
declaredContentLen int
|
|
declaredInvalidContentLen bool
|
|
actualContentLen int
|
|
wantTrimmed bool
|
|
}{
|
|
{
|
|
name: "declared accurate content length",
|
|
declaredContentLen: 100,
|
|
actualContentLen: 100,
|
|
},
|
|
{
|
|
name: "declared larger content length",
|
|
declaredContentLen: 100,
|
|
actualContentLen: 10,
|
|
},
|
|
{
|
|
name: "declared smaller content length",
|
|
declaredContentLen: 10,
|
|
actualContentLen: 100,
|
|
wantTrimmed: true,
|
|
},
|
|
{
|
|
name: "declared invalid content length",
|
|
declaredInvalidContentLen: true,
|
|
actualContentLen: 100,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
wantWrittenLen := min(tt.actualContentLen, tt.declaredContentLen)
|
|
if tt.declaredInvalidContentLen {
|
|
wantWrittenLen = tt.actualContentLen
|
|
}
|
|
synctestSubtest(t, tt.name, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Length", strconv.Itoa(tt.declaredContentLen))
|
|
if tt.declaredInvalidContentLen {
|
|
w.Header().Set("Content-Length", "not a number, should be ignored")
|
|
}
|
|
var written int
|
|
var lastErr error
|
|
for range tt.actualContentLen {
|
|
n, err := w.Write([]byte("a"))
|
|
written += n
|
|
lastErr = err
|
|
}
|
|
if tt.wantTrimmed != (lastErr != nil) {
|
|
t.Errorf("got %v error when writing response body, even though wantTrimmed is %v", lastErr, tt.wantTrimmed)
|
|
}
|
|
if written != wantWrittenLen {
|
|
t.Errorf("got %v bytes written by the server, want %v bytes", written, wantWrittenLen)
|
|
}
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantHeaders(nil)
|
|
reqStream.wantData(slices.Repeat([]byte("a"), wantWrittenLen))
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerExpect100Continue(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
streamIdle := make(chan bool)
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Expect: 100-continue header should not be accessible from the
|
|
// server handler.
|
|
if len(r.Header) > 0 {
|
|
t.Errorf("got %v, want request header to be empty", r.Header)
|
|
}
|
|
// Reading the body will cause the server to call w.WriteHeader(100).
|
|
<-streamIdle
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Implicitly calls w.WriteHeader(200) since non-1XX status code
|
|
// has been sent yet so far.
|
|
w.Write(body)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
// Client sends an Expect: 100-continue request.
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Expect": {"100-continue"},
|
|
}))
|
|
|
|
reqStream.wantIdle("stream is idle until server sends an HTTP 100 status")
|
|
streamIdle <- true
|
|
// Wait until server responds with HTTP status 100 before sending the
|
|
// body.
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"100"}})
|
|
body := []byte("body that will be echoed back if we get status 100")
|
|
reqStream.writeData(body)
|
|
reqStream.stream.stream.CloseWrite()
|
|
|
|
// Receive the server's response after sending the body.
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantData(body)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerExpect100ContinueRejected(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
rejectBody := []byte("not allowed")
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(403)
|
|
w.Write(rejectBody)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
// Client sends an Expect: 100-continue request.
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Expect": {"100-continue"},
|
|
}))
|
|
|
|
// Server rejects it.
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"403"}})
|
|
reqStream.wantData(rejectBody)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerNoExpect100ContinueAfterNormalResponse(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
w.(http.Flusher).Flush()
|
|
// This should not cause an HTTP 100 status to be sent since we
|
|
// have sent an HTTP 200 response already.
|
|
io.ReadAll(r.Body)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
// Client sends an Expect: 100-continue request.
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Expect": {"100-continue"},
|
|
}))
|
|
// Client sends a body prematurely. This should not happen, unless a
|
|
// client misbehaves. We do so here anyways so the server handler can
|
|
// read the request body without hanging, which would normally cause an
|
|
// HTTP 100 to be sent.
|
|
reqStream.writeData([]byte("some body"))
|
|
reqStream.stream.stream.CloseWrite()
|
|
|
|
// Verify that no HTTP 100 was sent.
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerReadReqWithNoBody(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
serverBody := []byte("hello from server!")
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := io.ReadAll(r.Body); err != nil {
|
|
t.Errorf("got %v err when reading from an empty request body, want nil", err)
|
|
}
|
|
w.Write(serverBody)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
// Case 1: we know that there is no body / DATA frame because the
|
|
// client closes the write direction of the stream.
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.stream.stream.CloseWrite()
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantData(serverBody)
|
|
reqStream.wantClosed("request is complete")
|
|
|
|
// Case 2: we know that there is no body / DATA frame because the
|
|
// client indicates a Content-Length of 0.
|
|
reqStream = tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Content-Length": {"0"},
|
|
}))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"200"}})
|
|
reqStream.wantData(serverBody)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerReadTrailer(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
body := []byte("some body")
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
wantTrailer := http.Header{
|
|
"Client-Trailer-A": nil,
|
|
"Client-Trailer-B": nil,
|
|
}
|
|
if !reflect.DeepEqual(r.Trailer, wantTrailer) {
|
|
t.Errorf("got %v; want trailer to be %v before reading the body", r.Trailer, wantTrailer)
|
|
}
|
|
if _, err := io.ReadAll(r.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantTrailer = http.Header{
|
|
"Client-Trailer-A": {"valuea"},
|
|
"Client-Trailer-B": {"valueb"},
|
|
}
|
|
if !reflect.DeepEqual(r.Trailer, wantTrailer) {
|
|
t.Errorf("got %v; want trailer to be %v after reading the body", r.Trailer, wantTrailer)
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Trailer": {"Client-Trailer-A, Client-Trailer-B"},
|
|
}))
|
|
reqStream.writeData(body)
|
|
reqStream.writeHeaders(http.Header{
|
|
"Client-Trailer-A": {"valuea"},
|
|
"Client-Trailer-B": {"valueb"},
|
|
"Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored.
|
|
})
|
|
reqStream.wantHeaders(nil)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerReadTrailerNoBody(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
wantTrailer := http.Header{
|
|
"Client-Trailer-A": nil,
|
|
"Client-Trailer-B": nil,
|
|
}
|
|
if !reflect.DeepEqual(r.Trailer, wantTrailer) {
|
|
t.Errorf("got %v; want trailer to be %v before reading the body", r.Trailer, wantTrailer)
|
|
}
|
|
if _, err := io.ReadAll(r.Body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantTrailer = http.Header{
|
|
"Client-Trailer-A": {"valuea"},
|
|
"Client-Trailer-B": {"valueb"},
|
|
}
|
|
if !reflect.DeepEqual(r.Trailer, wantTrailer) {
|
|
t.Errorf("got %v; want trailer to be %v after reading the body", r.Trailer, wantTrailer)
|
|
}
|
|
w.WriteHeader(200)
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Trailer": {"Client-Trailer-A, Client-Trailer-B"},
|
|
"Content-Length": {"0"},
|
|
}))
|
|
reqStream.writeHeaders(http.Header{
|
|
"Client-Trailer-A": {"valuea"},
|
|
"Client-Trailer-B": {"valueb"},
|
|
"Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored.
|
|
})
|
|
reqStream.wantHeaders(nil)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerWriteTrailer(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
body := []byte("some body")
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Trailer", "server-trailer-a, server-trailer-b") // Trailer header will be canonicalized.
|
|
w.Header().Add("Trailer", "Server-Trailer-C")
|
|
|
|
w.Write(body)
|
|
|
|
w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized.
|
|
w.Header().Set("Server-Trailer-C", "valuec") // skipping B
|
|
w.Header().Set("Server-Trailer-Not-Declared", "should be omitted")
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"200"},
|
|
"Trailer": {"Server-Trailer-A, Server-Trailer-B, Server-Trailer-C"},
|
|
})
|
|
reqStream.wantData(body)
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
"Server-Trailer-A": {"valuea"},
|
|
"Server-Trailer-C": {"valuec"},
|
|
})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerHandlerWriteTrailerNoBody(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Trailer", "server-trailer-a, server-trailer-b") // Trailer header will be canonicalized.
|
|
w.Header().Add("Trailer", "Server-Trailer-C")
|
|
|
|
w.(http.Flusher).Flush()
|
|
|
|
w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized.
|
|
w.Header().Set("Server-Trailer-C", "valuec") // skipping B
|
|
w.Header().Set("Server-Trailer-Not-Declared", "should be omitted")
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"200"},
|
|
"Trailer": {"Server-Trailer-A, Server-Trailer-B, Server-Trailer-C"},
|
|
})
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
"Server-Trailer-A": {"valuea"},
|
|
"Server-Trailer-C": {"valuec"},
|
|
})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServerInfersHeaders(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
flushedEarly bool
|
|
responseStatus int
|
|
does100Continue bool
|
|
declaredHeader http.Header
|
|
want http.Header
|
|
}{
|
|
{
|
|
name: "infers undeclared headers",
|
|
responseStatus: 200,
|
|
declaredHeader: http.Header{
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"Sat, 01 Jan 2000 00:00:00 GMT"}, // Synctest starting time.
|
|
"Content-Type": {"text/html; charset=utf-8"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
{
|
|
name: "does not write over declared header",
|
|
responseStatus: 200,
|
|
declaredHeader: http.Header{
|
|
"Date": {"some date"},
|
|
"Content-Type": {"some content type"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"some date"},
|
|
"Content-Type": {"some content type"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
{
|
|
name: "does not infer content type for response with no body",
|
|
responseStatus: 304, // 304 status response has no body.
|
|
declaredHeader: http.Header{
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"Sat, 01 Jan 2000 00:00:00 GMT"}, // Synctest starting time.
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
{
|
|
// See golang.org/issue/31753.
|
|
name: "does not infer content type for response with declared content encoding",
|
|
responseStatus: 200,
|
|
declaredHeader: http.Header{
|
|
"Content-Encoding": {"some encoding"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"Sat, 01 Jan 2000 00:00:00 GMT"}, // Synctest starting time.
|
|
"Content-Encoding": {"some encoding"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
{
|
|
name: "does not infer content type when header is flushed before body is written",
|
|
responseStatus: 200,
|
|
flushedEarly: true,
|
|
declaredHeader: http.Header{
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"Sat, 01 Jan 2000 00:00:00 GMT"}, // Synctest starting time.
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
{
|
|
name: "infers header for the header that comes after 100 continue",
|
|
responseStatus: 200,
|
|
does100Continue: true,
|
|
declaredHeader: http.Header{
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
want: http.Header{
|
|
"Date": {"Sat, 01 Jan 2000 00:00:00 GMT"}, // Synctest starting time.
|
|
"Content-Type": {"text/html; charset=utf-8"},
|
|
"Some-Other-Header": {"some value"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
synctestSubtest(t, tt.name, func(t *testing.T) {
|
|
body := []byte("<html>some html content</html>")
|
|
streamIdle := make(chan bool)
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if tt.does100Continue {
|
|
<-streamIdle
|
|
io.ReadAll(r.Body)
|
|
}
|
|
for name, values := range tt.declaredHeader {
|
|
for _, value := range values {
|
|
w.Header().Add(name, value)
|
|
}
|
|
}
|
|
w.WriteHeader(tt.responseStatus)
|
|
if tt.flushedEarly {
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
// Write the body one byte at a time. To confirm that body
|
|
// writes are buffered and that Content-Type will not be
|
|
// wrongly identified as text/plain rather than text/html.
|
|
for _, b := range body {
|
|
w.Write([]byte{b})
|
|
}
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
|
|
if tt.does100Continue {
|
|
reqStream.writeHeaders(requestHeader(http.Header{
|
|
"Expect": {"100-continue"},
|
|
}))
|
|
reqStream.wantIdle("stream is idle until server sends an HTTP 100 status")
|
|
streamIdle <- true
|
|
reqStream.wantHeaders(http.Header{":status": {"100"}})
|
|
}
|
|
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
tt.want.Add(":status", strconv.Itoa(tt.responseStatus))
|
|
reqStream.wantHeaders(tt.want)
|
|
if responseCanHaveBody(tt.responseStatus) {
|
|
reqStream.wantData(body)
|
|
}
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerBuffersBodyWrite(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bodyLen int
|
|
writeSize int
|
|
flushes bool
|
|
}{
|
|
{
|
|
name: "buffers small body content",
|
|
bodyLen: defaultBodyBufferCap * 10,
|
|
writeSize: 5,
|
|
flushes: false,
|
|
},
|
|
{
|
|
name: "does not buffer large body content",
|
|
bodyLen: defaultBodyBufferCap * 10,
|
|
writeSize: defaultBodyBufferCap * 2,
|
|
flushes: false,
|
|
},
|
|
{
|
|
name: "does not buffer flushed body content",
|
|
bodyLen: defaultBodyBufferCap * 10,
|
|
writeSize: 10,
|
|
flushes: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
synctestSubtest(t, tt.name, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
for n := 0; n < tt.bodyLen; n += tt.writeSize {
|
|
data := slices.Repeat([]byte("a"), min(tt.writeSize, tt.bodyLen-n))
|
|
n, err := w.Write(data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != len(data) {
|
|
t.Errorf("got %v bytes when writing in server handler, want %v", n, len(data))
|
|
}
|
|
if tt.flushes {
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantHeaders(nil)
|
|
switch {
|
|
case tt.writeSize > defaultBodyBufferCap:
|
|
// After using the buffer once, it is no longer used since the
|
|
// writeSize is larger than the buffer.
|
|
for n := 0; n < tt.bodyLen; n += tt.writeSize {
|
|
reqStream.wantData(slices.Repeat([]byte("a"), min(tt.writeSize, tt.bodyLen-n)))
|
|
}
|
|
case tt.flushes:
|
|
for n := 0; n < tt.bodyLen; n += tt.writeSize {
|
|
reqStream.wantData(slices.Repeat([]byte("a"), min(tt.writeSize, tt.bodyLen-n)))
|
|
}
|
|
case tt.writeSize <= defaultBodyBufferCap:
|
|
dataLen := defaultBodyBufferCap + tt.writeSize - (defaultBodyBufferCap % tt.writeSize)
|
|
for n := 0; n < tt.bodyLen; n += dataLen {
|
|
reqStream.wantData(slices.Repeat([]byte("a"), min(dataLen, tt.bodyLen-n)))
|
|
}
|
|
}
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer103EarlyHints(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
body := []byte("some body")
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := w.Header()
|
|
|
|
h.Add("Content-Length", "123") // Must be ignored
|
|
h.Add("Link", "</style.css>; rel=preload; as=style")
|
|
h.Add("Link", "</script.js>; rel=preload; as=script")
|
|
w.WriteHeader(http.StatusEarlyHints)
|
|
|
|
h.Add("Link", "</foo.js>; rel=preload; as=script")
|
|
w.WriteHeader(http.StatusEarlyHints)
|
|
|
|
w.Write(body) // Implicitly sends status 200.
|
|
w.WriteHeader(http.StatusEarlyHints) // Should be a no-op.
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantHeaders(http.Header{
|
|
":status": {"103"},
|
|
"Link": {
|
|
"</style.css>; rel=preload; as=style",
|
|
"</script.js>; rel=preload; as=script",
|
|
},
|
|
})
|
|
reqStream.wantHeaders(http.Header{
|
|
":status": {"103"},
|
|
"Link": {
|
|
"</style.css>; rel=preload; as=style",
|
|
"</script.js>; rel=preload; as=script",
|
|
"</foo.js>; rel=preload; as=script",
|
|
},
|
|
})
|
|
reqStream.wantSomeHeaders(http.Header{
|
|
":status": {"200"},
|
|
"Content-Length": {"123"},
|
|
})
|
|
reqStream.wantData(body)
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
func TestServer304NotModified(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
if _, err := w.Write([]byte("body should not be allowed")); !errors.Is(err, http.ErrBodyNotAllowed) {
|
|
t.Errorf("got %v error when calling Write after WriteHeader(304), want %v error", err, http.ErrBodyNotAllowed)
|
|
}
|
|
}))
|
|
tc := ts.connect()
|
|
tc.greet()
|
|
|
|
reqStream := tc.newStream(streamTypeRequest)
|
|
reqStream.writeHeaders(requestHeader(nil))
|
|
reqStream.wantSomeHeaders(http.Header{":status": {"304"}})
|
|
reqStream.wantClosed("request is complete")
|
|
})
|
|
}
|
|
|
|
type testServer struct {
|
|
t testing.TB
|
|
s *server
|
|
tn testNet
|
|
*testQUICEndpoint
|
|
|
|
addr netip.AddrPort
|
|
}
|
|
|
|
type testQUICEndpoint struct {
|
|
t testing.TB
|
|
e *quic.Endpoint
|
|
}
|
|
|
|
type testServerConn struct {
|
|
ts *testServer
|
|
|
|
*testQUICConn
|
|
control *testQUICStream
|
|
}
|
|
|
|
func newTestServer(t testing.TB, handler http.Handler) *testServer {
|
|
t.Helper()
|
|
ts := &testServer{
|
|
t: t,
|
|
s: &server{
|
|
config: &quic.Config{
|
|
TLSConfig: testTLSConfig,
|
|
},
|
|
handler: handler,
|
|
},
|
|
}
|
|
e := ts.tn.newQUICEndpoint(t, ts.s.config)
|
|
ts.addr = e.LocalAddr()
|
|
go ts.s.serve(e)
|
|
return ts
|
|
}
|
|
|
|
func (ts *testServer) connect() *testServerConn {
|
|
ts.t.Helper()
|
|
config := &quic.Config{TLSConfig: testTLSConfig}
|
|
e := ts.tn.newQUICEndpoint(ts.t, nil)
|
|
qconn, err := e.Dial(ts.t.Context(), "udp", ts.addr.String(), config)
|
|
if err != nil {
|
|
ts.t.Fatal(err)
|
|
}
|
|
tc := &testServerConn{
|
|
ts: ts,
|
|
testQUICConn: newTestQUICConn(ts.t, qconn),
|
|
}
|
|
synctest.Wait()
|
|
return tc
|
|
}
|
|
|
|
// greet performs initial connection handshaking with the server.
|
|
func (tc *testServerConn) greet() {
|
|
// Client creates a control stream.
|
|
tc.control = tc.newStream(streamTypeControl)
|
|
tc.control.writeVarint(int64(frameTypeSettings))
|
|
tc.control.writeVarint(0) // size
|
|
tc.control.Flush()
|
|
synctest.Wait()
|
|
}
|