http3: process 1xx status codes (#4437)

* process http 1xx status code

Signed-off-by: mchtech <michu_an@126.com>

* add integration tests

Signed-off-by: mchtech <michu_an@126.com>

* fix tests

---------

Signed-off-by: mchtech <michu_an@126.com>
Co-authored-by: Marten Seemann <martenseemann@gmail.com>
This commit is contained in:
mchtech
2024-04-21 17:56:45 +08:00
committed by GitHub
parent 3e7ba77a77
commit 86b53a2516
3 changed files with 186 additions and 3 deletions

View File

@@ -7,6 +7,8 @@ import (
"io"
"log/slog"
"net/http"
"net/http/httptrace"
"net/textproto"
"sync"
"time"
@@ -313,9 +315,37 @@ func (c *SingleDestinationRoundTripper) doRequest(req *http.Request, str quic.St
}()
}
res, err := hstr.ReadResponse()
if err != nil {
return nil, err
var (
res *http.Response
err error
)
// copy from net/http: support 1xx responses
trace := httptrace.ContextClientTrace(req.Context())
num1xx := 0 // number of informational 1xx headers received
const max1xxResponses = 5 // arbitrary bound on number of informational responses
for {
if res, err = hstr.ReadResponse(); err != nil {
return nil, err
}
resCode := res.StatusCode
is1xx := 100 <= resCode && resCode <= 199
// treat 101 as a terminal status, see https://github.com/golang/go/issues/26161
is1xxNonTerminal := is1xx && resCode != http.StatusSwitchingProtocols
if is1xxNonTerminal {
num1xx++
if num1xx > max1xxResponses {
return nil, errors.New("http: too many 1xx informational responses")
}
if trace != nil && trace.Got1xxResponse != nil {
if err := trace.Got1xxResponse(resCode, textproto.MIMEHeader(res.Header)); err != nil {
return nil, err
}
}
continue
}
break
}
connState := c.Connection.ConnectionState().TLS
res.TLS = &connState

View File

@@ -7,6 +7,8 @@ import (
"errors"
"io"
"net/http"
"net/http/httptrace"
"net/textproto"
"sync"
"time"
@@ -26,6 +28,10 @@ func encodeResponse(status int) []byte {
rstr := mockquic.NewMockStream(mockCtrl)
rstr.EXPECT().Write(gomock.Any()).Do(buf.Write).AnyTimes()
rw := newResponseWriter(newStream(rstr, nil), nil, false, nil)
if status == http.StatusEarlyHints {
rw.header.Add("Link", "</style.css>; rel=preload; as=style")
rw.header.Add("Link", "</script.js>; rel=preload; as=script")
}
rw.WriteHeader(status)
rw.Flush()
return buf.Bytes()
@@ -858,5 +864,75 @@ var _ = Describe("Client", func() {
Expect(rsp.Header.Get("Content-Encoding")).To(BeEmpty())
})
})
Context("1xx status code", func() {
It("continues to read next header if code is 103", func() {
var (
cnt int
status int
hdr textproto.MIMEHeader
)
header1 := "</style.css>; rel=preload; as=style"
header2 := "</script.js>; rel=preload; as=script"
ctx := httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
cnt++
status = code
hdr = header
return nil
},
})
req := req.WithContext(ctx)
rspBuf := bytes.NewBuffer(encodeResponse(103))
gomock.InOrder(
conn.EXPECT().HandshakeComplete().Return(handshakeChan),
conn.EXPECT().OpenStreamSync(ctx).Return(str, nil),
conn.EXPECT().ConnectionState().Return(quic.ConnectionState{}),
)
str.EXPECT().Write(gomock.Any()).AnyTimes().DoAndReturn(func(p []byte) (int, error) { return len(p), nil })
str.EXPECT().Close()
str.EXPECT().Read(gomock.Any()).DoAndReturn(rspBuf.Read).AnyTimes()
rsp, err := cl.RoundTrip(req)
Expect(err).ToNot(HaveOccurred())
Expect(rsp.Proto).To(Equal("HTTP/3.0"))
Expect(rsp.ProtoMajor).To(Equal(3))
Expect(rsp.StatusCode).To(Equal(200))
Expect(rsp.Header).To(HaveKeyWithValue("Link", []string{header1, header2}))
Expect(status).To(Equal(103))
Expect(cnt).To(Equal(1))
Expect(hdr).To(HaveKeyWithValue("Link", []string{header1, header2}))
Expect(rsp.Request).ToNot(BeNil())
})
It("doesn't continue to read next header if code is a terminal status", func() {
cnt := 0
status := 0
ctx := httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
cnt++
status = code
return nil
},
})
req := req.WithContext(ctx)
rspBuf := bytes.NewBuffer(encodeResponse(101))
gomock.InOrder(
conn.EXPECT().HandshakeComplete().Return(handshakeChan),
conn.EXPECT().OpenStreamSync(ctx).Return(str, nil),
conn.EXPECT().ConnectionState().Return(quic.ConnectionState{}),
)
str.EXPECT().Write(gomock.Any()).AnyTimes().DoAndReturn(func(p []byte) (int, error) { return len(p), nil })
str.EXPECT().Close()
str.EXPECT().Read(gomock.Any()).DoAndReturn(rspBuf.Read).AnyTimes()
rsp, err := cl.RoundTrip(req)
Expect(err).ToNot(HaveOccurred())
Expect(rsp.Proto).To(Equal("HTTP/3.0"))
Expect(rsp.ProtoMajor).To(Equal(3))
Expect(rsp.StatusCode).To(Equal(101))
Expect(status).To(Equal(0))
Expect(cnt).To(Equal(0))
Expect(rsp.Request).ToNot(BeNil())
})
})
})
})