rsvp

package module
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: Jan 29, 2026 License: CC0-1.0 Imports: 19 Imported by: 0

README

rsvp

net/http middleware with a type-driven Handler interface, handling content negotiation automatically.


The default net/http handler interface:

ServeHTTP(http.ResponseWriter, *http.Request)

rsvp's handler interface looks like this:

type Body struct {
    Data any,
}

ServeHTTP(ResponseWriter, *http.Request) Body

Features

  • Content Negotiation. rsvp will attempt to provide Data in a supported media type that is requested via the Accept header; or even the URL's file extension in the case of GET requests:
    • application/json
    • text/html
    • text/plain
    • text/csv (by implementing the rsvp.Csv interface)
    • application/octet-stream
    • application/xml
    • application/vnd.golang.gob (Golang's encoding/gob)
    • application/vnd.msgpack (optional extension behind -tags=rsvp_msgpack)
    • Others to be implemented?
  • Extension matching on GET requests:
    • /users/123 → Returns default media type (determined by the value of Body)
    • /users/123.json → Forces application/json
    • /users/123.xml → Forces application/xml
    • /users/123.csv → Forces text/csv
    • NOTE: Currently, this behaviour is hidden behind net/http's strict path matching. The above examples would require something like mux.Handle("/users/{filename}") with middleware that strips out the file extension, and matches the remaining file stem with its respective handler. rsvp does not currently provide a utility for this.

It's easy for me to lose track of what I've written to http.ResponseWriter. Occasionally receiving the old http: multiple response.WriteHeader calls

With this library I just return a value, which I can only ever do once, to execute an HTTP response write. Why write responses with a weird mutable reference from goodness knows where? YEUCH!

Having to remember to return separately from resolving the response? *wretch*

if r.Method != http.MethodPut {
	http.Error(w, "Use PUT", http.StatusMethodNotAllowed)
	return
}

Not with rsvp 🫠

if r.Method != http.MethodPut {
	return rsvp.Data("Use PUT").StatusMethodNotAllowed()
}

(Wrapping this with your own convenience method, i.e. func ErrorMethodNotAllowed(message string) rsvp.Body is encouraged. You can decide for yourself how errors are represented)

Comparison

Feature net/http Gin / Echo / Fiber rsvp
Response Style Imperative (w.Write) Context-based (c.JSON) Value-Oriented (return)
Content Negotiation Manual Manual / Middleware Built-in & Automatic
URL Extensions Manual parsing Generally unsupported Native (.json, .csv)
Response Handling Easy to forget return Side-effect based Compile-time enforced
Size Standard minimum Massive abstraction Lightweight Wrapper

When to use rsvp

You value Progressive Enhancement: You want your API to be easily browsable by a human (HTML/XML) but consumable by a script (JSON/CSV) using the same URL.

You hate "Multiple WriteHeader" logs: You want a handler signature that makes it impossible to write a partial or double response.

You want trivially testable handlers: Since handlers return a struct, you can unit test your logic by inspecting the returned rsvp.Response instead of mocking a whole http.ResponseWriter.

When to stick with net/http or others

High-performance binary streaming: If you are streaming gigabytes of data where every nanosecond of overhead matters, the abstraction of rsvp struct might not be for you.

OpenAPI-first workflows: If your primary goal is generating documentation from code, a schema-heavy framework like Huma might be a better fit. Although you might want to look into creating a proper RESTful self-documenting interface by combining rsvp with hyprctl: https://github.com/Teajey/hyprctl

Quickstart

func main() {
    mux := http.NewServeMux()
    cfg := rsvp.Config{}
    mux.HandleFunc("GET /users/{id}", rsvp.AdaptHandlerFunc(cfg, getUser))
    http.ListenAndServe(":8080", mux)
}

func getUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
    return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as, in order; JSON, XML, and encoding/gob.
}

[!IMPORTANT] nil Data renders as JSON "null\n", not an empty response.

Use rsvp.Data("") for a blank text/plain response body, or rsvp.Blank() for a blank response with no Content-Type.

Examples

Templates
rsvp.Config{
    HtmlTemplate: template.Must(template.ParseGlob("templates/html/*.gotmpl")),
    TextTemplate: template.Must(template.ParseGlob("templates/text/*.gotmpl")),
}

func showUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
    w.DefaultTemplateName("user.gotmpl") // Must exist in HtmlTemplate and/or TextTemplate for formats to match
    return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as JSON, XML, HTML, plain text, and encoding/gob.
}
Error responses

Build your own!

type APIError struct {
    Message string `json:"message"`
    Code    string `json:"code"`
}

func ErrorNotFound(msg string) rsvp.Body {
    return rsvp.Data(APIError{Message: msg, Code: "NOT_FOUND"}).StatusNotFound()
}
CSV
type UserList []User

var users UserList

func (ul UserList) MarshalCsv(w *csv.Writer) error {
    w.Write([]string{"ID", "Name", "Email"})
    for _, u := range ul {
        w.Write([]string{u.ID, u.Name, u.Email})
    }
    return nil
}

func userList(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
    return rsvp.Data(users) // In content negotiation this will be offered as JSON, XML, CSV, and encoding/gob.
}
net/http middleware compatibility

See middleware_test.go for an example of how to use this library with standard middleware.

Live

You can see it in action on my stupid little blog site, brightscroll.net. For instance, https://brightscroll.net/posts/2025-06-30.md vs. https://brightscroll.net/posts/2025-06-30.md.txt

Documentation

Overview

Package rsvp is a Go web framework built around content negotiation.

The framework automatically negotiates response format based on the Accept header, supporting JSON, XML, HTML, plain text, CSV, binary, Gob, and MessagePack (using -tags=rsvp_msgpack). This content negotiation extends to ALL responses, including redirects, allowing you to provide rich feedback in many contexts.

The Accept header should be expected to be used as standardized; weighting is supported. If an acceptable fallback is not reached, a 406 Not Acceptable will be returned, and the Content-Type and body will be set as if Accept: */* had been sent.

This makes rsvp particularly well-suited for APIs that serve multiple clients (browsers, mobile apps, CLI tools) and for taking advantage of principles such as REST and progressive enhancement.

Index

Constants

View Source
const (
	SupportedMediaTypePlaintext string = "text/plain"
	SupportedMediaTypeHtml      string = "text/html"
	SupportedMediaTypeCsv       string = "text/csv"
	SupportedMediaTypeBytes     string = "application/octet-stream"
	SupportedMediaTypeJson      string = "application/json"
	SupportedMediaTypeXml       string = "application/xml"
	SupportedMediaTypeGob       string = "application/vnd.golang.gob"
)

Variables

View Source
var ErrFailedToMatchHtmlTemplate = errors.New("TemplateName was set, but it failed to match within HtmlTemplate")
View Source
var ErrFailedToMatchTextTemplate = errors.New("TemplateName was set, but it failed to match within TextTemplate")

Functions

func AdaptHandler

func AdaptHandler(config Config, next Handler) http.Handler

AdaptHandler wraps a Handler as an http.Handler with the given config.

This is the primary entrypoint to using rsvp.

func AdaptHandlerFunc

func AdaptHandlerFunc(cfg Config, next HandlerFunc) http.HandlerFunc

AdaptHandlerFunc wraps a HandlerFunc as an http.HandlerFunc with the given config.

This is the primary entrypoint to using rsvp.

func Write

func Write(w io.Writer, cfg Config, wh http.Header, r *http.Request, handler HandlerFunc) (int, error)

Write the result of handler to w. Returns an HTTP status code, and may write headers to wh.

NOTE: This function is for advanced lower-level use cases.

This function, alongside WriteResponse, should be used to wrap Handler in middleware that requires _write_ access to http.ResponseWriter. [Handle] and [HandleFunc] may be used for simpler standard middleware that does not write to http.ResponseWriter.

See this test for an example: https://github.com/Teajey/rsvp/blob/main/middleware_test.go

func WriteHandler

func WriteHandler(cfg Config, rw http.ResponseWriter, r *http.Request, handler HandlerFunc) error

WriteHandler writes the result of handler to rw according to cfg.

NOTE: This function is for advanced lower-level use cases.

func WriteResponse

func WriteResponse(status int, w http.ResponseWriter, r io.Reader) error

WriteResponse calls w.WriteHeader(status) and copies r to w.

NOTE: This function is for advanced lower-level use cases.

This function, alongside Write, should be used to wrap Handler in middleware that requires _write_ access to http.ResponseWriter. AdaptHandler and AdaptHandlerFunc may be used for simpler standard middleware that does not write to http.ResponseWriter.

See this test for an example: https://github.com/Teajey/rsvp/blob/main/middleware_test.go

Types

type Body

type Body struct {
	// Data is the raw data of the response payload to be rendered.
	//
	// IMPORTANT: A nil Data renders as JSON "null\n", not an empty response.
	// Use Data("") for a blank text/plain response body, or [Blank] for a blank response with no Content-Type.
	Data any
	// TemplateName sets the template that this Body may attempt to select from
	// [Config.HtmlTemplate] or [Config.TextTemplate],
	//
	// [ResponseWriter.DefaultTemplateName] may also be used to avoiding setting TemplateName multiple times on every return point of a single handler.
	//
	// It is not an error if a template is not found for one of the two templates; other formats will be attempted.
	TemplateName string
	// contains filtered or unexported fields
}

Body represents the content body of an HTTP response.

By default, it represents a 200 OK response. The Body.Status* methods (e.g. Body.StatusFound) may be used to set a non-200 status.

func Blank

func Blank() Body

Blank will render as a blank response with no Content-Type.

Status 200 by default.

func Data

func Data(data any) Body

Data is a convenience function equivalent to instantiating Body{Data: data}

IMPORTANT: nil Data renders as JSON "null\n", not an empty response. Use Data("") for a blank text/plain response body, or Blank for a blank response with no Content-Type.

func (*Body) MediaTypes

func (res *Body) MediaTypes(cfg Config) iter.Seq[string]

MediaTypes returns the sequence of media types (e.g. text/plain) in the order that this Body will propose.

The order generally follows this pattern:

  1. Type-specific (Html wrapper, string, bytes)
  2. Generic structured (JSON, XML)
  3. Interface implementations (CSV)
  4. Template-based (HTML template, text template)
  5. Golang-native fallback (Gob)

func (Body) StatusAccepted

func (r Body) StatusAccepted() Body

StatusAccepted sets the response as 202 Accepted.

It indicates that the request has been accepted for processing, but the processing has not been completed.

func (Body) StatusBadRequest

func (r Body) StatusBadRequest() Body

StatusBadRequest sets the response as 400 Bad Request.

It indicates that the server cannot process the request due to client error, such as malformed request syntax or invalid request parameters.

func (Body) StatusConflict

func (r Body) StatusConflict() Body

StatusConflict sets the response as 409 Conflict.

It indicates that the request conflicts with the current state of the server, such as attempting to create a duplicate resource.

func (Body) StatusCreated

func (r Body) StatusCreated(location string) Body

StatusCreated sets the response as 201 Created and sets the Location header.

It indicates that a new resource has been successfully created at the given location.

func (Body) StatusForbidden

func (r Body) StatusForbidden() Body

StatusForbidden sets the response as 403 Forbidden.

It indicates that the server understood the request but refuses to authorize it. Unlike 401, authenticating will make no difference.

func (Body) StatusFound

func (r Body) StatusFound(location string) Body

StatusFound sets the response as 302 Found and sets the Location header.

It indicates that the requested resource has been temporarily moved to the given location.

func (Body) StatusGone

func (r Body) StatusGone() Body

StatusGone sets the response as 410 Gone.

It indicates that the requested resource is no longer available and will not be available again. This is a stronger statement than 404 Not Found.

func (Body) StatusInternalServerError

func (r Body) StatusInternalServerError() Body

StatusInternalServerError sets the response as 500 Internal Server Error.

It indicates that the server encountered an unexpected condition that prevented it from fulfilling the request.

func (Body) StatusMethodNotAllowed

func (r Body) StatusMethodNotAllowed() Body

StatusMethodNotAllowed sets the response as 405 Method Not Allowed.

It indicates that the request method is known by the server but is not supported by the target resource.

func (Body) StatusMovedPermanently

func (r Body) StatusMovedPermanently(location string) Body

StatusMovedPermanently sets the response as 301 Moved Permanently and sets the Location header.

Moved Permanently is intended for GET requests.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections#permanent_redirections

func (Body) StatusNoContent

func (r Body) StatusNoContent() Body

StatusNoContent sets the response as 204 No Content.

It indicates that the request was successful but there is no content to return. Commonly used for DELETE operations or updates with no response body.

func (Body) StatusNotAcceptable

func (r Body) StatusNotAcceptable() Body

StatusNotAcceptable sets the response as 406 Not Acceptable.

It indicates that the server cannot produce a response matching the list of acceptable values defined in the request's proactive content negotiation headers.

func (Body) StatusNotFound

func (r Body) StatusNotFound() Body

StatusNotFound sets the response as 404 Not Found.

It indicates that the server cannot find the requested resource.

func (Body) StatusNotImplemented

func (r Body) StatusNotImplemented() Body

StatusNotImplemented sets the response as 501 Not Implemented.

It indicates that the server does not support the functionality required to fulfill the request.

func (Body) StatusNotModified

func (r Body) StatusNotModified() Body

StatusNotModified sets the response as 304 Not Modified.

It indicates that the resource has not been modified since the version specified by the request headers. Used for conditional requests and caching.

func (Body) StatusPermanentRedirect

func (r Body) StatusPermanentRedirect(location string) Body

StatusPermanentRedirect sets the response as 308 Permanent Redirect and sets the Location header.

Permanent Redirect is intended for non-GET requests.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections#permanent_redirections

func (Body) StatusSeeOther

func (r Body) StatusSeeOther(location string) Body

StatusSeeOther sets the response as 303 See Other and sets the Location header.

See Other is used for redirection in response to POST requests.

func (Body) StatusServiceUnavailable

func (r Body) StatusServiceUnavailable() Body

StatusServiceUnavailable sets the response as 503 Service Unavailable.

It indicates that the server is currently unable to handle the request due to temporary overload or scheduled maintenance.

func (Body) StatusTemporaryRedirect

func (r Body) StatusTemporaryRedirect(location string) Body

StatusTemporaryRedirect sets the response as 307 Temporary Redirect and sets the Location header.

Temporary Redirect is like 302 Found but guarantees that the HTTP method will not be changed when the redirected request is made.

func (Body) StatusTooManyRequests

func (r Body) StatusTooManyRequests() Body

StatusTooManyRequests sets the response as 429 Too Many Requests.

It indicates that the user has sent too many requests in a given amount of time. Used for rate limiting.

func (Body) StatusUnauthorized

func (r Body) StatusUnauthorized() Body

StatusUnauthorized sets the response as 401 Unauthorized.

It indicates that authentication is required and has failed or has not been provided.

func (Body) StatusUnprocessableEntity

func (r Body) StatusUnprocessableEntity() Body

StatusUnprocessableEntity sets the response as 422 Unprocessable Entity.

It indicates that the request was well-formed but was unable to be followed due to semantic errors, such as validation failures.

type Config

type Config struct {
	// [Body.Data] may be passed to this template as data if content negotiation resolves to text/html and [Body.TemplateName] matches via [html.Template.Lookup].
	//
	// If both HtmlTemplate and TextTemplate match [Body.TemplateName], HtmlTemplate takes precedence.
	HtmlTemplate *html.Template
	// [Body.Data] may be passed to this template as data if content negotiation resolves to text/plain and [Body.TemplateName] matches via [text.Template.Lookup].
	//
	// If both HtmlTemplate and TextTemplate match [Body.TemplateName], HtmlTemplate takes precedence.
	TextTemplate *text.Template

	// JsonPrefix is used to set [json.Encoder.SetIndent]
	JsonPrefix string
	// JsonIndent is used to set [json.Encoder.SetIndent]
	JsonIndent string
	// XmlPrefix is used to set [xml.Encoder.Indent]
	XmlPrefix string
	// XmlIndent is used to set [xml.Encoder.Indent]
	XmlIndent string
}

Settings for writing the rsvp.Body

type Csv

type Csv interface {
	// MarshalCsv will be called if the Accept header contains text/csv and it is matched, or the URL path ends with .csv
	MarshalCsv(w *csv.Writer) error
}

Csv is used to provide rsvp with a way to render your type as text/csv.

type Handler

type Handler interface {
	ServeHTTP(w ResponseWriter, r *http.Request) Body
}

Handler is rsvp's counterpart to http.Handler.

type HandlerFunc

type HandlerFunc func(w ResponseWriter, r *http.Request) Body

HandlerFunc is a counterpart to http.HandlerFunc.

func (HandlerFunc) ServeHTTP

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *http.Request) Body

type ResponseWriter

type ResponseWriter interface {
	// Header is equivalent to [http.ResponseWriter.Header]
	Header() http.Header

	// DefaultTemplateName is used to associate a default template name with the current handler.
	//
	// It may be overridden by [Body.TemplateName].
	//
	// The intended use case for this method is to call it at the top of an [HandlerFunc] so that the TemplateName does not need to be set exhaustively on every instance of [Body] that the handler might return.
	DefaultTemplateName(name string)
}

ResponseWriter handles metadata and configuration of the response. It bears its "Writer" name mostly for the sake of keeping rsvp.Handler similar to http.Handler.

Its underlying type has a `write` function, but it is not available here because it is controlled indirectly by the Body value that Handler provides.

If you need access to http.ResponseWriter, especially for middleware, you should follow the example of [HandleFunc]'s source code for how to operate rsvp at a lower level from within an http.Handler.

Directories

Path Synopsis
internal
dev

Jump to

Keyboard shortcuts

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