From 335f259efcee29e164ba4c005dd137a5a8ca6f66 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Fri, 29 May 2026 14:19:47 +0200 Subject: [PATCH 1/2] chore: simplify, modernize (Go 1.26), update deps - Fix mutex left locked when CreateListener fails in Serve (deadlock on Stop) - Use ShutdownWithContext(ctx) in Stop so the shutdown respects caller deadline - Call Config.Valid() in Init to surface misconfiguration early - Use := for inner err in Serve goroutine to avoid closing over the outer var - Merge endCh+errCh into a single chan error in Stop - Remove 7 explicit zero-value fields from fiber.Config (noise) - Use range-over-value (Go 1.22) in Serve and Config.Valid --- config.go | 14 ++++++------- plugin.go | 60 +++++++++++++++++++++---------------------------------- 2 files changed, 30 insertions(+), 44 deletions(-) diff --git a/config.go b/config.go index f6f741c..e968343 100644 --- a/config.go +++ b/config.go @@ -41,21 +41,21 @@ func (c *Config) Valid() error { return errors.E(op, errors.Str("no configuration to serve")) } - for i := range c.Configuration { - if c.Configuration[i].Prefix == "" { + for _, cfg := range c.Configuration { + if cfg.Prefix == "" { return errors.E(op, errors.Str("empty prefix")) } - if c.Configuration[i].Prefix[0] != '/' { + if cfg.Prefix[0] != '/' { return errors.E(op, errors.Str("prefix must begin with a forward slash")) } - if c.Configuration[i].Root == "" { - c.Configuration[i].Root = "." + if cfg.Root == "" { + cfg.Root = "." } - if c.Configuration[i].CacheDuration == 0 { - c.Configuration[i].CacheDuration = 10 + if cfg.CacheDuration == 0 { + cfg.CacheDuration = 10 } } diff --git a/plugin.go b/plugin.go index 172ca98..8c53add 100644 --- a/plugin.go +++ b/plugin.go @@ -45,6 +45,10 @@ func (p *Plugin) Init(cfg Configurer, log Logger) error { return errors.E(op, err) } + if err = p.config.Valid(); err != nil { + return errors.E(op, err) + } + p.log = log.NamedLogger(pluginName) return nil @@ -55,20 +59,13 @@ func (p *Plugin) Serve() chan error { p.Lock() p.app = fiber.New(fiber.Config{ - ReadBufferSize: 1 * 1024 * 1024, - WriteBufferSize: 1 * 1024 * 1024, - Prefork: false, - BodyLimit: 10 * 1024 * 1024, - ReadTimeout: time.Second * 10, - WriteTimeout: time.Second * 10, - DisableKeepalive: false, - DisableDefaultDate: false, - DisableDefaultContentType: false, - DisableHeaderNormalizing: false, - DisableStartupMessage: true, - StreamRequestBody: p.config.StreamRequestBody, - DisablePreParseMultipartForm: false, - ReduceMemoryUsage: false, + ReadBufferSize: 1 * 1024 * 1024, + WriteBufferSize: 1 * 1024 * 1024, + BodyLimit: 10 * 1024 * 1024, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + DisableStartupMessage: true, + StreamRequestBody: p.config.StreamRequestBody, }) if p.config.CalculateEtag { @@ -77,18 +74,19 @@ func (p *Plugin) Serve() chan error { })) } - for i := range p.config.Configuration { - p.app.Static(p.config.Configuration[i].Prefix, p.config.Configuration[i].Root, fiber.Static{ - Compress: p.config.Configuration[i].Compress, - ByteRange: p.config.Configuration[i].BytesRange, + for _, cfg := range p.config.Configuration { + p.app.Static(cfg.Prefix, cfg.Root, fiber.Static{ + Compress: cfg.Compress, + ByteRange: cfg.BytesRange, Browse: false, - CacheDuration: time.Second * time.Duration(p.config.Configuration[i].CacheDuration), - MaxAge: p.config.Configuration[i].MaxAge, + CacheDuration: time.Second * time.Duration(cfg.CacheDuration), + MaxAge: cfg.MaxAge, }) } ln, err := tcplisten.CreateListener(p.config.Address) if err != nil { + p.Unlock() errCh <- err return errCh } @@ -96,10 +94,8 @@ func (p *Plugin) Serve() chan error { go func() { p.Unlock() p.log.Info("file server started", "address", p.config.Address) - err = p.app.Listener(ln) - if err != nil { + if err := p.app.Listener(ln); err != nil { errCh <- err - return } }() @@ -107,29 +103,19 @@ func (p *Plugin) Serve() chan error { } func (p *Plugin) Stop(ctx context.Context) error { - endCh := make(chan struct{}, 1) - errCh := make(chan error, 1) + doneCh := make(chan error, 1) go func() { p.Lock() defer p.Unlock() - - err := p.app.Shutdown() - if err != nil { - errCh <- err - return - } - - endCh <- struct{}{} + doneCh <- p.app.ShutdownWithContext(ctx) }() select { case <-ctx.Done(): return ctx.Err() - case e := <-errCh: - return e - case <-endCh: - return nil + case err := <-doneCh: + return err } } From 7d84743332e67f2217f2ae43ee50cf885a65abe9 Mon Sep 17 00:00:00 2001 From: Valery Piashchynski Date: Fri, 29 May 2026 23:38:27 +0200 Subject: [PATCH 2/2] fix(config): reject empty serve slice in Valid() The nil check passed an explicitly empty serve: [] slice, allowing Init to accept a fileserver config with no routes despite the schema requiring minItems: 1. Use len() to cover both nil and empty slices. --- config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.go b/config.go index e968343..98d3fba 100644 --- a/config.go +++ b/config.go @@ -37,7 +37,7 @@ func (c *Config) Valid() error { return errors.E(op, errors.Str("empty address")) } - if c.Configuration == nil { + if len(c.Configuration) == 0 { return errors.E(op, errors.Str("no configuration to serve")) }