Help us shape the future of Go! Take the 2026 Go Developer Survey

server

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 13 Imported by: 0

README

HTTP Server

arkoren.dev/core/http/server binds a listener at startup and serves any http.Handler. Construction opens no listener. With an existing handler named handler and application composer named compose, configure and register the three lifecycle methods:

srv := server.New(handler, func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})
compose.Cleanup(srv.Stop)
compose.Startup(srv.Start)
compose.Run(srv.Serve)

Plain HTTP is the default. Builder.TLS selects HTTPS with a shared native configuration; finish configuring it before Start. Startup validates static credentials without invoking application TLS callbacks.

When its context ends, Serve signals cooperative handlers through server.ShuttingDown and drains tracked HTTP connections for 15 seconds by default. An expired drain closes those connections and returns a deadline error; it cannot forcibly stop arbitrary application goroutines. Stop instead uses its caller's context as its drain budget, including an already-expired context.

Hijacked connections such as WebSockets are neither closed nor awaited. Their owners must close them and join associated work before releasing dependencies. With Builder.Events, Listening reports binding and ShutdownTimedOut reports an expired drain in Serve; neither event proves all handlers have finished.

See the Server guide for application wiring, TLS, and long-lived handlers.

Documentation

Overview

Package server serves any http.Handler over HTTP or HTTPS with secure default timeouts, startup-time port binding, and graceful shutdown.

New creates a Server without binding. Server.Start binds the listener so an unavailable address fails application startup, Server.Serve serves until its context ends and then drains in-flight requests, and Server.Stop releases the server. The package uses net/http interfaces, so it serves any handler.

Configure without opening a listener, then register its three lifecycle methods on an existing application composer:

srv := server.New(http.NotFoundHandler(), func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})
compose.Cleanup(srv.Stop)
compose.Startup(srv.Start)
compose.Run(srv.Serve)

Plaintext HTTP is the default. Builder.TLS selects HTTPS with a shared native TLS configuration. Load certificates before constructing the server; Start checks missing or malformed static credentials before binding. Certificate callbacks run during handshakes, not construction or startup. HTTPS retains native HTTP/2 negotiation. Configure native values before Start and leave them unchanged afterward, except through the callbacks supported by crypto/tls.

Beginning shutdown does not cancel active request contexts. Long-lived handlers watch ShuttingDown and cooperate with shutdown. A drain timeout closes tracked HTTP connections; it cannot forcibly stop application work. Hijacked connections such as WebSockets are neither closed nor awaited: their owners must close them and join associated work before releasing dependencies. Server.Stop always uses its caller's context as its budget.

With Builder.Events, Server.Start publishes Listening ("http.server.listening") with the bound address. Server.Serve publishes ShutdownTimedOut ("http.server.shutdown.timed-out") with the address and the shutdown timeout when tracked HTTP connections did not drain in time and were force-closed. A clean shutdown publishes nothing; serving and shutdown are otherwise reported by the application's lifecycle events when the server runs under an application.

Index

Examples

Constants

This section is empty.

Variables

View Source
var Listening = event.NewTopic[ListeningEvent]("http.server.listening")

Listening is published when Server.Start has bound the listener. Subscribe on the bus supplied through Builder.Events; callbacks run synchronously and should not perform slow work.

Example:

events := event.New()
unsubscribe, err := events.Listen(server.Listening,
	func(_ context.Context, notification server.ListeningEvent) error {
		fmt.Println(notification.Addr)

		return nil
	})
if err != nil {
	return err
}
defer unsubscribe()
View Source
var ShutdownTimedOut = event.NewTopic[ShutdownTimedOutEvent]("http.server.shutdown.timed-out")

ShutdownTimedOut is published when Server.Serve stops serving and tracked HTTP connections have not drained before the shutdown deadline, so those connections are force-closed. Hijacked connections remain caller-owned. A shutdown that drains in time, or one that fails for another reason, publishes nothing. Server.Stop publishes nothing, because its deadline belongs to the caller.

Example:

events := event.New()
unsubscribe, err := events.Listen(server.ShutdownTimedOut,
	func(_ context.Context, notification server.ShutdownTimedOutEvent) error {
		fmt.Println(notification.Addr, notification.Timeout)

		return nil
	})
if err != nil {
	return err
}
defer unsubscribe()

Functions

func ShuttingDown

func ShuttingDown(ctx context.Context) <-chan struct{}

ShuttingDown returns a channel that is closed when the Server that accepted the request starts shutting down. Pass the request context.

Beginning shutdown does not cancel active request contexts. Long-lived handlers must cooperate with this signal to finish before the drain deadline; closing HTTP connections cannot forcibly stop arbitrary application work. Hijacked connections, including WebSockets, are neither closed nor awaited by the server: their owners must close them and join any associated work. arkoren.dev/core/http/response.Stream and arkoren.dev/core/http/response.SSE, including their channel forms, watch this signal. Without a Server in ctx the result is nil, so that select case is disabled. In a long-lived router.Handler, check before the next operation:

Example:

select {
case <-server.ShuttingDown(request.Context()):
	return nil
default:
}
// Continue the next application-owned operation, which must also be cancelable.

Types

type Builder

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

Builder configures New before publication. Callbacks run directly once, synchronously in order on one fresh draft; nil entries panic. Retaining the draft cannot change published settings. Methods return the same draft.

Example:

listener := server.New(handler, func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})

func (*Builder) Addr

func (listener *Builder) Addr(addr string) *Builder

Addr is the TCP address to listen on, such as ":8080" or "127.0.0.1:9090". Defaults to ":8080".

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.Addr("127.0.0.1:8080")

func (*Builder) Events

func (listener *Builder) Events(bus *event.Bus) *Builder

Events receives Listening and ShutdownTimedOut. Nil, the default, publishes nothing.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.Events(bus)

func (*Builder) IdleTimeout

func (listener *Builder) IdleTimeout(timeout time.Duration) *Builder

IdleTimeout limits the keep-alive idle time between requests. Defaults to 120s.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.IdleTimeout(time.Second)

func (*Builder) MaxHeaderBytes

func (listener *Builder) MaxHeaderBytes(size int) *Builder

MaxHeaderBytes limits the size of request headers. Defaults to 1 MB.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.MaxHeaderBytes(1 << 20)

func (*Builder) ReadHeaderTimeout

func (listener *Builder) ReadHeaderTimeout(timeout time.Duration) *Builder

ReadHeaderTimeout limits the time allowed to read request headers, protecting against Slowloris attacks. Defaults to 10s.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.ReadHeaderTimeout(time.Second)

func (*Builder) ReadTimeout

func (listener *Builder) ReadTimeout(timeout time.Duration) *Builder

ReadTimeout limits the time to read the whole request, including the body. Defaults to 30s.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.ReadTimeout(time.Second)

func (*Builder) ShutdownTimeout

func (listener *Builder) ShutdownTimeout(timeout time.Duration) *Builder

ShutdownTimeout bounds native graceful draining in Server.Serve. Zero selects 15s; a negative value expires the drain deadline immediately. A timeout closes tracked HTTP connections, not hijacked connections or arbitrary handler goroutines. Server.Stop instead uses the context supplied by its caller.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.ShutdownTimeout(time.Second)

func (*Builder) TLS

func (listener *Builder) TLS(config *tls.Config) *Builder

TLS selects HTTPS using config; nil restores the default plaintext HTTP. The native configuration, its certificates, and its callback captures remain shared, not cloned. Finish configuration before Server.Start; subsequent configuration mutation is unsupported. Runtime TLS callbacks may select certificates or return separately prepared configurations as crypto/tls allows.

New performs no TLS I/O or validation. Server.Start rejects missing credentials and obviously invalid static certificates before binding. Callback results and full certificate suitability are checked during the handshake; callbacks are never invoked to validate construction or startup. HTTPS never falls back to plaintext.

Example:

srv := server.New(handler, func(listener *server.Builder) {
	listener.TLS(&tls.Config{
		Certificates: []tls.Certificate{identity},
		MinVersion:   tls.VersionTLS12,
	})
})
if err := srv.Start(ctx); err != nil {
	return err
}

func (*Builder) WriteTimeout

func (listener *Builder) WriteTimeout(timeout time.Duration) *Builder

WriteTimeout limits the time to write the response. Defaults to 60s. A negative value disables the deadline for long-lived streams.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

In a New callback:

listener.WriteTimeout(time.Second)

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits the construction draft shared by New's callbacks. Callbacks run synchronously in order; a supplied nil callback panics, while expanding a nil or empty callback slice makes no edits. Published settings are independent of the draft; native TLS configurations remain shared.

Example:

configure := server.BuilderFunc(func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})
srv := server.New(http.NotFoundHandler(), configure)
fmt.Println(srv.Addr())
Example

ExampleBuilderFunc configures a server without binding a listener, showing that Addr reports configuration until Start acquires the native address.

package main

import (
	"fmt"
	"net/http"

	"arkoren.dev/core/http/server"
)

func main() {
	configure := server.BuilderFunc(func(listener *server.Builder) {
		listener.Addr("127.0.0.1:8080")
	})

	srv := server.New(http.NotFoundHandler(), configure)
	fmt.Println(srv.Addr())
}
Output:
127.0.0.1:8080

type ListeningEvent

type ListeningEvent struct {
	// Addr is the bound address, with the chosen port when the configured
	// port is zero. It is a listener address, not a public URL.
	//
	// Example:
	//
	//	notification := server.ListeningEvent{Addr: "127.0.0.1:8080"}
	//	fmt.Println(notification.Addr)
	Addr string
}

ListeningEvent reports the TCP address acquired by Server.Start. Binding does not mean Serve has begun accepting requests or a TLS handshake succeeded.

Example:

notification := server.ListeningEvent{Addr: "127.0.0.1:8080"}
fmt.Println(notification.Addr)

type Server

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

Server is an HTTP server with a separate bind step, so an unavailable address fails startup before serving begins. Register it on an application composer as startup work, acquired cleanup, and a runner:

srv := server.New(handler)
compose.Cleanup(srv.Stop)
compose.Startup(srv.Start)
compose.Run(srv.Serve)

func New

func New(handler http.Handler, configure ...BuilderFunc) *Server

New creates a Server serving handler with completed construction settings. Callbacks run directly once in order; nil entries panic. Zero limits and timeouts receive secure defaults after the final callback. The server retains independent settings, not the draft. It does not bind; call Server.Start. Plaintext HTTP is the default; Builder.TLS opts into HTTPS. Native TLS configuration remains shared and is validated at Start, not construction.

Example:

listener := server.New(handler, func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})

func (*Server) Addr

func (server *Server) Addr() string

Addr returns the bound listener address, or the configured address when the server has not started. After Server.Start, a port-zero address reports the operating system's chosen port; reading it does not start the server.

Example:

srv := server.New(http.NotFoundHandler(), func(listener *server.Builder) {
	listener.Addr("127.0.0.1:8080")
})
fmt.Println(srv.Addr())

func (*Server) HTTP

func (server *Server) HTTP() *http.Server

HTTP returns the shared underlying http.Server for native configuration. Set its fields, including TLSConfig, before Server.Start. Configuration mutation after Start is unsupported. A non-nil TLSConfig selects HTTPS just as Builder.TLS does; nil selects plaintext HTTP.

Example:

srv := server.New(handler)
srv.HTTP().TLSConfig = &tls.Config{Certificates: []tls.Certificate{identity}}

func (*Server) Serve

func (server *Server) Serve(ctx context.Context) error

Serve serves requests on the bound listener until ctx ends, then asks net/http to drain for at most Builder.ShutdownTimeout. An expired drain deadline closes remaining tracked HTTP connections, publishes ShutdownTimedOut, and returns an error matching context.DeadlineExceeded. Hijacked connections are not closed or awaited, and arbitrary handler goroutines cannot be forcibly stopped. Their owners must coordinate completion before releasing dependencies. Serve returns nil after normal shutdown and an error when Server.Start has not run or serving fails. HTTPS uses http.Server.ServeTLS with in-memory credentials, retaining native HTTP/2 negotiation and TLS callbacks; no certificate files are loaded.

Example:

if err := srv.Serve(ctx); err != nil {
	return err
}

func (*Server) Start

func (server *Server) Start(ctx context.Context) error

Start binds the TCP listener and publishes Listening when Builder.Events is set. It does not serve. Starting twice returns an error. Non-nil http.Server.TLSConfig selects HTTPS: missing credentials and obviously invalid static certificates fail before binding or publishing. TLS callbacks are not called here; handshake-time errors remain possible. Do not mutate server or TLS configuration after Start.

Example:

if err := srv.Start(ctx); err != nil {
	return err
}

func (*Server) Stop

func (server *Server) Stop(ctx context.Context) (err error)

Stop signals shutdown and drains tracked HTTP connections until ctx ends, then closes those connections even when ctx was already expired. Its waiting budget comes from ctx, not Builder.ShutdownTimeout. When Serve never ran it also closes the listener bound by Start. Stop is safe to call repeatedly and after Serve returned; shutdown and listener-close errors remain available through errors.Is. Hijacked connections remain caller-owned and are not closed or awaited. Closing a connection does not forcibly stop its handler goroutine.

Example:

srv := server.New(http.NotFoundHandler())
if err := srv.Stop(context.Background()); err != nil {
	return err
}
Example

ExampleServer_Stop shows that cleanup is safe even when startup never acquired a listener. No network operation is needed for this ownership path.

package main

import (
	"context"
	"fmt"
	"net/http"

	"arkoren.dev/core/http/server"
)

func main() {
	srv := server.New(http.NotFoundHandler())

	if err := srv.Stop(context.Background()); err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println("stopped")
}
Output:
stopped

type ShutdownTimedOutEvent

type ShutdownTimedOutEvent struct {
	// Addr is the bound listener address, not an inferred public URL.
	//
	// Example:
	//
	//	notification := server.ShutdownTimedOutEvent{Addr: "127.0.0.1:8080"}
	//	fmt.Println(notification.Addr)
	Addr string

	// Timeout is the configured drain budget, not the total elapsed Serve time.
	//
	// Example:
	//
	//	notification := server.ShutdownTimedOutEvent{Timeout: 15 * time.Second}
	//	fmt.Println(notification.Timeout)
	Timeout time.Duration
}

ShutdownTimedOutEvent describes the native HTTP drain deadline exceeded by Server.Serve. It does not assert that every handler goroutine has stopped; hijacked connections are outside native HTTP draining.

Example:

notification := server.ShutdownTimedOutEvent{
	Addr:    "127.0.0.1:8080",
	Timeout: 15 * time.Second,
}
fmt.Println(notification.Timeout)

Jump to

Keyboard shortcuts

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