Skip to content

Commit 7197cba

Browse files
committed
Read Binary body with application/octet-stream Content-Type header
1 parent d228d23 commit 7197cba

File tree

3 files changed

+83
-4
lines changed

3 files changed

+83
-4
lines changed

ctx.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"html/template"
7+
"io"
78
"io/fs"
89
"log/slog"
910
"net/http"
@@ -124,8 +125,10 @@ type ContextNoBody struct {
124125
readOptions readOptions
125126
}
126127

127-
var _ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx.
128-
var _ context.Context = ContextNoBody{} // Check that ContextNoBody implements context.Context.
128+
var (
129+
_ ctx[any] = ContextNoBody{} // Check that ContextNoBody implements Ctx.
130+
_ context.Context = ContextNoBody{} // Check that ContextNoBody implements context.Context.
131+
)
129132

130133
func (c ContextNoBody) Body() (any, error) {
131134
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) {
423426
case "application/x-www-form-urlencoded", "multipart/form-data":
424427
body, err = readURLEncoded[B](c.Req, c.readOptions)
425428
case "application/xml":
426-
return readXML[B](c.Req.Context(), c.Req.Body, c.readOptions)
429+
body, err = readXML[B](c.Req.Context(), c.Req.Body, c.readOptions)
427430
case "application/x-yaml":
428-
return readYAML[B](c.Req.Context(), c.Req.Body, c.readOptions)
431+
body, err = readYAML[B](c.Req.Context(), c.Req.Body, c.readOptions)
432+
case "application/octet-stream":
433+
// Read c.Req Body to bytes
434+
bytes, err := io.ReadAll(c.Req.Body)
435+
if err != nil {
436+
return body, err
437+
}
438+
respBytes, ok := any(bytes).(B)
439+
if !ok {
440+
return body, fmt.Errorf("could not convert bytes to %T. To read binary data from the request, use []byte as the body type", body)
441+
}
442+
body = respBytes
429443
case "application/json":
430444
fallthrough
431445
default:

ctx_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,23 @@ func TestContext_Body(t *testing.T) {
170170
require.Equal(t, 30, body.Age)
171171
})
172172

173+
t.Run("can read JSON body with Content-Type application/json", func(t *testing.T) {
174+
// Create new Reader
175+
a := strings.NewReader(`{"name":"John","age":30}`)
176+
177+
// Test an http request
178+
w := httptest.NewRecorder()
179+
r := httptest.NewRequest("GET", "http://example.com/foo", a)
180+
r.Header.Add("Content-Type", "application/json")
181+
182+
c := NewContext[testStruct](w, r, readOptions{})
183+
184+
body, err := c.Body()
185+
require.NoError(t, err)
186+
require.Equal(t, "John", body.Name)
187+
require.Equal(t, 30, body.Age)
188+
})
189+
173190
t.Run("can read JSON body twice", func(t *testing.T) {
174191
a := strings.NewReader(`{"name":"John","age":30}`)
175192

@@ -251,6 +268,37 @@ func TestContext_Body(t *testing.T) {
251268
require.Equal(t, 30, body.Age)
252269
})
253270

271+
t.Run("can read bytes", func(t *testing.T) {
272+
// Create new Reader with pure bytes from an image
273+
a := bytes.NewReader([]byte(`image`))
274+
275+
// Test an http request
276+
w := httptest.NewRecorder()
277+
r := httptest.NewRequest("GET", "http://example.com/foo", a)
278+
r.Header.Add("Content-Type", "application/octet-stream")
279+
280+
c := NewContext[[]byte](w, r, readOptions{})
281+
body, err := c.Body()
282+
require.NoError(t, err)
283+
require.Equal(t, []byte(`image`), body)
284+
})
285+
286+
t.Run("cannot read bytes if expected type is different than bytes", func(t *testing.T) {
287+
// Create new Reader with pure bytes from an image
288+
a := bytes.NewReader([]byte(`image`))
289+
290+
// Test an http request
291+
w := httptest.NewRecorder()
292+
r := httptest.NewRequest("GET", "http://example.com/foo", a)
293+
r.Header.Add("Content-Type", "application/octet-stream")
294+
295+
c := NewContext[*struct{}](w, r, readOptions{})
296+
body, err := c.Body()
297+
require.Error(t, err)
298+
require.ErrorContains(t, err, "use []byte as the body type")
299+
require.Equal(t, (*struct{})(nil), body)
300+
})
301+
254302
t.Run("can read XML body", func(t *testing.T) {
255303
a := bytes.NewReader([]byte(`
256304
<TestStruct>

documentation/docs/guides/serialization.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ Fuego automatically serializes and deserializes inputs and outputs with standard
66

77
<FlowChart selected="resp" />
88

9+
## Deserialize binary data
10+
11+
If you just want to read the body of the request as a byte slice, you can use the `[]byte` receiver type.
12+
13+
Don't forget to set the request `Content-Type` header to `application/octet-stream`.
14+
15+
```go
16+
fuego.Put(s, "/blob", func(c *fuego.ContextWithBody[[]byte]) (any, error) {
17+
body, err := c.Body()
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
return body, nil
23+
})
24+
```
25+
926
## Custom serialization
1027

1128
But you can also use the `Serialize` and `Deserialize` functions to manually serialize and deserialize data.

0 commit comments

Comments
 (0)