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

net/http: support content negotiation #19307

Open
kevinburke opened this issue Feb 27, 2017 · 44 comments
Open

net/http: support content negotiation #19307

kevinburke opened this issue Feb 27, 2017 · 44 comments
Labels
help wanted NeedsFix The path to resolution is known, but the work has not been done. Proposal Proposal-Accepted
Milestone

Comments

@kevinburke
Copy link
Contributor

kevinburke commented Feb 27, 2017

Content negotiation is, roughly, the process of figuring out what content type the response should take, based on an Accept header present in the request.

An example might be an image server that figures out which image format to send to the client, or an API that wants to return HTML to browsers but JSON to command line clients.

It's tricky to get right because the client may accept multiple content types, and the server may have multiple types available, and it can be difficult to match these correctly. I think this is a good fit for Go standard (or adjacent) libraries because:

  • there's a formal specification for how it should behave: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
  • it's annoying to implement yourself, correctly; you have to write a mini-parser.
  • it would take one function to implement, which makes it annoying to import an entire third party library for (assuming you find the right one)

I've seen people hack around this in various ways:

  • checking whether the Accept header contains a certain string
  • checking for the first matching value,
  • returning different content types based on the User-Agent
  • requiring different URI's to get different content.

There's a sample implementation here with a pretty good API: https://godoc.org/github.com/golang/gddo/httputil#NegotiateContentType

// NegotiateContentType returns the best offered content type for the request's
// Accept header. If two offers match with equal weight, then the more specific
// offer is preferred.  For example, text/* trumps */*. If two offers match
// with equal weight and specificity, then the offer earlier in the list is
// preferred. If no offers match, then defaultOffer is returned.
func NegotiateContentType(r *http.Request, offers []string, defaultOffer string) string

offers are content-types that the server can respond with, and can include wildcards like text/* or */*. defaultOffer is the default content type, if nothing in offers matches. The returned value never has wildcards.

So you'd call it with something like

availableTypes := []string{"application/json", "text/plain", "text/html", "text/*"}
ctype: = NegotiateContentType(req, availableTypes, "application/json")

If the first value in offers is text/* and the client requests text/plain, NegotiateContentType will return text/plain. This is why you have to have a default - you can't just return the first value in offers because it might include a wildcard.

In terms of where it could live, I'm guessing that net/http is frozen at this point. Maybe one of the packages in x/net would be a good fit? There is also a similar function for parsing Accept-Language headers in x/text/language. Open to ideas.

@quentinmit
Copy link
Contributor

Funny you should mention this, I was just looking for this for use in x/perf. Your proposed API is reasonable, but you should explicitly document what happens in exceptional circumstances:

  • What happens when multiple content types end up with the same q? I think I would expect the first one in offers to be chosen.
  • What happen if the Accept header(s) cannot be parsed?

@kevinburke
Copy link
Contributor Author

What happens when multiple content types end up with the same q? I think I would expect the first one in offers to be chosen.

That sounds reasonable to me.

What happen if the Accept header(s) cannot be parsed?

The spec includes a grammar but doesn't really say what happens if you can't match it, other than you should return a 406 if you can't match anything. I think we should just return the default?

@quentinmit quentinmit added this to the Proposal milestone Feb 28, 2017
@quentinmit quentinmit added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Feb 28, 2017
@kevinburke
Copy link
Contributor Author

@bradfitz suggested (if we want to go forward with this proposal) it could live in net/http/httputil.

@bradfitz
Copy link
Contributor

bradfitz commented Mar 1, 2017

I also mentioned that it seems like defaultOffer string could go away and we say that the first element in the slice was the default.

@kevinburke
Copy link
Contributor Author

kevinburke commented Mar 1, 2017

Sorry, I tried to edit the description to cover that case.

I also mentioned that it seems like defaultOffer string could go away and we say that the first element in the slice was the default.

I think the problem with this would be, if you had "text/*" as the first element in offers, it would match if the client sent text/plain and text/plain would be returned from the function.

If we return the first value in offers as the default, then the response value would contain a wildcard ("text/*"), which it doesn't in any other circumstance. Maybe that's okay.

@jimmyfrasche
Copy link
Member

@kevinburke What's wrong with returning text/plain if the first offer is text/*? I don't see why it has to return the exact string passed in—don't you need to know the negotiated type to set the header in the response?

Also, if negotiation fails does it return the empty string? I'd rather have an error to distinguish between expected failure, negotiation failed, and unexpected failure, bad request.

@rsc rsc changed the title proposal: add a function for HTTP content negotiation proposal: net/http: support content negotiation Mar 6, 2017
@bradfitz
Copy link
Contributor

@kevinburke, any response to @jimmyfrasche? This seems fine if the simplified signature works. You want to prepare a CL?

@bradfitz bradfitz added Proposal Proposal-Accepted and removed NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. labels Mar 13, 2017
@bradfitz bradfitz modified the milestones: Go1.9Maybe, Proposal Mar 13, 2017
@bradfitz
Copy link
Contributor

Also, we like to make sure we append "; charset=utf-8" to Content-Types to be explicit. Can we make sure that that's still automatic?

@bradfitz bradfitz modified the milestones: Go1.9Maybe, Go1.10 Jul 20, 2017
@cellfish
Copy link

cellfish commented Jul 20, 2017

offers are content-types that the server can respond with, and can include wildcards like text/* or */*

I don't think wildcards should be allowed in the offers. The server knows exactly what content types it supports and returning a wildcard content type seems weird. Nor do I think the RFC says wildcards are OK in the content-type header.
Content-Type (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17) mentions media-type (https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7) that does not have wild cards.
Accept (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1) mentions media-range which includes wild cards.

I agree with @jimmyfrasche that returning an error seems better so that the caller can know the reason if a default (first offered) is returned. maybe even not return a default at all. If there is a default the caller can handle that rather than build it as part of this method. For example there is a difference between not finding a match in an existing accept header and when the accept header is missing IMO.

Since the RFC for Accept supports multiple parameters (such as charset) how about returning something more than just a string like:

type NegotiatedContentType struct {
   ContentType string
   Params map[string]string
}

func NegotiateContentType(r *http.Request, offers []string) (NegotiatedContentType, error)

@bradfitz
Copy link
Contributor

@kevinburke, what's the status here?

It can also live in x/net if you want.

It's probably too late for Go 1.10, though.

@bradfitz bradfitz modified the milestones: Go1.10, Go1.11 Nov 15, 2017
@kevinburke
Copy link
Contributor Author

It's too late and I'm buried in work stuff unfortunately, I probably won't have time to try to offer an implementation. I remember when I tried to implement it I ran into non-trivial problems with the design and some kinds of inputs.

@bradfitz bradfitz modified the milestones: Go1.11, Unplanned Nov 16, 2017
@bradfitz bradfitz changed the title proposal: net/http: support content negotiation x/net/http: support content negotiation Nov 16, 2017
@leighmcculloch
Copy link
Contributor

@markusthoemmes I see in your port of goautoneg that you're proposing in its readme be internalized into the stdlib you dropped the Negotiate function. Many web apps are also going to need to match against a content-type they have as a supported content type and shouldn't just use the results of the parser verbatim. Providing a Matcher like @flimzy suggested would go a long way and might be a good replacement for goautoneg's Negotiate if it's going to be left out.

As an example we use the goautoneg Negotiate function here in a way that I think many web apps would.

@elnormous
Copy link

Since Go standard library (and no library I could find) does not support content negotiation that follows RFC 7231, I wrote my own: https://github.com/elnormous/contenttype
I would be happy if you could take a look at it and consider including the functionality in the standard library.

@jamietanna
Copy link

jamietanna commented Aug 5, 2022

I've also written a library for this - which is BSD-3 licensed (like Go) and handles a lot of the edge cases I've hit before when writing these (more info on the blog)

gitlab.com/jamietanna/content-negotiation-go

I'd be happy to look at what we'd need to do to make this work within Go's codebase and follow the standard library's style?

@awagner-mainz
Copy link

Just for the record, I have written (in Go) a content negotiation plugin for the caddy webserver:

https://github.com/mpilhlt/caddy-conneg

It relies on @elnormous's library mentioned above, adding stuff from go's own x/text/language libraries. (Will have a look at @jamietanna's library, too - when I find the time to do so, that is.)

Thanks to you all for this issue and discussion, and for all your efforts, obviously. I am posting this here so that it may eventually provide some ideas about use cases and parameters that one would maybe like to throw at the library/libraries.

@fgm
Copy link

fgm commented Feb 15, 2023

FWIW, I just noticed Gin provides another implementation for this, with the gin.Context.NegotiateFormat(accepted ...string) string function

https://pkg.go.dev/github.com/gin-gonic/gin#Context.NegotiateFormat

It does not handle quality factors, AFAICS, though.

@vault-thirteen
Copy link

Many people have been writing the same or similar code for their projects. Me also is not an exception. I remember writing a library for this functionality. Unfortunately, it was done for a closed-source project which is not controlled by me, so all the code is lost there. But this is not the case. I am surprised how long does it take for developers of this language to accept offers made by people across the Internet. We see that there are a lot of examples of already written code which is already working, but this issue is still opened and we do not see this fuctionality in the standard Go language.

@iamcalledrob
Copy link

I ended up here trying to find this in the standard library. Echoing what others have said, I expected it to be part of the standard library because:

  1. Many applications of nontrivial complexity will need this eventually
  2. The spec is tricky to implement properly

Many of the 3rd party packages linked here have a complicated API or don't implement the spec as-per the RFC, e.g. the quality scores.

For my own use-cases, the API I'd most like to see would be a single func along the lines of:

func ResponseType(accept string, offers ...string) string

// or

var r *http.Request
r.ResponseType(offers ...string) string

Rationale:

  1. This makes switching on the result possible
  2. Default/zero behaviour is delegated to the caller
  3. Doesn't introduce errors to handle, as some packages do
  4. (method on *http.Request) Doesn't require the caller to manually extract the "Accept" header value, which is tedious.

A common use case might look like:

switch r.ResponseType("text/html", "application/json") {
case "text/html":
    writeHtmlResponse(w, ...)
case "application/json":
    writeJsonResponse(w, ...)
default:
    w.WriteHeader(http.StatusNotAcceptable)
}

In case it helps anyone, I use a shim for this for my own projects as follows, using github.com/golang/gddo/httputil which seems to be a solid implementation:

import "github.com/golang/gddo/httputil"

func ResponseType(r *http.Request, offers ...string) string {
    return httputil.NegotiateContentType(r, offers, "")
}

@wwaites
Copy link

wwaites commented Jun 27, 2024

Hi there. Original author of the goautoneg library here. My apologies @markusthoemmes, I never saw your bug reports and only just today learned that it got used in things like Kubernetes and Prometheus (because I was wondering if Caddy could do autoneg). As Go developed more sophisticated and somewhat opinionated packaging infrastructure, now long ago, I probably got tired of keeping up with it. I also no longer had the original use case that made me write it to begin with.

Anyways, though it's a bit late now, but just to say that I am perfectly happy for it to get used wherever you like under some sort of free license. Sorry for stuffing license information into the README, that was silly! It would make me happy if some small piece of it made it into Go's system libraries.

@bwplotka
Copy link

bwplotka commented Jan 21, 2025

👋🏽 We (Prometheus team) are finding even more use cases for a common content negotiation logic, so instead of providing local one for Prometheus, I went ahead and proposed something as a x/net/http/httpcontent package here.

Discussion here was quite rich and useful (thanks for so many ideas and implementations!), so I attempted to summarize all findings and aspects in the design doc. Feel free to give feedback in CL, doc comment or here!

It felt like the one of the main debates here was if we should do the generic string based or rich structured signature. I went for something that (hopefully) has the most chances to end up in the standard non-experimental package one day, so string-based. I also attempted to make one function that supports all "Accept" like headers that follow RFC 2616 Accept format. Trade-offs are discussed here.

Proposed implementation (https://go-review.googlesource.com/c/net/+/642956):

// Negotiate returns the best offered content for the provided accepted values.
// Accept values is expected to follow the Accept header format as defined in
// https://datatracker.ietf.org/doc/html/rfc2616#section-14.1. Negotiate is expected
// to also support similar Accept-* HTTP 1.1. headers like -Charset, -Encoding
// -Language, etc., as long as they follow a similar (even if simplified) format.
//
// If two offers match with equal quality factor, then the more specific
// offer is preferred.  For example, text/* trumps */*. If two offers match
// with equal weight and specificity, then the offer earlier in the list is
// preferred. If no offers match, then the empty string is returned.
func Negotiate(accepts []string, offers []string) string

Example use:

// On client side:
r, _ := http.NewRequest("", "http://example.com", nil)
r.Header.Set("Accept", "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5")

// In your ServeHTTP handler:
negotiated := httpcontent.Negotiate(r.Header.Values("Accept"), []string{"text/html", "text/html;level=1"})
if negotiated == "" {
   // Can't agree on the content, return 415 or try default content type...
}

fmt.Println(negotiated)

// Output:
//  text/html;level=1

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/642956 mentions this issue: x/net/http/httpcontent: Add content Negotiate function.

@neild
Copy link
Contributor

neild commented Jan 27, 2025

This proposal was accepted some years ago. We have a proposed implementation now in https://go.dev/cl/642956.

I'm not sure which package the accepted proposal was adding the new function to. The issue title references "x/net/http" (which does not exist), #19307 (comment) mentions "net/http/httputil" (which does, but we kind of regret it these days).

As I understand our current stance on x/ repo packages, we are trying to avoid adding new x/ packages for public use that don't have a path to inclusion in the standard library. If it's worth it for the Go project to provide and support a package, we should do so in std.

Given that, I see a few possibilities here:

  1. Add the NegotiateContent function to net/http. The net/http package already has too large an API surface, but this function stands alone and doesn't complicate the rest of the package.
  2. Add the NegotiateContent function to net/http/httputil. I'm not a fan of this option, since we've generally settled on ...util packages as a grab-bag of miscellaneous functionality being a bad idea. httputil is mostly used these days for httputil.ReverseProxy, and we really should move that to a better-named package of its own.
  3. Add a net/http/httpcontent package. This is the cleanest separation, and provides a good place to add a matcher type (as proposed in net/http: support content negotiation #19307 (comment)) if we want one later. The bar for new packages in std is generally pretty high, though.
  4. Add it in golang.org/x/net/http/httpcontent, my comments above about x/ repos notwithstanding.
  5. Unaccept the proposal and decide that this should be a third-party package.

I don't really have any strong opinion here (other than I don't like putting it in net/http/httputil).

@mitar
Copy link
Contributor

mitar commented Jan 27, 2025

I would just go with net/http. When I was learning Go it was much easier for me just to go through all functions in the package to learn what is there. Only later I learned about httputil and x packages.

@seankhliao
Copy link
Member

would putting it under mime be out of place considering it operates on mime types?

@neild
Copy link
Contributor

neild commented Jan 27, 2025

It's actually more general than MIME types; RFC 9110 defines four headers (Accept, Accept-Charset, Accept-Encoding, and Accept-Language) that all use the same negotiation algorithm. And the negotiation is part of the HTTP spec rather than the MIME spec.

Given that these headers are defined in the core HTTP RFCs, perhaps net/http is the best place. "If it's in RFC 9110, it belongs in net/http (if we support it at all)" is fairly principled.

@neild
Copy link
Contributor

neild commented Jan 28, 2025

Requesting re-review by proposal committee as a final check, given the time since this was accepted.

Specific proposal:

We add a function to net/http which performs content type negotiation as specified by RFC 9110:

// NegotiateContent returns the best content to offer from a set of possible
// values, based on the preferences represented by the accept values.
// For example, NegotiateContent can be used on the HTTP servers to find the
// best "Content-Type" to provide to HTTP user agents, based on the "Accept"
// request header. This is also known as a proactive negotiation (or "server-driven"
// negotiation).
//
// NegotiateContent may be used to negotiate several fields, e.g.:
//
//	Response field    Request header
//	--------------    --------------
//	Content-Type      Accept
//	Content-Charset 	Accept-Charset
//	Content-Encoding  Accept-Encoding
//	Content-Language  Accept-Language
//
// The accepts parameter is the appropriate request header value(s),
// and is interpreted as in RFC 9110 section 12.
//
// The offers parameter is a list of possible values to offer.
//
// If no offers match, it returns the empty string.
// If more than one offer matches with equal weight and specificity,
// it returns the one earliest in the list.
// If no accept values are provided (representing no preference case), and offers
// are non-empty, the first offer is returned.
//
// NegotiateContent only considers the first 32 accept values to
// avoid DOS attacks.
func NegotiateContent(accepts []string, offers []string) string

The function signature varies slightly from the original proposal above. In particular, the offers may not include wildcards and there is no default offer. The lack of wildcards is because the server presumably knows the complete set of content types it can offer, and the lack of a default offer is because it is unnecessary--NegotiateContent returns the best offer, or the empty string if none are suitable.

I propose to add the function to net/http, on the grounds that content type negotiation is specified as a core HTTP feature in RFC 9110 and there doesn't seem to be a better place for it. (See above discussion for some alternatives.)

@seankhliao seankhliao changed the title x/net/http: support content negotiation net/http: support content negotiation Jan 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted NeedsFix The path to resolution is known, but the work has not been done. Proposal Proposal-Accepted
Projects
None yet
Development

No branches or pull requests