render

package
v0.0.0-...-c05fae0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2023 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package render implements parsing and serializing HTTP requests and responses. This package was originally forked from https://github.com/go-chi/render. Its functionality can be split into three parts that complement each other:

  1. Parsing Request Data
  2. Serializing Response Data
  3. Content Type Negotiation

General Design Decisions

This package aims to be compliant with defined HTTP standard behavior, giving you the opportunity to hook into the process at different points. This generally means that the render package makes decoding decisions based on the Content-Type header of a request and maes encoding decisions based on its Accept header. If you want to enforce a certain behavior (e.g. always use a specific content type) the intended way is to implement a middleware that overwrites the respective request headers.

The render package becomes relevant at different points in an application's lifecycle:

  • During the initialization phase you import packages that use RegisterDecoder and RegisterEncoder to provide implementations for various data formats.
  • When the request reaches a handler you might use the functions Bind or Decode to automatically parse the request data using one of the registered decoders. See below for details.
  • During the processing of a request you might want to use the ContentTypeNegotiation middleware or the functions GetAcceptedMediaTypes and NegotiateContentType to determine the format and content of response you want to send.
  • To send the response data you can use the Render or Respond function. This function encodes the response using the results of content type negotiation and one of the registered encoders.

Content Type Negotiation

The render package supports content negotiation using the "Accept" header. The required algorithms are implemented largely by the github.com/Karaoke-Manager/karman/pkg/mediatype package. Content type negotiation can be done in several ways:

  • The most convenient way is probably to use the ContentTypeNegotiation middleware. This middleware automatically selects a fitting content type from the intersection of the Accept header and a list of available types.
  • In your code you can then use the functions GetAcceptedMediaTypes and GetNegotiatedContentType to make decisions based on the result. If necessary you can also initiate a re-negotiation using the NegotiateContentType function.
  • The Render and Respond functions will then use the negotiation result to decide how to encode the response. See the documentation on Respond for details.

Other types of content negotiation (read: other Accept-* headers) are not handled by this package.

Decoding and Binding Requests

The render package supports automatic decoding of requests using the Decode function. The decoding process works like this:

  1. The request is routed through middlewares that potentially restrict the set of allowed content types. This is also where you might want to use the middleware DefaultRequestContentType and SetRequestContentType to potentially force a specific decoding behavior in step 3.
  2. You request handler hands the request to the Decode or the Bind function (which internally also uses Decode).
  3. The Decode function selects one of the decoders registered at initialization time via RegisterDecoder based on the Content-Type of the request. See the documentation on those functions for details. The request body is then decoded into a Go object by the chosen decoder.
  4. If you used the Bind function the resulting value's [Binder.Bind] method is called to finish the decoding process.
  5. Any errors along the way are passed to the caller in step 2 to handle the error appropriately.

Rendering Responses

Similarly to the decoding process there exists an analogous process for rendering and encoding responses. The response process works like this:

  1. During the handling of the request, content type negotiation is performed to determine an appropriate response format. The determined format may or may not influence the behavior of your handler.
  2. Your handler passes the response data as a Go value to Respond or Render (which uses Respond internally).
  3. If you used the Render function the response value's [Renderer.Render] function gets called to prepare the response.
  4. If the response object implements the Responder interface the [Responder.PrepareResponse] method is called, yielding the final response value.
  5. Using the content type from step 1 an appropriate encoder is chosen among the ones registered via RegisterEncoder.
  6. The chosen encoder serializes the response, finishing the process. Most errors are returned to the caller of Render or Respond. There are however some errors that are considered programmer errors that cause a panic. See Respond for details.

Using Encoders and Decoders

The render package intentionally does not register any encoders or decoders by default. There are, however, several subpackages that provide implementations for various common formats. Usually packages that provide encoders or decoders register these in their init() functions so it is enough to import these packages unnamed.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingContentType indicates that the request did not specify a Content-Type header.
	// If you want to default to a fixed decoder (e.g. JSON as a default) use the [DefaultRequestContentType] middleware.
	ErrMissingContentType = errors.New("missing content-type header")
	// ErrInvalidContentType indicates that the Content-Type header was present but not correctly formatted.
	ErrInvalidContentType = errors.New("invalid content-type header")
	// ErrNoMatchingDecoder indicates that no decoder has been registered for the Content-Type provided by the request.
	ErrNoMatchingDecoder = errors.New("no matching decoder has been registered")
)

These are known errors that happen during decoding of requests.

View Source
var DefaultMediaType = mediatype.ApplicationJSON

DefaultMediaType is the fallback media type that will be used by Render and Respond if no content type negotiation has been performed. It is usually recommended to use the content negotiation mechanisms to derive the response content type. This default mainly exists to lower the barrier of using this package, so that you can start using it without having to dive into the content type negotiation stuff.

Functions

func Bind

func Bind(r *http.Request, v Binder) error

Bind decodes a request via the Decode function and executes the v.Bind method. If v is a struct, map or slice type and any fields, slice or map values implement the Binder interface the Bind methods of those values are called recursively in a bottom-up fashion. Note however that the recursion stops at the first value that does not implement the Binder interface. If you do not need to implement Binder yourself but want to give your struct fields the opportunity to perform their Bind operations, use the NopBinder type to add a noop Binder implementation to your type.

For details on the decoding process see the Decode function.

There are two main error types returned by this function:

  • A DecodeError indicates an error during the decoding phase. This usually corresponds to a 400 status code.
  • A BindError indicates an error during the invocation of a Bind method (not necessarily v itself but maybe one of its struct fields). This usually corresponds to a 422 status code.

func ContentTypeNegotiation

func ContentTypeNegotiation(available ...string) func(next http.Handler) http.Handler

ContentTypeNegotiation returns a middleware that performs content type negotiation on each request using the provided available media types. If there is an intersection of accepted types provided in the request's Accept header and the types provided as available, this middleware will find it and use SetNegotiatedContentType to set the resulting type in the request context. If the intersection of types is empty (aka the content type negotiation failed) and the NotAcceptableHandler middleware has been used to set a handler for this case, the request is routed to that handler. If no such handler is defined, the request chain will continue (with a negotiated type of mediatype.Nil).

The Respond and Render functions require content type negotiation to produce a concrete type. Be careful with using wildcard types as an available type as it may cause a panic if the wildcard is not resolved before the response is sent.

This middleware will also add a Vary:Accept response header.

func Decode

func Decode(r *http.Request, v any) error

Decode reads the request body, decodes it and stores it into v.

The heavy lifting of the Decode function is done by a decoder, previously registered via RegisterDecoder. The Decode function inspects the Content-Type of r and then chooses an appropriate decoder to decode r. A decoder is chosen by the following priorities:

  • If a decoder has been registered for the exact Content-Type of r, this decoder is chosen.
  • If a decoder has been registered for the subtype suffix of the request's Content-Type, that decoder is chosen.
  • If a decoder has been registered for a wildcard subtype of the major type of r's Content-Type, that decoder is chosen.
  • If a decoder has been registered for "*/*", this catchall decoder is used.

If no decoder is found, ErrNoMatchingDecoder is returned without reading the request body.

In your handling of a request you should make sure that the value you provide is compatible with the possible decoders. It is a programmer error if the type of v is incompatible with the decoder, causing Decode to panic.

func DefaultRequestContentType

func DefaultRequestContentType(v string) func(next http.Handler) http.Handler

DefaultRequestContentType returns a middleware that provides requests with a default Content-Type. Requests that do not have a Content-Type header set, will have it set to v.

Use this middleware if you want to be able to Decode requests that do not specify a Content-Type.

func GetAcceptedMediaTypes

func GetAcceptedMediaTypes(r *http.Request) mediatype.MediaTypes

GetAcceptedMediaTypes parses the Accept header of the request and returns the resulting list of media types. The parsed result is stored in the request context so that multiple invocations of this function are not terribly inefficient. The resulting list is sorted by media type priority.

func GetNegotiatedContentType

func GetNegotiatedContentType(r *http.Request) (t mediatype.MediaType, ok bool)

GetNegotiatedContentType fetches the result of content type negotiation from the context. If no content type negotiation has been performed yet, ok will be false. If content type negotiation has been performed but has not yielded any usable media types, t will be mediatype.Nil.

func GetStatus

func GetStatus(r *http.Request) (v int, ok bool)

GetStatus returns the HTTP status code previously set via SetStatus. If no such code exists, ok will be false.

func MustGetNegotiatedContentType

func MustGetNegotiatedContentType(r *http.Request) mediatype.MediaType

MustGetNegotiatedContentType works just like GetNegotiatedContentType but panics if no content type negotiation has been performed yet. Use this function if you can be sure that negotiation has been performed (e.g. by a middleware).

func MustGetStatus

func MustGetStatus(r *http.Request) int

MustGetStatus works just like GetStatus but panics if no status value has been set.

func MustNegotiateContentType

func MustNegotiateContentType(r *http.Request, available ...mediatype.MediaType) mediatype.MediaType

MustNegotiateContentType works like NegotiateContentType but if the intersection of accepted and available media types is empty instead of mediatype.Nil the highest priority available type is chosen.

You might want to use this function over NegotiateContentType if having no resulting content type is not an option (such as for error responses). This is usually not the primary content type negotiation but used as a re-negotiation if the primary negotiation was inconclusive.

The Respond and Render functions require content type negotiation to produce a concrete type. Be careful with using wildcard types as an available type as it may cause a panic if the wildcard is not resolved before the response is sent.

func NegotiateContentType

func NegotiateContentType(r *http.Request, available ...mediatype.MediaType) mediatype.MediaType

NegotiateContentType performs content type negotiation using the Accept header of r and the available media types. The result of this operation will be the highest priority intersection of the accepted media types and the available media types. If no such intersection exists, the result will be mediatype.Nil. In any case the result will be stored in the request context as the negotiated media type.

The Respond and Render functions require content type negotiation to produce a concrete type. Be careful with using wildcard types as an available type as it may cause a panic if the wildcard is not resolved before the response is sent.

func NoContent

func NoContent(w http.ResponseWriter, _ *http.Request) error

NoContent is a convenience function that writes HTTP 204 "No Content" to w. This function bypasses the entire rendering mechanism and mainly exists for consistency reasons, so you can write render.NoContent(w, r) as you would render.Render(w, r, v).

func NotAcceptable

func NotAcceptable(w http.ResponseWriter, r *http.Request)

NotAcceptable invokes a handler, previously registered via the NotAcceptableHandler middleware. If no handler has been registered, this function panics. Use this function if you are (re-)negotiating content types.

func NotAcceptableHandler

func NotAcceptableHandler(h http.HandlerFunc) func(next http.Handler) http.Handler

NotAcceptableHandler returns a middleware that registers h as a handler for failed content type negotiation. This is used by the ContentTypeNegotiation middleware.

func RegisterDecoder

func RegisterDecoder(d DecodeFunc, mediaTypes ...string)

RegisterDecoder registers the decoding function d for the specified media types. The provided types can be concrete types or wildcard types that indicate that a decoder may be used for any types matching that wildcard. If there are decoders registered for concrete types as well as wildcards, the more concrete types always take precedence.

It is not possible to register multiple decoders for the same type. If you do register multiple decoders the later registration will take precedence. The mediaTypes cannot have parameters.

RegisterDecoder is not safe for concurrent use.

func RegisterEncoder

func RegisterEncoder(e EncodeFunc, mediaTypes ...string)

RegisterEncoder registers the given encoder function for the specified media types. Encoder functions can only be registered for types without parameters. Violations will cause a panic.

It is not possible to register multiple encoders for the same type. A later registration will always take precedence.

RegisterEncoder is not safe for concurrent use.

func Render

func Render(w http.ResponseWriter, r *http.Request, v Renderer) error

Render executes v.Render and then encodes v using the Respond function. If v is a struct, map or slice type and any of its fields implement the Renderer interface, the respective Render methods will be called recursively in a bottom-up fashion. Note however that the recursion stops at the first value that does not implement the Renderer interface. If you do not need to implement Renderer yourself but want to give your struct fields the opportunity to perform their Render operations, use the NopRenderer type to add a noop Renderer implementation to your type.

For details on the encoding process see the Respond function.

There are two main error types returned by this function:

  • A RenderError indicates an error during the rendering phase. This means that one of the Render implementations has returned an error.
  • A RespondError indicates an error during the invocation of the Respond function. This is usually an indication of a network error.

func RenderList

func RenderList(w http.ResponseWriter, r *http.Request, l []Renderer) error

RenderList works like Render but takes a slice of payloads. See Render for details.

func Respond

func Respond(w http.ResponseWriter, r *http.Request, v any) error

Respond writes v to w using a previously registered encoder.

If v implements the Responder interface, the first thing this function does is call the [Responder.PrepareResponse] method, giving v the opportunity to do last-minute transformations. Only v itself is considered for this mechanism. If v is a struct value and any child values implement Responder it does not have any effect.

Respond then determines the intended content type of the response. This is usually done via prior content type negotiation in the request chain. If you want to use a specific response format you can override the content type negotiation using SetNegotiatedContentType. If no content negotiation has happened or the negotiation did not yield an acceptable media type, the response header Content-Type of w is inspected. If content type negotiation yielded a wildcard type, the response headers may also be used to determine a concrete Content-Type for the response. If this process yields a non-concrete media type, Respond panics. If no other means are available, the DefaultMediaType is used.

Using the intended content type of the response Respond will then choose an encoder previously registered using RegisterEncoder. An encoder is chosen by the following rules:

  • If an encoder has been registered for the exact type, this encoder is chosen.
  • If an encoder has been registered for the subtype suffix, that encoder is chosen.
  • If an encoder has been registered for a wildcard subtype of the major type, that encoder is chosen.
  • If an encoder has been registered for "*/*", we chose this catchall encoder.

In a last step before encoding v using the encoder Respond sets the Content-Type of the response (if unset) to the intended content type (or rather the concrete type chosen with the encoder). It a status code has been set via SetStatus, that status code is sent before the encoding process is started.

func SetNegotiatedContentType

func SetNegotiatedContentType(r *http.Request, t mediatype.MediaType)

SetNegotiatedContentType sets t as the negotiated content type in the request context. This overrides any previously stored negotiation result. There are two primary use cases for this function:

  1. You have implemented your own content type negotiation and want to provide the resulting media type to the render package.
  2. You want to override the result of the content type negotiation with another value. This might be useful if you want to send error responses that are not compatible with any of the accepted media types.

The stored content type can be extracted from the returned context via GetNegotiatedContentType and MustGetNegotiatedContentType.

func SetRequestContentType

func SetRequestContentType(v string) func(next http.Handler) http.Handler

SetRequestContentType returns a middleware that overwrites the Content-Type header of all requests with v.

Use this middleware if you want to disregard the provided Content-Type header and use the same decoder for all requests (e.g. if you want to only support JSON). Usually a better approach is to use a middleware that responds with a 415 status code to unsupported Content-Type values.

func SetStatus

func SetStatus(r *http.Request, status int)

SetStatus sets an HTTP response status code hint into request context. This context value is respected by the Respond function.

Types

type BindError

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

A BindError indicates that the request could be successfully parsed but could not be bound to the model instance. This is usually an indication that some kind of constraint imposed by the model's [Binder.Bind] method was violated.

func (BindError) Error

func (b BindError) Error() string

Error implements the error interface.

func (BindError) Unwrap

func (b BindError) Unwrap() error

Unwrap implements the error interface.

type Binder

type Binder interface {
	// The Bind method is invoked when an object is bound to request data.
	// Implementations should perform schema validations that cannot be expressed otherwise
	// and potentially perform data normalization.
	//
	// The error returned by this method is returned (as a wrapped error) to the caller of the [Bind] function.
	Bind(r *http.Request) error
}

Binder interface for managing request payloads. Implementing this interface allows a value to be used in the Bind function. Binding happens after decoding is finished and gives you the opportunity to do as much post-processing as necessary.

Implementing this interface also gives way to perform schema validations.

type DecodeError

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

A DecodeError indicates that the request data could not be properly decoded into the desired data format. This could be because of a syntax error or a schema validation error. You may inspect the underlying error for more details.

func (DecodeError) Error

func (d DecodeError) Error() string

Error implements the error interface.

func (DecodeError) Unwrap

func (d DecodeError) Unwrap() error

Unwrap implements the error interface.

type DecodeFunc

type DecodeFunc func(r io.Reader, mediaType mediatype.MediaType, v any) error

DecodeFunc is the signature of a decoding function that can be registered via RegisterDecoder. A decoding function takes the following values:

  • r is an io.Reader for the request body. This is the data to be decoded.
  • mediaType is the Content-Type of the request that caused this decoder to be chosen. If a decoder is registered for multiple media types this allows you to disambiguate based on the concrete type. Note that mediaType is basically just the Content-Type of the request, which may be a wildcard type.
  • v is the receiving value into which the data from r should be decoded.

If an error occurs during decoding, an error should be returned. A decoding function may panic if the type of v is not appropriate (e.g. not a pointer type). A decoding function should not panic if it cannot handle the mediaType or data.

Decoding functions must be safe for concurrent use.

type EncodeFunc

type EncodeFunc func(w io.Writer, v any) error

EncodeFunc is the signature of encoder functions that can be registered via RegisterEncoder. An encoder function takes the following values: - w is the writer to which the response should be written. - v is the value that should be encoded.

If an error occurs during encoding the encoder function may return it. Keep in mind however that at this point in the request lifecycle part of the response may already have been sent, so it is unlikely that the error can be handled in a meaningful manner.

An encoder function may panic if the type of v is not compatible.

Encoding functions must be safe for concurrent use.

type NopBinder

type NopBinder struct{}

NopBinder implements the Binder interface. The implementation does nothing.

func (NopBinder) Bind

func (NopBinder) Bind(*http.Request) error

Bind is an empty implementation of the Binder interface.

type NopRenderer

type NopRenderer struct{}

NopRenderer implements the Renderer interface. The implementation does nothing.

func (NopRenderer) Render

Render is an empty implementation of the Renderer interface.

type RenderError

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

A RenderError indicates that an error occurred during the rendering process of the response. This kind of error is often an indication of a programming error or can be an indication of malformed data.

func (RenderError) Error

func (r RenderError) Error() string

Error implements the error interface.

func (RenderError) Unwrap

func (r RenderError) Unwrap() error

Unwrap implements the error interface.

type Renderer

type Renderer interface {
	// The Render method is invoked when an object is about to be rendered (aka serialized).
	// Implementing this method gives you the opportunity to perform data sanitization and normalization
	// before an object is marshalled.
	//
	// Any errors returned by this method will be wrapped and passed on to the caller of the [Render] function.
	// Note however that this late in the request lifecycle there are usually few options of actually handling such an error.
	Render(w http.ResponseWriter, r *http.Request) error
}

Renderer interface for managing response payloads. Implementing this interface allows a value to be used in the Render function. Rendering occurs before a response is sent, allowing you to perform normalization and cleanup.

Implementing this interface also gives way to ensure schema compliance.

type RespondError

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

A RespondError indicates that an error occurred during sending the rendered response. This is often an indication of a network problem or some kind of system failure outside the control of the program.

func (RespondError) Error

func (r RespondError) Error() string

Error implements the error interface.

func (RespondError) Unwrap

func (r RespondError) Unwrap() error

Unwrap implements the error interface.

type Responder

type Responder interface {
	// PrepareResponse is called before the receiver gets encoded as a response to a request.
	// This method may be implemented to do different things:
	//  - Set response headers commonly associated with this type of value.
	//  - Re-negotiate the response content type (this is especially useful for error responses).
	//  - Provide a completely different value that should be serialized.
	//
	// PrepareResponse is not invoked recursively.
	// The replacement value returned by this method is used as-is.
	// No further transformations will be made to it.
	PrepareResponse(w http.ResponseWriter, r *http.Request) any
}

The Responder interface can be implemented by types that are intended to be passed to the Render and Respond functions. If a value implements this interface it gets a chance to do last-minute transformations or even provide a completely different replacement object.

See the Respond function for details.

Directories

Path Synopsis
Package form implements a request decoder for form data.
Package form implements a request decoder for form data.
Package html implements a response encoder for HTML data.
Package html implements a response encoder for HTML data.
Package json implements a request decoder and a response encoder for JSON data.
Package json implements a request decoder and a response encoder for JSON data.
Package raw implements a request decoder and a response encoder for raw bytes.
Package raw implements a request decoder and a response encoder for raw bytes.
Package xml implements a request decoder and a response necoder for XML data.
Package xml implements a request decoder and a response necoder for XML data.

Jump to

Keyboard shortcuts

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