Skip to content

Commit f01d03f

Browse files
committed
Address issue #258
Make sure the body bytes are available and reset for reading.
1 parent ae3b0dd commit f01d03f

3 files changed

Lines changed: 368 additions & 16 deletions

File tree

requests/validate_body.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
package requests
55

66
import (
7-
"bytes"
87
"encoding/json"
98
"fmt"
10-
"io"
119
"net/http"
1210
"strings"
1311

@@ -92,10 +90,8 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req
9290
return true, nil
9391
}
9492

95-
if request != nil && request.Body != nil {
96-
requestBody, _ := io.ReadAll(request.Body)
97-
_ = request.Body.Close()
98-
93+
if request != nil && (request.Body != nil || request.GetBody != nil) {
94+
requestBody := readAndResetRequestBody(request)
9995
stringedBody := string(requestBody)
10096
var jsonBody any
10197
var prevalidationErrors []*errors.ValidationError
@@ -121,7 +117,7 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req
121117
}
122118
}
123119

124-
request.Body = io.NopCloser(bytes.NewBuffer(transformedBytes))
120+
setRequestBody(request, transformedBytes)
125121
}
126122
}
127123

requests/validate_body_test.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"encoding/json"
99
"fmt"
10+
"io"
1011
"net/http"
1112
"sync"
1213
"testing"
@@ -642,6 +643,278 @@ paths:
642643
assert.Len(t, errors, 0)
643644
}
644645

646+
func TestValidateBody_UsesGetBodyWhenBodyAlreadyConsumed(t *testing.T) {
647+
spec := `openapi: 3.1.0
648+
paths:
649+
/burgers/createBurger:
650+
post:
651+
requestBody:
652+
required: true
653+
content:
654+
application/json:
655+
schema:
656+
type: object
657+
required: [name, patties, vegetarian]
658+
properties:
659+
name:
660+
type: string
661+
patties:
662+
type: integer
663+
vegetarian:
664+
type: boolean`
665+
666+
doc, _ := libopenapi.NewDocument([]byte(spec))
667+
668+
m, _ := doc.BuildV3Model()
669+
v := NewRequestBodyValidator(&m.Model)
670+
671+
body := map[string]interface{}{
672+
"name": "Big Mac",
673+
"patties": 2,
674+
"vegetarian": true,
675+
}
676+
bodyBytes, _ := json.Marshal(body)
677+
678+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
679+
bytes.NewReader(bodyBytes))
680+
request.Header.Set("Content-Type", "application/json")
681+
_, _ = io.ReadAll(request.Body)
682+
683+
valid, validationErrors := v.ValidateRequestBody(request)
684+
require.True(t, valid)
685+
require.Empty(t, validationErrors)
686+
687+
restoredBody, err := io.ReadAll(request.Body)
688+
require.NoError(t, err)
689+
require.JSONEq(t, string(bodyBytes), string(restoredBody))
690+
691+
replayedBody, err := request.GetBody()
692+
require.NoError(t, err)
693+
replayedBytes, err := io.ReadAll(replayedBody)
694+
require.NoError(t, err)
695+
require.NoError(t, replayedBody.Close())
696+
require.JSONEq(t, string(bodyBytes), string(replayedBytes))
697+
}
698+
699+
func TestValidateBody_PrefersAssignedBodyOverStaleGetBody(t *testing.T) {
700+
spec := `openapi: 3.1.0
701+
paths:
702+
/burgers/createBurger:
703+
post:
704+
requestBody:
705+
required: true
706+
content:
707+
application/json:
708+
schema:
709+
type: object
710+
required: [name, patties, vegetarian]
711+
properties:
712+
name:
713+
type: string
714+
patties:
715+
type: integer
716+
vegetarian:
717+
type: boolean`
718+
719+
doc, _ := libopenapi.NewDocument([]byte(spec))
720+
721+
m, _ := doc.BuildV3Model()
722+
v := NewRequestBodyValidator(&m.Model)
723+
724+
staleBodyBytes, _ := json.Marshal(map[string]interface{}{
725+
"name": "Big Mac",
726+
"patties": false,
727+
"vegetarian": true,
728+
})
729+
currentBodyBytes, _ := json.Marshal(map[string]interface{}{
730+
"name": "Big Mac",
731+
"patties": 2,
732+
"vegetarian": true,
733+
})
734+
735+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
736+
bytes.NewReader(staleBodyBytes))
737+
request.Header.Set("Content-Type", "application/json")
738+
request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes))
739+
740+
valid, validationErrors := v.ValidateRequestBody(request)
741+
require.True(t, valid)
742+
require.Empty(t, validationErrors)
743+
}
744+
745+
func TestValidateBody_DoesNotUseStaleGetBodyForConsumedDifferentBodySameLength(t *testing.T) {
746+
spec := `openapi: 3.1.0
747+
paths:
748+
/burgers/createBurger:
749+
post:
750+
requestBody:
751+
required: true
752+
content:
753+
application/json:
754+
schema:
755+
type: object
756+
required: [patties]
757+
properties:
758+
patties:
759+
type: integer`
760+
761+
doc, _ := libopenapi.NewDocument([]byte(spec))
762+
763+
m, _ := doc.BuildV3Model()
764+
v := NewRequestBodyValidator(&m.Model)
765+
766+
staleBodyBytes := []byte(`{"patties":12345}`)
767+
currentBodyBytes := []byte(`{"patties":false}`)
768+
require.Len(t, currentBodyBytes, len(staleBodyBytes))
769+
770+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
771+
bytes.NewReader(staleBodyBytes))
772+
request.Header.Set("Content-Type", "application/json")
773+
request.Body = io.NopCloser(bytes.NewReader(currentBodyBytes))
774+
_, _ = io.ReadAll(request.Body)
775+
776+
valid, validationErrors := v.ValidateRequestBody(request)
777+
require.False(t, valid)
778+
require.Len(t, validationErrors, 1)
779+
require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message)
780+
781+
replayedBody, err := request.GetBody()
782+
require.NoError(t, err)
783+
replayedBytes, err := io.ReadAll(replayedBody)
784+
require.NoError(t, err)
785+
require.NoError(t, replayedBody.Close())
786+
require.Empty(t, replayedBytes)
787+
}
788+
789+
func TestValidateBody_DoesNotUseStaleGetBodyForExplicitEmptyBody(t *testing.T) {
790+
spec := `openapi: 3.1.0
791+
paths:
792+
/burgers/createBurger:
793+
post:
794+
requestBody:
795+
required: true
796+
content:
797+
application/json:
798+
schema:
799+
type: object
800+
required: [name, patties, vegetarian]
801+
properties:
802+
name:
803+
type: string
804+
patties:
805+
type: integer
806+
vegetarian:
807+
type: boolean`
808+
809+
doc, _ := libopenapi.NewDocument([]byte(spec))
810+
811+
m, _ := doc.BuildV3Model()
812+
v := NewRequestBodyValidator(&m.Model)
813+
814+
staleBodyBytes, _ := json.Marshal(map[string]interface{}{
815+
"name": "Big Mac",
816+
"patties": 2,
817+
"vegetarian": true,
818+
})
819+
820+
tests := []struct {
821+
name string
822+
body io.ReadCloser
823+
}{
824+
{
825+
name: "http no body",
826+
body: http.NoBody,
827+
},
828+
{
829+
name: "empty reader",
830+
body: io.NopCloser(bytes.NewReader(nil)),
831+
},
832+
}
833+
834+
for _, tc := range tests {
835+
t.Run(tc.name, func(t *testing.T) {
836+
request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
837+
bytes.NewReader(staleBodyBytes))
838+
request.Header.Set("Content-Type", "application/json")
839+
request.Body = tc.body
840+
841+
valid, validationErrors := v.ValidateRequestBody(request)
842+
require.False(t, valid)
843+
require.Len(t, validationErrors, 1)
844+
require.Equal(t, "POST request body is empty for '/burgers/createBurger'", validationErrors[0].Message)
845+
846+
replayedBody, err := request.GetBody()
847+
require.NoError(t, err)
848+
replayedBytes, err := io.ReadAll(replayedBody)
849+
require.NoError(t, err)
850+
require.NoError(t, replayedBody.Close())
851+
require.Empty(t, replayedBytes)
852+
})
853+
}
854+
}
855+
856+
func TestRequestBodyHelpers_NilRequest(t *testing.T) {
857+
setRequestBody(nil, []byte(`{"ok":true}`))
858+
require.Nil(t, readAndResetRequestBody(nil))
859+
}
860+
861+
type requestBodyReaderTestBody struct{}
862+
863+
func (r *requestBodyReaderTestBody) Read(_ []byte) (int, error) {
864+
return 0, io.EOF
865+
}
866+
867+
func (r *requestBodyReaderTestBody) Close() error {
868+
return nil
869+
}
870+
871+
type failingReplayableBody struct{}
872+
873+
func (r *failingReplayableBody) Read(_ []byte) (int, error) {
874+
return 0, io.EOF
875+
}
876+
877+
func (r *failingReplayableBody) Close() error {
878+
return nil
879+
}
880+
881+
func (r *failingReplayableBody) ReadAt(_ []byte, _ int64) (int, error) {
882+
return 0, io.ErrUnexpectedEOF
883+
}
884+
885+
func (r *failingReplayableBody) Size() int64 {
886+
return 1
887+
}
888+
889+
func TestRequestBodyReader_DefensiveBranches(t *testing.T) {
890+
require.Nil(t, requestBodyReader(nil))
891+
require.Nil(t, requestBodyReader(http.NoBody))
892+
893+
var nilBody *requestBodyReaderTestBody
894+
require.Nil(t, requestBodyReader(nilBody))
895+
896+
body := &requestBodyReaderTestBody{}
897+
require.Same(t, body, requestBodyReader(body))
898+
}
899+
900+
func TestRequestBodySnapshot_DefensiveBranches(t *testing.T) {
901+
snapshot, ok := requestBodySnapshot(nil)
902+
require.False(t, ok)
903+
require.Nil(t, snapshot)
904+
905+
snapshot, ok = requestBodySnapshot(&http.Request{Body: &requestBodyReaderTestBody{}})
906+
require.False(t, ok)
907+
require.Nil(t, snapshot)
908+
909+
snapshot, ok = requestBodySnapshot(&http.Request{Body: io.NopCloser(bytes.NewReader(nil))})
910+
require.False(t, ok)
911+
require.Nil(t, snapshot)
912+
913+
snapshot, ok = requestBodySnapshot(&http.Request{Body: &failingReplayableBody{}})
914+
require.False(t, ok)
915+
require.Nil(t, snapshot)
916+
}
917+
645918
func TestValidateBody_ValidBasicSchema_WithFullContentTypeHeader(t *testing.T) {
646919
spec := `openapi: 3.1.0
647920
paths:

0 commit comments

Comments
 (0)