httpserver

package module
v0.0.34 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MPL-2.0 Imports: 27 Imported by: 0

README

go-httpserver

github.com/thanhminhmr/go-httpserver is a typed HTTP server layer built on the standard library http.ServeMux. It binds requests into Go structs, applies defaults and validation, provides composable middleware, and builds responses through a shared request Context.

The package keeps routing on the standard library instead of introducing a separate router API. See the package Godoc for the complete request-tag contract and API details.

Example

package main

import (
    "net/http"

    "github.com/rs/zerolog"
    "github.com/thanhminhmr/go-common/ctrl"
    "github.com/thanhminhmr/go-httpserver"
)

type GetUserRequest struct {
    ID      string `url:"id" validate:"required"`
    Verbose bool   `query:"verbose" default:"false"`
}

func main() {
    ctrl.Control(func(globalCtx context.Context) {
        ctrl.Register(ctrl.ShutdownOnSignal)

        config := &httpserver.ServerConfig{
            Port:              8080,
            ReadHeaderTimeout: 5,
            IdleTimeout:       60,
            MaxHeaderBytes:    4096,
            ShutdownOnError:   true,
        }

        router := httpserver.NewServer(config).WithLogger(zerolog.Ctx(globalCtx))
        router.Handle("GET /users/{id}", httpserver.RequestParser(
            func(ctx *httpserver.Context, request GetUserRequest) {
                ctx.NewResponse(http.StatusOK).JsonBody(map[string]any{
                    "id":      request.ID,
                    "verbose": request.Verbose,
                })
            },
        ))
    })
}

NewServer expects ServerConfig to have already had defaults applied and been validated. The example fills every field explicitly for that reason.

Request binding

RequestParser and MiddlewareParser bind exported struct fields from header, cookie, query, ServeMux url wildcards, URL-encoded form bodies, json bodies, multipart bodies, or a raw body. default values are applied before binding and validate rules are checked afterward.

Use an empty source tag such as query:"" or header:"" when the handler needs the whole source. Form and JSON bodies are bounded and read with a timeout; multipart and raw bodies expose the live request stream and should be consumed by the handler that receives them.

The package Godoc is the authoritative reference for tag types, precedence, body media-type selection, JSON behavior, and error handling.

Middleware and responses

Router.Group appends middleware without changing the parent router. Middleware calls next to continue, may return without calling next to short-circuit, and may inspect or replace the downstream response after next returns.

Handlers create responses with Context.NewResponse. The router writes the response only after the complete middleware and handler chain returns. If no response was created, the router returns 500 Internal Server Error.

Go compatibility

ServeMux wildcard binding intentionally uses an unsafe mirror of unexported net/http request state to avoid reparsing Request.Pattern on every request. This makes the package sensitive to Go standard-library layout changes. When upgrading Go, the unsafe-layout regression test must remain passing.

License

Mozilla Public License 2.0. See LICENSE.txt.

Documentation

Overview

Package httpserver provides typed request binding and response construction on top of the standard library http.ServeMux.

Request flow

Create a server with NewServer, optionally derive routers with Router.Group, and register routes with Router.Handle.

RequestParser turns a typed request handler into a Handler:

type GetUserRequest struct {
	ID      string `url:"id" validate:"required"`
	Verbose bool   `query:"verbose" default:"false"`
}

router.Handle("GET /users/{id}", RequestParser(
	func(ctx *Context, request GetUserRequest) {
		ctx.NewResponse(http.StatusOK).JsonBody(map[string]any{
			"id":      request.ID,
			"verbose": request.Verbose,
		})
	},
))

MiddlewareParser provides the same typed request binding for Middleware.

For each request, defaults are applied first, request values are bound next, and validation runs last. The typed handler or middleware runs only when all steps succeed.

Route patterns use standard http.ServeMux syntax. URL tags bind wildcards from the matched pattern.

Request tags

Request fields are bound with tags of the form `source:"name"`:

type Request struct {
	ID     string `url:"id"`
	Search string `query:"q"`
	Token  string `header:"Authorization"`
}

The supported sources are:

header    HTTP headers
cookie    cookies
query     URL query parameters
url       ServeMux wildcards
form      application/x-www-form-urlencoded fields
json      JSON object fields
multipart multipart body parts (stream; see below)
body      raw request body (stream; see below)

Named values are converted to the destination field type. Conversion failures are request errors and prevent the typed handler or middleware from running.

An empty tag binds the complete source instead of one named value:

header:""    -> http.Header
cookie:""    -> KeyValues
query:""     -> KeyValues
url:""       -> KeyValue
form:""      -> KeyValues

`json:""` is slightly different: it decodes the complete JSON value directly into the field.

For a source, use either named fields or one whole-source field; do not mix both forms.

Multipart and raw bodies are exposed as streams:

multipart:""             -> *multipart.Reader
body:""                  -> io.ReadCloser
body:"type/subtype ..."  -> io.ReadCloser for the listed media types

The framework applies no size cap or read timeout to these streams; the handler owns any size or time budget (e.g. via http.MaxBytesReader, the request context, or a self-imposed deadline). Form and JSON bindings are bounded by maxBodyLength (1 MiB) and maxReadBodyDuration (5s).

`default:"value"` supplies a value before request binding.

`validate:"rule"` validates the completed request after all binding has finished.

Binding

A request starts at its zero value. Values are applied in this order:

default -> header -> cookie -> query -> URL -> body -> validation

Later sources may overwrite values supplied by earlier sources.

Body binding is considered for POST, PUT, PATCH, and DELETE requests. The body binder is selected from form, JSON, multipart, or raw body according to the request Content-Type.

Form and JSON bodies are buffered and decoded before the typed handler runs. Multipart and raw body tags instead expose the live request stream and should be consumed during the handler or middleware that receives them. The framework applies no size or time cap to these streams; the handler owns any budget.

Responses and middleware

Handlers construct responses with Context.NewResponse and Response.

A response is written only after the complete middleware and handler chain returns. Middleware can therefore inspect or replace the downstream response after calling next:

func(ctx *Context, next func()) {
	// Before the downstream chain.

	next()

	// After the downstream chain.
}

Middleware may short-circuit a request by returning without calling next.

If the chain completes without creating a response, Router.Handle returns 500 Internal Server Error. Servers created by NewServer also recover panics at the HTTP boundary and return 500 when no final response has been committed.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Context

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

Context is the per-request state passed to Handler and Middleware. It implements context.Context by delegating to the underlying HTTP request and owns the response state assembled by the handler chain.

Context values are created by Router.Handle. The zero value is invalid, and a Context must not be copied after first use.

func (*Context) Deadline added in v0.0.34

func (c *Context) Deadline() (deadline time.Time, ok bool)

Deadline delegates to the HTTP request context.

func (*Context) Done added in v0.0.34

func (c *Context) Done() <-chan struct{}

Done delegates to the HTTP request context.

func (*Context) Err added in v0.0.34

func (c *Context) Err() error

Err delegates to the HTTP request context.

func (*Context) NewResponse added in v0.0.34

func (c *Context) NewResponse(status int) Response

NewResponse starts a new response with status and returns its handle. It clears the previous body and all response headers. The response is not written until the Router.Handle middleware and handler chain returns.

NewResponse panics unless status is between 200 and 599.

func (*Context) Response

func (c *Context) Response() Response

Response returns a handle to the current response without changing it. Its status is zero until Context.NewResponse is called.

func (*Context) Value added in v0.0.34

func (c *Context) Value(key any) any

Value delegates to the HTTP request context.

type Handler added in v0.0.34

type Handler = func(ctx *Context)

Handler handles one HTTP request through a Context. A handler normally creates or replaces the response with Context.NewResponse. If the complete middleware and handler chain returns without creating a response, Router.Handle sends 500 Internal Server Error.

func RequestParser

func RequestParser[Request any](handler RequestHandler[Request]) Handler

RequestParser converts a typed RequestHandler into a Handler.

Request must be a non-pointer struct. Its default values and request-binding tag layout are checked when RequestParser is called; an invalid request definition panics. For each HTTP request, RequestParser creates a fresh Request value, applies defaults, binds request data, validates the result, and then calls handler.

Binding or validation failures configure an empty HTTP error response and do not call handler. RequestParser does not write the response itself; the enclosing Router.Handle writes it after the middleware and handler chain returns. Panics from handler propagate to the server boundary, where servers created by NewServer recover them.

type KeyValue

type KeyValue = map[string]string

KeyValue contains all named ServeMux path wildcard values for an empty `url:""` request tag.

type KeyValues

type KeyValues = map[string][]string

KeyValues contains all values for an empty `cookie:""`, `query:""`, or `form:""` request tag.

type Middleware

type Middleware = func(ctx *Context, next func())

Middleware wraps a Handler in a chain. Call next to continue to the next middleware or the route handler. Returning without calling next short-circuits the chain. Code after next runs on the way out and may inspect or replace the downstream response through Context.Response or Context.NewResponse.

func MiddlewareParser added in v0.0.34

func MiddlewareParser[Request any](handler MiddlewareHandler[Request]) Middleware

MiddlewareParser converts a typed MiddlewareHandler into Middleware. Request defaults, binding, and validation follow the same rules as RequestParser.

Parser middleware shares the same Context with downstream middleware and the route handler. Request bodies are not buffered or rewound, so a body consumed by one parser cannot be parsed again downstream.

A binding or validation failure configures an error response and stops the chain. The response is written later by Router.Handle.

type MiddlewareHandler added in v0.0.34

type MiddlewareHandler[Request any] = func(ctx *Context, request Request, next func())

MiddlewareHandler handles a parsed request around the next middleware or route handler. Call next to continue the chain. Returning without calling next short-circuits the chain; code after next may inspect or replace the downstream response.

type RequestHandler

type RequestHandler[Request any] = func(ctx *Context, request Request)

RequestHandler handles a request after defaults, request binding, and validation have completed. The handler normally creates its response through Context.NewResponse.

type Response

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

Response is a handle to response state owned by a Context. Copies share the same state. The zero value is invalid.

func (Response) Body added in v0.0.34

func (r Response) Body() any

Body returns the configured body value, or nil if no body is set.

func (Response) BytesBody

func (r Response) BytesBody(body []byte)

BytesBody sets a raw byte body without setting Content-Type.

func (Response) Cookie

func (r Response) Cookie(cookie http.Cookie)

Cookie appends a Set-Cookie header for cookie to the response.

func (Response) Header

func (r Response) Header() http.Header

Header returns the live response header map. A later Context.NewResponse call clears it.

func (Response) JsonBody

func (r Response) JsonBody(body any)

JsonBody stores body for JSON marshaling when the response is written. Successful marshaling sets Content-Type to "application/json; charset=utf-8". A marshal failure writes 500 Internal Server Error with an empty body.

func (Response) MarshalZerologObject

func (r Response) MarshalZerologObject(e *zerolog.Event)

MarshalZerologObject implements zerolog.LogObjectMarshaler for the configured status, headers, and body.

func (Response) OctetsBody

func (r Response) OctetsBody(body []byte)

OctetsBody sets body with Content-Type "application/octet-stream".

func (Response) PlainTextBody

func (r Response) PlainTextBody(body string)

PlainTextBody sets body with Content-Type "text/plain; charset=utf-8".

func (Response) Status

func (r Response) Status() int

Status returns the configured HTTP status, or zero before Context.NewResponse is called.

func (Response) StreamBody

func (r Response) StreamBody(body func(io.Writer) error)

StreamBody sets a body writer without setting Content-Type. The HTTP status is committed before body runs, so an error returned by body can be logged but cannot change the response status.

func (Response) StringBody

func (r Response) StringBody(body string)

StringBody sets a raw string body without setting Content-Type.

type Router added in v0.0.34

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

Router registers Handler values on a shared http.ServeMux with an ordered middleware chain. Routers returned by Router.Group share the same ServeMux but keep independent middleware slices.

The zero value is invalid. Create a Router with NewServer or derive one from an existing Router with Router.Group.

func NewServer

func NewServer(config *ServerConfig) Router

NewServer creates a Router backed by a new http.ServeMux and registers its HTTP server with the ctrl lifecycle. The server wraps all requests with request/response logging and panic recovery.

The server listens on ":<config.Port>" when the lifecycle starts and shuts down during cleanup. config must already have defaults applied and be validated before NewServer is called, and it should not be modified afterward. If serving fails unexpectedly, config.ShutdownOnError controls whether the application lifecycle is canceled.

func (Router) Group added in v0.0.34

func (r Router) Group(middlewares ...Middleware) Router

Group returns a Router that shares r's routes and logger and appends middlewares to r's middleware chain. Group does not mutate r and does not retain the caller's middleware slice.

func (Router) Handle added in v0.0.34

func (r Router) Handle(pattern string, handler Handler)

Handle registers handler for pattern using http.ServeMux pattern syntax. Requests run r's middleware in order followed by handler. Middleware may stop the chain by returning without calling next.

The response is written after the complete chain returns, so middleware may inspect or replace a downstream response after next returns. If the chain returns without creating a response, Handle writes 500 Internal Server Error. Registration errors and pattern conflicts follow http.ServeMux behavior.

func (Router) WithLogger added in v0.0.34

func (r Router) WithLogger(logger *zerolog.Logger) Router

WithLogger returns a Router that reports route registrations to logger. The receiver is not modified.

type ServerConfig

type ServerConfig struct {
	// Port is the TCP port to listen on all interfaces.
	Port uint16 `cfg:"port" validate:"required" default:"8080"`

	// ReadHeaderTimeout limits time spent reading request headers, in seconds.
	ReadHeaderTimeout int `cfg:"read_header_timeout" validate:"min=1,max=60" default:"5"`

	// IdleTimeout limits idle keep-alive time, in seconds.
	IdleTimeout int `cfg:"idle_timeout" validate:"min=1,max=3600" default:"60"`

	// MaxHeaderBytes limits request header size in bytes.
	MaxHeaderBytes int `cfg:"max_header_bytes" validate:"min=0,max=65536" default:"4096"`

	// ShutdownOnError cancels the application when serving fails unexpectedly.
	ShutdownOnError bool `cfg:"shutdown_on_error" default:"true"`
}

ServerConfig configures the http.Server registered by NewServer. Timeout values are in seconds. NewServer does not apply defaults or validate the configuration tags.

Jump to

Keyboard shortcuts

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