Skip to content

Commit

Permalink
🩹[Fix]: Added respects body immutability to ctx.Body() and ctx.BodyRa…
Browse files Browse the repository at this point in the history
…w() 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
  • Loading branch information
asyslinux authored Feb 2, 2024
1 parent a348c1d commit bbfe9ac
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 0 deletions.
9 changes: 9 additions & 0 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ func (c *DefaultCtx) BaseURL() string {
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (c *DefaultCtx) BodyRaw() []byte {
if c.app.config.Immutable {
return utils.CopyBytes(c.fasthttp.Request.Body())
}
return c.fasthttp.Request.Body()
}

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

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

if c.app.config.Immutable {
return utils.CopyBytes(body)
}
return body
}

Expand Down
229 changes: 229 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,46 @@ func Test_Ctx_Body(t *testing.T) {
require.Equal(t, []byte("john=doe"), c.Body())
}

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

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

c.Request().SetBody([]byte(input))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = c.Body()
}

require.Equal(b, []byte(input), c.Body())
}

// go test -run Test_Ctx_Body_Immutable
func Test_Ctx_Body_Immutable(t *testing.T) {
t.Parallel()
app := New()
app.config.Immutable = true
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed

c.Request().SetBody([]byte("john=doe"))
require.Equal(t, []byte("john=doe"), c.Body())
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Body_Immutable -benchmem -count=4
func Benchmark_Ctx_Body_Immutable(b *testing.B) {
const input = "john=doe"

app := New()
app.config.Immutable = true
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed

c.Request().SetBody([]byte(input))
b.ReportAllocs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
_ = c.Body()
}
Expand Down Expand Up @@ -539,9 +572,205 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
},
}

b.ReportAllocs()
b.ResetTimer()
for _, ct := range compressionTests {
b.Run(ct.contentEncoding, func(b *testing.B) {
app := New()
const input = "john=doe"
c := app.NewCtx(&fasthttp.RequestCtx{})

c.Request().Header.Set("Content-Encoding", ct.contentEncoding)
compressedBody, err := ct.compressWriter([]byte(input))
require.NoError(b, err)

c.Request().SetBody(compressedBody)
for i := 0; i < b.N; i++ {
_ = c.Body()
}

require.Equal(b, []byte(input), c.Body())
})
}
}

// go test -run Test_Ctx_Body_With_Compression_Immutable
func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
t.Parallel()
tests := []struct {
name string
contentEncoding string
body []byte
expectedBody []byte
}{
{
name: "gzip",
contentEncoding: "gzip",
body: []byte("john=doe"),
expectedBody: []byte("john=doe"),
},
{
name: "unsupported_encoding",
contentEncoding: "undefined",
body: []byte("keeps_ORIGINAL"),
expectedBody: []byte("keeps_ORIGINAL"),
},
{
name: "gzip then unsupported",
contentEncoding: "gzip, undefined",
body: []byte("Go, be gzipped"),
expectedBody: []byte("Go, be gzipped"),
},
{
name: "invalid_deflate",
contentEncoding: "gzip,deflate",
body: []byte("I'm not correctly compressed"),
expectedBody: []byte(zlib.ErrHeader.Error()),
},
}

for _, testObject := range tests {
tCase := testObject // Duplicate object to ensure it will be unique across all runs
t.Run(tCase.name, func(t *testing.T) {
t.Parallel()
app := New()
app.config.Immutable = true
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)

if strings.Contains(tCase.contentEncoding, "gzip") {
var b bytes.Buffer
gz := gzip.NewWriter(&b)

_, err := gz.Write(tCase.body)
require.NoError(t, err)

err = gz.Flush()
require.NoError(t, err)

err = gz.Close()
require.NoError(t, err)
tCase.body = b.Bytes()
}

c.Request().SetBody(tCase.body)
body := c.Body()
require.Equal(t, tCase.expectedBody, body)

// Check if body raw is the same as previous before decompression
require.Equal(
t, tCase.body, c.Request().Body(),
"Body raw must be the same as set before",
)
})
}
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression_Immutable -benchmem -count=4
func Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) {
encodingErr := errors.New("failed to encoding data")

var (
compressGzip = func(data []byte) ([]byte, error) {
var buf bytes.Buffer
writer := gzip.NewWriter(&buf)
if _, err := writer.Write(data); err != nil {
return nil, encodingErr
}
if err := writer.Flush(); err != nil {
return nil, encodingErr
}
if err := writer.Close(); err != nil {
return nil, encodingErr
}
return buf.Bytes(), nil
}
compressDeflate = func(data []byte) ([]byte, error) {
var buf bytes.Buffer
writer := zlib.NewWriter(&buf)
if _, err := writer.Write(data); err != nil {
return nil, encodingErr
}
if err := writer.Flush(); err != nil {
return nil, encodingErr
}
if err := writer.Close(); err != nil {
return nil, encodingErr
}
return buf.Bytes(), nil
}
)
compressionTests := []struct {
contentEncoding string
compressWriter func([]byte) ([]byte, error)
}{
{
contentEncoding: "gzip",
compressWriter: compressGzip,
},
{
contentEncoding: "gzip,invalid",
compressWriter: compressGzip,
},
{
contentEncoding: "deflate",
compressWriter: compressDeflate,
},
{
contentEncoding: "gzip,deflate",
compressWriter: func(data []byte) ([]byte, error) {
var (
buf bytes.Buffer
writer interface {
io.WriteCloser
Flush() error
}
err error
)

// deflate
{
writer = zlib.NewWriter(&buf)
if _, err = writer.Write(data); err != nil {
return nil, encodingErr
}
if err = writer.Flush(); err != nil {
return nil, encodingErr
}
if err = writer.Close(); err != nil {
return nil, encodingErr
}
}

data = make([]byte, buf.Len())
copy(data, buf.Bytes())
buf.Reset()

// gzip
{
writer = gzip.NewWriter(&buf)
if _, err = writer.Write(data); err != nil {
return nil, encodingErr
}
if err = writer.Flush(); err != nil {
return nil, encodingErr
}
if err = writer.Close(); err != nil {
return nil, encodingErr
}
}

return buf.Bytes(), nil
},
},
}

b.ReportAllocs()
b.ResetTimer()
for _, ct := range compressionTests {
b.Run(ct.contentEncoding, func(b *testing.B) {
app := New()
app.config.Immutable = true
const input = "john=doe"
c := app.NewCtx(&fasthttp.RequestCtx{})

Expand Down

0 comments on commit bbfe9ac

Please sign in to comment.