Skip to content

Commit

Permalink
🔥 Add support for application/problem+json (#2704)
Browse files Browse the repository at this point in the history
🔥 Add support for custom JSON content headers
  • Loading branch information
rhburt committed Nov 13, 2023
1 parent 1e55045 commit 9f082af
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 8 deletions.
8 changes: 6 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,16 @@ func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent {
}

// JSON sends a JSON request.
func (a *Agent) JSON(v interface{}) *Agent {
func (a *Agent) JSON(v interface{}, ctype ...string) *Agent {
if a.jsonEncoder == nil {
a.jsonEncoder = json.Marshal
}

a.req.Header.SetContentType(MIMEApplicationJSON)
if len(ctype) > 0 {
a.req.Header.SetContentType(ctype[0])
} else {
a.req.Header.SetContentType(MIMEApplicationJSON)
}

if body, err := a.jsonEncoder(v); err != nil {
a.errs = append(a.errs, err)
Expand Down
14 changes: 14 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ func Test_Client_Agent_RetryIf(t *testing.T) {

func Test_Client_Agent_Json(t *testing.T) {
t.Parallel()
// Test without ctype parameter
handler := func(c *Ctx) error {
utils.AssertEqual(t, MIMEApplicationJSON, string(c.Request().Header.ContentType()))

Expand All @@ -662,6 +663,19 @@ func Test_Client_Agent_Json(t *testing.T) {
}

testAgent(t, handler, wrapAgent, `{"success":true}`)

// Test with ctype parameter
handler = func(c *Ctx) error {
utils.AssertEqual(t, "application/problem+json", string(c.Request().Header.ContentType()))

return c.Send(c.Request().Body())
}

wrapAgent = func(a *Agent) {
a.JSON(data{Success: true}, "application/problem+json")
}

testAgent(t, handler, wrapAgent, `{"success":true}`)
}

func Test_Client_Agent_Json_Error(t *testing.T) {
Expand Down
21 changes: 17 additions & 4 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,15 +373,22 @@ func decoderBuilder(parserConfig ParserConfig) interface{} {
// BodyParser binds the request body to a struct.
// It supports decoding the following content types based on the Content-Type header:
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
// All JSON extenstion mime types are supported (eg. application/problem+json)
// If none of the content types above are matched, it will return a ErrUnprocessableEntity error
func (c *Ctx) BodyParser(out interface{}) error {
// Get content-type
ctype := utils.ToLower(c.app.getString(c.fasthttp.Request.Header.ContentType()))

ctype = utils.ParseVendorSpecificContentType(ctype)

// Only use ctype string up to and excluding byte ';'
ctypeEnd := strings.IndexByte(ctype, ';')
if ctypeEnd != -1 {
ctype = ctype[:ctypeEnd]
}

// Parse body accordingly
if strings.HasPrefix(ctype, MIMEApplicationJSON) {
if strings.HasSuffix(ctype, "json") {
return c.app.config.JSONDecoder(c.Body(), out)
}
if strings.HasPrefix(ctype, MIMEApplicationForm) {
Expand Down Expand Up @@ -886,14 +893,20 @@ func (c *Ctx) Is(extension string) bool {
// Array and slice values encode as JSON arrays,
// except that []byte encodes as a base64-encoded string,
// and a nil slice encodes as the null JSON value.
// This method also sets the content header to application/json.
func (c *Ctx) JSON(data interface{}) error {
// If the ctype parameter is given, this method will set the
// Content-Type header equal to ctype. If ctype is not given,
// The Content-Type header will be set to application/json.
func (c *Ctx) JSON(data interface{}, ctype ...string) error {
raw, err := c.app.config.JSONEncoder(data)
if err != nil {
return err
}
c.fasthttp.Response.SetBodyRaw(raw)
c.fasthttp.Response.Header.SetContentType(MIMEApplicationJSON)
if len(ctype) > 0 {
c.fasthttp.Response.Header.SetContentType(ctype[0])
} else {
c.fasthttp.Response.Header.SetContentType(MIMEApplicationJSON)
}
return nil
}

Expand Down
61 changes: 61 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ func Test_Ctx_BodyParser(t *testing.T) {
testDecodeParser(MIMEApplicationForm, "name=john")
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")

// Ensure JSON extension MIME type gets parsed as JSON
testDecodeParser("application/problem+json", `{"name":"john"}`)

testDecodeParserError := func(contentType, body string) {
c.Request().Header.SetContentType(contentType)
c.Request().SetBody([]byte(body))
Expand Down Expand Up @@ -708,6 +711,30 @@ func Benchmark_Ctx_BodyParser_JSON(b *testing.B) {
utils.AssertEqual(b, "john", d.Name)
}

// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON_Extension -benchmem -count=4
func Benchmark_Ctx_BodyParser_JSON_Extension(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type Demo struct {
Name string `json:"name"`
}
body := []byte(`{"name":"john"}`)
c.Request().SetBody(body)
c.Request().Header.SetContentType("application/problem+json")
c.Request().Header.SetContentLength(len(body))
d := new(Demo)

b.ReportAllocs()
b.ResetTimer()

for n := 0; n < b.N; n++ {
_ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below
}
utils.AssertEqual(b, nil, c.BodyParser(d))
utils.AssertEqual(b, "john", d.Name)
}

// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_XML -benchmem -count=4
func Benchmark_Ctx_BodyParser_XML(b *testing.B) {
app := New()
Expand Down Expand Up @@ -2927,6 +2954,7 @@ func Test_Ctx_JSON(t *testing.T) {

utils.AssertEqual(t, true, c.JSON(complex(1, 1)) != nil)

// Test without ctype
err := c.JSON(Map{ // map has no order
"Name": "Grame",
"Age": 20,
Expand All @@ -2935,6 +2963,15 @@ func Test_Ctx_JSON(t *testing.T) {
utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body()))
utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("content-type")))

// Test with ctype
err = c.JSON(Map{ // map has no order
"Name": "Grame",
"Age": 20,
}, "application/problem+json")
utils.AssertEqual(t, nil, err)
utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body()))
utils.AssertEqual(t, "application/problem+json", string(c.Response().Header.Peek("content-type")))

testEmpty := func(v interface{}, r string) {
err := c.JSON(v)
utils.AssertEqual(t, nil, err)
Expand Down Expand Up @@ -2990,6 +3027,30 @@ func Benchmark_Ctx_JSON(b *testing.B) {
utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body()))
}

// go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4
func Benchmark_Ctx_JSON_Ctype(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
type SomeStruct struct {
Name string
Age uint8
}
data := SomeStruct{
Name: "Grame",
Age: 20,
}
var err error
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
err = c.JSON(data, "application/problem+json")
}
utils.AssertEqual(b, nil, err)
utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body()))
utils.AssertEqual(b, "application/problem+json", string(c.Response().Header.Peek("content-type")))
}

// go test -run Test_Ctx_JSONP
func Test_Ctx_JSONP(t *testing.T) {
t.Parallel()
Expand Down
20 changes: 18 additions & 2 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -797,11 +797,11 @@ app.Get("/", func(c *fiber.Ctx) error {
Converts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package.
:::info
JSON also sets the content header to **application/json**.
JSON also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to **application/json**.
:::
```go title="Signature"
func (c *Ctx) JSON(data interface{}) error
func (c *Ctx) JSON(data interface{}, ctype ...string) error
```
```go title="Example"
Expand All @@ -827,6 +827,22 @@ app.Get("/json", func(c *fiber.Ctx) error {
})
// => Content-Type: application/json
// => "{"name": "Grame", "age": 20}"

return c.JSON(fiber.Map{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
}, "application/problem+json")
// => Content-Type: application/problem+json
// => "{
// => "type": "https://example.com/probs/out-of-credit",
// => "title": "You do not have enough credit.",
// => "status": 403,
// => "detail": "Your current balance is 30, but that costs 50.",
// => "instance": "/account/12345/msgs/abc",
// => }"
})
```
Expand Down

0 comments on commit 9f082af

Please sign in to comment.