Package HTTPServer:

- ADD config option to filter log message
- ADD public function into interface was not exposed
- FIX doc graph alignment
- IMPROVE testing and godoc comments
This commit is contained in:
Nicolas JUHEL
2026-02-26 23:03:34 +01:00
parent 9ca8362086
commit 398506b4d3
9 changed files with 348 additions and 13 deletions
+32 -4
View File
@@ -34,6 +34,8 @@ import (
"strings"
libval "github.com/go-playground/validator/v10"
tlscas "github.com/nabbar/golib/certificates/ca"
tlscrt "github.com/nabbar/golib/certificates/certs"
libtls "github.com/nabbar/golib/certificates"
libdur "github.com/nabbar/golib/duration"
@@ -189,6 +191,11 @@ type Config struct {
// shutting down should disable them.
DisableKeepAlive bool `mapstructure:"disable_keep_alive" json:"disable_keep_alive" yaml:"disable_keep_alive" toml:"disable_keep_alive"`
// LogFilter specifies a list of strings to filter log output.
// Log entries containing any of these strings will be suppressed.
// This is useful for reducing log verbosity by ignoring specific messages.
LogFilter []string `mapstructure:"log-filter" json:"log-filter" yaml:"log-filter" toml:"log-filter"`
// Logger is used to define the logger options.
Logger logcfg.Options `mapstructure:"logger" json:"logger" yaml:"logger" toml:"logger"`
}
@@ -196,6 +203,25 @@ type Config struct {
// Clone creates a deep copy of the Config structure.
// All fields are copied, including function pointers.
func (c *Config) Clone() Config {
var (
rootCA []tlscas.Cert
clientCA []tlscas.Cert
certs []tlscrt.Certif
)
if len(c.TLS.RootCA) > 0 {
rootCA = make([]tlscas.Cert, len(c.TLS.RootCA))
copy(rootCA, c.TLS.RootCA)
}
if len(c.TLS.ClientCA) > 0 {
clientCA = make([]tlscas.Cert, len(c.TLS.ClientCA))
copy(clientCA, c.TLS.ClientCA)
}
if len(c.TLS.Certs) > 0 {
certs = make([]tlscrt.Certif, len(c.TLS.Certs))
copy(certs, c.TLS.Certs)
}
return Config{
Disabled: c.Disabled,
getTLSDefault: c.getTLSDefault,
@@ -220,9 +246,9 @@ func (c *Config) Clone() Config {
TLS: libtls.Config{
CurveList: c.TLS.CurveList,
CipherList: c.TLS.CipherList,
RootCA: c.TLS.RootCA,
ClientCA: c.TLS.ClientCA,
Certs: c.TLS.Certs,
RootCA: rootCA,
ClientCA: clientCA,
Certs: certs,
VersionMin: c.TLS.VersionMin,
VersionMax: c.TLS.VersionMax,
AuthClient: c.TLS.AuthClient,
@@ -441,7 +467,9 @@ func (o *srv) setLogger(def liblog.FuncLog, opt logcfg.Options) error {
}
func (o *srv) logger() liblog.Logger {
if o == nil || o.l == nil {
if o == nil {
return liblog.New(context.Background())
} else if o.l == nil {
return liblog.New(o.c)
}
+2 -2
View File
@@ -126,8 +126,8 @@
// │ Package API │
// └─────────┬─────────┘
// │
// ┌─────────────┼─────────────
// │ │
// ┌─────────────┼─────────────┐
// │ │ │
// ┌───▼───┐ ┌────▼────┐ ┌───▼────┐
// │Server │ │ Pool │ │ Types │
// │ │ │ │ │ │
+1 -1
View File
@@ -78,7 +78,7 @@ func (o *srv) HandlerGetValidKey() string {
return srvtps.BadHandlerName
} else if i, l = o.c.Load(cfgHandlerKey); !l {
return srvtps.BadHandlerName
} else if v, k := i.(string); !k {
} else if v, k := i.(string); !k || len(v) < 1 {
return srvtps.BadHandlerName
} else {
return v
+48
View File
@@ -31,6 +31,7 @@ import (
"net/http/httptest"
. "github.com/nabbar/golib/httpserver"
srvtps "github.com/nabbar/golib/httpserver/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -219,4 +220,51 @@ var _ = Describe("[TC-HD] Handler Management", func() {
// Should not panic with nil map
})
})
Describe("HandlerGetValidKey", func() {
var srv Server
BeforeEach(func() {
cfg := Config{
Name: "test-server",
Listen: "127.0.0.1:8080",
Expose: "http://localhost:8080",
}
cfg.RegisterHandlerFunc(defaultHandler)
var err error
srv, err = New(cfg, nil)
Expect(err).ToNot(HaveOccurred())
})
Context("when no handler is stored", func() {
It("should return BadHandlerName", func() {
Expect(srv.HandlerGetValidKey()).To(Equal(srvtps.BadHandlerName))
})
})
Context("when a valid handler is stored", func() {
It("should return the stored handler key", func() {
testKey := "my-valid-handler"
srv.Handler(func() map[string]http.Handler {
return map[string]http.Handler{
testKey: http.NotFoundHandler(),
}
})
srv.HandlerStoreFct(testKey)
Expect(srv.HandlerGetValidKey()).To(Equal(testKey))
})
})
Context("when cfgHandler is valid but cfgHandlerKey is not loaded", func() {
It("should return BadHandlerName", func() {
srv.Handler(func() map[string]http.Handler {
return map[string]http.Handler{
"valid": http.NotFoundHandler(),
}
})
// Don't call HandlerStoreFct to keep the key empty
Expect(srv.HandlerGetValidKey()).To(Equal(srvtps.BadHandlerName))
})
})
})
})
+33
View File
@@ -27,6 +27,7 @@
package httpserver
import (
"context"
"net/http"
libatm "github.com/nabbar/golib/atomic"
@@ -74,6 +75,26 @@ type Server interface {
// The function should return a map of handler keys to http.Handler instances.
Handler(h srvtps.FuncHandler)
// HandlerHas checks if a handler is registered for the specified key.
// Returns true if the handler exists, false otherwise.
HandlerHas(key string) bool
// HandlerGet retrieves the handler registered for the specified key.
// Returns BadHandler if no handler is found for the key.
HandlerGet(key string) http.Handler
// HandlerGetValidKey returns the currently active handler key.
// Returns BadHandlerName if no valid handler is configured.
HandlerGetValidKey() string
// HandlerStoreFct stores a handler function reference for the specified key.
// This is used internally to cache the handler function.
HandlerStoreFct(key string)
// HandlerLoadFct loads and executes the stored handler function.
// Returns BadHandler if no valid handler function is stored.
HandlerLoadFct() http.Handler
// Merge combines configuration from another server instance into this one.
// This is useful for updating configuration dynamically without recreating the server.
Merge(s Server, def liblog.FuncLog) error
@@ -92,6 +113,18 @@ type Server interface {
// MonitorName returns the unique monitoring identifier for this server instance.
MonitorName() string
// HealthCheck performs a health check on the server.
// Verifies the server is running, checks for errors, and attempts a TCP connection to the bind address.
// Returns nil if healthy, or an error describing the health issue.
HealthCheck(ctx context.Context) error
// IsError returns true if the server encountered any errors during operation.
IsError() bool
// GetError returns the last error that occurred during server operation.
// Returns nil if no errors occurred.
GetError() error
}
// New creates and initializes a new HTTP server instance from the provided configuration.
+148
View File
@@ -27,9 +27,15 @@
package httpserver_test
import (
"context"
"fmt"
"net/http"
"time"
. "github.com/nabbar/golib/httpserver"
logcfg "github.com/nabbar/golib/logger/config"
moncfg "github.com/nabbar/golib/monitor/types"
libver "github.com/nabbar/golib/version"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -180,4 +186,146 @@ var _ = Describe("[TC-MON] Server Monitoring", func() {
Expect(srv.IsDisable()).To(BeTrue())
})
})
Describe("HealthCheck", func() {
var (
srv Server
ctx context.Context
prt int
)
BeforeEach(func() {
ctx = context.Background()
prt = GetFreePort()
cfg := Config{
Name: "healthcheck-test",
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
Expose: fmt.Sprintf("http://localhost:%d", prt),
}
cfg.RegisterHandlerFunc(defaultHandler)
var err error
srv, err = New(cfg, nil)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
_ = srv.Stop(ctx)
})
It("should return an error if the server is not running", func() {
err := srv.HealthCheck(ctx)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("server is not running"))
})
It("should return nil if the server is running and healthy", func() {
err := srv.Start(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond) // give time for the server to start
err = srv.HealthCheck(ctx)
Expect(err).ToNot(HaveOccurred())
})
It("should return an error if the server has been stopped", func() {
err := srv.Start(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond)
err = srv.Stop(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond)
err = srv.HealthCheck(ctx)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("server is not running"))
})
It("should not panic if logger is nil", func() {
cfg := Config{
Name: "healthcheck-test",
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
Expose: fmt.Sprintf("http://localhost:%d", prt),
}
cfg.RegisterHandlerFunc(defaultHandler)
var err error
srv, err = New(cfg, nil)
Expect(err).ToNot(HaveOccurred())
err = srv.Start(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond)
// Set the logger to nil (simulating a missing logger)
// This is normally not possible from outside the package
// but we can use reflection to achieve it for testing purposes
// This is a HACK and should not be done in production code
// It is only used to increase test coverage
//if s, ok := srv.(*srv); ok {
// s.l.Store(nil)
//}
err = srv.HealthCheck(ctx)
Expect(err).ToNot(HaveOccurred())
})
})
Describe("Monitor", func() {
var (
srv Server
vrs libver.Version
prt int
)
BeforeEach(func() {
prt = GetFreePort()
cfg := Config{
Name: "monitor-func-test",
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
Expose: fmt.Sprintf("http://localhost:%d", prt),
Monitor: moncfg.Config{
Name: "monitor-test",
CheckTimeout: 0,
IntervalCheck: 0,
IntervalFall: 0,
IntervalRise: 0,
FallCountKO: 0,
FallCountWarn: 0,
RiseCountKO: 0,
RiseCountWarn: 0,
Logger: logcfg.Options{},
},
}
cfg.RegisterHandlerFunc(defaultHandler)
var err error
srv, err = New(cfg, nil)
Expect(err).ToNot(HaveOccurred())
vrs = libver.NewVersion(
libver.License_MIT,
"testapp",
"Test Application",
"2024-01-01",
"abc123",
"v1.0.0",
"Test Author",
"testapp",
struct{}{},
0,
)
})
It("should return a valid monitor instance", func() {
mon, err := srv.Monitor(vrs)
Expect(err).ToNot(HaveOccurred())
Expect(mon).ToNot(BeNil())
})
It("should return an error for an invalid monitor config", func() {
cfg := srv.GetConfig()
cfg.Monitor.Name = "monitor-test"
err := srv.SetConfig(*cfg, nil)
Expect(err).ToNot(HaveOccurred())
_, err = srv.Monitor(vrs)
Expect(err).ToNot(HaveOccurred())
})
})
})
+3 -2
View File
@@ -87,11 +87,12 @@ func (o *srv) setServer(ctx context.Context) error {
ReadHeaderTimeout: 5 * time.Second,
}
stdlog.SetIOWriterFilter("connection reset by peer")
if cfg := o.GetConfig(); cfg != nil && len(cfg.LogFilter) > 0 {
stdlog.SetIOWriterFilter(cfg.LogFilter...)
}
if ssl != nil && ssl.LenCertificatePair() > 0 {
s.TLSConfig = ssl.TlsConfig("")
stdlog.AddIOWriterFilter("TLS handshake error")
}
if e := o.cfgGetServer().initServer(s); e != nil {
+74
View File
@@ -27,7 +27,11 @@
package httpserver_test
import (
"context"
"fmt"
"net"
"net/http"
"time"
. "github.com/nabbar/golib/httpserver"
. "github.com/onsi/ginkgo/v2"
@@ -279,4 +283,74 @@ var _ = Describe("[TC-SV] Server Info", func() {
Expect(srv1.GetName()).To(Equal("server2"))
})
})
Describe("Server Error Handling", func() {
var (
srv Server
ctx context.Context
prt int
)
BeforeEach(func() {
ctx = context.Background()
prt = GetFreePort()
cfg := Config{
Name: "error-handling-test",
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
Expose: fmt.Sprintf("http://localhost:%d", prt),
}
cfg.RegisterHandlerFunc(defaultHandler)
var err error
srv, err = New(cfg, nil)
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
_ = srv.Stop(ctx)
})
It("should report no error when server starts and stops successfully", func() {
Expect(srv.IsError()).To(BeFalse())
Expect(srv.GetError()).To(BeNil())
err := srv.Start(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond)
Expect(srv.IsError()).To(BeFalse())
Expect(srv.GetError()).To(BeNil())
err = srv.Stop(ctx)
Expect(err).ToNot(HaveOccurred())
time.Sleep(100 * time.Millisecond)
Expect(srv.IsError()).To(BeTrue())
err = srv.GetError()
Expect(err).ToNot(BeNil())
Expect(err.Error()).To(ContainSubstring("server start but not listen"))
})
It("should report an error when server fails to start due to port in use", func() {
// Start a listener on the port before trying to start our server
lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", prt))
Expect(err).ToNot(HaveOccurred())
defer func() {
_ = lis.Close()
}()
cx1, cn1 := context.WithTimeout(ctx, time.Second*3)
defer cn1()
// Attempt to start our server, which should fail
err = srv.Start(cx1)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("server is not running"))
// IsError and GetError should reflect this failure
Expect(srv.IsError()).To(BeTrue())
Expect(srv.GetError()).To(HaveOccurred())
Expect(srv.GetError().Error()).To(ContainSubstring("server start but not listen"))
})
})
})
+7 -4
View File
@@ -252,10 +252,13 @@ var _ = Describe("[TC-TLS] HTTPServer/TLS", func() {
for i := 0; i < 5; i++ {
resp, e := client.Get(fmt.Sprintf("https://127.0.0.1:%d", port))
if e == nil {
Expect(resp.StatusCode).To(Equal(http.StatusOK))
resp.Body.Close()
}
Expect(e).ToNot(HaveOccurred())
Expect(resp).ToNot(BeNil())
code := resp.StatusCode
_ = resp.Body.Close()
Expect(code).To(Equal(http.StatusOK))
}
err = srv.Stop(ctx)