acceptable

package module
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2021 License: MIT Imports: 8 Imported by: 0

README

Acceptable

GoDoc Build Status Issues

This is a library that handles Accept headers, which form the basis of content negotiation in HTTP server applications written in Go. It provides an implementation of the proactive server-driven content negotiation algorithm specified in RFC-7231.

Accept parsing

The Accept header is parsed using ParseMediaRanges(hdr), which returns the slice of media ranges, e.g.

    // handle Accept-Language
    mediaRanges := header.ParseMediaRanges("application/json;q=0.8, application/xml, application/*;q=0.1")

The resulting slice is ready-sorted according to precedence and quality rules, so in this example the order is {"application/xml", "application/json", "application/*"} because the middle item has an implied quality of 1, whereas the first item has a lower quality.

Accept-Language and Accept-Charset parsing

The other important content-negotiation headers, Accept-Language and Accept-Charset, are handled by the header.Parse method, e.g.

    // handle Accept-Language
    acceptLanguages := header.Parse("en-GB,fr;q=0.5,en;q=0.8")

This will contain {"en-GB", "en", "fr"} in a header.PrecedenceValues slice, sorted according to precedence rules with the most preferred first.

The acceptable.Parse function can be used for Accept-Encoding as well as Accept-Language and Accept-Charset. However, the Go standard library deals with Accept-Encoding, so you won't need to.

Putting it together - simple content negotiation

Finding the best match depends on you listing your available response representations. This is all rolled up into a simple-to-use function acceptable.RenderBestMatch

    en := ... obtain some content in English
    fr := ... obtain some content in English

    // long-hand construction of an offer for indented JSON
    offer1 := acceptable.OfferOf("application/json").Using(processor.JSON("  ")).With("en", en).With("fr", fr)

    // short-hand construction of an XML offer
    offer2 := processor.DefaultXMLOffer.With("en", en).With("fr", fr)
    // equivalent to
    //offer2 := acceptable.OfferOf("application/xml").Using(processor.XML()).With("en", en).With("fr", fr)

    // a catch-all offer is optional
    catchAll := acceptable.OfferOf("*/*").Using(processor.TXT()).With("en", en).With("fr", fr)
    
    err := acceptable.RenderBestMatch(request, offer1, offer2, catchAll)

The best result will be the one that best matches the request headers. If none match, the response will be 406-Not Acceptable. If you need to have a catch-all case, include acceptable.OfferOf("*/*") last in the list.

The offers will usually hold a suitable rendering function. This is attached with the Using method. Sub-packages processor and templates provide useful renderers but you can also provide your own.

The offers can also be restricted by language matching. This is done either using OfferOf varags parameters, or via the With method. The language(s) is matched against Accept-Language using the basic prefix algorithm. This means for example that if you specify "en" it will match "en", "en-GB" and everything else beginning with "en-", but if you specify "en-GB", it only matches "en-GB" and "en-GB-*", but won't match "en-US" or even "en".

Sometimes, the With method might not care about language, so simply use the wildcard instead. For example, offer.With("*", data) attaches data to the offer and doesn't restrict the offer to any particular language. This could also be used as a catch-all case if it comes after one or more With with a specified language. However, the standard (RFC-7231) advises that a response should be returned even when language matching has failed; this implementation will do this by picking the first language listed, so the catch-all case is only necessary if its data is different to that of the first case.

Providing response data

The response data (en and fr above) can be structs, slices, maps, or other values. Alternatively they can be data.Data values. These allow for lazy evaluation of the content

    en := data.Lazy(func(template, language string) (interface{}, error) {
        return ...
    })

and they can even be nested, returning another such function which will be evaluated in turn. The template and language parameters are used for templated/web content data; otherwise they are ignored.

The selected response processor will render the actual response using the data provided, for example a struct will become JSON text if processor.JSON renders it.

Character set transcoding

Most responses will be UTF-8, sometimes UTF-16. All other character sets (e.g. Windows-1252) are now deprecated.

However, transcoding is implemented by Match.ApplyHeaders so that the Accept-Charset content negotiation can be implemented. This depends on finding an encoder in golang.org/x/text/encoding/htmlindex (no other encoders are supported).

Whenever possible, responses will be UTF-8. Not only is this strongly recommended, it also avoids any transcoding processing overhead. It means for example that Accept-Charset: iso-8859-1, utf-8 will ignore the iso-8859-1 preference and use UTF-8.

Status

This API is well-tested and known to work but not yet fully released because it may yet require breaking API changes.

Documentation

Overview

Package acceptable is a library that handles headers for content negotiation in web applications written in Go. Content negotiation is specified by RFC (http://tools.ietf.org/html/rfc7231) and, less formally, by Ajax (https://en.wikipedia.org/wiki/XMLHttpRequest).

Accept

from https://tools.ietf.org/html/rfc7231#section-5.3.2:

The "Accept" header field can be used by user agents to specify response media types that are acceptable. Accept header fields can be used to indicate that the request is specifically limited to a small set of desired types, as in the case of a request for an in-line image.

A request without any Accept header field implies that the user agent will accept any media type in response.

If the header field is present in a request and none of the available representations for the response have a media type that is listed as acceptable, the origin server can either honor the header field by sending a 406 (Not Acceptable) response, or disregard the header field by treating the response as if it is not subject to content negotiation.

Accept-Language

from https://tools.ietf.org/html/rfc7231#section-5.3.5:

The "Accept-Language" header field can be used by user agents to indicate the set of natural languages that are preferred in the response.

A request without any Accept-Language header field implies that the user agent will accept any language in response.

If the header field is present in a request and none of the available representations for the response have a matching language tag, the origin server can either disregard the header field by treating the response as if it is not subject to content negotiation or honor the header field by sending a 406 (Not Acceptable) response. However, the latter is not encouraged, as doing so can prevent users from accessing content that they might be able to use (with translation software, for example).

Index

Examples

Constants

This section is empty.

Variables

View Source
var Debug = func(string, ...interface{}) {}

Debug can be used for observing decisions made by the negotiator. By default it is no-op.

Functions

func IsAjax

func IsAjax(req *http.Request) bool

IsAjax tests whether a request has the Ajax header sent by browsers for XHR requests.

func RenderBestMatch added in v0.7.0

func RenderBestMatch(w http.ResponseWriter, req *http.Request, template string, available ...Offer) error

RenderBestMatch uses BestRequestMatch to find the best matching offer for the request, and then renders the response.

Example
package main

import (
	"net/http"
	"net/http/httptest"

	"github.com/rickb777/acceptable"
	"github.com/rickb777/acceptable/data"
	"github.com/rickb777/acceptable/processor"
	"github.com/rickb777/acceptable/templates"
)

func main() {
	// In this example, the same content is available in three languages. Three different
	// approaches can be used.

	// 1. simple values can be used
	en := "Hello!" // get English content

	// 2. values can be wrapped in a data.Data
	fr := data.Of("Bonjour!") // get French content

	// 3. this uses a lazy evaluation function, wrapped in a data.Data
	es := data.Lazy(func(template string, language string) (interface{}, error) {
		return "Hola!", nil // get Spanish content - eg from database
	})

	// We're implementing an HTTP handler, so we are given a request and a response.

	req := &http.Request{}                               // some incoming request
	var res http.ResponseWriter = httptest.NewRecorder() // replace with the server's response writer

	// Now do the content negotiation. This example has six supported content types, all of them
	// able to serve any of the three example languages.
	//
	// The first offer is for JSON - this is often the most widely used because it also supports
	// Ajax requests.

	acceptable.RenderBestMatch(res, req, "home.html",
		acceptable.OfferOf("application/json", "en").Using(processor.JSON("  ")).
			With("en", en).With("fr", fr).With("es", es),

		acceptable.OfferOf("application/xml").Using(processor.XML("  ")).
			With("en", en).With("fr", fr).With("es", es),

		acceptable.OfferOf("text/csv").Using(processor.CSV()).
			With("en", en).With("fr", fr).With("es", es),

		acceptable.OfferOf("text/plain").Using(processor.TXT()).
			With("en", en).With("fr", fr).With("es", es),

		templates.TextHtmlOffer("en", "templates/en", ".html", nil).
			With("en", en).With("fr", fr).With("es", es),

		templates.ApplicationXhtmlOffer("en", "templates/en", ".html", nil).
			With("en", en).With("fr", fr).With("es", es),
	)

	// RenderBestMatch returns an error which should be checked
}
Output:

Types

type Match added in v0.3.0

type Match struct {
	Type     string
	Subtype  string
	Language string
	Charset  string
	Vary     []string
	Data     data.Data
	Render   Processor
}

Match holds the best-matched offer after content negotiation and is used for response rendering.

func BestRequestMatch

func BestRequestMatch(req *http.Request, available ...Offer) *Match

BestRequestMatch finds the content type and language that best matches the accepted media ranges and languages contained in request headers. The result contains the best match, based on the rules of RFC-7231. On exit, the result will contain the preferred language and charset, if these are known.

Whenever the result is nil, the response should be 406-Not Acceptable.

For all Ajax requests, the available offers are filtered so that only those capable of providing an Ajax response are considered by the content negotiation algorithm. The other offers are discarded.

The order of offers is important. It determines the order they are compared against the request headers, and it determines what defaults will be used when exact matching is not possible.

If no available offers are provided, the response will always be nil. Note too that Ajax requests will result in nil being returned if no offer is capable of handling them, even if other offers are provided.

func (Match) ApplyHeaders added in v0.3.0

func (m Match) ApplyHeaders(rw http.ResponseWriter) io.Writer

ApplyHeaders sets response headers so that the user agent is notified of the content negotiation decisons made. Four headers may be set, depending on context.

  • Content-Type is always set.
  • Content-Language is set when a language was selected.
  • Content-Encoding is set when the character set is being transcoded
  • Vary is set to list the accept headers that led to the three decisions above.

func (Match) String added in v0.7.0

func (m Match) String() string

type Offer

type Offer struct {
	// ContentType is the content type that is to be matched.
	// Wildcard values may be used.
	header.ContentType
	// contains filtered or unexported fields
}

Offer holds information about one particular resource representation that can potentially provide an acceptable response.

func OfferOf

func OfferOf(contentType string, language ...string) Offer

OfferOf constructs an Offer easily. Zero or more languages can be specified. If the language is absent, it is assumed to be the wildcard "*". If the content type is blank, it is assumed to be the full wildcard "*/*". Also, contentType can be a partial wildcard "type/*".

func (Offer) String

func (o Offer) String() string

func (Offer) Using added in v0.5.0

func (o Offer) Using(processor Processor) Offer

Using attaches a processor function to an offer and returns the modified offer. The original offer is unchanged.

func (Offer) With

func (o Offer) With(language string, d interface{}) Offer

With attaches response data to an offer. The data can be a value (struct, slice, etc) or a data.Data. It may also be nil, which serves to add the language to the Offer's supported languages.

The original offer is unchanged.

type Offers

type Offers []Offer

Offers holds a slice of Offer.

func (Offers) Filter

func (offers Offers) Filter(typ, subtype string) Offers

Filter returns only the offers that match specified type and subtype.

type Processor added in v0.3.0

type Processor func(w http.ResponseWriter, match Match, template string) error

Processor is a function that renders content according to the matched result.

Directories

Path Synopsis
package data provides wrappers for response data, optionally including response headers such as ETag.
package data provides wrappers for response data, optionally including response headers such as ETag.
package header provides parsing rules for content negotiation headers according to RFC-7231.
package header provides parsing rules for content negotiation headers according to RFC-7231.
Package processor contains flexible implementations for rendering JSON, XML, CSV and plain text.
Package processor contains flexible implementations for rendering JSON, XML, CSV and plain text.
Package templates provides tree-based template loading and rendering using HTML templates from the Go standard library.
Package templates provides tree-based template loading and rendering using HTML templates from the Go standard library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL