Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content negotiate custom mediatype #106

Open
asbjornu opened this issue May 1, 2023 · 6 comments
Open

Content negotiate custom mediatype #106

asbjornu opened this issue May 1, 2023 · 6 comments

Comments

@asbjornu
Copy link

asbjornu commented May 1, 2023

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
Copy link
Author

asbjornu commented May 1, 2023

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
Copy link
Author

asbjornu commented May 1, 2023

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
Copy link
Author

asbjornu commented May 1, 2023

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
Copy link
Author

asbjornu commented May 5, 2023

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?

@jarrodhroberson
Copy link

jarrodhroberson commented May 8, 2023 via email

@asbjornu
Copy link
Author

@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 #106 (comment), 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().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants