Skip to content

Commit bbfe9ac

Browse files
authored
🩹[Fix]: Added respects body immutability to ctx.Body() and ctx.BodyRaw() functions. (#2812)
* Functions ctx.Body() and ctx.BodyRaw() respects immutability * Tests for immutable request body * Added b.ReportAllocs() & b.ResetTimer() in benchmarks of request body
1 parent a348c1d commit bbfe9ac

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

Diff for: ctx.go

+9
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ func (c *DefaultCtx) BaseURL() string {
187187
// Returned value is only valid within the handler. Do not store any references.
188188
// Make copies or use the Immutable setting instead.
189189
func (c *DefaultCtx) BodyRaw() []byte {
190+
if c.app.config.Immutable {
191+
return utils.CopyBytes(c.fasthttp.Request.Body())
192+
}
190193
return c.fasthttp.Request.Body()
191194
}
192195

@@ -259,6 +262,9 @@ func (c *DefaultCtx) Body() []byte {
259262
// rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5
260263
encodingOrder = getSplicedStrList(headerEncoding, encodingOrder)
261264
if len(encodingOrder) == 0 {
265+
if c.app.config.Immutable {
266+
return utils.CopyBytes(c.fasthttp.Request.Body())
267+
}
262268
return c.fasthttp.Request.Body()
263269
}
264270

@@ -273,6 +279,9 @@ func (c *DefaultCtx) Body() []byte {
273279
return []byte(err.Error())
274280
}
275281

282+
if c.app.config.Immutable {
283+
return utils.CopyBytes(body)
284+
}
276285
return body
277286
}
278287

Diff for: ctx_test.go

+229
Original file line numberDiff line numberDiff line change
@@ -355,13 +355,46 @@ func Test_Ctx_Body(t *testing.T) {
355355
require.Equal(t, []byte("john=doe"), c.Body())
356356
}
357357

358+
// go test -v -run=^$ -bench=Benchmark_Ctx_Body -benchmem -count=4
358359
func Benchmark_Ctx_Body(b *testing.B) {
359360
const input = "john=doe"
360361

361362
app := New()
362363
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed
363364

364365
c.Request().SetBody([]byte(input))
366+
b.ReportAllocs()
367+
b.ResetTimer()
368+
for i := 0; i < b.N; i++ {
369+
_ = c.Body()
370+
}
371+
372+
require.Equal(b, []byte(input), c.Body())
373+
}
374+
375+
// go test -run Test_Ctx_Body_Immutable
376+
func Test_Ctx_Body_Immutable(t *testing.T) {
377+
t.Parallel()
378+
app := New()
379+
app.config.Immutable = true
380+
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed
381+
382+
c.Request().SetBody([]byte("john=doe"))
383+
require.Equal(t, []byte("john=doe"), c.Body())
384+
}
385+
386+
// go test -v -run=^$ -bench=Benchmark_Ctx_Body_Immutable -benchmem -count=4
387+
func Benchmark_Ctx_Body_Immutable(b *testing.B) {
388+
const input = "john=doe"
389+
390+
app := New()
391+
app.config.Immutable = true
392+
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed
393+
394+
c.Request().SetBody([]byte(input))
395+
b.ReportAllocs()
396+
b.ResetTimer()
397+
365398
for i := 0; i < b.N; i++ {
366399
_ = c.Body()
367400
}
@@ -539,9 +572,205 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
539572
},
540573
}
541574

575+
b.ReportAllocs()
576+
b.ResetTimer()
577+
for _, ct := range compressionTests {
578+
b.Run(ct.contentEncoding, func(b *testing.B) {
579+
app := New()
580+
const input = "john=doe"
581+
c := app.NewCtx(&fasthttp.RequestCtx{})
582+
583+
c.Request().Header.Set("Content-Encoding", ct.contentEncoding)
584+
compressedBody, err := ct.compressWriter([]byte(input))
585+
require.NoError(b, err)
586+
587+
c.Request().SetBody(compressedBody)
588+
for i := 0; i < b.N; i++ {
589+
_ = c.Body()
590+
}
591+
592+
require.Equal(b, []byte(input), c.Body())
593+
})
594+
}
595+
}
596+
597+
// go test -run Test_Ctx_Body_With_Compression_Immutable
598+
func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
599+
t.Parallel()
600+
tests := []struct {
601+
name string
602+
contentEncoding string
603+
body []byte
604+
expectedBody []byte
605+
}{
606+
{
607+
name: "gzip",
608+
contentEncoding: "gzip",
609+
body: []byte("john=doe"),
610+
expectedBody: []byte("john=doe"),
611+
},
612+
{
613+
name: "unsupported_encoding",
614+
contentEncoding: "undefined",
615+
body: []byte("keeps_ORIGINAL"),
616+
expectedBody: []byte("keeps_ORIGINAL"),
617+
},
618+
{
619+
name: "gzip then unsupported",
620+
contentEncoding: "gzip, undefined",
621+
body: []byte("Go, be gzipped"),
622+
expectedBody: []byte("Go, be gzipped"),
623+
},
624+
{
625+
name: "invalid_deflate",
626+
contentEncoding: "gzip,deflate",
627+
body: []byte("I'm not correctly compressed"),
628+
expectedBody: []byte(zlib.ErrHeader.Error()),
629+
},
630+
}
631+
632+
for _, testObject := range tests {
633+
tCase := testObject // Duplicate object to ensure it will be unique across all runs
634+
t.Run(tCase.name, func(t *testing.T) {
635+
t.Parallel()
636+
app := New()
637+
app.config.Immutable = true
638+
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed
639+
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)
640+
641+
if strings.Contains(tCase.contentEncoding, "gzip") {
642+
var b bytes.Buffer
643+
gz := gzip.NewWriter(&b)
644+
645+
_, err := gz.Write(tCase.body)
646+
require.NoError(t, err)
647+
648+
err = gz.Flush()
649+
require.NoError(t, err)
650+
651+
err = gz.Close()
652+
require.NoError(t, err)
653+
tCase.body = b.Bytes()
654+
}
655+
656+
c.Request().SetBody(tCase.body)
657+
body := c.Body()
658+
require.Equal(t, tCase.expectedBody, body)
659+
660+
// Check if body raw is the same as previous before decompression
661+
require.Equal(
662+
t, tCase.body, c.Request().Body(),
663+
"Body raw must be the same as set before",
664+
)
665+
})
666+
}
667+
}
668+
669+
// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression_Immutable -benchmem -count=4
670+
func Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) {
671+
encodingErr := errors.New("failed to encoding data")
672+
673+
var (
674+
compressGzip = func(data []byte) ([]byte, error) {
675+
var buf bytes.Buffer
676+
writer := gzip.NewWriter(&buf)
677+
if _, err := writer.Write(data); err != nil {
678+
return nil, encodingErr
679+
}
680+
if err := writer.Flush(); err != nil {
681+
return nil, encodingErr
682+
}
683+
if err := writer.Close(); err != nil {
684+
return nil, encodingErr
685+
}
686+
return buf.Bytes(), nil
687+
}
688+
compressDeflate = func(data []byte) ([]byte, error) {
689+
var buf bytes.Buffer
690+
writer := zlib.NewWriter(&buf)
691+
if _, err := writer.Write(data); err != nil {
692+
return nil, encodingErr
693+
}
694+
if err := writer.Flush(); err != nil {
695+
return nil, encodingErr
696+
}
697+
if err := writer.Close(); err != nil {
698+
return nil, encodingErr
699+
}
700+
return buf.Bytes(), nil
701+
}
702+
)
703+
compressionTests := []struct {
704+
contentEncoding string
705+
compressWriter func([]byte) ([]byte, error)
706+
}{
707+
{
708+
contentEncoding: "gzip",
709+
compressWriter: compressGzip,
710+
},
711+
{
712+
contentEncoding: "gzip,invalid",
713+
compressWriter: compressGzip,
714+
},
715+
{
716+
contentEncoding: "deflate",
717+
compressWriter: compressDeflate,
718+
},
719+
{
720+
contentEncoding: "gzip,deflate",
721+
compressWriter: func(data []byte) ([]byte, error) {
722+
var (
723+
buf bytes.Buffer
724+
writer interface {
725+
io.WriteCloser
726+
Flush() error
727+
}
728+
err error
729+
)
730+
731+
// deflate
732+
{
733+
writer = zlib.NewWriter(&buf)
734+
if _, err = writer.Write(data); err != nil {
735+
return nil, encodingErr
736+
}
737+
if err = writer.Flush(); err != nil {
738+
return nil, encodingErr
739+
}
740+
if err = writer.Close(); err != nil {
741+
return nil, encodingErr
742+
}
743+
}
744+
745+
data = make([]byte, buf.Len())
746+
copy(data, buf.Bytes())
747+
buf.Reset()
748+
749+
// gzip
750+
{
751+
writer = gzip.NewWriter(&buf)
752+
if _, err = writer.Write(data); err != nil {
753+
return nil, encodingErr
754+
}
755+
if err = writer.Flush(); err != nil {
756+
return nil, encodingErr
757+
}
758+
if err = writer.Close(); err != nil {
759+
return nil, encodingErr
760+
}
761+
}
762+
763+
return buf.Bytes(), nil
764+
},
765+
},
766+
}
767+
768+
b.ReportAllocs()
769+
b.ResetTimer()
542770
for _, ct := range compressionTests {
543771
b.Run(ct.contentEncoding, func(b *testing.B) {
544772
app := New()
773+
app.config.Immutable = true
545774
const input = "john=doe"
546775
c := app.NewCtx(&fasthttp.RequestCtx{})
547776

0 commit comments

Comments
 (0)