examples icon indicating copy to clipboard operation
examples copied to clipboard

Content negotiate custom mediatype

Open asbjornu opened this issue 1 year ago • 6 comments

As with #41, I'm trying to have Gin negotiate a custom mediatype. From the client, I'm sending the following request header:

Accept: application/problem+json, application/json

And on the server, I'm trying to negotiate all errors (with middleware) as such:

problem := Problem{
    Detail: errorText,
    Status: status,
    Title:  http.StatusText(status),
}
c.Negotiate(status, gin.Negotiate{
    Offered:  []string{"application/problem+json", gin.MIMEJSON, gin.MIMEHTML},
    HTMLName: "error",
    HTMLData: &problem,
    JSONData: &problem,
})

However, somewhere before I'm able to handle the error, Gin intercepts and for some reason decides that the requested Accept header can't be satisfied, writes the following to the log, and responds with 406 Not Acceptable:

Error #01: the accepted formats are not offered by the server

I would love to see a full example of how content negotiation a custom mediatype in Gin works. Would you be able to contribute your working code @jarrodhroberson?

asbjornu avatar May 01 '23 19:05 asbjornu

It's weird, because if I do c.NegotiateFormat("application/problem+json", gin.MIMEJSON, gin.MIMEHTML), I get application/problem+json in return as expected. However, c.Negotiate() somehow comes to another conclusion and responds with 406 Not Acceptable.

asbjornu avatar May 01 '23 21:05 asbjornu

Ok, after digging into the Gin source code, I think I understand what's going on. In context.go:1110-1135, Negotiate() only supports MIMEJSON, MIMEHTML, MIMEXML, MIMEYAML and MIMETOML:

func (c *Context) Negotiate(code int, config Negotiate) {
	switch c.NegotiateFormat(config.Offered...) {
	case binding.MIMEJSON:
		data := chooseData(config.JSONData, config.Data)
		c.JSON(code, data)

	case binding.MIMEHTML:
		data := chooseData(config.HTMLData, config.Data)
		c.HTML(code, config.HTMLName, data)

	case binding.MIMEXML:
		data := chooseData(config.XMLData, config.Data)
		c.XML(code, data)

	case binding.MIMEYAML:
		data := chooseData(config.YAMLData, config.Data)
		c.YAML(code, data)

	case binding.MIMETOML:
		data := chooseData(config.TOMLData, config.Data)
		c.TOML(code, data)

	default:
		c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
	}
}

Which of course makes some sense. It would be nice if +json and +xml was translated into MIMEJSON and MIMEXL respectively, but knowing this, I should be able to circumvent it somehow.

asbjornu avatar May 01 '23 21:05 asbjornu

Hm, no. I seem unable to properly set the Content-Type of the response to … anything, really. For some weird reason Gin responds with:

Content-Type: text/plain; charset=utf-8

Even though I've explicitly set c.Header("Content-Type", "application/problem+json"). This is my handler code now:

problem := Problem{
    Detail: errorText,
    Status: status,
    Title:  http.StatusText(status),
}
allMimeTypes := []string{"application/problem+json", gin.MIMEJSON, gin.MIMEHTML)
negotiatedMimeType := c.NegotiateFormat(allMimeTypes...)
switch negotiatedMimeType {
case gin.MIMEHTML:
    c.HTML(status, "error", &problem)
default:
    c.JSON(status, &problem)
}
c.Header("Content-Type", negotiatedMimeType)
c.Abort()

Why doesn't c.Header("Content-Type", negotiatedMimeType) work here?

asbjornu avatar May 01 '23 21:05 asbjornu

As you closed #41, were you able to set the Content-Type of the response @jarrodhroberson? If so, could you please post a full example of how you got it to work?

asbjornu avatar May 05 '23 09:05 asbjornu

my solution is in #41 and the example I posted works when combined with the solution I provided when I closed that issue.

On Fri, May 5, 2023 at 5:14 AM Asbjørn Ulsberg @.***> wrote:

As you closed #41 https://github.com/gin-gonic/examples/issues/41, were you able to set the Content-Type of the response @jarrodhroberson https://github.com/jarrodhroberson? If so, could you please post a full example of how you got it to work?

— Reply to this email directly, view it on GitHub https://github.com/gin-gonic/examples/issues/106#issuecomment-1535966532, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABF773MRIAP5IBNC6KZQELXETAF5ANCNFSM6AAAAAAXSDPBRU . You are receiving this because you were mentioned.Message ID: @.***>

-- Jarrod Roberson 678.551.2852

jarrodhroberson avatar May 08 '23 21:05 jarrodhroberson

@jarrodhroberson, so with the following, you're able to have Gin respond with Content-Type: application/vnd.health.json;version=1.0.0?

func Health(c *gin.Context) {
	startupTime := c.MustGet("startupTime").(time.Time)
	status := models.NewHealth(startupTime)
	c.Negotiate(http.StatusOK, gin.Negotiate{
		Offered:  []string{"application/vnd.health.json;version=1.0.0", gin.MIMEJSON, gin.MIMEYAML, gin.MIMEXML, gin.MIMEHTML},
		HTMLName: "",
		HTMLData: status,
		JSONData: status,
		XMLData:  status,
		YAMLData: status,
		Data:     status,
	})
}

If you read https://github.com/gin-gonic/examples/issues/106#issuecomment-1530323693, I can't see how that actually works, because c.Negotiate() only supports MIMEJSON, MIMEHTML, MIMEXML, MIMEYAML and MIMETOML. Any other MIME type and it will do c.AbortWithError().

asbjornu avatar May 10 '23 19:05 asbjornu