Skip to content

Commit fd18830

Browse files
authored
🐛 bug: Fix Body() handling of Content-Encoding per RFC 9110 (#3543)
* Adjust Body() decoding loop * Handle identity and compress encodings * Adjust Body status handling * test: cover multi-encoding decode * Address review feedback
1 parent d79fa01 commit fd18830

File tree

4 files changed

+137
-49
lines changed

4 files changed

+137
-49
lines changed

constants.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,13 @@ const (
294294

295295
// Compression types
296296
const (
297-
StrGzip = "gzip"
298-
StrBr = "br"
299-
StrDeflate = "deflate"
300-
StrBrotli = "brotli"
301-
StrZstd = "zstd"
297+
StrGzip = "gzip"
298+
StrCompress = "compress"
299+
StrIdentity = "identity"
300+
StrBr = "br"
301+
StrDeflate = "deflate"
302+
StrBrotli = "brotli"
303+
StrZstd = "zstd"
302304
)
303305

304306
// Cookie SameSite

ctx.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -307,32 +307,33 @@ func (c *DefaultCtx) tryDecodeBodyInOrder(
307307
decodesRealized uint8
308308
)
309309

310-
for index, encoding := range encodings {
310+
for idx := range encodings {
311+
i := len(encodings) - 1 - idx
312+
encoding := encodings[i]
311313
decodesRealized++
312314
switch encoding {
313-
case StrGzip:
315+
case StrGzip, "x-gzip":
314316
body, err = c.fasthttp.Request.BodyGunzip()
315317
case StrBr, StrBrotli:
316318
body, err = c.fasthttp.Request.BodyUnbrotli()
317319
case StrDeflate:
318320
body, err = c.fasthttp.Request.BodyInflate()
319321
case StrZstd:
320322
body, err = c.fasthttp.Request.BodyUnzstd()
323+
case StrIdentity:
324+
body = c.fasthttp.Request.Body()
325+
case StrCompress, "x-compress":
326+
return nil, decodesRealized - 1, ErrNotImplemented
321327
default:
322-
decodesRealized--
323-
if len(encodings) == 1 {
324-
body = c.fasthttp.Request.Body()
325-
}
326-
return body, decodesRealized, nil
328+
return nil, decodesRealized - 1, ErrUnsupportedMediaType
327329
}
328330

329331
if err != nil {
330332
return nil, decodesRealized, err
331333
}
332334

333-
// Only execute body raw update if it has a next iteration to try to decode
334-
if index < len(encodings)-1 && decodesRealized > 0 {
335-
if index == 0 {
335+
if i > 0 && decodesRealized > 0 {
336+
if i == len(encodings)-1 {
336337
tempBody := c.fasthttp.Request.Body()
337338
*originalBody = make([]byte, len(tempBody))
338339
copy(*originalBody, tempBody)
@@ -358,7 +359,7 @@ func (c *DefaultCtx) Body() []byte {
358359
)
359360

360361
// Get Content-Encoding header
361-
headerEncoding = utils.UnsafeString(c.Request().Header.ContentEncoding())
362+
headerEncoding = utils.ToLower(utils.UnsafeString(c.Request().Header.ContentEncoding()))
362363

363364
// If no encoding is provided, return the original body
364365
if len(headerEncoding) == 0 {
@@ -368,6 +369,9 @@ func (c *DefaultCtx) Body() []byte {
368369
// Split and get the encodings list, in order to attend the
369370
// rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5
370371
encodingOrder = getSplicedStrList(headerEncoding, encodingOrder)
372+
for i := range encodingOrder {
373+
encodingOrder[i] = utils.ToLower(encodingOrder[i])
374+
}
371375
if len(encodingOrder) == 0 {
372376
return c.getBody()
373377
}
@@ -380,6 +384,12 @@ func (c *DefaultCtx) Body() []byte {
380384
c.fasthttp.Request.SetBodyRaw(originalBody)
381385
}
382386
if err != nil {
387+
switch {
388+
case errors.Is(err, ErrUnsupportedMediaType):
389+
_ = c.SendStatus(StatusUnsupportedMediaType) //nolint:errcheck // It is fine to ignore the error
390+
case errors.Is(err, ErrNotImplemented):
391+
_ = c.SendStatus(StatusNotImplemented) //nolint:errcheck // It is fine to ignore the error
392+
}
383393
return []byte(err.Error())
384394
}
385395

ctx_test.go

Lines changed: 108 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -560,24 +560,42 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
560560
body: []byte("john=doe"),
561561
expectedBody: []byte("john=doe"),
562562
},
563+
{
564+
name: "gzip twice",
565+
contentEncoding: "gzip, gzip",
566+
body: []byte("double"),
567+
expectedBody: []byte("double"),
568+
},
563569
{
564570
name: "unsupported_encoding",
565571
contentEncoding: "undefined",
566572
body: []byte("keeps_ORIGINAL"),
567-
expectedBody: []byte("keeps_ORIGINAL"),
573+
expectedBody: []byte("Unsupported Media Type"),
574+
},
575+
{
576+
name: "compress_not_implemented",
577+
contentEncoding: "compress",
578+
body: []byte("foo"),
579+
expectedBody: []byte("Not Implemented"),
568580
},
569581
{
570582
name: "gzip then unsupported",
571583
contentEncoding: "gzip, undefined",
572584
body: []byte("Go, be gzipped"),
573-
expectedBody: []byte("Go, be gzipped"),
585+
expectedBody: []byte("Unsupported Media Type"),
574586
},
575587
{
576588
name: "invalid_deflate",
577589
contentEncoding: "gzip,deflate",
578590
body: []byte("I'm not correctly compressed"),
579591
expectedBody: []byte(zlib.ErrHeader.Error()),
580592
},
593+
{
594+
name: "identity",
595+
contentEncoding: "identity",
596+
body: []byte("bar"),
597+
expectedBody: []byte("bar"),
598+
},
581599
}
582600

583601
for _, testObject := range tests {
@@ -588,25 +606,45 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
588606
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
589607
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)
590608

591-
if strings.Contains(tCase.contentEncoding, "gzip") {
592-
var b bytes.Buffer
593-
gz := gzip.NewWriter(&b)
594-
595-
_, err := gz.Write(tCase.body)
596-
require.NoError(t, err)
597-
598-
err = gz.Flush()
599-
require.NoError(t, err)
600-
601-
err = gz.Close()
602-
require.NoError(t, err)
603-
tCase.body = b.Bytes()
609+
encs := strings.Split(tCase.contentEncoding, ",")
610+
for _, enc := range encs {
611+
enc = strings.TrimSpace(enc)
612+
if strings.Contains(tCase.name, "invalid_deflate") && enc == StrDeflate {
613+
continue
614+
}
615+
switch enc {
616+
case "gzip":
617+
var b bytes.Buffer
618+
gz := gzip.NewWriter(&b)
619+
_, err := gz.Write(tCase.body)
620+
require.NoError(t, err)
621+
require.NoError(t, gz.Flush())
622+
require.NoError(t, gz.Close())
623+
tCase.body = b.Bytes()
624+
case StrDeflate:
625+
var b bytes.Buffer
626+
fl := zlib.NewWriter(&b)
627+
_, err := fl.Write(tCase.body)
628+
require.NoError(t, err)
629+
require.NoError(t, fl.Flush())
630+
require.NoError(t, fl.Close())
631+
tCase.body = b.Bytes()
632+
}
604633
}
605634

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

639+
switch {
640+
case strings.Contains(tCase.name, "unsupported"):
641+
require.Equal(t, StatusUnsupportedMediaType, c.Response().StatusCode())
642+
case strings.Contains(tCase.name, "compress_not_implemented"):
643+
require.Equal(t, StatusNotImplemented, c.Response().StatusCode())
644+
default:
645+
require.Equal(t, StatusOK, c.Response().StatusCode())
646+
}
647+
610648
// Check if body raw is the same as previous before decompression
611649
require.Equal(
612650
t, tCase.body, c.Request().Body(),
@@ -663,7 +701,7 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
663701
compressWriter: compressGzip,
664702
},
665703
{
666-
contentEncoding: "deflate",
704+
contentEncoding: StrDeflate,
667705
compressWriter: compressDeflate,
668706
},
669707
{
@@ -751,24 +789,42 @@ func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
751789
body: []byte("john=doe"),
752790
expectedBody: []byte("john=doe"),
753791
},
792+
{
793+
name: "gzip twice",
794+
contentEncoding: "gzip, gzip",
795+
body: []byte("double"),
796+
expectedBody: []byte("double"),
797+
},
754798
{
755799
name: "unsupported_encoding",
756800
contentEncoding: "undefined",
757801
body: []byte("keeps_ORIGINAL"),
758-
expectedBody: []byte("keeps_ORIGINAL"),
802+
expectedBody: []byte("Unsupported Media Type"),
803+
},
804+
{
805+
name: "compress_not_implemented",
806+
contentEncoding: "compress",
807+
body: []byte("foo"),
808+
expectedBody: []byte("Not Implemented"),
759809
},
760810
{
761811
name: "gzip then unsupported",
762812
contentEncoding: "gzip, undefined",
763813
body: []byte("Go, be gzipped"),
764-
expectedBody: []byte("Go, be gzipped"),
814+
expectedBody: []byte("Unsupported Media Type"),
765815
},
766816
{
767817
name: "invalid_deflate",
768818
contentEncoding: "gzip,deflate",
769819
body: []byte("I'm not correctly compressed"),
770820
expectedBody: []byte(zlib.ErrHeader.Error()),
771821
},
822+
{
823+
name: "identity",
824+
contentEncoding: "identity",
825+
body: []byte("bar"),
826+
expectedBody: []byte("bar"),
827+
},
772828
}
773829

774830
for _, testObject := range tests {
@@ -780,25 +836,45 @@ func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
780836
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
781837
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)
782838

783-
if strings.Contains(tCase.contentEncoding, "gzip") {
784-
var b bytes.Buffer
785-
gz := gzip.NewWriter(&b)
786-
787-
_, err := gz.Write(tCase.body)
788-
require.NoError(t, err)
789-
790-
err = gz.Flush()
791-
require.NoError(t, err)
792-
793-
err = gz.Close()
794-
require.NoError(t, err)
795-
tCase.body = b.Bytes()
839+
encs := strings.Split(tCase.contentEncoding, ",")
840+
for _, enc := range encs {
841+
enc = strings.TrimSpace(enc)
842+
if strings.Contains(tCase.name, "invalid_deflate") && enc == StrDeflate {
843+
continue
844+
}
845+
switch enc {
846+
case "gzip":
847+
var b bytes.Buffer
848+
gz := gzip.NewWriter(&b)
849+
_, err := gz.Write(tCase.body)
850+
require.NoError(t, err)
851+
require.NoError(t, gz.Flush())
852+
require.NoError(t, gz.Close())
853+
tCase.body = b.Bytes()
854+
case StrDeflate:
855+
var b bytes.Buffer
856+
fl := zlib.NewWriter(&b)
857+
_, err := fl.Write(tCase.body)
858+
require.NoError(t, err)
859+
require.NoError(t, fl.Flush())
860+
require.NoError(t, fl.Close())
861+
tCase.body = b.Bytes()
862+
}
796863
}
797864

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

869+
switch {
870+
case strings.Contains(tCase.name, "unsupported"):
871+
require.Equal(t, StatusUnsupportedMediaType, c.Response().StatusCode())
872+
case strings.Contains(tCase.name, "compress_not_implemented"):
873+
require.Equal(t, StatusNotImplemented, c.Response().StatusCode())
874+
default:
875+
require.Equal(t, StatusOK, c.Response().StatusCode())
876+
}
877+
802878
// Check if body raw is the same as previous before decompression
803879
require.Equal(
804880
t, tCase.body, c.Request().Body(),
@@ -855,7 +931,7 @@ func Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) {
855931
compressWriter: compressGzip,
856932
},
857933
{
858-
contentEncoding: "deflate",
934+
contentEncoding: StrDeflate,
859935
compressWriter: compressDeflate,
860936
},
861937
{

docs/api/ctx.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ app.Get("/", func(c fiber.Ctx) error {
543543

544544
### Body
545545

546-
As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent, it will perform as [BodyRaw](#bodyraw).
546+
As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent (or when it is set to `identity`), it will perform as [BodyRaw](#bodyraw). If an unknown or unsupported encoding is encountered, the response status will be `415 Unsupported Media Type` or `501 Not Implemented`.
547547

548548
```go title="Signature"
549549
func (c fiber.Ctx) Body() []byte

0 commit comments

Comments
 (0)