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 ¶
- Variables
- func ShuttingDown(ctx context.Context) <-chan struct{}
- type Builder
- func (listener *Builder) Addr(addr string) *Builder
- func (listener *Builder) Events(bus *event.Bus) *Builder
- func (listener *Builder) IdleTimeout(timeout time.Duration) *Builder
- func (listener *Builder) MaxHeaderBytes(size int) *Builder
- func (listener *Builder) ReadHeaderTimeout(timeout time.Duration) *Builder
- func (listener *Builder) ReadTimeout(timeout time.Duration) *Builder
- func (listener *Builder) ShutdownTimeout(timeout time.Duration) *Builder
- func (listener *Builder) TLS(config *tls.Config) *Builder
- func (listener *Builder) WriteTimeout(timeout time.Duration) *Builder
- type BuilderFunc
- type ListeningEvent
- type Server
- type ShutdownTimedOutEvent
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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()
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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)