negotiator

package module
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2020 License: MIT Imports: 6 Imported by: 0

README

Negotiator

GoDoc Build Status Issues

This is a library that handles content negotiation in HTTP server applications written in Go.

Usage

Simple

To return JSON/XML out of the box simple put this in your route handler:

import "github.com/rickb777/negotiator"
...
func getUser(w http.ResponseWriter, req *http.Request) {
    user := &User{"Joe","Bloggs"}
    n := negotiator.NewWithJSONAndXML()
    n.MustNegotiate(w, req, negotiator.Offer{Data: user})
}
Custom

To add your own negotiator, for example you want to write a PDF with your model, do the following:

  1. Create a type that conforms to the ResponseProcessor interface

  2. Where you call negotiator.New(responseProcessors ...ResponseProcessor), pass in a your custom processor. When your request handler calls negotiator.Negotiate(w, req, offers...) it will render a PDF if your Accept header defined it wanted a PDF response.

When a request is Not Acceptable

Having created a Negotiator with one or more response processors, if a request is handled that is not claimed by and processor, a Not Acceptable (406) response is returned.

By default, this uses the standard http.Error function (from net/http) to render the response, If needed, a custom error handler can be plugged in using Negotiator.WithErrorHandler(myHandler).

Accept Handling

The Accept header is parsed using header.ParseMediaRanges(), 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 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.

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

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

This will contain {"en-GB", "en"} in a header.PrecedenceValues slice, sorted according to precedence rules.

This can be used for Accept-Language, Accept-Charset and Accept-Encoding, as well as Accept. The negotiator in this API uses the Accept header only, though.

Acknowledgement

Many thanks to Jonathan Channon (https://github.com/jchannon) for the original concepts and work on which this was based.

Documentation

Overview

Package negotiator is a library that handles 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).

A Negotiator contains a list of ResponseProcessor. For each call to Negotiate, one or more offers of possibly-matching data is compared with the headers in the request. The best matching response processor is chosen and given the task of sending the response.

For more information visit http://github.com/rickb777/negotiator

  import "github.com/rickb777/negotiator"
  ...
	 func getUser(w http.ResponseWriter, req *http.Request) {
	     user := &User{"Joe", "Bloggs"}
	     negotiator.NegotiateWithJSONAndXML(w, req, negotiator.Offer{Data: user})
	 }

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

View Source
const (
	Accept         = "Accept"
	AcceptLanguage = "Accept-Language"
	AcceptCharset  = "Accept-Charset"

	XRequestedWith = "X-Requested-With"
	XMLHttpRequest = "XMLHttpRequest"
)

Variables

View Source
var Printer = func(level byte, message string, data map[string]interface{}) {}

Printer is something that allows printing log entries. This is only used for diagnostics.

View Source
var StdLogger = func(level byte, message string, data map[string]interface{}) {
	buf := &strings.Builder{}
	fmt.Fprintf(buf, "%c: %s", level, message)
	for k, v := range data {
		fmt.Fprintf(buf, ", %q: %v", k, v)
	}
	log.Printf(buf.String())
}

StdLogger adapts the standard Go logger to be usable for the negotiator.

Functions

func IsAjax

func IsAjax(req *http.Request) bool

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

Types

type CodedRender added in v0.7.0

type CodedRender interface {
	Render
	StatusCode() int
}

CodedRender extends Render with a status code. This provides compatibility with the Gin Context.Render method.

type ErrorHandler

type ErrorHandler func(w http.ResponseWriter, error string, code int)

ErrorHandler is called for NotAcceptable and InternalServerError situations.

type Negotiator

type Negotiator struct {
	// contains filtered or unexported fields
}

Negotiator is responsible for content negotiation when using custom response processors.

func New

func New(responseProcessors ...processor.ResponseProcessor) *Negotiator

New creates a Negotiator with a list of custom response processors. The error handler invokes http.Error and the diagnostic printer is no-op; change these if required.

func (*Negotiator) Append added in v0.6.0

func (n *Negotiator) Append(responseProcessors ...processor.ResponseProcessor) *Negotiator

Append more response processors. A new Negotiator is returned with the original processors plus the extra processors. The extra processors are appended last. Because the processors are checked in order, any overlap of matching media range goes to the first such matching processor.

func (*Negotiator) N added in v0.6.0

func (n *Negotiator) N() int

N returns the number of processors.

func (*Negotiator) Negotiate

func (n *Negotiator) Negotiate(w http.ResponseWriter, req *http.Request, offers ...Offer) error

Negotiate negotiates your model based on the HTTP Accept and Accept-... headers. Any error arising will result in a panic.

Example (SingleOffer)

Negotiate applies the negotiation algorithm, choosing the response based on the Accept header in the request, if present. It returns either a successful response or a 406-Not Acceptable, or possibly a 500-Internal server error.

In this example, there is only one offer and it will be used by whichever response processor matches the request.

package main

import (
	"net/http"

	"github.com/rickb777/negotiator"
)

type User struct {
	Name string
}

func main() {
	// getUser is a 'standard' handler function
	getUser := func(w http.ResponseWriter, req *http.Request) {
		// some data; this will be wrapped in an Offer{}
		user := &User{Name: "Joe Bloggs"}

		// the negotiator determines the response format based on the request headers
		negotiator.New().WithDefaults().Negotiate(w, req, negotiator.Offer{Data: user})
	}

	// normal handling
	http.Handle("/user", http.HandlerFunc(getUser))
}
Output:

func (*Negotiator) Processor added in v0.6.0

func (n *Negotiator) Processor(i int) processor.ResponseProcessor

Processor gets the ith processor.

func (*Negotiator) Render added in v0.7.0

func (n *Negotiator) Render(req *http.Request, offers ...Offer) CodedRender

Render computes the best matching response, if there is one, and returns a suitable renderer that is compatible with Gin (github.com/gin-gonic/gin).

Example (SingleOffer)

Negotiate applies the negotiation algorithm, choosing the response based on the Accept header in the request, if present. It returns either a successful response or a 406-Not Acceptable.

In this example, there is only one offer and it will be used by whichever response processor matches the request. The example integrates the negotiator seamlessly with Gin using the Context.Render method.

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/rickb777/negotiator"
)

type User struct {
	Name string
}

func main() {
	// create and configure Gin engine, e.g.
	engine := gin.Default()

	// getUser is a 'standard' handler function
	getUser := func(c *gin.Context) {
		// some data; this will be wrapped in an Offer{}
		user := &User{Name: "Joe Bloggs"}

		// the negotiator determines the response format based on the request headers
		// returning a CodedRender value
		cr := negotiator.New().WithDefaults().Render(c.Request, negotiator.Offer{Data: user})

		// pass the negotiation result to Gin; the status code will be one of
		// 200-OK, 204-No content, or 406-Not acceptable
		c.Render(cr.StatusCode(), cr)
	}

	// normal handling
	engine.GET("/user", getUser)
}
Output:

func (*Negotiator) WithDefaults added in v0.8.0

func (n *Negotiator) WithDefaults() *Negotiator

WithDefaults adds the default processors JSON, XML, CSV and TXT.

func (*Negotiator) WithErrorHandler added in v0.6.0

func (n *Negotiator) WithErrorHandler(eh ErrorHandler) *Negotiator

WithErrorHandler adds a custom error handler. This is used for 406-Not Acceptable cases and dealing with 500-Internal Server Error in Negotiate.

type Offer added in v0.6.0

type Offer struct {
	MediaType string // e.g. "text/html" or blank not relevant
	Language  string // blank if not relevant
	Template  string // blank if not relevant
	Data      interface{}
}

Offer holds the set of parameters that are offered to the content negotiation. Note that Data will be passed to a ResponseProcessor, having first checked

* if it is a func(language string) interface{}, that function will have been called with the chosen language as its parameter.

* if it is a func() interface{}, that function will have been called

The above checks are repeated until the data is neither kind of function.

If the (resulting) data is nil, the response will have 204-Not Content status instead of 200-OK.

type Offers added in v0.6.0

type Offers []Offer

Offers is a slice of Offer.

func (Offers) MediaTypes added in v0.6.0

func (offers Offers) MediaTypes() []string

MediaTypes gets the media types from the offers, keeping the same order.

type Render added in v0.7.0

type Render interface {
	// Render writes data with custom ContentType.
	Render(http.ResponseWriter) error
	// WriteContentType writes custom ContentType.
	WriteContentType(w http.ResponseWriter)
}

Render defines the interface for content renderers. Note that it happens to match render.Render in github.com/gin-gonic/gin/render. This means that this negotiator package can be used with Gin directly.

Directories

Path Synopsis
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 defines what a ResponseProcessor is, and provides four standard implementations: JSON, XML, CSV and plain text.
package processor defines what a ResponseProcessor is, and provides four standard implementations: JSON, XML, CSV and plain text.

Jump to

Keyboard shortcuts

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