Added support for identity responses. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4 for details

This commit is contained in:
Aliaksandr Valialkin
2015-11-13 13:33:32 +02:00
parent 16632cbaa4
commit f1e8e6bf25
5 changed files with 97 additions and 45 deletions
+10 -5
View File
@@ -20,7 +20,9 @@ type ResponseHeader struct {
// Response content length read from Content-Length header.
//
// It may be negative on chunked response.
// It may be negative:
// -1 means Transfer-Encoding: chunked.
// -2 means Transfer-Encoding: identity.
ContentLength int
// Set to true if response contains 'Connection: close' header.
@@ -48,7 +50,8 @@ type RequestHeader struct {
// Request content length read from Content-Length header.
//
// It may be negative on chunked request.
// It may be negative:
// -1 means Transfer-Encoding: chunked.
ContentLength int
// Set to true if request contains 'Connection: close' header.
@@ -838,6 +841,7 @@ func (h *RequestHeader) parseFirstLine(buf []byte) (b []byte, err error) {
}
func (h *ResponseHeader) parseHeaders(buf []byte) ([]byte, error) {
// 'identity' content-length by default
h.ContentLength = -2
var s headerScanner
@@ -861,7 +865,7 @@ func (h *ResponseHeader) parseHeaders(buf []byte) ([]byte, error) {
}
}
case bytes.Equal(s.key, strTransferEncoding):
if bytes.Equal(s.value, strChunked) {
if !bytes.Equal(s.value, strIdentity) {
h.ContentLength = -1
}
case bytes.Equal(s.key, strConnection):
@@ -886,7 +890,8 @@ func (h *ResponseHeader) parseHeaders(buf []byte) ([]byte, error) {
return nil, fmt.Errorf("missing required Content-Type header in %q", buf)
}
if h.ContentLength == -2 {
return nil, fmt.Errorf("missing both Content-Length and Transfer-Encoding: chunked in %q", buf)
// Close connection after 'identity' response.
h.ConnectionClose = true
}
return s.b, nil
}
@@ -917,7 +922,7 @@ func (h *RequestHeader) parseHeaders(buf []byte) ([]byte, error) {
}
}
case bytes.Equal(s.key, strTransferEncoding):
if bytes.Equal(s.value, strChunked) {
if !bytes.Equal(s.value, strIdentity) {
h.ContentLength = -1
}
case bytes.Equal(s.key, strConnection):
+10 -3
View File
@@ -747,6 +747,16 @@ func TestResponseHeaderReadSuccess(t *testing.T) {
// blank lines before the first line
testResponseHeaderReadSuccess(t, h, "\r\nHTTP/1.1 200 OK\r\nContent-Type: aa\r\nContent-Length: 0\r\n\r\nsss",
200, 0, "aa", "sss")
if h.ConnectionClose {
t.Fatalf("unexpected connection: close")
}
// no content-length (identity transfer-encoding)
testResponseHeaderReadSuccess(t, h, "HTTP/1.1 200 OK\r\nContent-Type: foo/bar\r\n\r\nabcdefg",
200, -2, "foo/bar", "abcdefg")
if !h.ConnectionClose {
t.Fatalf("expecting connection: close for identity response")
}
}
func TestRequestHeaderReadSuccess(t *testing.T) {
@@ -882,9 +892,6 @@ func TestResponseHeaderReadError(t *testing.T) {
// no content-type
testResponseHeaderReadError(t, h, "HTTP/1.1 200 OK\r\nContent-Length: 123\r\n\r\n")
// no content-length
testResponseHeaderReadError(t, h, "HTTP/1.1 200 OK\r\nContent-Type: foo/bar\r\n\r\n")
}
func TestRequestHeaderReadError(t *testing.T) {
+59 -37
View File
@@ -148,12 +148,11 @@ func (req *Request) Read(r *bufio.Reader) error {
}
if req.Header.IsMethodPost() {
body, err := readBody(r, req.Header.ContentLength, req.Body)
req.Body, err = readBody(r, req.Header.ContentLength, req.Body)
if err != nil {
req.Clear()
return err
}
req.Body = body
req.Header.ContentLength = len(req.Body)
}
return nil
@@ -168,12 +167,11 @@ func (resp *Response) Read(r *bufio.Reader) error {
}
if !isSkipResponseBody(resp.Header.StatusCode) && !resp.SkipBody {
body, err := readBody(r, resp.Header.ContentLength, resp.Body)
resp.Body, err = readBody(r, resp.Header.ContentLength, resp.Body)
if err != nil {
resp.Clear()
return err
}
resp.Body = body
resp.Header.ContentLength = len(resp.Body)
}
return nil
@@ -314,53 +312,77 @@ func writeChunk(w *bufio.Writer, b []byte) error {
var copyBufPool sync.Pool
func readBody(r *bufio.Reader, contentLength int, b []byte) ([]byte, error) {
b = b[:0]
func readBody(r *bufio.Reader, contentLength int, dst []byte) ([]byte, error) {
dst = dst[:0]
if contentLength >= 0 {
return readBodyFixedSize(r, contentLength, b)
return appendBodyFixedSize(r, dst, contentLength)
}
return readBodyChunked(r, b)
if contentLength == -1 {
return readBodyChunked(r, dst)
}
return readBodyIdentity(r, dst)
}
func readBodyFixedSize(r *bufio.Reader, n int, buf []byte) ([]byte, error) {
func readBodyIdentity(r *bufio.Reader, dst []byte) ([]byte, error) {
dst = dst[:cap(dst)]
if len(dst) == 0 {
dst = make([]byte, 1024)
}
offset := 0
for {
nn, err := r.Read(dst[offset:])
if nn <= 0 {
if err != nil {
if err == io.EOF {
return dst[:offset], nil
}
return dst[:offset], err
}
panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn))
}
offset += nn
if len(dst) == offset {
b := make([]byte, round2(2*offset))
copy(b, dst)
dst = b
}
}
}
func appendBodyFixedSize(r *bufio.Reader, dst []byte, n int) ([]byte, error) {
if n == 0 {
return buf, nil
return dst, nil
}
bufLen := len(buf)
bufCap := bufLen + n
if cap(buf) < bufCap {
b := make([]byte, bufLen, round2(bufCap))
copy(b, buf)
buf = b
offset := len(dst)
dstLen := offset + n
if cap(dst) < dstLen {
b := make([]byte, round2(dstLen))
copy(b, dst)
dst = b
}
buf = buf[:bufCap]
b := buf[bufLen:]
dst = dst[:dstLen]
for {
nn, err := r.Read(b)
nn, err := r.Read(dst[offset:])
if nn <= 0 {
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return nil, err
return dst[:offset], err
}
panic(fmt.Sprintf("BUF: bufio.Read() returned (%d, nil)", nn))
panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn))
}
if nn == n {
return buf, nil
offset += nn
if offset == dstLen {
return dst, nil
}
if nn > n {
panic(fmt.Sprintf("BUF: read more than requested: %d vs %d", nn, n))
}
n -= nn
b = b[nn:]
}
}
func readBodyChunked(r *bufio.Reader, b []byte) ([]byte, error) {
if len(b) > 0 {
func readBodyChunked(r *bufio.Reader, dst []byte) ([]byte, error) {
if len(dst) > 0 {
panic("BUG: expected zero-length buffer")
}
@@ -368,18 +390,18 @@ func readBodyChunked(r *bufio.Reader, b []byte) ([]byte, error) {
for {
chunkSize, err := parseChunkSize(r)
if err != nil {
return nil, err
return dst, err
}
b, err = readBodyFixedSize(r, chunkSize+strCRLFLen, b)
dst, err = appendBodyFixedSize(r, dst, chunkSize+strCRLFLen)
if err != nil {
return nil, err
return dst, err
}
if !bytes.Equal(b[len(b)-strCRLFLen:], strCRLF) {
return nil, fmt.Errorf("cannot find crlf at the end of chunk")
if !bytes.Equal(dst[len(dst)-strCRLFLen:], strCRLF) {
return dst, fmt.Errorf("cannot find crlf at the end of chunk")
}
b = b[:len(b)-strCRLFLen]
dst = dst[:len(dst)-strCRLFLen]
if chunkSize == 0 {
return b, nil
return dst, nil
}
}
}
+17
View File
@@ -312,10 +312,27 @@ func TestResponseReadSuccess(t *testing.T) {
testResponseReadSuccess(t, resp, "HTTP/1.1 300 OK\r\nContent-Length: 5\r\nContent-Type: bar\r\n\r\n56789aaa",
300, 5, "bar", "56789", "aaa")
// no conent-length ('identity' transfer-encoding)
testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: foobar\r\n\r\nzxxc",
200, 4, "foobar", "zxxc", "")
// explicitly stated 'Transfer-Encoding: identity'
testResponseReadSuccess(t, resp, "HTTP/1.1 234 ss\r\nContent-Type: xxx\r\n\r\nxag",
234, 3, "xxx", "xag", "")
// big 'identity' response
body := string(createFixedBody(100500))
testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: aa\r\n\r\n"+body,
200, 100500, "aa", body, "")
// chunked response
testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nqwer\r\n2\r\nty\r\n0\r\n\r\nzzzzz",
200, 6, "text/html", "qwerty", "zzzzz")
// chunked response with non-chunked Transfer-Encoding.
testResponseReadSuccess(t, resp, "HTTP/1.1 230 OK\r\nContent-Type: text\r\nTransfer-Encoding: aaabbb\r\n\r\n2\r\ner\r\n2\r\nty\r\n0\r\n\r\nwe",
230, 4, "text", "erty", "we")
// zero chunked response
testResponseReadSuccess(t, resp, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nzzz",
200, 0, "text/html", "", "zzz")
+1
View File
@@ -40,5 +40,6 @@ var (
strClose = []byte("close")
strChunked = []byte("chunked")
strIdentity = []byte("identity")
strPostArgsContentType = []byte("application/x-www-form-urlencoded")
)