From 1995dc2c0f66453cf2c337b91631d9837cdb9b67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 05:21:40 +0000 Subject: [PATCH 1/3] feat(webkit): add Kit.Use for pre-routing middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugs registered via Plug() run inside handle(), which is only reached after chi has matched a (method, path) pair. This means they miss requests chi rejects at the routing layer — most visibly, OPTIONS preflights to paths that only register GET (chi returns 405 before any plug fires). Use() wraps each Plug as chi router middleware via mux.Use(), so it executes before route matching and sees every request, including those chi would otherwise reject with 405. Plug() is unchanged; all existing tests continue to pass. https://claude.ai/code/session_01Y1KRWLLr2YC6Sisy2w2ZAJ --- pkg/webkit/mux.go | 24 ++++++++++++++++++++++++ pkg/webkit/mux_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pkg/webkit/mux.go b/pkg/webkit/mux.go index 794f61cd..1eb0dcbf 100644 --- a/pkg/webkit/mux.go +++ b/pkg/webkit/mux.go @@ -69,6 +69,30 @@ func (k *Kit) Plug(plugs ...Plug) { k.plugs = append(k.plugs, plugs...) } +// Use registers middleware functions that run before route matching. Unlike +// Plug, Use wraps each plug as chi router middleware so it fires on every +// request — including OPTIONS preflights to paths that only register other +// HTTP methods. +// +// For example: app.Use(middleware.CORS) +func (k *Kit) Use(plugs ...Plug) { + for _, plug := range plugs { + k.mux.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := NewContext(w, r) + h := plug(func(c *Context) error { + r = c.Request + next.ServeHTTP(w, r) + return nil + }) + if err := h(ctx); err != nil { + k.handleError(ctx, err) + } + }) + }) + } +} + // Start starts the HTTP server. func (k *Kit) Start(address string) error { server := &http.Server{ diff --git a/pkg/webkit/mux_test.go b/pkg/webkit/mux_test.go index 01019966..c09ccc86 100644 --- a/pkg/webkit/mux_test.go +++ b/pkg/webkit/mux_test.go @@ -142,6 +142,46 @@ func TestKit_Mount(t *testing.T) { assert.Equal(t, http.StatusNotFound, rr.Code) } +func TestKit_Use(t *testing.T) { + t.Parallel() + + t.Run("Runs on matched route", func(t *testing.T) { + t.Parallel() + app := New() + app.Use(func(next Handler) Handler { + return func(ctx *Context) error { + ctx.Response.Header().Set("X-Use-Middleware", "ran") + return next(ctx) + } + }) + app.Get("/thing", handler) + rr := httptest.NewRecorder() + app.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/thing", nil)) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "ran", rr.Header().Get("X-Use-Middleware")) + }) + + t.Run("Runs on OPTIONS preflight to GET-only route", func(t *testing.T) { + t.Parallel() + app := New() + app.Use(func(next Handler) Handler { + return func(ctx *Context) error { + ctx.Response.Header().Set("X-Use-Middleware", "ran") + if ctx.Request.Method == http.MethodOptions { + ctx.Response.WriteHeader(http.StatusOK) + return nil + } + return next(ctx) + } + }) + app.Get("/thing", handler) + rr := httptest.NewRecorder() + app.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/thing", nil)) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "ran", rr.Header().Get("X-Use-Middleware")) + }) +} + func TestKit_Group(t *testing.T) { app := New() From 3e92cc4aea95385f0e08b1847d3268f3683c731d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 05:31:28 +0000 Subject: [PATCH 2/3] fix(webkit): make Plug run before route matching like chi.Use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously Plug appended to k.plugs, which were applied inside handle() — reached only after chi matched a (method, path) pair. This meant plugs missed any request chi rejected at the routing layer, most visibly OPTIONS preflights to paths that only register GET (chi returned 405 before any plug ran). Plug now wraps each middleware directly as chi router middleware via mux.Use(), so it fires before route matching on every request. The k.plugs field and its handle() loop are removed. Group is simplified accordingly: parent mux middleware already runs before chi's Mount hands off to the sub-router, so the manual parent-plug propagation loop is no longer needed. https://claude.ai/code/session_01Y1KRWLLr2YC6Sisy2w2ZAJ --- pkg/webkit/mux.go | 44 ++------------------- pkg/webkit/mux_test.go | 88 ++++++++++++++++++------------------------ 2 files changed, 41 insertions(+), 91 deletions(-) diff --git a/pkg/webkit/mux.go b/pkg/webkit/mux.go index 1eb0dcbf..9ad94c5f 100644 --- a/pkg/webkit/mux.go +++ b/pkg/webkit/mux.go @@ -23,7 +23,6 @@ type ( ErrorHandler ErrorHandler NotFoundHandler Handler mux *chi.Mux - plugs []Plug } // Handler is a function that handles HTTP requests. Handler func(c *Context) error @@ -39,7 +38,6 @@ func New() *Kit { ErrorHandler: DefaultErrorHandler, NotFoundHandler: DefaultNotFoundHandler, mux: chi.NewRouter(), - plugs: []Plug{}, } } @@ -61,21 +59,13 @@ func (k *Kit) Add(method string, pattern string, handler Handler, plugs ...Plug) }) } -// Plug adds a middleware function to the chain. These are called after -// the funcs that are passed directly to the route-level handlers. +// Plug registers middleware that runs before route matching, mirroring chi's +// Use. It fires on every request — including OPTIONS preflights to paths that +// only register other HTTP methods — making it suitable for cross-cutting +// concerns such as CORS, logging, and request IDs. // // For example: app.Plug(middleware.Logger) func (k *Kit) Plug(plugs ...Plug) { - k.plugs = append(k.plugs, plugs...) -} - -// Use registers middleware functions that run before route matching. Unlike -// Plug, Use wraps each plug as chi router middleware so it fires on every -// request — including OPTIONS preflights to paths that only register other -// HTTP methods. -// -// For example: app.Use(middleware.CORS) -func (k *Kit) Use(plugs ...Plug) { for _, plug := range plugs { k.mux.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -160,10 +150,6 @@ func (k *Kit) handle(w http.ResponseWriter, r *http.Request, handler Handler, pl h = plugs[i](h) } - for i := len(k.plugs) - 1; i >= 0; i-- { - h = k.plugs[i](h) - } - if err := h(ctx); err != nil { k.handleError(ctx, err) } @@ -247,37 +233,15 @@ func (k *Kit) Mount(pattern string, handler http.Handler) { // Group allows you to group multiple routes together under a common path. // The provided function can use the `kit` to add routes, middleware, etc. func (k *Kit) Group(pattern string, groupFunc func(kit *Kit)) { - // Create a sub-router using Chi. subRouter := chi.NewRouter() - // Apply middleware from parent to the subRouter. - for _, plug := range k.plugs { - subRouter.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := NewContext(w, r) - h := plug(func(c *Context) error { - r = c.Request - next.ServeHTTP(w, r) - return nil - }) - if err := h(ctx); err != nil { - k.handleError(ctx, err) - } - }) - }) - } - - // Create a new kit with the sub-router and inherit error handlers. subKit := &Kit{ ErrorHandler: k.ErrorHandler, NotFoundHandler: k.NotFoundHandler, mux: subRouter, - plugs: k.plugs, // Inherit parent plugs } - // Call the provided function with the sub-kit. groupFunc(subKit) - // Mount the sub router to the parent router. k.mux.Mount(pattern, subRouter) } diff --git a/pkg/webkit/mux_test.go b/pkg/webkit/mux_test.go index c09ccc86..f6d6ad3d 100644 --- a/pkg/webkit/mux_test.go +++ b/pkg/webkit/mux_test.go @@ -45,20 +45,45 @@ func TestAdd(t *testing.T) { } func TestKit_Plug(t *testing.T) { - app := New() - app.Plug(func(next Handler) Handler { - return func(ctx *Context) error { - ctx.Set("test", "test") - return next(ctx) - } + t.Parallel() + + t.Run("Sets context value visible to handler", func(t *testing.T) { + t.Parallel() + app := New() + app.Plug(func(next Handler) Handler { + return func(ctx *Context) error { + ctx.Set("test", "test") + return next(ctx) + } + }) + app.Get("/", func(ctx *Context) error { + assert.Equal(t, "test", ctx.Get("test")) + return ctx.String(http.StatusOK, "test") + }) + rr := httptest.NewRecorder() + app.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil)) + assert.Equal(t, http.StatusOK, rr.Code) }) - app.Get("/", func(ctx *Context) error { - assert.Equal(t, "test", ctx.Get("test")) - return ctx.String(http.StatusOK, "test") + + t.Run("Runs on OPTIONS preflight to GET-only route", func(t *testing.T) { + t.Parallel() + app := New() + app.Plug(func(next Handler) Handler { + return func(ctx *Context) error { + ctx.Response.Header().Set("X-Plug-Middleware", "ran") + if ctx.Request.Method == http.MethodOptions { + ctx.Response.WriteHeader(http.StatusOK) + return nil + } + return next(ctx) + } + }) + app.Get("/thing", handler) + rr := httptest.NewRecorder() + app.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/thing", nil)) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "ran", rr.Header().Get("X-Plug-Middleware")) }) - rr := httptest.NewRecorder() - app.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil)) - assert.Equal(t, http.StatusOK, rr.Code) } func TestKit_Connect(t *testing.T) { @@ -142,45 +167,6 @@ func TestKit_Mount(t *testing.T) { assert.Equal(t, http.StatusNotFound, rr.Code) } -func TestKit_Use(t *testing.T) { - t.Parallel() - - t.Run("Runs on matched route", func(t *testing.T) { - t.Parallel() - app := New() - app.Use(func(next Handler) Handler { - return func(ctx *Context) error { - ctx.Response.Header().Set("X-Use-Middleware", "ran") - return next(ctx) - } - }) - app.Get("/thing", handler) - rr := httptest.NewRecorder() - app.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/thing", nil)) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "ran", rr.Header().Get("X-Use-Middleware")) - }) - - t.Run("Runs on OPTIONS preflight to GET-only route", func(t *testing.T) { - t.Parallel() - app := New() - app.Use(func(next Handler) Handler { - return func(ctx *Context) error { - ctx.Response.Header().Set("X-Use-Middleware", "ran") - if ctx.Request.Method == http.MethodOptions { - ctx.Response.WriteHeader(http.StatusOK) - return nil - } - return next(ctx) - } - }) - app.Get("/thing", handler) - rr := httptest.NewRecorder() - app.ServeHTTP(rr, httptest.NewRequest(http.MethodOptions, "/thing", nil)) - assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "ran", rr.Header().Get("X-Use-Middleware")) - }) -} func TestKit_Group(t *testing.T) { app := New() From e0b8e622d1327e442c1ae88d6e4abed5cb3c4168 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 05:37:11 +0000 Subject: [PATCH 3/3] fix(lint): resolve gofumpt and govet lint failures - Remove double blank line in mux_test.go (gofumpt) - Replace deprecated reflect.Ptr with reflect.Pointer in jsonformat/formatter.go and cache/mem.go (govet inline) https://claude.ai/code/session_01Y1KRWLLr2YC6Sisy2w2ZAJ --- internal/appdef/jsonformat/formatter.go | 4 ++-- pkg/cache/mem.go | 2 +- pkg/webkit/mux_test.go | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/appdef/jsonformat/formatter.go b/internal/appdef/jsonformat/formatter.go index 6bbb6017..e9468ddb 100644 --- a/internal/appdef/jsonformat/formatter.go +++ b/internal/appdef/jsonformat/formatter.go @@ -22,7 +22,7 @@ func RegisterType(t reflect.Type) { // scanType recursively scans a type for inline tags. func scanType(t reflect.Type) { - for t.Kind() == reflect.Ptr { + for t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct { @@ -52,7 +52,7 @@ func scanType(t reflect.Type) { // Recurse into struct fields. fieldType := field.Type - for fieldType.Kind() == reflect.Ptr || fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Map { + for fieldType.Kind() == reflect.Pointer || fieldType.Kind() == reflect.Slice || fieldType.Kind() == reflect.Map { fieldType = fieldType.Elem() } if fieldType.Kind() == reflect.Struct { diff --git a/pkg/cache/mem.go b/pkg/cache/mem.go index 195450e9..02c24eb1 100644 --- a/pkg/cache/mem.go +++ b/pkg/cache/mem.go @@ -53,7 +53,7 @@ func (c *MemCache) Get(_ context.Context, key string, value interface{}) error { } // Check if value is a pointer - if reflect.TypeOf(value).Kind() != reflect.Ptr { + if reflect.TypeOf(value).Kind() != reflect.Pointer { return errors.New("value must be a pointer") } diff --git a/pkg/webkit/mux_test.go b/pkg/webkit/mux_test.go index f6d6ad3d..d57c0d4f 100644 --- a/pkg/webkit/mux_test.go +++ b/pkg/webkit/mux_test.go @@ -167,7 +167,6 @@ func TestKit_Mount(t *testing.T) { assert.Equal(t, http.StatusNotFound, rr.Code) } - func TestKit_Group(t *testing.T) { app := New()