Skip to content

Commit 7a84b65

Browse files
authored
Merge pull request #149 from go-openapi/peek-body
look at the body to determine if it has content or not
2 parents 58872d9 + 09f01ee commit 7a84b65

3 files changed

Lines changed: 162 additions & 27 deletions

File tree

middleware/validation_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"bytes"
1919
"net/http"
2020
"net/http/httptest"
21+
"strings"
2122
"testing"
2223

2324
"github.com/go-openapi/errors"
@@ -81,7 +82,7 @@ func TestContentTypeValidation(t *testing.T) {
8182
assert.Equal(t, "application/json", recorder.Header().Get("content-type"))
8283

8384
recorder = httptest.NewRecorder()
84-
request, _ = http.NewRequest("POST", "/api/pets", nil)
85+
request, _ = http.NewRequest("POST", "/api/pets", strings.NewReader(`{"name":"dog"}`))
8586
request.Header.Add("Accept", "application/json")
8687
request.Header.Add("content-type", "text/html")
8788
request.TransferEncoding = []string{"chunked"}

request.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package runtime
1616

1717
import (
18+
"bufio"
1819
"io"
1920
"net/http"
2021
"strings"
@@ -42,7 +43,68 @@ func AllowsBody(r *http.Request) bool {
4243

4344
// HasBody returns true if this method needs a content-type
4445
func HasBody(r *http.Request) bool {
45-
return len(r.TransferEncoding) > 0 || r.ContentLength > 0
46+
// happy case: we have a content length set
47+
if r.ContentLength > 0 {
48+
return true
49+
}
50+
51+
if r.Header.Get(http.CanonicalHeaderKey("content-length")) != "" {
52+
// in this case, no Transfer-Encoding should be present
53+
// we have a header set but it was explicitly set to 0, so we assume no body
54+
return false
55+
}
56+
57+
rdr := newPeekingReader(r.Body)
58+
r.Body = rdr
59+
return rdr.HasContent()
60+
}
61+
62+
func newPeekingReader(r io.ReadCloser) *peekingReader {
63+
if r == nil {
64+
return nil
65+
}
66+
return &peekingReader{
67+
underlying: bufio.NewReader(r),
68+
orig: r,
69+
}
70+
}
71+
72+
type peekingReader struct {
73+
underlying interface {
74+
Buffered() int
75+
Peek(int) ([]byte, error)
76+
Read([]byte) (int, error)
77+
}
78+
orig io.ReadCloser
79+
}
80+
81+
func (p *peekingReader) HasContent() bool {
82+
if p == nil {
83+
return false
84+
}
85+
if p.underlying.Buffered() > 0 {
86+
return true
87+
}
88+
b, err := p.underlying.Peek(1)
89+
if err != nil {
90+
return false
91+
}
92+
return len(b) > 0
93+
}
94+
95+
func (p *peekingReader) Read(d []byte) (int, error) {
96+
if p == nil {
97+
return 0, io.EOF
98+
}
99+
return p.underlying.Read(d)
100+
}
101+
102+
func (p *peekingReader) Close() error {
103+
p.underlying = nil
104+
if p.orig != nil {
105+
return p.orig.Close()
106+
}
107+
return nil
46108
}
47109

48110
// JSONRequest creates a new http request with json headers set

request_test.go

Lines changed: 97 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,130 @@
1515
package runtime
1616

1717
import (
18+
"bufio"
19+
"bytes"
20+
"io"
21+
"io/ioutil"
1822
"net/url"
23+
"strings"
1924
"testing"
2025

26+
"github.com/stretchr/testify/require"
27+
2128
"github.com/stretchr/testify/assert"
2229
)
2330

24-
/*
25-
type tstreadcloser struct {
26-
closed bool
31+
type eofReader struct{}
32+
33+
func (e *eofReader) Read(d []byte) (int, error) {
34+
return 0, io.EOF
35+
}
36+
37+
func closeReader(rdr io.Reader) *closeCounting {
38+
return &closeCounting{
39+
rdr: rdr,
40+
}
41+
}
42+
43+
type closeCounting struct {
44+
rdr io.Reader
45+
closed int
46+
}
47+
48+
func (c *closeCounting) Read(d []byte) (int, error) {
49+
return c.rdr.Read(d)
2750
}
2851

29-
func (t *tstreadcloser) Read(p []byte) (int, error) { return 0, nil }
30-
func (t *tstreadcloser) Close() error {
31-
t.closed = true
52+
func (c *closeCounting) Close() error {
53+
c.closed++
54+
if cr, ok := c.rdr.(io.ReadCloser); ok {
55+
return cr.Close()
56+
}
3257
return nil
3358
}
3459

60+
type countingBufioReader struct {
61+
buffereds int
62+
peeks int
63+
reads int
64+
65+
br interface {
66+
Buffered() int
67+
Peek(int) ([]byte, error)
68+
Read([]byte) (int, error)
69+
}
70+
}
71+
72+
func (c *countingBufioReader) Buffered() int {
73+
c.buffereds++
74+
return c.br.Buffered()
75+
}
76+
77+
func (c *countingBufioReader) Peek(v int) ([]byte, error) {
78+
c.peeks++
79+
return c.br.Peek(v)
80+
}
81+
82+
func (c *countingBufioReader) Read(p []byte) (int, error) {
83+
c.reads++
84+
return c.br.Read(p)
85+
}
86+
3587
func TestPeekingReader(t *testing.T) {
3688
// just passes to original reader when nothing called
3789
exp1 := []byte("original")
38-
pr1 := &peekingReader{rdr: ioutil.NopCloser(bytes.NewReader(exp1))}
90+
pr1 := newPeekingReader(closeReader(bytes.NewReader(exp1)))
3991
b1, err := ioutil.ReadAll(pr1)
4092
if assert.NoError(t, err) {
4193
assert.Equal(t, exp1, b1)
4294
}
4395

4496
// uses actual when there was some buffering
4597
exp2 := []byte("actual")
46-
pt1, pt2 := []byte("a"), []byte("ctual")
47-
pr2 := &peekingReader{
48-
rdr: ioutil.NopCloser(bytes.NewReader(exp1)),
49-
actual: io.MultiReader(bytes.NewReader(pt1), bytes.NewReader(pt2)),
50-
peeked: pt1,
51-
}
98+
pr2 := newPeekingReader(closeReader(bytes.NewReader(exp2)))
99+
peeked, err := pr2.underlying.Peek(1)
100+
require.NoError(t, err)
101+
require.Equal(t, "a", string(peeked))
52102
b2, err := ioutil.ReadAll(pr2)
53103
if assert.NoError(t, err) {
54-
assert.Equal(t, exp2, b2)
104+
assert.Equal(t, string(exp2), string(b2))
55105
}
56106

57-
// closes original reader
58-
tr := new(tstreadcloser)
59-
pr3 := &peekingReader{
60-
rdr: tr,
61-
actual: ioutil.NopCloser(bytes.NewBuffer(nil)),
62-
peeked: pt1,
107+
// passes close call through to original reader
108+
cr := closeReader(closeReader(bytes.NewReader(exp2)))
109+
pr3 := newPeekingReader(cr)
110+
require.NoError(t, pr3.Close())
111+
require.Equal(t, 1, cr.closed)
112+
113+
// returns false when the stream is empty
114+
pr4 := newPeekingReader(closeReader(&eofReader{}))
115+
require.False(t, pr4.HasContent())
116+
117+
// returns true when the stream has content
118+
rdr := closeReader(strings.NewReader("hello"))
119+
pr := newPeekingReader(rdr)
120+
cbr := &countingBufioReader{
121+
br: bufio.NewReader(rdr),
63122
}
64-
65-
66-
// returns true when peeked previously with data
67-
// returns true when peeked with data
123+
pr.underlying = cbr
124+
125+
require.True(t, pr.HasContent())
126+
require.Equal(t, 1, cbr.buffereds)
127+
require.Equal(t, 1, cbr.peeks)
128+
require.Equal(t, 0, cbr.reads)
129+
require.True(t, pr.HasContent())
130+
require.Equal(t, 2, cbr.buffereds)
131+
require.Equal(t, 1, cbr.peeks)
132+
require.Equal(t, 0, cbr.reads)
133+
134+
b, err := ioutil.ReadAll(pr)
135+
require.NoError(t, err)
136+
require.Equal(t, "hello", string(b))
137+
require.Equal(t, 2, cbr.buffereds)
138+
require.Equal(t, 1, cbr.peeks)
139+
require.Equal(t, 2, cbr.reads)
140+
require.Equal(t, 0, cbr.br.Buffered())
68141
}
69-
*/
70142

71143
func TestJSONRequest(t *testing.T) {
72144
req, err := JSONRequest("GET", "/swagger.json", nil)

0 commit comments

Comments
 (0)