🔥 Add support for application/problem+json (#2704)

🔥 Add support for custom JSON content headers
pull/2721/head
Reid Hurlburt 2023-11-13 10:18:05 -04:00 committed by GitHub
parent 1e55045a30
commit 9f082af045
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 8 deletions

View File

@ -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)

View File

@ -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()))
@ -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) {

21
ctx.go
View File

@ -373,6 +373,7 @@ 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
@ -380,8 +381,14 @@ func (c *Ctx) BodyParser(out interface{}) error {
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) {
@ -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
}

View File

@ -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))
@ -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()
@ -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,
@ -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)
@ -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()

View File

@ -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"
@ -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",
// => }"
})
```