gin-gonic / examples

A repository to host examples and tutorials for Gin.
https://gin-gonic.com/docs/
MIT License
3.75k stars 650 forks source link

Content negotiate custom mediatype #106

Open asbjornu opened 1 year ago

asbjornu commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

asbjornu commented 1 year ago

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