routeit

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Nov 28, 2025 License: MIT Imports: 29 Imported by: 1

README

routeit Go Reference license routeit examples Go Report Card

routeit is a lightweight web framework built in Go that conforms to the HTTP/1.1 spec. This is meant to be exploratory and educational and is not fit for production purposes. routeit can be seen as a simpler version of net/http that allows for building simple servers and has been built from the ground up with no usage of non-standard libraries. It includes some features, such as parameterised dynamic routing, built-in error handling and JSON handling that net/http does not handle out of the box.

Getting Started

Before beginning, make sure you have installed Go and setup your GOPATH correctly. Create a .go file called server.go and add the following code:

package main

import "github.com/sktylr/routeit"

type HelloWorld struct {
	Hello string `json:"hello"`
}

func main() {
	srv := routeit.NewServer(routeit.ServerConfig{Debug: true})
	srv.RegisterRoutes(routeit.RouteRegistry{
		"/hello": routeit.Get(func(rw *routeit.ResponseWriter, req *routeit.Request) error {
			body := HelloWorld{Hello: "World"}
			return rw.Json(body)
		}),
	})
	srv.StartOrPanic()
}

Install routeit from the command line:

$ go get github.com/sktylr/routeit

Run the server:

$ go run server.go

The server is now running on localhost:8080 and will respond to GET requests on the /hello endpoint.

$ curl -D - http://localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 01 Sep 2025 12:06:32 GMT
Server: routeit
Content-Length: 17

{"hello":"World"}

Check out the examples/ directory for further examples of using routeit's features.

Documentation

Documentation for the latest released version can be found on pkg.go.dev/github.com/sktylr/routeit. The repository also contains a docs/ directory that contains notes and further details regarding the library.

Documentation for the current development version can be generated using godoc and requires the repository to be cloned.

# Install the package if not already installed
$ go install golang.org/x/tools/cmd/godoc@latest

# Run the documentation server on port 3000
$ godoc -http=:3000

The documentation can now be viewed at localhost:3000/pkg/github.com/sktylr/routeit/.

Features

HTTP Version Support: Only HTTP/1.1 is supported. My implementation is mostly based off RFC-9112 and Mozilla developer specs.

HTTP Method Supported? Notes
GET
HEAD Cannot be implemented by the integrator, it is baked into the server implementation.
POST
PUT
DELETE
CONNECT Will never be implemented since I will not support tunnelling
OPTIONS Baked into the server implementation.
TRACE Baked into the server implementation but is defaulted OFF. Can be turned on using the AllowTraceRequests configuration option
PATCH
Content Types Request supported? Response supported? Notes
application/json Parsing and encoding is handled automatically by routeit
text/plain
... Any request or response type can be supported, but the integrator must handling the parsing and marshalling. The ResponseWriter.RawWithContentType and Request.BodyFromRaw methods can be used correspondingly
HTTPS

routeit supports both HTTP and HTTPS. Unlike with net/http and other libraries, a single routeit server can support both HTTP and HTTPS without needing to explicitly manage the separate threads and ports. The HttpConfig allows the HTTP(s) ports to be specified, as well as the TLS config required if a server wants to accept HTTPS communication.

TLS is backed by the crypto/tls standard library, which is the same used in net/http. This provides sensible out-of-the-box defaults while allowing high levels of cusomisation if required. Once a TLS config is provided to a routeit server, the server will only listen for HTTPS communication, unless plain HTTP communication is explicitly enabled. routeit also comes with built-in HTTPS upgrade mechanisms, which will instruct clients to upgrade their connections to HTTPS before they will be accepted, which is controlled through the HttpConfig.UpgradeToHttps and HttpConfig.UpgradeInstructionMaxAge properties. Check out examples/https for example setups showcasing each of the 3 configuration options that use HTTPS.

Handlers

Each resource on the server is served by a Handler. Handlers can respond to multiple HTTP methods, or just one. routeit handles method delegation and will respond with a 405: Method Not Allowed response with the correct Allow header if the resource exists but does not respond to the request method.

Get, Post, Put, Patch and Delete can all be used to construct a handler that responds to a single method, and accept a function of func(*routeit.ResponseWriter, *routeit.Request) error signature. If an endpoint should respond to multiple HTTP methods, MultiMethod can be used, which accepts a struct to allow selection of the methods the handler should respond to. The error returned from the handler function does not need to be a specific routeit error in every situation.

Middleware

routeit gives the developer the ability to write custom middleware to perform actions such as rate-limiting or authorisation handling. Examples can be found in examples/middleware. Linking is performed through a Chain interface which is passed as an argument to the middleware function. Multiple middleware functions can be attached to a single server. The order of attachment is important, as that is the order used when processing the middleware for each incoming request. Middleware can choose to block a request (by not invoking Chain.Proceed) but be aware that the server will always attempt to send a response to the client for every incoming request. It is more common to block a request by returning a specific error that can be conformed to a HTTP response.

Routing

routeit supports both static and dynamic routing, as well as allowing for enforcing specific prefixes and/or suffixes to be part of a dynamic match. Routes are registered to the server using Server.RegisterRoutes and Server.RegisterRoutesUnderNamespace using a map from route to handler. The server setup allows for namespaces - both global and local. These reduce the complexity of the routing setup by avoiding redundant duplication. For example, we might set the server's global namespace to "api", meaning that all routes are registered with /api as the prefix, without needing to write it out for each route.

Dynamic matches are denoted using colon (:) syntax, with the characters after the colon used as the name for the dynamic component. A dynamic component can be forced to contain a required prefix and/or suffix using pipe (|) notation, as shown in the example below. Once matched, the handler (or middleware) can use Request.PathParam to extract the name path segment. Critically, Request.PathParam will always return a non-empty string so long as the provided name does appear in the matched path. If prefixes or suffixes are enforced, the path component will be the entire component - including a prefix and/or suffix.

"/:foo|pre/bar/:baz||suf/:qux/:corge|pre|suf": routeit.Get(func(rw *routeit.ResponseWriter, req *routeit.Request) error {
	// The first path segment - this must be prefixed by "pre" to match
	foo := req.PathParam("foo")
	// The third path segment - this must end with "suf" to match
	baz := req.PathParam("baz")
	// The fourth path segment
	qux := req.PathParam("qux")
	// The fifth path segment - this must start with "pre" and end with "suf",
	// with at least 1 character between them to match
	corge := req.PathParam("corge")

	// ...
	return nil
})

In the above example, the router would invoke this handler if a URI such as /prefix/bar/my-suf/anything/prefix-then-suf was received at the edge. In this case, foo would be "prefix", baz would be "my-suf", qux would be "anything" and corge would be "prefix-then-suf".

Routes can also be rewritten before being routed or processed. For example, we might want to rewrite / to /static/index.html, as / is what the browser will request and is a much cleaner URI than the actual URI needed for the resource. Rewriting is covered in docs/rewrites.md as well as by example in examples/static/rewrites.

Further details about the structure of routing can be found in docs/trie.md, which also covers key information such as prioritisation of routing if multiple routes can match the incoming URI.

Testing

Testing is baked into the routeit library and can be used to increase confidence in the server. There are two level of tests supported, both of which are currently experimental.

The TestClient allows for E2E-like tests and operates on a full server instance. To reduce flakiness, TCP connections are not opened, however every other piece of the server is tested - parsing, URI rewriting, routing, middleware, handling, error management and static asset loading. Making a request with a TestClient instance will return a TestResponse which allows assertions to be made on the status code, headers, and response body. Usage of TestClient is recommended for high-level validation that all moving parts of the server work as expected, without caring about the specific implementation details. Each example project in examples/ contains E2E tests using TestClient.

For finer isolation, middleware and handlers can both be tested independently, using TestMiddleware and TestHandler respectively. Both functions accept a handler or middleware instance, and a TestRequest, which can be constructed using NewTestRequest. They will also return an error and TestResponse for making assertions. In the case of testing middleware, a boolean is also returned to indicate whether the middleware proceeded to the next piece of middleware or not.

A key point to note is that, unlike with TestClient, the error is not run through any error mapping or handling. So if the error is returned from TestHandler or TestMiddleware, it is the exact error that the corresponding handler or middleware returned. In these cases, TestResponse will not be nil, but most meaningful assertions will not pass, unless the handler or middleware explicitly wrote a header or response body before returning an error.

Errors

Application code can return errors of any type to the library in their handlers. A number of helpful error functions are exposed which allow the application code to conform their errors to HTTP responses. If non-library errors are returned (or the application code panics with an error), we attempt to infer the reason or cause and map that to a HTTP error. The integrator can provide a custom mapper using the ServerConfig.ErrorMapper which can provide additional custom logic in mapping from an error type to an error the server understands. If the ErrorMapper cannot assign a more appropriate error type, it can return nil which will pass off the default inference which maps common errors to sensible defaults. For example, if an ErrNotExist error is returned, we map that to a 404: Not Found HTTP error. We fallback to mapping to a 500: Internal Server Error if we cannot establish a mapping.

Additionally, custom handling can be provided for specific HTTP status codes, if routeit's default response is not sufficient. This allows for additional logging, new headers, or transformation of the response body to something more meaningful than routeit's default response, for example. Common use cases include for 404: Not Found, and 500: Internal Server Error. These can be registered using Server.RegisterErrorHandlers.

examples/errors contains examples for how custom error handling and mapping can be performed using routeit.

Logging

Each valid incoming request is logged with the corresponding method, path (both edge and rewritten) and response status. 4xx responses are logged using the WARN log level, 5xx responses are logged using ERROR and all other responses are logged using INFO. Only requests that can be successfully parsed are logged.

Documentation

Overview

Package routeit is a lightweight web framework for Go.

Licensed under the MIT License. You may obtain a copy of the License at https://opensource.org/licenses/MIT

This package provides HTTP/1.1 request parsing, routing, middleware chaining, error handling, and testing utilities. It is designed as a learning framework for Go, demonstrating core web server patterns without external dependencies.

Example usage:

server := routeit.NewServer(routeit.ServerConfig{Debug: true})
server.RegisterRoutes(routeit.RouteRegistry{
	"/hello": func(rw *routeit.ResponseWriter, req *routeit.Request) error {
		rw.Text("Hello, World!")
		return nil
	},
})
server.StartOrPanic()

Index

Constants

View Source
const (
	Byte RequestSize = 1
	KiB              = 1024 * Byte
	MiB              = 1024 * KiB
)

Variables

View Source
var (
	CTApplicationFormUrlEncoded = ContentType{/* contains filtered or unexported fields */}
	CTApplicationGraphQL        = ContentType{/* contains filtered or unexported fields */}
	CTApplicationJavaScript     = ContentType{/* contains filtered or unexported fields */}
	CTApplicationJson           = ContentType{/* contains filtered or unexported fields */}
	CTApplicationOctetStream    = ContentType{/* contains filtered or unexported fields */}
	CTApplicationPdf            = ContentType{/* contains filtered or unexported fields */}
	CTApplicationXml            = ContentType{/* contains filtered or unexported fields */}
	CTApplicationZip            = ContentType{/* contains filtered or unexported fields */}

	CTTextCss        = ContentType{/* contains filtered or unexported fields */}
	CTTextCsv        = ContentType{/* contains filtered or unexported fields */}
	CTTextHtml       = ContentType{/* contains filtered or unexported fields */}
	CTTextJavaScript = ContentType{/* contains filtered or unexported fields */}
	CTTextMarkdown   = ContentType{/* contains filtered or unexported fields */}
	CTTextPlain      = ContentType{/* contains filtered or unexported fields */}

	CTImageAvif = ContentType{/* contains filtered or unexported fields */}
	CTImageGif  = ContentType{/* contains filtered or unexported fields */}
	CTImageJpeg = ContentType{/* contains filtered or unexported fields */}
	CTImagePng  = ContentType{/* contains filtered or unexported fields */}
	CTImageSvg  = ContentType{/* contains filtered or unexported fields */}
	CTImageWebp = ContentType{/* contains filtered or unexported fields */}

	CTAudioMpeg = ContentType{/* contains filtered or unexported fields */}
	CTAudioOgg  = ContentType{/* contains filtered or unexported fields */}
	CTAudioWav  = ContentType{/* contains filtered or unexported fields */}

	CTVideoMp4  = ContentType{/* contains filtered or unexported fields */}
	CTVideoOgg  = ContentType{/* contains filtered or unexported fields */}
	CTVideoWebm = ContentType{/* contains filtered or unexported fields */}

	CTMultipartByteranges = ContentType{/* contains filtered or unexported fields */}
	CTMultipartFormData   = ContentType{/* contains filtered or unexported fields */}

	CTAcceptAll = ContentType{/* contains filtered or unexported fields */}
)
View Source
var (
	GET     = HttpMethod{/* contains filtered or unexported fields */}
	HEAD    = HttpMethod{/* contains filtered or unexported fields */}
	POST    = HttpMethod{/* contains filtered or unexported fields */}
	PUT     = HttpMethod{/* contains filtered or unexported fields */}
	DELETE  = HttpMethod{/* contains filtered or unexported fields */}
	PATCH   = HttpMethod{/* contains filtered or unexported fields */}
	OPTIONS = HttpMethod{/* contains filtered or unexported fields */}
	TRACE   = HttpMethod{/* contains filtered or unexported fields */}
)
View Source
var (
	StatusContinue           = HttpStatus{100, "Continue"}
	StatusSwitchingProtocols = HttpStatus{101, "Switching Protocols"}
	StatusProcessing         = HttpStatus{102, "Processing"}
	StatusEarlyHints         = HttpStatus{103, "Early Hints"}
)

1xx codes

View Source
var (
	StatusOK                          = HttpStatus{200, "OK"}
	StatusCreated                     = HttpStatus{201, "Created"}
	StatusAccepted                    = HttpStatus{202, "Accepted"}
	StatusNonAuthoritativeInformation = HttpStatus{203, "Non-Authoritative Information"}
	StatusNoContent                   = HttpStatus{204, "No Content"}
	StatusResetContent                = HttpStatus{205, "Reset Content"}
	StatusPartialContent              = HttpStatus{206, "Partial Content"}
	StatusMultiStatus                 = HttpStatus{207, "Multi-Status"}
	StatusAlreadyReported             = HttpStatus{208, "Already Reported"}
	StatusIMUsed                      = HttpStatus{226, "IM Used"}
)

2xx codes

View Source
var (
	StatusMultipleChoices   = HttpStatus{300, "Multiple Choices"}
	StatusMovedPermanently  = HttpStatus{301, "Moved Permanently"}
	StatusFound             = HttpStatus{302, "Found"}
	StatusSeeOther          = HttpStatus{303, "See Other"}
	StatusNotModified       = HttpStatus{304, "Not Modified"}
	StatusUseProxy          = HttpStatus{305, "Use Proxy"}
	StatusUnused            = HttpStatus{306, "Unused"}
	StatusTemporaryRedirect = HttpStatus{307, "Temporary Redirect"}
	StatusPermanentRedirect = HttpStatus{308, "Permanent Redirect"}
)

3xx codes

View Source
var (
	StatusBadRequest                  = HttpStatus{400, "Bad Request"}
	StatusUnauthorized                = HttpStatus{401, "Unauthorized"}
	StatusPaymentRequired             = HttpStatus{402, "Payment Required"}
	StatusForbidden                   = HttpStatus{403, "Forbidden"}
	StatusNotFound                    = HttpStatus{404, "Not Found"}
	StatusMethodNotAllowed            = HttpStatus{405, "Method Not Allowed"}
	StatusNotAcceptable               = HttpStatus{406, "Not Acceptable"}
	StatusProxyAuthenticationRequired = HttpStatus{407, "Proxy Authentication Required"}
	StatusRequestTimeout              = HttpStatus{408, "Request Timeout"}
	StatusConflict                    = HttpStatus{409, "Conflict"}
	StatusGone                        = HttpStatus{410, "Gone"}
	StatusLengthRequired              = HttpStatus{411, "Length Required"}
	StatusPreconditionFailed          = HttpStatus{412, "Precondition Failed"}
	StatusContentTooLarge             = HttpStatus{413, "Content Too Large"}
	StatusURITooLong                  = HttpStatus{414, "URI Too Long"}
	StatusUnsupportedMediaType        = HttpStatus{415, "Unsupported Media Type"}
	StatusRangeNotSatisfiable         = HttpStatus{416, "Range Not Satisfiable"}
	StatusExpectationFailed           = HttpStatus{417, "Expectation Failed"}
	StatusImATeapot                   = HttpStatus{418, "I'm a teapot"}
	StatusMisdirectedRequest          = HttpStatus{421, "Misdirected Request"}
	StatusUnprocessableContent        = HttpStatus{422, "Unprocessable Content"}
	StatusLocked                      = HttpStatus{423, "Locked"}
	StatusFailedDependency            = HttpStatus{424, "Failed Dependency"}
	StatusTooEarly                    = HttpStatus{425, "Too Early"}
	StatusUpgradeRequired             = HttpStatus{426, "Upgrade Required"}
	StatusPreconditionRequired        = HttpStatus{428, "Precondition Required"}
	StatusTooManyRequests             = HttpStatus{429, "Too Many Requests"}
	StatusRequestHeaderFieldsTooLarge = HttpStatus{431, "Request Header Fields Too Large"}
	StatusUnavailableForLegalReasons  = HttpStatus{451, "Unavailable For Legal Reasons"}
)

4xx codes

View Source
var (
	StatusInternalServerError           = HttpStatus{500, "Internal Server Error"}
	StatusNotImplemented                = HttpStatus{501, "Not Implemented"}
	StatusBadGateway                    = HttpStatus{502, "Bad Gateway"}
	StatusServiceUnavailable            = HttpStatus{503, "Service Unavailable"}
	StatusGatewayTimeout                = HttpStatus{504, "Gateway Timeout"}
	StatusHttpVersionNotSupported       = HttpStatus{505, "HTTP Version Not Supported"}
	StatusVariantAlsoNegotiates         = HttpStatus{506, "Variant Also Negotiates"}
	StatusInsufficientStorage           = HttpStatus{507, "Insufficient Storage"}
	StatusLoopDetected                  = HttpStatus{508, "Loop Detected"}
	StatusNotExtended                   = HttpStatus{510, "Not Extended"}
	StatusNetworkAuthenticationRequired = HttpStatus{511, "Network Authentication Required"}
)

5xx codes

Functions

func ContextValueAs

func ContextValueAs[T any](req *Request, key any) (T, bool)

ContextValueAs is a shorthand to casting and using Request.ContextValue It will return a value and true whenever the request context has a key that matches the generic type, and will return false if either the context does not have a matching key, or the value stored under that key does not match the generic type.

func NewTlsConfigForCertAndKey added in v1.2.0

func NewTlsConfigForCertAndKey(certPath, keyPath string) *tls.Config

This is a convenience method for instantiating a TLS config with a single certificate and key. This will panic if the certificate or key cannot be loaded. crypto/tls sets sensible defaults for TLS config, so this is safe to use unless specific fine-grained control is needed.

Types

type AllowOriginFunc

type AllowOriginFunc func(*Request, string) (bool, []string)

This function should validate the provided origin, returning true if the server should accept cross-origin requests from this origin. Optionally, the function can choose to return values for the Vary response header, to ensure that caches are serving responses correctly. This will be appended to the existing Vary response header, which will already contain Origin.

type Chain

type Chain interface {
	Proceed(rw *ResponseWriter, req *Request) error
}

The Chain manages the arrangement of middleware and can be used to invoke the next piece of middleware.

type ContentType

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

func (ContentType) Matches

func (a ContentType) Matches(b ContentType) bool

Compare two content types for equality. For ease, this considers two content types to be equal of they share the same part and subtype, and their charset is the same OR one charset is UTF-8 and the other is unset. This is because UTF-8 is the default charset used by routeit but is sometimes omitted due to being the de-facto standard across the web.

func (ContentType) WithCharset

func (ct ContentType) WithCharset(cs string) ContentType

Destructively sets the charset of the content type

type CorsConfig

type CorsConfig struct {
	// A list of origins the server can accept cross-origin requests from.
	// These can either be literal matches for the origin (case sensitive), or
	// wildcard matches, denoted by the * character. There can only be at most
	// 1 wildcard character in each element in this list.
	//
	// Example:
	//	["http://localhost:*", "http://example.com", "http://*.example.com"]
	//
	// This will match against any requests from localhost, regardless of the
	// port, any request from example.com, and any request from a subdomain of
	// example.com. If the Origin header is present and does not match any of
	// these patterns, we will reject the request.
	AllowedOrigins []string
	// Set to true if all origins should be allowed. This is equivalent to
	// setting [CorsConfig.AllowedOrigins] = ["*"] or always returning true, ""
	// from [CorsConfig.AllowOriginFunc]. Takes precedence over
	// [CorsConfig.AllowedOrigins] if both are set.
	AllowAllOrigins bool
	// A function that determines if the request's origin should be allowed.
	// This is only called when the request contains the Origin header, and can
	// also return additional Vary header values. Will take precedence over
	// [CorsConfig.AllowAllOrigins] and [CorsConfig.AllowedOrigins].
	AllowOriginFunc AllowOriginFunc
	// The allowed methods for cross-origin requests. This will always contain
	// simple methods (GET, HEAD and POST), as well as OPTIONS, which is
	// required for pre-flight requests. Note: this only carries relevance when
	// handling pre-flight requests initiated by the browser. It will not block
	// actual requests containing these methods. If you would like to block
	// specific HTTP methods from reaching the server, it is best to implement
	// your own middleware. Otherwise, you can simply avoid providing an
	// implementation of the corresponding method (i.e. never implement a PUT
	// handler); in this way the server will never respond successfully to
	// requests of those methods.
	AllowedMethods []HttpMethod
	// A list of the headers that the server will also accept. These do not
	// need to include the CORS safe headers (Accept, Accept-Language,
	// Content-Language, Content-Type and Range) and should be headers that
	// this server is willing to accept in incoming requests. This only
	// confirms to the client that it is permitted to send a request with these
	// headers. The server may still employ additional middleware or a handler
	// function that rejects the request if the header is not correctly
	// populated.
	AllowedHeaders []string
	// The maximum age that the client (or intermediaries) should cache the
	// pre-flight response for. Set to 0 or leave unset if you don't want this
	// to be included. Set to a negative number to explicitly set to 0 - i.e.
	// the client cannot cache responses.
	MaxAge time.Duration
	// The headers that should be exposed to the client JavaScript when
	// receiving a response to a cross-origin request. By default, browsers
	// will expose the Cache-Control, Content-Language, Content-Type, Expires,
	// Last-Modified and Pragma response headers to the client JavaScript. If
	// additional headers should be included (e.g. X-My-Custom-Header), then
	// they should be included here. The casing of the header values does not
	// matter.
	ExposeHeaders []string
	// Determines whether the Access-Control-Allow-Credentials header should be
	// included in responses and set to "true". This is required if the client
	// and server want the client to send credentials (cookies, HTTP
	// authentication, TLS certificates etc.).
	IncludeCredentials bool
}

func DefaultCors

func DefaultCors() CorsConfig

Default CORS config that will allow all origins and all methods and not include any special headers or authentication details

type ErrorMapper

type ErrorMapper func(e error) *HttpError

The ErrorMapper can be implemented to provide more granular control over error mapping in the server. This function will be called whenever an application error is returned or recovered from a panic and can provide extra control over how errors are transformed. By default, the server will handle known mappings, such as interpreting an ErrNotExists error as a 404: Not Found error. If no mapping can be found, the server will default to a 500: Internal Server Error. If the custom ErrorMapper cannot identify the HttpError it should return, it may return nil

type ErrorResponseHandler

type ErrorResponseHandler func(erw *ErrorResponseWriter, req *Request)

type ErrorResponseWriter

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

The ErrorResponseWriter is very similar to the ResponseWriter, except it does not allow mutation of the status code of the response. It is used in custom error handlers that can provide a uniform response to the client in the event of a 4xx or 5xx error. Common use cases include a custom 404 response.

func (*ErrorResponseWriter) Error

func (erw *ErrorResponseWriter) Error() (error, bool)

Returns the underlying error that caused the application to return a 4xx or 5xx response. This error is not always present, such as when the server returns 404: Not Found.

func (*ErrorResponseWriter) Json

func (erw *ErrorResponseWriter) Json(v any)

Write a Json response. If the input cannot be marshalled to Json, the original default routeit response for the corresponding status code will be used.

func (*ErrorResponseWriter) Raw

func (erw *ErrorResponseWriter) Raw(raw []byte)

func (*ErrorResponseWriter) RawWithContentType

func (erw *ErrorResponseWriter) RawWithContentType(raw []byte, ct ContentType)

func (*ErrorResponseWriter) Text

func (erw *ErrorResponseWriter) Text(text string)

func (*ErrorResponseWriter) Textf

func (erw *ErrorResponseWriter) Textf(format string, a ...any)

type Handler

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

func Delete

func Delete(fn HandlerFunc) Handler

Creates a handler that responds to DELETE requests

func Get

func Get(fn HandlerFunc) Handler

Creates a handler that will handle GET request. Internally this will also handle HEAD requests which behave the same as GET requests, except the response does not contain the body, instead it only contains the headers that the GET request would return.

func MultiMethod

func MultiMethod(mmh MultiMethodHandler) Handler

Creates a handler that responds to multiple HTTP methods (e.g. GET and POST on the same route). The router internally will decide which handler to invoke depending on the method of the request. An implementation does not need to be provided for each of the methods in MultiMethodHandler, it is sufficient to only implement the methods that the endpoint should respond to. The handler will ensure that any non-implemented methods return a 405: Method Not Allowed response.

func Patch

func Patch(fn HandlerFunc) Handler

Creates a handler that responds to PATCH requests

func Post

func Post(fn HandlerFunc) Handler

Creates a handler that responds to POST requests

func Put

func Put(fn HandlerFunc) Handler

Creates a handler that responds to PUT requests

type HandlerFunc

type HandlerFunc func(rw *ResponseWriter, req *Request) error

type HttpConfig added in v1.2.0

type HttpConfig struct {
	// This is the port that the HTTP listener will listen on. If the entire
	// [HttpConfig] is left empty, this will default to port 8080. If a
	// [tls.Config] is set, this will be left empty (meaning the server will
	// not respond to plain HTTP messages) unless explicitly configured.
	HttpPort uint16
	// The port that HTTPS messages are expected to be sent to. This only has
	// relevance if [HttpConfig.TlsConfig] is non-nil. If the HTTPS port is set
	// with no TLS config, server setup will panic. When a TLS config is
	// provided, the server by default listens on port 443 for HTTPS requests,
	// which can be changed with this property if required.
	HttpsPort uint16
	// The TLS config for the server. This is required if the server wishes to
	// receive and respond to HTTPS messages. When provided with no ports
	// configured, the server will listen for HTTPS messages on port 443, and
	// will not expect HTTP messages. Configure the [HttpConfig.HttpPort]
	// explicitly if it is desirable to listen to both HTTP and HTTPS requests.
	TlsConfig *tls.Config
	// Set this flag if you want the server to instruct clients to upgrade
	// their HTTP messages to HTTPS. Enabling this flag with a valid TLS config
	// and no HTTP port selected will default to the server listening for plain
	// HTTP messages on port 80, and HTTPS messages on the chosen port (or
	// defaulted to 443).
	UpgradeToHttps bool
	// Determines how long clients are instructed to remember to use HTTPS for
	// the server and its host subdomains. Set to 0 if the client should not
	// choose to remember. Only relevant when [HttpConfig.UpgradeToHttps] is
	// set, otherwise its value is ignored.
	UpgradeInstructionMaxAge time.Duration
}

The HttpConfig is used to specify details of the port(s) that the server will listen on. If left empty, the server will respond to HTTP requests on port 8080. If a tls.Config is provided, the server will respond to HTTPS requests, defaulting to listening on port 443. If it is desirable to listen to both HTTP and HTTPS requests, the HTTP port will need to be explicitly configured, commonly to port 80. In such cases, the HTTPS port only needs to be set if listening to HTTPS requests on port 443 is not desired. When HTTPS is enabled, the server can also instruct client to upgrade any HTTP communication to HTTPS, controlled using [HttpConfig.UpgradeToHttps] and [HttpConfig.UpgradeInstructionMaxAge]. If a HTTP port is not selected, but UpgradeToHttps is enabled, the server will listen for HTTP messages on port 80.

type HttpError

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

func ErrBadGateway

func ErrBadGateway() *HttpError

func ErrBadRequest

func ErrBadRequest() *HttpError

func ErrConflict

func ErrConflict() *HttpError

func ErrContentTooLarge

func ErrContentTooLarge() *HttpError

func ErrExpectationFailed

func ErrExpectationFailed() *HttpError

func ErrFailedDependency

func ErrFailedDependency() *HttpError

func ErrForbidden

func ErrForbidden() *HttpError

func ErrGatewayTimeout

func ErrGatewayTimeout() *HttpError

func ErrGone

func ErrGone() *HttpError

func ErrHttpVersionNotSupported

func ErrHttpVersionNotSupported() *HttpError

func ErrImATeapot

func ErrImATeapot() *HttpError

func ErrInsufficientStorage

func ErrInsufficientStorage() *HttpError

func ErrInternalServerError

func ErrInternalServerError() *HttpError

func ErrLengthRequired

func ErrLengthRequired() *HttpError

func ErrLocked

func ErrLocked() *HttpError

func ErrLoopDetected

func ErrLoopDetected() *HttpError

func ErrMethodNotAllowed

func ErrMethodNotAllowed(allowed ...HttpMethod) *HttpError

func ErrMisdirectedRequest

func ErrMisdirectedRequest() *HttpError

func ErrNetworkAuthenticationRequired

func ErrNetworkAuthenticationRequired() *HttpError

func ErrNotAcceptable

func ErrNotAcceptable() *HttpError

func ErrNotExtended

func ErrNotExtended() *HttpError

func ErrNotFound

func ErrNotFound() *HttpError

func ErrNotImplemented

func ErrNotImplemented() *HttpError

func ErrPaymentRequired

func ErrPaymentRequired() *HttpError

func ErrPreconditionFailed

func ErrPreconditionFailed() *HttpError

func ErrPreconditionRequired

func ErrPreconditionRequired() *HttpError

func ErrProxyAuthenticationRequired

func ErrProxyAuthenticationRequired() *HttpError

func ErrRangeNotSatisfiable

func ErrRangeNotSatisfiable() *HttpError

func ErrRequestHeaderFieldsTooLarge

func ErrRequestHeaderFieldsTooLarge() *HttpError

func ErrRequestTimeout

func ErrRequestTimeout() *HttpError

func ErrServiceUnavailable

func ErrServiceUnavailable() *HttpError

func ErrTooEarly

func ErrTooEarly() *HttpError

func ErrTooManyRequests

func ErrTooManyRequests() *HttpError

func ErrURITooLong

func ErrURITooLong() *HttpError

func ErrUnauthorized

func ErrUnauthorized() *HttpError

func ErrUnavailableForLegalReasons

func ErrUnavailableForLegalReasons() *HttpError

func ErrUnprocessableContent

func ErrUnprocessableContent() *HttpError

func ErrUnsupportedMediaType

func ErrUnsupportedMediaType(accepted ...ContentType) *HttpError

func ErrUpgradeRequired

func ErrUpgradeRequired() *HttpError

func ErrVariantAlsoNegotiates

func ErrVariantAlsoNegotiates() *HttpError

func (*HttpError) Error

func (e *HttpError) Error() string

func (*HttpError) Is

func (e *HttpError) Is(err error) bool

Is is used to compare error types. routeit considers two HttpError's to be the same if they share the same status code, so long as the status code is valid (i.e. >= 100, < 600)

func (*HttpError) Status

func (he *HttpError) Status() HttpStatus

func (*HttpError) WithCause

func (he *HttpError) WithCause(cause error) *HttpError

func (*HttpError) WithMessage

func (he *HttpError) WithMessage(message string) *HttpError

Add a custom message to the response exception. This is destructive and overwrites the previous message if present.

func (*HttpError) WithMessagef

func (he *HttpError) WithMessagef(format string, args ...any) *HttpError

Shorthand for calling HttpError.WithMessage using a format string.

type HttpMethod

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

type HttpStatus

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

Http Status codes for responses. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status

func (HttpStatus) Is1xx

func (s HttpStatus) Is1xx() bool

func (HttpStatus) Is2xx

func (s HttpStatus) Is2xx() bool

func (HttpStatus) Is3xx

func (s HttpStatus) Is3xx() bool

func (HttpStatus) Is4xx

func (s HttpStatus) Is4xx() bool

func (HttpStatus) Is5xx

func (s HttpStatus) Is5xx() bool

type LogAttrExtractor

type LogAttrExtractor func(*Request, HttpStatus) []slog.Attr

The LogAttrExtractor can be used to output additional request metadata to the default log line that is produced on every request. By default, routeit will log the request path (including the rewritten and edge path), the HTTP status of the response, the request's User-Agent field, the request's method and the client's IP address.

type Middleware

type Middleware func(c Chain, rw *ResponseWriter, req *Request) error

A middleware function is called for all incoming requests that reach the server. It can choose to block the request, or pass it off to the next middleware in the chain using [Chain.Proceed]. Common use cases include authentication or rate-limiting. The order with which middleware is registered to the server is important, as it defines the order of the chain. If a middleware chooses to block a request (by returning an error), it will not be propagated through to the rest of the chain, nor the handler defined by the application for the route and method of the request. If headers are set on the response, using ResponseWriter.Headers, the headers will be propagated to the response - even if the handler or intermediary middleware returns an error or panics. The error's headers take precedence and will overwrite any headers of the same name that are already set.

func CorsMiddleware

func CorsMiddleware(cc CorsConfig) Middleware

Returns middleware that allows CORS requests from the client to function properly on the server. This should be placed early in the middleware stack and ideally before authentication middleware or most custom middleware. This middleware will handle any cross-origin request, including pre-flight and actual requests.

type MultiMethodHandler

type MultiMethodHandler struct {
	Get    HandlerFunc
	Post   HandlerFunc
	Put    HandlerFunc
	Delete HandlerFunc
	Patch  HandlerFunc
}

type QueryParams

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

func (*QueryParams) All

func (q *QueryParams) All(key string) ([]string, bool)

Access a query parameter if present. This returns a slice that may contain multiple elements, some of which may be empty (e.g. if the client sends `?foo=`).

func (*QueryParams) First

func (q *QueryParams) First(key string) (string, bool)

Access the first query parameter for the given key, if present

func (*QueryParams) Last

func (q *QueryParams) Last(key string) (string, bool)

Access the last query parameter for the given key, if present

func (*QueryParams) Only

func (q *QueryParams) Only(key string) (string, bool, error)

Access a query parameter, asserting that it is only present exactly once in the request URI. This will return false if the query parameter is not present, and a 400: Bad Request error if the query parameter is present more than once.

type Request

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

func (*Request) AcceptsContentType

func (req *Request) AcceptsContentType(other ContentType) bool

Can be used to determine whether the client will accept the provided ContentType

func (*Request) BodyFromJson

func (req *Request) BodyFromJson(to any) error

Parses the Json request body into the destination. Ensures that the Content-Type header is application/json and will return a 415: Unsupported Media Type error if this is not the case. Will panic if the destination is not a pointer. Will also panic if the request cannot contain a request body, such as GET requests. Should be preferred to Request.UnsafeBodyFromJson.

func (*Request) BodyFromRaw

func (req *Request) BodyFromRaw(ct ContentType) ([]byte, error)

Returns the raw body content provided it matches the given content type, otherwise a 415: Unsupported Media Type error is returned. Will panic if this is called on a method that cannot support a request body, such as GET, HEAD or OPTIONS. Should be preferred to Request.UnsafeBodyFromRaw.

func (*Request) BodyFromText

func (req *Request) BodyFromText() (string, error)

Parses the text/plain content from the request. This method checks that the Content-Type header is set to text/plain, returning a 415: Unsupported Media Type error if that is not the case. Panics if the request method is GET, since GET requests cannot support bodies. Should be preferred to Request.UnsafeBodyFromText.

func (*Request) ClientIP

func (req *Request) ClientIP() string

The client's IP address that established connection with the server

func (*Request) ContentType

func (req *Request) ContentType() ContentType

Access the content type of the request body. Does not return a meaningful value for requests without the Content-Type header, or requests that should not contain a body, such as GET. The ContentType.Matches method can be used to determine whether two content types are equivalent.

func (*Request) Context

func (req *Request) Context() context.Context

Access the request's context. This should be used for context aware calculations within handlers or middleware, for example to load an object from a database.

func (*Request) ContextValue

func (req *Request) ContextValue(key any) (any, bool)

Access a value stored on the request's context.

func (*Request) Headers

func (req *Request) Headers() *RequestHeaders

Access the headers of the request.

func (*Request) Host

func (req *Request) Host() string

The Host header of the request. This will always be present and non-empty

func (*Request) Id added in v1.1.0

func (req *Request) Id() string

The ID of the request. This will only be populated if an implementation is provided to [ServerConfig.RequestIdProvider].

func (*Request) Method

func (req *Request) Method() HttpMethod

Access the request's HTTP method

func (*Request) NewContextValue

func (req *Request) NewContextValue(key any, val any)

Store a value on the request's context. This is particularly useful for middleware, which can set particular values the can be read in later middleware or the handler itself.

func (*Request) Path

func (req *Request) Path() string

The request's URL excluding the host. Does not include query parameters. Where the server has URL rewrites configured, this will be the rewritten URL. This URL has been escaped correctly, so cannot be used for additional routing where dynamic path segment routing is used in the server, as decoded "/"s could inadvertently be treated as control characters. If additional routing needs to be performed based on the path delimiter, use Request.RawPath.

func (*Request) PathParam

func (req *Request) PathParam(param string) string

Extract a path parameter from the request path. The name must exactly match the name of the parameter when it was registered to the router. For example, if the route was registered under `/:foO|prefix|suffix`, then this method should be called with `"foO"`. This will always be a non-empty value corresponding to the named path segment in the request, unless the name does not match any segments, in which case it will be empty.

func (*Request) Queries

func (req *Request) Queries() *QueryParams

Access the query parameters of the request URI. This will always return a non-nil pointer, even if the URI contains no query parameters. See the QueryParams type for access methods to retrieve individual keys.

func (*Request) RawPath

func (req *Request) RawPath() string

The raw path received at the edge of the server. This is not url-decoded and has not been rewritten if URL rewriting is enabled for the server.

func (*Request) Tls added in v1.2.0

func (req *Request) Tls() *tls.ConnectionState

The request's TLS connection state. This is nil when the request was received over HTTP and non-nil whenever the request was made using HTTPS. It contains details about the TLS connection, such as the version and cipher suites.

func (*Request) UnsafeBodyFromJson

func (req *Request) UnsafeBodyFromJson(to any) error

Parses the Json request body into the destination. Does not check the Content-Type header to confirm that the request body has application/json type body. Will panic if the destination is not a pointer, or the request cannot support a body (such as GET requests).

func (*Request) UnsafeBodyFromRaw

func (req *Request) UnsafeBodyFromRaw() []byte

Returns the raw body content. Will panic if this is called on a method that cannot support a request body, such as GET, HEAD or OPTIONS. This does not assert that the body is present nor contains a corresponding Content-Type header

func (*Request) UnsafeBodyFromText

func (req *Request) UnsafeBodyFromText() string

Returns the raw body content as a string. Will panic if this is called on a method that cannot support a request body, such as GET, HEAD or OPTIONS.

func (*Request) UserAgent

func (req *Request) UserAgent() string

The User-Agent header of the request. May be empty if not included by the client

type RequestHeaders

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

The RequestHeaders type can be used to read the headers on the incoming request. Lookup is case insensitive, and the header may appear multiple times within the request.

func (*RequestHeaders) All

func (rh *RequestHeaders) All(key string) ([]string, bool)

Access all request header values for the given key.

func (*RequestHeaders) First

func (rh *RequestHeaders) First(key string) (string, bool)

A wrapper over RequestHeaders.All that extracts the first element, if present.

func (*RequestHeaders) Last

func (rh *RequestHeaders) Last(key string) (string, bool)

A wrapper over RequestHeaders.All that extracts the last element, if present.

func (*RequestHeaders) Only

func (rh *RequestHeaders) Only(key string) (string, bool, error)

Use RequestHeaders.Only when the header is expected to appear exactly once in the request. It will return an error if the headers is present but does not contain exactly 1 value.

type RequestIdProvider added in v1.1.0

type RequestIdProvider func(*Request) string

A RequestIdProvider is used to provide each incoming request with an ID. Each valid request that reaches the server will be tagged with an ID returned from this function. The resultant ID will be available on the request using Request.Id.

type RequestSize

type RequestSize uint32

The RequestSize type is used to define what size request the server is willing to accept from the client. It accounts for the entire request - including the request line and headers.

type ResponseHeaders

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

The ResponseHeaders can be used to mutate the headers for a given ResponseWriter. It allows writing of header values and uses case- insensitive insertion for the header key.

func (*ResponseHeaders) Append

func (rh *ResponseHeaders) Append(key, val string)

Append the given value to the current header value for the key. Prefer this to ResponseHeaders.Set unless the header explicitly needs to be overwritten. Header key and values will be sanitised per HTTP spec before being added to the response. It is the user's responsibility to ensure that the headers are safe and non-conflicting. For example, it is heavily discouraged to modify the Content-Type or Content-Length headers as they are managed implicitly whenever a body is written to a response and can cause issues on the client if they contain incorrect values.

func (*ResponseHeaders) Set

func (rh *ResponseHeaders) Set(key, val string)

Use ResponseHeaders.Set to completely overwrite whatever headers are already set for the given (case-insensitive) key. Prefer ResponseHeaders.Append where possible. This is destructive, meaning repeated calls using the same key will preserve the last value. Header key and values will be sanitised per HTTP spec before being added to the response. It is the user's responsibility to ensure that the headers are safe and non-conflicting. For example, it is heavily discouraged to modify the Content-Type or Content-Length headers as they are managed implicitly whenever a body is written to a response and can cause issues on the client if they contain incorrect values.

type ResponseWriter

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

func (*ResponseWriter) Headers

func (rw *ResponseWriter) Headers() *ResponseHeaders

Access the headers for the response so they can be mutated. It is the user's responsibility to ensure that the headers are safe and non-conflicting. For example, it is heavily discouraged to modify the Content-Type or Content-Length headers as they are managed implicitly whenever a body is written to a response and can cause issues on the client if they contain incorrect values.

func (*ResponseWriter) Json

func (rw *ResponseWriter) Json(v any) error

Adds a JSON response body to the response and sets the corresponding Content-Length and Content-Type headers. This is a destructive operation, meaning repeated calls to Json(...) only preserve the last invocation.

func (*ResponseWriter) Raw

func (rw *ResponseWriter) Raw(raw []byte)

Adds a raw response body to the response and sets the corresponding Content-Length and Content-Type headers. This is a destructive operation, meaning repeated calls to Raw(...) only preserve the last invocation. The mimetype of the body is inferred from its content.

func (*ResponseWriter) RawWithContentType

func (rw *ResponseWriter) RawWithContentType(raw []byte, ct ContentType)

Destructively sets the body of the response and updates headers accordingly

func (*ResponseWriter) Status

func (rw *ResponseWriter) Status(s HttpStatus)

Sets the status of the response. The server sets an opinionated default depending on the incoming request. POST requests default to 201: Created and DELETE requests default to 204: No Content. All other request methods default to 200: OK.

func (*ResponseWriter) Text

func (rw *ResponseWriter) Text(text string)

Adds a plaintext response body to the response and sets the corresponding Content-Length and Content-Type headers. This is a destructive operation, meaning repeated calls to Text(...) only preserve the last invocation.

func (*ResponseWriter) Textf

func (rw *ResponseWriter) Textf(format string, a ...any)

Shorthand for the Text function using a format string.

type RouteRegistry

type RouteRegistry map[string]Handler

The RouteRegistry is used to associate routes with their corresponding handlers. Routing supports both static and dynamic routes. The keys of the RouteRegistry represent the route that the handler will be matched against. They can optionally include a leading slash and all trailing slashes will be stripped. Static routes can be supplied by providing the exact path the route should match against. Dynamic routes can be provided using a colon-notation. Optional prefixes and/or suffixes can be provided for dynamic routes, that will only match against the incoming URI if the corresponding path segments start or end with the given prefix or suffix. The extracted path parameter contains the entire matched path segment - including the prefix and/or suffix. The substring between the prefix and suffix must have non-zero length.

Examples:

  • "/foo/:bar" -> This will match against "/foo/<anything>" and name the first matched parameter "bar".
  • "/:foo/bar/:baz" -> This will match against "/<anything>/bar/<anything>". The first matched parameter will be named "foo", while the second will be named "baz".
  • "/:foo|pref" -> This will match against "/pref<anything>".
  • "/:foo||suf" -> This will match against "/<anything>suf".
  • "/:foo|pref|suf" -> This will match against "/pref<anything>suf".

Registering routes with dynamic components with the same name (such as "/:foo/bar/:foo") will cause the application to panic.

Names parameters can be accessed using Request.PathParam, providing the case-sensitive name of the parameter as provided in the route registration.

type Server

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

func NewServer

func NewServer(conf ServerConfig) *Server

Constructs a new server given the config. Defaults are provided for all options to provide a base of sane values. When setting up for the first time make sure to set [ServerConfig.Debug] to true, or include valid hosts in [ServerConfig.AllowedHosts], to allow the server to receive requests that it will accept.

func (*Server) RegisterErrorHandlers

func (s *Server) RegisterErrorHandlers(handlers map[HttpStatus]ErrorResponseHandler)

Register specific handlers for a given status code. These are called automatically after the entire request has finished processing, and allow the integrator to uniformly respond to certain 4xx or 5xx status codes. Common use cases include for 401 or 404 handling. For example, it may be desired for all 404 responses to return application/json content, which can be done in one place. This method will panic if handlers are attempted to be registered for non 4xx or 5xx status codes.

func (*Server) RegisterMiddleware

func (s *Server) RegisterMiddleware(ms ...Middleware)

Registers middleware to the server. The order of registration matters, where the first middleware registered will be the first middleware called in the chain, the second will be the second and so on.

func (*Server) RegisterRoutes

func (s *Server) RegisterRoutes(rreg RouteRegistry)

Register all routes in the provided registry to the router on the server. All routes will already obey the global namespace (if configured). This is a destructive operation, meaning that if there are multiple calls to RegisterRoutes with overlapping values, the latest value takes precedence.

func (*Server) RegisterRoutesUnderNamespace

func (s *Server) RegisterRoutesUnderNamespace(namespace string, rreg RouteRegistry)

RegisterRoutesUnderNamespace registers all routes in the registry under a specific namespace. All routes already obey the global namespace (if configured). This is a destructive operation - for example, if the /api/foo route has already been registered, and this function is called with the /api namespace and the registry contains a /foo route, this function will overwrite the original routing entry. The local namespace used here may contain dynamic path components, and will match in the same manner that regular dynamic path components do in their routing.

Examples:

Namespace = /api
RegisterRoutesUnderNamespace("/foo", {"/bar": ...})
The route registered will be /api/foo/bar

Namespace = <not initialised>
RegisterRoutesUnderNamespace("/foo/bar", {"/baz": ...})
The route will be registered under /foo/bar/baz

func (*Server) Start

func (s *Server) Start() error

Starts the server using the config and registered routes. This should be the last line of a main function as any code after this call is not executable. The server's config is thread-safe - meaning that if thread A initialised the server, registered routes and started the server, and thread B attempted to register additional routes to the same server, then thread B would panic. The server may also not be started multiple times from different threads as this will also cause a panic

func (*Server) StartOrPanic

func (s *Server) StartOrPanic()

Attempts to start the server, panicking if that fails

type ServerConfig

type ServerConfig struct {
	// The maximum request size (headers, request line and body inclusive) that
	// the server will accept. Anything above this will be rejected.
	RequestSize RequestSize
	// The read deadline to leave the connection with the client open for.
	ReadDeadline time.Duration
	// The write deadline that the connection is left open with the client
	// for responses.
	WriteDeadline time.Duration
	// A global namespace that **all** routes are registered under. Common
	// examples include /api. Does not need to include a leading slash. The
	// global namespace may not contain dynamic routing segments - e.g. a
	// global namespace of /:foo will register all routes on the server
	// directly under the literal "/:foo" route.
	Namespace string
	// The location of the statically loaded files served by the server. All
	// requests to reach these files must also start with this prefix and may
	// also need the global Namespace if configured. The path must be a
	// subdirectory of the project's root and the server will panic if this is
	// not the case. The path does not have to point to a _valid_ directory as
	// this allows the server to dynamically write to disk and serve files from
	// there, though this is discouraged. The path is interpreted as a relative
	// path, not an absolute path, regardless of the presence of a leading slash.
	StaticDir string
	// Enables debug information, such as logs. Do not enable for production
	// servers. Example behaviour includes logging request bodies for 4xx or
	// 5xx responses.
	Debug bool
	// The path to the configuration file holding the URL rewrite information
	// for the server. It may be anywhere on the system, but must exist and be
	// readable by the server, otherwise the setup will panic. The file must be
	// a .conf file following the URL rewrite syntax. If the rewrites are
	// illegal (e.g. conflicting entries, invalid URI's or malformed in
	// general), the server setup will panic. Rewriting is not recursive, i.e.
	// if a server has rules /foo -> /bar and /bar -> /baz, an incoming request
	// to /foo will only be rewritten to /bar, it will not take the second step
	// to /baz. The [Request.Path] method always returns the request path
	// **after** rewriting.
	URLRewritePath string
	// An optional mapper to map application errors to routeit [HttpError]s
	// that are transformed to valid responses. Called whenever the application
	// code returns or panics an error
	ErrorMapper ErrorMapper
	// The allowed hosts that this server can serve from. If the incoming
	// request contains a Host header that is not satisfied by this list,
	// routeit will reject the request. Fully qualified domains can be specific
	// (e.g. www.example.com), or the domain can be prepended with . to match
	// against all subdomains. For example, .example.com will match against
	// api.example.com, www.example.com, example.com and any other subdomains
	// of example.com. Only a single layer of subdomains is considered (i.e.
	// this will not match against site.web.example.com). When Debug is enabled
	// this defaults to [".localhost", "127.0.0.1", "[::1]"] if no list is
	// specified.
	AllowedHosts []string
	// When enabled, the server will only return responses to the client that
	// strictly match the client's Accept header, if it is included in the
	// request. If the application code returns a non-compliant response
	// Content-Type, the server will automatically transform this to a 406: Not
	// Acceptable response.
	StrictClientAcceptance bool
	// By default, routeit registers TRACE handlers for all routes registered
	// to the server. If this is not desirable, leave AllowTraceRequests at its
	// default value of false. If you are happy to support TRACE requests, set
	// AllowTraceRequests to true.
	AllowTraceRequests bool
	// The logging handler used for server-specific logging. Logging within the
	// application code may use its own logging handler that is independent of
	// the given handler. If not supplied, this will use a [slog.JSONHandler]
	// that outputs to [os.Stdout]. Level defaults to INFO but will be set to
	// DEBUG if [ServerConfig.Debug] is true.
	LoggingHandler slog.Handler
	// Http request and response headers may appear multiple times in the
	// message. For certain headers, repeated entries can pose security risks
	// or not make sense when attempting to interpret the request. routeit will
	// block requests that repeat specific header values automatically. By
	// default, the list of headers where only 0 or 1 value is allowed is
	// "Authorization", "Content-Length", "Content-Type", "Cookie", "Expect",
	// "Host", "Origin", "Range", "Referer" and "User-Agent". If you have
	// additional headers that should be limited to 0 or 1 appearances, they
	// can be included in this property. Lookup is case-insensitive.
	StrictSingletonHeaders []string
	// Use [LogAttrExtractor] to include additional metadata in the default
	// request line that is dumped for all incoming requests.
	LogAttrExtractor LogAttrExtractor
	// Use [RequestIdProvider] to tag each incoming request with an ID. This
	// will automatically be logged and will be included as the "X-Request-Id"
	// header in the response. Note that the ID returned by this function is
	// allowed to be empty. In such cases, routeit will proceed the request as
	// normal, since the lack of request ID is not a strong enough reason to
	// entirely block a request. If such behaviour is desirable, it is
	// recommended to introduce custom middleware that blocks requests that
	// contain empty request ID's.
	RequestIdProvider RequestIdProvider
	// The header that each request ID is given in the response. This will
	// default to "X-Request-Id" if [ServerConfig.RequestIdProvider] is
	// non-nil. Otherwise, the header value will be ignored.
	RequestIdHeader string
	// Use [HttpConfig] to control whether the server responds to HTTP
	// requests, HTTPS requests, or both.
	HttpConfig
}

type TestClient

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

func NewTestClient

func NewTestClient(s *Server) TestClient

Instantiates a test client that can be used to perform end-to-end tests on the server.

func NewTestTlsClient added in v1.2.0

func NewTestTlsClient(s *Server, tlsState *tls.ConnectionState) TestClient

Creates a client that will simulate having a valid TLS connection to the server. Use this method when the handlers or middleware performs TLS dependent actions.

func (TestClient) Delete

func (tc TestClient) Delete(path string, h ...string) *TestResponse

Makes a DELETE request against the resource. Can include an arbitrary number of headers, specific individually by their keys and values

func (TestClient) Get

func (tc TestClient) Get(path string, h ...string) *TestResponse

Makes a GET request against the specific path. Should not include a trailing slash but may optionally omit a leading slash. Can include an arbitrary number of headers, specified after the path. Keys and values of headers should be individual arguments.

func (TestClient) Head

func (tc TestClient) Head(path string, h ...string) *TestResponse

Makes a HEAD request against the specific path. Should not include a trailing slash but may optionally omit the leading slash. Can include an arbitrary number of headers, specified after the path. Keys and values of headers should be individual arguments.

func (TestClient) Options

func (tc TestClient) Options(path string, h ...string) *TestResponse

Makes an OPTIONS request against the specified endpoint. Can include key, value pairs representing the headers of the request.

func (TestClient) PatchJson

func (tc TestClient) PatchJson(path string, body any, h ...string) *TestResponse

Makes a PATCH request against the specified path, using a Json request body. Will panic if the Json marshalling fails. Can include an arbitrary number of headers, specified as key, value pairs after the request body.

func (TestClient) PatchRaw

func (tc TestClient) PatchRaw(path string, body []byte, ct ContentType, h ...string) *TestResponse

Makes a PATCH request with the corresponding body and content type. Content length is calculated automatically. Additional headers can also be specified in pairs after the content type.

func (TestClient) PatchText

func (tc TestClient) PatchText(path string, text string, h ...string) *TestResponse

Makes a PATCH request against the specified path, using a text request body. Allows for inclusion of an arbitrary number of headers, specified in key, value format after the body.

func (TestClient) PostJson

func (tc TestClient) PostJson(path string, body any, h ...string) *TestResponse

Makes a POST request against the specified path, using the second argument as the request body, which is converted to Json. Panics if the Json conversion fails. Can include an arbitrary number of headers, specified after the request body. Keys and values of headers should be individual arguments.

func (TestClient) PostRaw

func (tc TestClient) PostRaw(path string, body []byte, ct ContentType, h ...string) *TestResponse

Makes a POST request with the corresponding body and content type. Content length is calculated automatically. Additional headers can also be specified in pairs after the content type.

func (TestClient) PostText

func (tc TestClient) PostText(path string, text string, h ...string) *TestResponse

Makes a POST request against the specified path, using a text request body. Can include an arbitrary number of headers, specified after the request body. Keys and values of headers should be individual arguments.

func (TestClient) PutJson

func (tc TestClient) PutJson(path string, body any, h ...string) *TestResponse

Makes a PUT request against the specified path, using a Json request body. Will panic if the Json marshalling fails. Can include an arbitrary number of headers, specified as key, value pairs after the request body.

func (TestClient) PutRaw

func (tc TestClient) PutRaw(path string, body []byte, ct ContentType, h ...string) *TestResponse

Makes a PUT request with the corresponding body and content type. Content length is calculated automatically. Additional headers can also be specified in pairs after the content type.

func (TestClient) PutText

func (tc TestClient) PutText(path string, text string, h ...string) *TestResponse

Makes a PUT request against the specified path, using a text request body. Allows for inclusion of an arbitrary number of headers, specified in key, value format after the body.

func (TestClient) Trace

func (tc TestClient) Trace(path string, h ...string) *TestResponse

Makes a TRACE request against the specified endpoint. Can include key, value pairs representing the headers of the request. This will only be accepted by the server if it supports the TRACE method, which is disabled by default.

func (TestClient) WithTestConfig

func (tc TestClient) WithTestConfig(c TestConfig) TestClient

Instantiate a new test client with the given overwrite config. These values (so long as they are non-zero) will overwrite the server's configuration used in the test. They are especially useful for time related tests, as they can be used to reduce the write deadline for the server, helping to keep tests running quickly

type TestConfig

type TestConfig struct {
	WriteDeadline time.Duration
}

type TestRequest

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

The TestRequest object can be used when unit testing specific components of a routeit application, such as middleware. It should be constructed using NewTestRequest and provides ways to verify behaviour that happens to a request, for example if a context value was added properly.

func NewTestRequest

func NewTestRequest(t testing.TB, path string, m HttpMethod, opts TestRequestOptions) *TestRequest

This will create a new test request object that can be used in tests, for example when unit testing middleware.

func (*TestRequest) ContextValue

func (tr *TestRequest) ContextValue(key string) (any, bool)

func (*TestRequest) NewContextValue

func (tr *TestRequest) NewContextValue(key string, val any)

type TestRequestOptions

type TestRequestOptions struct {
	// The raw body of the request. This must be marshalled by the user of
	// [TestRequestOptions] and the corresponding Content-Type header should be
	// included in [TestRequestOptions.Headers] if the handler or middleware
	// uses safe body loads. Additionally, the Content-Length header should
	// also be provided.
	Body []byte
	// The headers of the request, if any. These should be specific as key,
	// value pairs in order. For example:
	// 	[]string{"Authorization", "Bearer foo", "Content-Type": "text/plain"}
	Headers []string
	// The Ip address of the client. This may be useful for security middleware
	Ip string
	// Specific path parameters that the URI should contain and extract.
	// Currently this needs to be explicitly defined since unit tests do not go
	// through the entire handling flow, meaning requests are not routed
	// properly so path parameters are not extracted.
	PathParams map[string]string
	// The TLS connection state. Use this if the middleware or handler under
	// test is TLS aware (e.g. middleware that may block clients if they are
	// not using TLS)
	TlsConnectionState *tls.ConnectionState
}

The TestRequestOptions allow you to specify certain traits the request will have under test. You do not need to use any of the fields, though it can be helpful to set certain state on the request, such as a specific header, if the unit under test relies on the state to perform an action.

type TestResponse

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

func TestHandler

func TestHandler(h Handler, tr *TestRequest) (*TestResponse, error)

The TestHandler function allows for isolated unit tests of a handler function. The TestRequest can be composed using NewTestRequest and can take whatever headers, body and context the test needs to test the handler appropriately. Under test circumstances, the handler will **not** reject the request if the URI does not match the URI the handler is registered to.

func TestMiddleware

func TestMiddleware(m Middleware, tr *TestRequest) (*TestResponse, bool, error)

The TestMiddleware function can be used in unit tests to test a middleware's implementation. A TestResponse is returned alongside a boolean flag and an error. The TestResponse allows assertions to be made on the ResponseWriter, for example to assert that a particular header was included or not. The boolean confirms whether the middleware proceeded or not, and the error is the error that the middleware ultimately returned.

func (*TestResponse) AssertBodyContainsString

func (tr *TestResponse) AssertBodyContainsString(t testing.TB, want string)

Assert that a body contains the given substring. Supports improper substrings (i.e. where the substring is exactly equal to the superstring).

func (*TestResponse) AssertBodyEmpty

func (tr *TestResponse) AssertBodyEmpty(t testing.TB)

Assert that the response body is empty

func (*TestResponse) AssertBodyMatchesString

func (tr *TestResponse) AssertBodyMatchesString(t testing.TB, want string)

Assert that a body exactly matches the given string

func (*TestResponse) AssertBodyMatchesStringf

func (tr *TestResponse) AssertBodyMatchesStringf(t testing.TB, wantf string, args ...any)

Assert that a body exactly matches the given string with format options This is the same as formatting the string using fmt.Sprintf and calling TestResponse.AssertBodyMatchesString directly

func (*TestResponse) AssertBodyStartsWithString

func (tr *TestResponse) AssertBodyStartsWithString(t testing.TB, want string)

Assert that a body starts with the given prefix. Supports improper substrings (i.e. where the prefix exactly equals the whole body).

func (*TestResponse) AssertHeaderContains

func (tr *TestResponse) AssertHeaderContains(t testing.TB, header, want string)

Assert a header is present and that it contains the given string. For clarity, this will only compare over a single slice element, it will not join multiple elements into 1 string for comparison.

func (*TestResponse) AssertHeaderMatches

func (tr *TestResponse) AssertHeaderMatches(t testing.TB, header string, want []string)

Assert that a header is present and matches the given slice of strings

func (*TestResponse) AssertHeaderMatchesString

func (tr *TestResponse) AssertHeaderMatchesString(t testing.TB, header, want string)

Similar to TestResponse.AssertHeaderMatches, except we assert there is only exactly 1 element in the header slice, and it matches the given string exactly.

func (*TestResponse) AssertStatusCode

func (tr *TestResponse) AssertStatusCode(t testing.TB, want HttpStatus)

Assert that the status code of the response matches

func (*TestResponse) BodyToJson

func (tr *TestResponse) BodyToJson(t testing.TB, to any)

Parses the Json response into a destination object. Fails if the Json parsing fails or if the response is not a Json response. The destination must be passed by reference and not by value.

func (*TestResponse) RefuteHeaderPresent

func (tr *TestResponse) RefuteHeaderPresent(t testing.TB, header string)

Asserts that a header key is not present in the response

Directories

Path Synopsis
internal
cmp
requestid module

Jump to

Keyboard shortcuts

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