Repository navigation
Replies: 3 comments
Revision: the open questions answered, on humus.MainThe charter's revision 5 moved this proposal to wave 3, behind Proposal: humus.Main, which has landed. This revision answers the four open questions, applies humus.Main, and takes in the charter's revision 7. The open questions:
What humus.Main changes:
From the charter's revision 7, REST and exposing health probes are now wave 4, failure injection strips scenarios at this server in wave 5, and the deferrals to "later" have waves. Each section below replaces the section of the same name in the post. Background and References are unchanged, apart from the wording fixes at the end. SummaryA new package,
func main() {
httpserver.Main(app.Init, humus.WithTelemetry(buildTelemetry))
}
The foundation decides nothing about REST, health or failure injection. It is general enough that REST's Service is built on it, that health probes can be served on a server of their own or attached to the API's server, and that failure injection can strip scenarios from a request before the end user's handler sees it. Proposed APIThe checks other than package httpserver // github.com/z5labs/humus/httpserver
// Settings configures a Service. Every field is a plain value, and a field's
// zero value means zero, never "use the default": start from DefaultSettings
// and change the fields you source. TLS has no default, so it is always one
// of them.
type Settings struct {
// Addr is the TCP address the Service listens on through the Host, as
// host:port. An empty host listens on every address. Port 0 picks a free
// port, which Service.Addr reports.
//
// Default: ":8443". Valid: host:port, with a port from 0 to 65535.
// Check: CheckAddr.
Addr string
// TLS is the config the Service serves HTTPS with. It is required: the
// Service serves nothing in plain text.
//
// Default: none. Valid: a config with Certificates, GetCertificate or
// GetConfigForClient set, that HTTP/2 can use. Check: CheckTLS.
TLS *tls.Config
// HTTP2Only turns HTTP/1.1 off, so the Service serves HTTP/2 alone. When
// it is false, the Service serves HTTP/2 to every client that offers it
// and HTTP/1.1 to the rest. When it is true, a client that offers only
// HTTP/1.1 completes the TLS handshake and is then disconnected without
// a response.
//
// Default: false. Valid: either.
HTTP2Only bool
// ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout and
// MaxHeaderBytes are unchanged.
}
// CheckTLS reports why c can't serve HTTPS, or nil if it can. c must not be
// nil, must have Certificates, GetCertificate or GetConfigForClient set, and
// must allow HTTP/2: a MaxVersion of TLS 1.2 or later, or none, and when it
// sets CipherSuites with a MinVersion below TLS 1.3, one of the two suites
// HTTP/2 requires.
func CheckTLS(c *tls.Config) error
// NewService runs every check on settings, reporting every bad field in one
// error that names each one, rejects a nil handler, and listens on
// settings.Addr through s.Host. It registers the listener's close with
// s.OnStop, so a listener whose Service never serves, because Init failed
// after NewService, is still closed.
//
// The Service serves a clone of settings.TLS, so the end user's config is
// never changed. The clone's ALPN protocols are h2, then http/1.1 unless
// settings.HTTP2Only is set, then any others settings.TLS lists.
//
// h is served wrapped in the Service's instrumentation, which takes its
// providers and propagator from s.Telemetry.
func NewService(s *humus.Setup, settings Settings, h http.Handler) (*Service, error)
// Defaults sets an HTTP service's defaults over humus.Main's: a drain of
// humus.DrainDelay(5 * time.Second) and a 10s grace period. The Host and
// the telemetry Builder stay humus.Main's.
func Defaults() humus.Option
// Main is humus.Main with Defaults applied before opts, so the end user's
// options win over the package's. It never returns.
func Main(init func(ctx context.Context, s *humus.Setup) ([]humus.Service, error), opts ...humus.Option)package httpservertest // github.com/z5labs/humus/httpserver/httpservertest
// Start is humustest.Start with httpserver.Defaults applied before opts, so
// a test runs its App with the drain and the grace period that
// httpserver.Main gives it in production. Options given with
// humustest.WithApp come after the defaults, and win over them.
func Start(t testing.TB, init func(ctx context.Context, s *humus.Setup) ([]humus.Service, error), opts ...humustest.Option) (*bedrocktest.Process, <-chan bedrock.ExitStatus)
// NewClient returns an http.Client that dials h, trusts only the
// certificates in roots, and speaks only HTTP/2. It is built on
// h.HTTPClient, so it keeps no idle connection. It works with or without
// Start.
func NewClient(h *humustest.Host, roots *x509.CertPool) *http.ClientBoth packages are in the core module. The ServiceUnchanged, except that these paragraphs replace the ones with the same heading, and Protocols is new: Serving. Protocols. The Service sets
A client that offers only protocols the clone doesn't list fails the handshake, and Defaults. Settings
In the end user's func Init(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
d := httpserver.DefaultSettings()
env := func(name string) config.Value[string] { return config.Env(s.Process.Environment, name) }
settings, err := config.Struct(func(r *config.Resolver) httpserver.Settings {
return httpserver.Settings{
Addr: r.Get(env("ADDR").Default(d.Addr).Validate(httpserver.CheckAddr)),
// No .Default: TLS is required, so an unset TLS_PEM_FILE fails as not found.
TLS: r.Get(env("TLS_PEM_FILE").Parse(readFile(s.Host.FS)).Parse(serverTLS).Validate(httpserver.CheckTLS)),
HTTP2Only: r.Get(env("HTTP2_ONLY").Parse(strconv.ParseBool).Default(d.HTTP2Only)),
ReadHeaderTimeout: r.Get(env("READ_HEADER_TIMEOUT").Parse(time.ParseDuration).Default(d.ReadHeaderTimeout).Validate(httpserver.CheckReadHeaderTimeout)),
ReadTimeout: r.Get(env("READ_TIMEOUT").Parse(time.ParseDuration).Default(d.ReadTimeout).Validate(httpserver.CheckReadTimeout)),
WriteTimeout: r.Get(env("WRITE_TIMEOUT").Parse(time.ParseDuration).Default(d.WriteTimeout).Validate(httpserver.CheckWriteTimeout)),
IdleTimeout: r.Get(env("IDLE_TIMEOUT").Parse(time.ParseDuration).Default(d.IdleTimeout).Validate(httpserver.CheckIdleTimeout)),
MaxHeaderBytes: r.Get(env("MAX_HEADER_BYTES").Parse(strconv.Atoi).Default(d.MaxHeaderBytes).Validate(httpserver.CheckMaxHeaderBytes)),
}
}).Read(ctx)
if err != nil {
return nil, err // exit 78, naming every bad variable
}
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", hello)
svc, err := httpserver.NewService(s, settings, mux)
if err != nil {
return nil, err
}
return []humus.Service{svc}, nil
}
Main and its defaultsThis section replaces The App. func Defaults() humus.Option {
return func(a *humus.App) {
a.Drain = humus.DrainDelay(5 * time.Second)
a.ShutdownGracePeriod = 10 * time.Second
}
}
func Main(init func(ctx context.Context, s *humus.Setup) ([]humus.Service, error), opts ...humus.Option) {
humus.Main(init, append([]humus.Option{Defaults()}, opts...)...)
}
httpservertestThis section is new.
The client is API because HTTP/2 over a test Host is easy to get wrong. A Go
The package sits beside What waves 4 and 5 build on thisThis section replaces What wave 3 builds on this. The three later proposals that build on this server can do so with this API, and nothing in it is about REST, health or failure injection. Proposal: REST endpoint DSL and OpenAPI (wave 4). REST produces an // A REST service, sketched on httpserver. Not a proposal for REST's API.
func main() {
httpserver.Main(Init, humus.WithTelemetry(buildTelemetry))
}
func Init(ctx context.Context, s *humus.Setup) ([]humus.Service, error) {
settings, err := readSettings(ctx, s) // httpserver.Settings, sourced as under Settings
if err != nil {
return nil, err
}
api, err := newAPI(ctx, s) // REST's handler, with its OpenAPI endpoint
if err != nil {
return nil, err
}
svc, err := httpserver.NewService(s, settings, api)
if err != nil {
return nil, err
}
return []humus.Service{svc}, nil
}So REST inherits TLS, HTTP/2, the instrumentation and the panic handling. Its services use Proposal: exposing health probes (wave 4). Both ways of serving probes over HTTP still work with // On a server of their own, as a job or an event-processor pod needs: a
// second Service on a second address.
probeSvc, err := httpserver.NewService(s, probeSettings, probes)
// Attached to the API's server: one handler that serves both.
mux := http.NewServeMux()
mux.Handle("/", api)
mux.Handle("/health/", probes)
apiSvc, err := httpserver.NewService(s, settings, mux)With TLS required, a probes server built on this package serves HTTPS and needs a certificate. Kubelet's Proposal: failure injection (wave 5) strips scenarios from what a server receives, before the end user's code sees them. Its seam in this package is the chain the Service wraps around the end user's handler. Design decisionsUnchanged, except that this bullet replaces the one with the same heading:
These bullets replace "Plain HTTP by default, on
TestingThese tests are sketches and have not been run. Every test runs inside a // serves returns an Init whose only Service serves h on DefaultSettings,
// over TLS with cert.
func serves(cert *humustest.Certificate, h http.Handler) func(context.Context, *humus.Setup) ([]humus.Service, error) {
return func(_ context.Context, s *humus.Setup) ([]humus.Service, error) {
settings := httpserver.DefaultSettings()
settings.TLS = &tls.Config{Certificates: []tls.Certificate{cert.TLS()}}
svc, err := httpserver.NewService(s, settings, h)
if err != nil {
return nil, err
}
return []humus.Service{svc}, nil
}
}The Service passes the contract: func TestService_meets_the_Service_contract(t *testing.T) {
humustest.TestService(t, func(t *testing.T, s *humus.Setup) humus.Service {
settings := httpserver.DefaultSettings()
cert := humustest.NewCA(t).Issue(t, "localhost")
settings.TLS = &tls.Config{Certificates: []tls.Certificate{cert.TLS()}}
svc, err := httpserver.NewService(s, settings, http.NotFoundHandler())
require.NoError(t, err)
return svc
})
}The Service keeps serving after A whole App on the package's defaults, over HTTP/2, with its telemetry recorded: func TestMain_serves_HTTP2_and_records_the_span(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ca := humustest.NewCA(t)
h := humustest.NewHost(t)
tel := humustest.NewTelemetry()
mux := http.NewServeMux()
mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, "hello, "+r.PathValue("name"))
})
p, status := httpservertest.Start(t, serves(ca.Issue(t, "localhost"), mux),
humustest.WithApp(humus.WithHost(h.Host()), humus.WithTelemetry(tel.Builder())),
)
<-h.Listening("localhost:8443")
resp, err := httpservertest.NewClient(h, ca.Pool()).Get("https://localhost:8443/hello/humus")
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 2, resp.ProtoMajor)
require.Equal(t, "hello, humus", string(body))
p.Signal(syscall.SIGTERM)
require.Zero(t, (<-status).Code())
spans := tel.Spans()
require.Len(t, spans, 1)
require.Equal(t, "GET /hello/{name}", spans[0].Name())
require.Empty(t, p.Stderr())
})
}The end user's func TestInit_without_a_certificate_exits_78(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
_, status := httpservertest.Start(t, Init)
st := <-status
require.Equal(t, humus.ExitConfig, st.Code())
require.ErrorContains(t, st.Cause(), "config TLS_PEM_FILE: not found")
})
}The tests also show the following. Unless a bullet says otherwise, the App runs with
The package's example is a Migration from todayUnchanged, except for these rows, plus a new row on HTTP/2:
Wording fixes to the post
Open questionsNone. |
Revision 2: the address is requiredThe first revision gave
Each section below replaces the part of the same name in the first revision. Everything else in it stands. SummaryThe bullet on TLS becomes:
Proposed API
// Settings configures a Service. Every field is a plain value, and a field's
// zero value means zero, never "use the default": start from DefaultSettings
// and change the fields you source. Addr and TLS have no default, so they are
// always among them.
type Settings struct {
// Addr is the TCP address the Service listens on through the Host, as
// host:port. It is required: humus imposes no port. An empty host listens
// on every address. Port 0 picks a free port, which Service.Addr reports.
//
// Default: none. Valid: host:port, with a port from 0 to 65535.
// Check: CheckAddr.
Addr string
// TLS, HTTP2Only, ReadHeaderTimeout, ReadTimeout, WriteTimeout,
// IdleTimeout and MaxHeaderBytes are unchanged.
}SettingsThe table's
The bullet "
In the end user's // No .Default: Addr is required, so an unset ADDR fails as not found.
Addr: r.Get(env("ADDR").Validate(httpserver.CheckAddr)),Design decisionsThe bullet "TLS is required, on
TestingThe sketches set the address explicitly. In settings.Addr = ":8443"The whole-App sketch is otherwise unchanged: it still waits on The end user's func TestInit_without_an_address_or_a_certificate_exits_78(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
_, status := httpservertest.Start(t, Init)
st := <-status
require.Equal(t, humus.ExitConfig, st.Code())
require.ErrorContains(t, st.Cause(), "config ADDR: not found")
require.ErrorContains(t, st.Cause(), "config TLS_PEM_FILE: not found")
})
}Two bullets change:
Migration from todayThe
Open questionsNone. |
Uh oh!
There was an error while loading. Please reload this page.
Note
Part of the humus re-evaluation; see Proposal: humus on the bedrock process model, wave 2. It builds on Proposal: the humus App lifecycle, Proposal: configuration for humus apps, Proposal: OpenTelemetry without globals and Proposal: humustest. It replaces
internal/httpserverand the server half ofrest. It blocks two wave-3 proposals: Proposal: REST endpoint DSL and OpenAPI, which is built on this package, and Proposal: exposing health probes, which serves probes over HTTP with it.Summary
A new package,
httpserver, serves anhttp.Handleras ahumus.Service. It is the HTTP half of whatrest.Rundid before the reset, without the REST DSL:httpserver.Serviceserves a handler on a listener from the App'sHost. The listener is opened when the Service is built, inInit, so connections wait in the backlog untilServeruns. The Service keeps serving through the drain delay, andShutdowndrains it within the grace period, then closes whatever is left.httpserver.Settingsholds the address, the TLS config and thenet/httplimits, as plain Go values with aDefaultSettings()and a check per field (#631).otelhttpand the App'stelemetry.SDK.net/http's error log goes to the App's telemetry, never to the real standard error.net/httpalready does. The Service writes no 500 for it.httpserver.Appis abedrock.Appbuilt onhumus.Appfor a service that is one HTTP server. The end user'sInitreturns theSettingsand the handler, and the App builds the Service. Its drain delay defaults to 5s and its grace period to 10s.The foundation decides nothing about REST or health. It is general enough that REST's Service and App are built on it, and that health probes can be served on a server of their own or attached to the API's server.
Background
restdepends on removed bedrock APIs.rest.Runbuilds abedrockhttp.Runtimefrombedrock/runtime/httpand runs it underDefaultRunner,RecoverPanicsandNotifyOnSignal(rest/rest.go#L201-L223). All of these are gone from bedrock v0.22.0.HUMUS_REST_PORT(8443),HUMUS_REST_READ_TIMEOUT(5s),HUMUS_REST_READ_HEADER_TIMEOUT(2s),HUMUS_REST_WRITE_TIMEOUT(10s),HUMUS_REST_IDLE_TIMEOUT(120s) andHUMUS_REST_MAX_HEADER_BYTES(1 MiB) are read withbedrockconfig.Envfrom the real environment (rest/rest.go#L62-L96). Under #631, humus packages read no variables.buildTLSConfigreads a PKCS#12 file fromHUMUS_REST_TLS_PKCS12_FILEwithos.ReadFile, and generates a self-signed certificate forlocalhostwhen the variable isn't set (rest/tls.go#L28-L32). A misconfigured service starts with a certificate no client trusts, rather than failing.otelhttp.NewHandler(h, "rest")and no options, so it reads the global providers and propagator (rest/rest.go#L287).internal/httpservercallsShutdown(context.Background()), which waits for every in-flight request however long it takes (internal/httpserver/server.go#L32-L47).net/http's error log goes to the real standard error. Neitherrestnorinternal/httpserversetshttp.Server.ErrorLog, so TLS handshake errors and handler panics are written by thelogpackage's default logger, around the App'sProcess.Init(#630). TheHosthands out listeners, which humustest makes in-memory.Setup.Telemetrycarries the providers (#632).humustest.TestServicechecks the Service contract.example/httpserverserves on a listener it is handed, setsErrorLogso nothing reaches the real stderr, starts the grace period from a context the signal doesn't cancel, and callsClosewhenShutdownruns out of time. This proposal does the same, split into a Service and the humus lifecycle.Proposed API
The package is in the core module. Its one new dependency is
otelhttp(see Telemetry below).The Service
Listening.
NewServicecallss.Host.Listen("tcp", settings.Addr), as the lifecycle decided: listeners are opened inInit, so a connection that arrives beforeServewaits in the backlog rather than being refused. A failure to listen, such asEADDRINUSE, isNewService's error, and the App exits 1 with it.Serving.
Servecallshttp.Server.Serveon the listener, orhttp.Server.ServeTLSwithsettings.TLSas the server'sTLSConfigwhen it is set.ServeTLSis what turns on HTTP/2 over TLS: it addsh2to the config's ALPN protocols. Plain HTTP serves HTTP/1.1 only.The two shutdown signals (#630):
Serve's context being done changes nothing. The Service keeps serving through the drain delay, while the load balancer stops sending it traffic.Shutdowncallshttp.Server.Shutdownwith the grace-period context. When that context is done before the in-flight requests finish, it callshttp.Server.Close, which closes every connection and so cancels each in-flight request's context, and returns an error wrapping the context's error. The lifecycle turns that into a*humus.GracePeriodError.Request contexts. A request's context does not derive from
Serve's context.Serve's context is done as soon as shutdown starts, and a request that arrives during the drain delay must still be served. A request's context is done when its client goes away, or whenShutdownruns out of time and closes its connection.The listener's release.
http.Server.Servecloses the listener when it returns, and it does so even whenShutdowncame first. TheOnStopfunction closes it too, for the case whereServewas never called, and ignoresnet.ErrClosed.Defaults.
DisableGeneralOptionsHandlerstays atnet/http's default, soOPTIONS *is answered with 200. Nothing else onhttp.Serveris set beyond theSettingsfields,ErrorLogand the TLS config.Settings
Addr":8080"host:port, port 0 to 65535CheckAddrHUMUS_REST_PORT, default 8443TLSnil, plain HTTPnil, or a config that has a certificateCheckTLSHUMUS_REST_TLS_PKCS12_FILE, or a self-signed certificateReadHeaderTimeout2sCheckReadHeaderTimeoutHUMUS_REST_READ_HEADER_TIMEOUTReadTimeout5sCheckReadTimeoutHUMUS_REST_READ_TIMEOUTWriteTimeout10sCheckWriteTimeoutHUMUS_REST_WRITE_TIMEOUTIdleTimeout120sCheckIdleTimeoutHUMUS_REST_IDLE_TIMEOUTMaxHeaderBytes1 << 20CheckMaxHeaderBytesHUMUS_REST_MAX_HEADER_BYTESnet/http's meaning of zero, which is "no timeout" or "useReadTimeout". That is the case Proposal: configuration for humus apps #631 gives for "a zero field means zero":DefaultSettings()supplies the defaults, and a zero the end user sets is thenet/httpzero.MaxHeaderBytesmust be positive. Innet/http, zero means 1 MiB, which is a default applied to a zero value. Proposal: configuration for humus apps #631 rules that out, so zero is rejected instead of silently meaning 1 MiB.CheckTLSrejects a config with no certificate.ServeTLSwith noCertificates,GetCertificateorGetConfigForClientfails only whenServeruns, afterInit, with an error about a file named"". The check fails startup instead, naming the variable that sourced the config.TLSis the only optional field, andnilis its "not set". Every other field always has a value.In the end user's
Init, sourced as #631 and the configuration page describe, with whatever variable names the service uses:readFileandserverTLSare the end user's helpers, as in #631. How a certificate is stored and encoded, PEM or PKCS#12, is the end user's decision.The App
httpserver.Appis the convenience App the lifecycle revision calls for: "Services for composition, plus an App per runtime package". ItsRunis, in full:terminationGracePeriodSecondsof 30s.App. An operator setsHUMUS_SHUTDOWN_GRACE_PERIODandHUMUS_SHUTDOWN_DRAIN_DELAYin the same manifest that setsterminationGracePeriodSeconds, which is where the three have to agree. A service that wants other defaults in code useshumus.AppandNewServicedirectly.Telemetryis required, as it is onhumus.App. A default oftelemetry.NewwithDefaultSettings()would name every serviceunknown_service, which only shows up once its logs are in a backend. The end user'sBuilderis the place that names the service, and a test passestelemetry.NoopBuilderor humustest's recorder.InitreturnsSettingsand a handler, not a Service. The App builds the Service, so a later proposal can change what the App builds around the handler, such as attaching probes, without changingInit's shape. The end user who needs more than one Service in a process useshumus.Appdirectly, which the lifecycle documents as the advanced path.Appdoesn't break the end user's code.Telemetry
The Service takes everything from
s.Telemetryand never touches OTel's globals (#632):GET /carts/{id}, when the handler is anhttp.ServeMuxor anything else that setsRequest.Pattern, and after the method alone otherwise. This isotelhttp's default name. It follows the semantic conventions, and it is why the operation name passed above is empty.http.server.request.duration, recorded byotelhttpon the App'sMeterProvider.s.Telemetry.LoggerProvider.Logger("github.com/z5labs/humus/httpserver"), with the OTel Logs API, as the lifecycle does. It emits:Servestarts,listening, with the attributeserver.addressset toAddr();net/httpwrites tohttp.Server.ErrorLog, such as a TLS handshake error or a failure to accept a connection.ErrorLogis a*log.Loggeron an unexportedio.Writerthat emits each line as a record. Without it,net/httpwrites to thelogpackage's default logger, which is the real standard error;otelhttpis the one new dependency. It is OpenTelemetry's own instrumentation fornet/http, it follows the HTTP semantic conventions as they change, andrestused it before the reset. Writing the same spans and metrics by hand would be a second implementation of the conventions to keep in step. It is pinned to the contrib release that matches humus'sgo.opentelemetry.io/otelversion (v0.72.0 for v1.47.0).otelhttpreads OTel's global propagator andMeterProvideras defaults before applying its options. The Service passes all three providers, so the globals are never used.Panics
The lifecycle never recovers a panic, and the charter leaves a panic inside one request to each protocol. For HTTP the decision is:
net/httpdoes today.net/httprecovers a handler's panic, logs its stack toErrorLog, and closes the connection, or resets the stream over HTTP/2. The client sees an interrupted response. The Service keeps serving other requests.otelhttpand the end user's handler, an unexported layer recovers the panic, records it on the request's span as an exception with its stack trace and an error status, emits an ERROR record in the request's context, so the record carries the trace and span IDs, and then panics again withhttp.ErrAbortHandler.net/httpaborts the response as before, and doesn't log the panic a second time.http.ErrAbortHandleritself is passed through untouched, which isnet/http's documented way to abort a response without logging.What wave 3 builds on this
Both wave-3 proposals that need HTTP are expressible with this API, without anything in it that is about REST or health.
Proposal: REST endpoint DSL and OpenAPI. REST produces an
http.Handler, and its Service is this Service serving that handler. Its App can be this App with REST'sInitin front. For illustration only, since REST's proposal designs its own API:So REST inherits this App's defaults, its instrumentation and its panic handling, and whatever wave 3 adds to this App for health.
Proposal: exposing health probes. Both ways of serving probes over HTTP work with
humus.Appand two building blocks of this package, given a probe handler that the wave-3 proposal provides (writtenprobesbelow):Exposing probes by default from
httpserver.Appis the wave-3 proposal's decision. SinceAppbuilds the Service from the handlerInitreturns, the App can wrap that handler, or add a second Service and a health Service next to it, by adding fields toApp. Whether probe requests get spans, and whether that needs a filter on this package's instrumentation, is decided there too, and its stories make the change here.Design decisions
httpserver. Proposal: configuration for humus apps #631 and the configuration page already usehttpserver.Settings,httpserver.DefaultSettings(),httpserver.CheckReadTimeoutandhttpserver.CheckAddras their example, and the name says what it is: the server, not the protocol.httpwould clash withnet/httpat every call site.http.Handler. Routing, middleware and composing handlers are the end user'snet/httpcode, or REST's. A foundation that owned routing would decide REST's design.NewService. That is the lifecycle's rule. It also makesAddr()meaningful beforeServe, which a test with port 0 needs, and it makes a port that is already taken failInitrather than the firstServe.Shutdown. The drain delay exists for HTTP: during it, a load balancer stops routing new requests to the pod, and requests already routed must still be answered. SoServe's context is not a signal to stop, and a request's context doesn't derive from it.Shutdowncloses what the grace period didn't drain.http.Server.Shutdownreturns when its context is done, but leaves the connections open.Closeends them, so their handlers' contexts are cancelled and the App can exit. That is bedrock'sexample/httpserver.:8080. In Kubernetes, TLS is usually terminated by an ingress or a mesh, and a pod serves plain HTTP. Before the reset, TLS was always on, with a self-signed certificate when none was configured. That certificate is one no client trusts, so a service that forgot its certificate started anyway and failed every request. Now TLS is on when the end user setsSettings.TLS, andCheckTLSfails startup when it is set but empty.net/http's default. Unencrypted HTTP/2 (h2c) is open question 2.DefaultSettings(), not zero values (#631). The timeouts keepnet/http's zero, andMaxHeaderBytesrejects the zero thatnet/httpwould replace with 1 MiB.ErrorLoggoes to telemetry. It is the only output ofnet/httpthat bypasses the handler, and left unset it reaches the real standard error. The Service writes it through the Logs API, as the lifecycle writes its own records, so humus needs no slog bridge in library code.Telemetry. See The App above.Addr()is API. It is the only way to learn the port whenAddrhas port 0, which humustest supports and tests use. It is not a convenience over something the end user can do.NewService. Every knob is a field ofSettings, so it can be sourced like the rest. A later proposal that needs a knob that isn't a setting, such as a filter on instrumentation for probes, adds what it needs in its own stories.Testing
These tests are sketches and have not been run. Every test runs inside a
testing/synctestbubble, on bedrocktest's process and a humustestHost, and asserts withtestify/require.The Service passes the contract:
The Service keeps serving after
Serve's context is done, soTestService's subtest forShutdownafterServehas returned on its own reports SKIP, asTestServicedocuments.A whole App, with its telemetry recorded:
Over TLS, with a CA generated for the test, so no key, certificate or password is fixed:
The tests also show the following:
Serve's context is done at once, the drain delay is 5s, and the response arrives 3s after the handler started.HUMUS_SHUTDOWN_GRACE_PERIOD=2s, a handler blocked onr.Context()sees its context cancelled exactly 5s + 2s after SIGTERM, and the App exits 1 with a*humus.GracePeriodError.r.Context()is caught. It outlives the App onceClosehas dropped its connection, and synctest reports the goroutine still blocked when the test ends. That is the lifecycle's leak detector at work (#630, Testing), and the package doc says so, because the end user's tests run into it too.trace_idandspan_id, nothing is written top.Stderr(), and a second request succeeds.ErrorLogreaches telemetry. A plain-HTTP request to a TLS Service produces an ERROR record with the scopegithub.com/z5labs/humus/httpserverand the handshake error, and nothing onp.Stderr().Init. Two Services on:8080in onehumus.Appexit 1 withEADDRINUSE, and the first one's listener is closed by itsOnStopfunction: a dial to:8080is then refused.Initfails afterNewService. AnInitthat fails after building the Service exits 1, and a dial to its address is refused.:0reports the port the Host picked throughAddr(), and a request to it succeeds.NewServicenames each bad field, and two bad fields are reported together;DefaultSettings()passesNewService; the package never reads abedrock.Environmentand doesn't importbedrock/config.The package's example is a
package mainwith anapp_test.go, or anExamplefunction, as the charter requires, and the package is held to the 80% coverage gate. A story documents the package on the docs site, in a new page, "Serving HTTP", next to the lifecycle and configuration pages.Migration from today
rest.Run(ctx, opts...), its server halfhttpserver.App, orhumus.Appwithhttpserver.NewService. Its REST half moves to Proposal: REST endpoint DSL and OpenAPI (wave 3).internal/httpserver.Apphttpserver.Servicerest.Port(...),HUMUS_REST_PORT=8443Settings.Addr, default":8080", sourced from whatever variable the end user namesrest.ReadTimeout,ReadHeaderTimeout,WriteTimeout,IdleTimeout,MaxHeaderBytesand theirHUMUS_REST_*variablesSettingsfields of the same names, with the same defaultsrest.TLSConfig(...),HUMUS_REST_TLS_PKCS12_FILEand_PASSWORDSettings.TLS, a*tls.Configthe end user decodes from a file read throughs.Host.FS. PKCS#12 decoding is the end user's, with go-pkcs12 if they like.Settings.TLSis nil; a config with no certificate failsCheckTLSotelhttp.NewHandler(h, "rest")on the globalss.Telemetry's providers and propagator, and spans named after the routeShutdown(context.Background())Shutdownbounded by the grace period, thenCloseRecoverPanicsaround the runtimenet/http's error log on the real stderrThe root package's tests keep their test-only
httpServiceinapp_http_test.go. They test the lifecycle, which shouldn't depend on a runtime package.Open questions
":8080"with plain HTTP, as proposed, or":8443"with TLS required, as before the reset and as Proposal: configuration for humus apps #631's illustration had it? Requiring TLS means every service needs a certificate, even behind a mesh that already encrypts. Recommendation:":8080", plain HTTP.net/httpcan serve HTTP/1.1 and h2c on one port throughhttp.Server.Protocols. That matters for a mesh sidecar that speaks HTTP/2 to the pod, or for gRPC and HTTP on one port. Add aSettingsfield now, or wait for a proposal that needs it? Recommendation: wait, so the field is designed against a real use.ErrorLoglines. Every line at ERROR, as proposed, or TLS handshake errors at WARN? A load balancer that probes the port with TCP checks causes a handshake error on every probe. Telling the lines apart means matchingnet/http's message text, which isn't API. Recommendation: ERROR for every line; a team that is bothered by the noise filters it in its log pipeline.Out of scope
httpserver.Appexposes them by default: Proposal: exposing health probes (wave 3).http.Server.ShutdownandClosedon't track a hijacked connection, so a handler that hijacks one closes it itself.References
Init, the two shutdown signals, Apps per runtime package, panics left to each protocolSettings,DefaultSettings,Check<Field>, and thehttpserverexampletelemetry.SDK, no globals, and the wave-2 consequence forBuildersHTTPClient,NewCAandTestServiceexample/httpserver, the reference servicenet/http'sHandlerdocumentation, on panics andErrAbortHandlerotelhttpv0.72.0, and OpenTelemetry's semantic conventions for HTTPrest/rest.go,rest/tls.go,internal/httpserver/server.goAll reactions