From 7197cbad9d1b47077414e0c63c735579fde6cc80 Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Fri, 5 Apr 2024 09:05:30 +0200 Subject: [PATCH] Read Binary body with application/octet-stream Content-Type header --- ctx.go | 22 ++++++++-- ctx_test.go | 48 +++++++++++++++++++++ documentation/docs/guides/serialization.mdx | 17 ++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/ctx.go b/ctx.go index 75d830ec..a0826086 100644 --- a/ctx.go +++ b/ctx.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "html/template" + "io" "io/fs" "log/slog" "net/http" @@ -124,8 +125,10 @@ type ContextNoBody struct { readOptions readOptions } -var _ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx. -var _ context.Context = ContextNoBody{} // Check that ContextNoBody implements context.Context. +var ( + _ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx. + _ context.Context = ContextNoBody{} // Check that ContextNoBody implements context.Context. +) func (c ContextNoBody) Body() (any, error) { slog.Warn("this method should not be called. It probably happened because you passed the context to another controller.") @@ -423,9 +426,20 @@ func body[B any](c ContextNoBody) (B, error) { case "application/x-www-form-urlencoded", "multipart/form-data": body, err = readURLEncoded[B](c.Req, c.readOptions) case "application/xml": - return readXML[B](c.Req.Context(), c.Req.Body, c.readOptions) + body, err = readXML[B](c.Req.Context(), c.Req.Body, c.readOptions) case "application/x-yaml": - return readYAML[B](c.Req.Context(), c.Req.Body, c.readOptions) + body, err = readYAML[B](c.Req.Context(), c.Req.Body, c.readOptions) + case "application/octet-stream": + // Read c.Req Body to bytes + bytes, err := io.ReadAll(c.Req.Body) + if err != nil { + return body, err + } + respBytes, ok := any(bytes).(B) + if !ok { + return body, fmt.Errorf("could not convert bytes to %T. To read binary data from the request, use []byte as the body type", body) + } + body = respBytes case "application/json": fallthrough default: diff --git a/ctx_test.go b/ctx_test.go index b33d352d..59fedf3a 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -170,6 +170,23 @@ func TestContext_Body(t *testing.T) { require.Equal(t, 30, body.Age) }) + t.Run("can read JSON body with Content-Type application/json", func(t *testing.T) { + // Create new Reader + a := strings.NewReader(`{"name":"John","age":30}`) + + // Test an http request + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://example.com/foo", a) + r.Header.Add("Content-Type", "application/json") + + c := NewContext[testStruct](w, r, readOptions{}) + + body, err := c.Body() + require.NoError(t, err) + require.Equal(t, "John", body.Name) + require.Equal(t, 30, body.Age) + }) + t.Run("can read JSON body twice", func(t *testing.T) { a := strings.NewReader(`{"name":"John","age":30}`) @@ -251,6 +268,37 @@ func TestContext_Body(t *testing.T) { require.Equal(t, 30, body.Age) }) + t.Run("can read bytes", func(t *testing.T) { + // Create new Reader with pure bytes from an image + a := bytes.NewReader([]byte(`image`)) + + // Test an http request + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://example.com/foo", a) + r.Header.Add("Content-Type", "application/octet-stream") + + c := NewContext[[]byte](w, r, readOptions{}) + body, err := c.Body() + require.NoError(t, err) + require.Equal(t, []byte(`image`), body) + }) + + t.Run("cannot read bytes if expected type is different than bytes", func(t *testing.T) { + // Create new Reader with pure bytes from an image + a := bytes.NewReader([]byte(`image`)) + + // Test an http request + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://example.com/foo", a) + r.Header.Add("Content-Type", "application/octet-stream") + + c := NewContext[*struct{}](w, r, readOptions{}) + body, err := c.Body() + require.Error(t, err) + require.ErrorContains(t, err, "use []byte as the body type") + require.Equal(t, (*struct{})(nil), body) + }) + t.Run("can read XML body", func(t *testing.T) { a := bytes.NewReader([]byte(` diff --git a/documentation/docs/guides/serialization.mdx b/documentation/docs/guides/serialization.mdx index 532d93bd..42018e07 100644 --- a/documentation/docs/guides/serialization.mdx +++ b/documentation/docs/guides/serialization.mdx @@ -6,6 +6,23 @@ Fuego automatically serializes and deserializes inputs and outputs with standard +## Deserialize binary data + +If you just want to read the body of the request as a byte slice, you can use the `[]byte` receiver type. + +Don't forget to set the request `Content-Type` header to `application/octet-stream`. + +```go +fuego.Put(s, "/blob", func(c *fuego.ContextWithBody[[]byte]) (any, error) { + body, err := c.Body() + if err != nil { + return nil, err + } + + return body, nil +}) +``` + ## Custom serialization But you can also use the `Serialize` and `Deserialize` functions to manually serialize and deserialize data.