maybe related #633

Description

invalid content type

How to reproduce

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type obj struct {
    Message string
}

func main() {
    r := gin.New()
    r.POST("/", func(c *gin.Context) {
        body := obj{}
        err := c.BindJSON(&body)
        if err != nil {
            c.JSON(http.StatusBadRequest, obj{"bad message"})
            return
        }
        c.JSON(http.StatusOK, obj{"ok"})
    })
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

Expectations

I received expected Content-Type header (application/json) with valid json body {}

curl -vvv -d '{}' -H "Content-Type: application/json" -X POST http://localhost:8080/
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 2
> 
* upload completely sent off: 2 out of 2 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Thu, 12 Mar 2020 18:50:55 GMT
< Content-Length: 17
< 
{"Message":"ok"}
* Connection #0 to host localhost left intact

Actual result

I received invalid Content-Type header with invalid post body {

$ curl -vvv -d '{' -H "Content-Type: application/json" -X POST http://localhost:8080/
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1:8080...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 1
> 
* upload completely sent off: 1 out of 1 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Date: Thu, 12 Mar 2020 18:46:09 GMT
< Content-Length: 26
< Content-Type: text/plain; charset=utf-8
< 
{"Message":"bad message"}
* Connection #0 to host localhost left intact

Environment

  • go version:
go env

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/me/.cache/go-build"
GOENV="/home/me/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/me/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/tmp/issue/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build332976201=/tmp/go-build -gno-record-gcc-switches"
  • gin version (or commit ref): 1.5.0
  • operating system: linux

Comment From: chyroc

please use ShouldBindWith.

BindJSON well call ShouldBindWith and AbortWithError(when err occur) and set status-code and header

code:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type obj struct {
    Message string
}

func main() {
    r := gin.New()
    r.POST("/", func(c *gin.Context) {
        body := obj{}
        err := c.ShouldBindJSON(&body)
        if err != nil {
            c.JSON(http.StatusOK, obj{"bad message"})
            return
        }
        c.JSON(http.StatusOK, obj{"ok"})
    })
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

Comment From: celogeek

Why do we write the header here?

func (c *Context) AbortWithStatus(code int) {
    c.Status(code)
    c.Writer.WriteHeaderNow()
    c.Abort()
}

The issue here is the c.Writer.WriteHeaderNow()

For example if we have a middleware that handler errors, then we want to do a AbortWithStatusError, and the handler format the error in json format.

But because of the

c.Writer.WriteHeaderNow()

We need to write the "application/json" header before we call AbortWithStatusError.

So calling c.JSON in the middleware won't set the header properly. it will keep the "plain/text" as it seems to be the default.