mirror of https://github.com/gofiber/fiber.git
🔥 Add support for application/problem+json (#2704)
🔥 Add support for custom JSON content headers
pull/2721/head
parent
1e55045a30
commit
9f082af045
|
@ -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)
|
||||
|
|
|
@ -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
21
ctx.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
61
ctx_test.go
61
ctx_test.go
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
// => }"
|
||||
})
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue