diff --git a/context.go b/context.go index c603030..9015159 100644 --- a/context.go +++ b/context.go @@ -54,7 +54,11 @@ func (c *Context) Next() { } func (c *Context) prepare(ctx *Context) { - ctx.Request.httpRequest.ParseMultipartForm(int64(app.Config.Request.MaxUploadFileSize)) + // Only parse multipart form if it hasn't been parsed already + // This prevents race conditions when multiple goroutines might access the same request + if ctx.Request.httpRequest.MultipartForm == nil { + ctx.Request.httpRequest.ParseMultipartForm(int64(app.Config.Request.MaxUploadFileSize)) + } } func (c *Context) GetPathParam(key string) interface{} { diff --git a/core.go b/core.go index 967eaca..9afef81 100644 --- a/core.go +++ b/core.go @@ -6,6 +6,7 @@ package core import ( + "context" "embed" "fmt" "log" @@ -51,12 +52,18 @@ type configContainer struct { // App is a struct representing the main application container and its core components. type App struct { - t int // for tracking hooks + t int // for tracking hooks (deprecated - maintained for backward compatibility) chain *chain hooks *Hooks Config *configContainer } +// requestContext holds request-specific chain execution state +type requestContext struct { + chainIndex int + chainNodes []interface{} +} + // app is a global instance of the App structure used to manage application configuration and lifecycle. var app *App @@ -113,6 +120,26 @@ func (app *App) Run(router *httprouter.Router) { router.ServeFiles("/public/*filepath", http.Dir("storage/public")) } + // check if app engine is enable + AppEnableStr := os.Getenv("APP_ENABLE") + if "" == AppEnableStr { + AppEnableStr = "false" + } + AppEnable, _ := strconv.ParseBool(AppEnableStr) + if AppEnable { + router.ServeFiles("/app/*filepath", http.Dir("storage/app")) + } + + // check if app engine is enable + CDNEnableStr := os.Getenv("CDN_ENABLE") + if "" == CDNEnableStr { + CDNEnableStr = "false" + } + CDNEnable, _ := strconv.ParseBool(CDNEnableStr) + if CDNEnable { + router.ServeFiles("/cdn/*filepath", http.Dir("storage/cdn")) + } + useHttpsStr := os.Getenv("App_USE_HTTPS") if useHttpsStr == "" { useHttpsStr = "false" @@ -211,10 +238,25 @@ func (app *App) RegisterRoutes(routes []Route, router *httprouter.Router) *httpr // makeHTTPRouterHandlerFunc creates an httprouter.Handle that handles HTTP requests with the specified controller and hooks. func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - + // Create request-specific chain state + reqCtx := &requestContext{ + chainIndex: 0, + } + + // Prepare the chain nodes for this request + mw := app.hooks.GetHooks() + for _, v := range mw { + reqCtx.chainNodes = append(reqCtx.chainNodes, v) + } + rhs := app.combHandlers(h, ms) + for _, v := range rhs { + reqCtx.chainNodes = append(reqCtx.chainNodes, v) + } + + // Store chain state in request context ctx := &Context{ Request: &Request{ - httpRequest: r, + httpRequest: r.WithContext(context.WithValue(r.Context(), "goffeeChain", reqCtx)), httpPathParams: ps, }, Response: &Response{ @@ -237,10 +279,16 @@ func (app *App) makeHTTPRouterHandlerFunc(h Controller, ms []Hook) httprouter.Ha } ctx.prepare(ctx) - rhs := app.combHandlers(h, ms) - app.prepareChain(rhs) - app.t = 0 - app.chain.execute(ctx) + + // Execute the first handler in the chain + if len(reqCtx.chainNodes) > 0 { + n := reqCtx.chainNodes[0] + if f, ok := n.(Hook); ok { + f(ctx) + } else if ff, ok := n.(Controller); ok { + ff(ctx) + } + } for _, header := range ctx.Response.headers { w.Header().Add(header.key, header.val) @@ -343,18 +391,32 @@ func UseHook(mw Hook) { // Next advances to the next middleware or controller in the chain and invokes it with the given context if available. func (app *App) Next(c *Context) { - app.t = app.t + 1 - n := app.chain.getByIndex(app.t) - if n != nil { - f, ok := n.(Hook) - if ok { - f(c) - } else { - ff, ok := n.(Controller) - if ok { + // Get request-specific chain state from context + if reqCtx, ok := c.Request.httpRequest.Context().Value("goffeeChain").(*requestContext); ok { + reqCtx.chainIndex++ + if reqCtx.chainIndex < len(reqCtx.chainNodes) { + n := reqCtx.chainNodes[reqCtx.chainIndex] + if f, ok := n.(Hook); ok { + f(c) + } else if ff, ok := n.(Controller); ok { ff(c) } } + } else { + // Fallback to old behavior (not thread-safe) + app.t = app.t + 1 + n := app.chain.getByIndex(app.t) + if n != nil { + f, ok := n.(Hook) + if ok { + f(c) + } else { + ff, ok := n.(Controller) + if ok { + ff(c) + } + } + } } } @@ -390,6 +452,17 @@ func (app *App) prepareChain(hs []interface{}) { } } +// prepareRequestChain prepares a request-specific chain to avoid race conditions +func (app *App) prepareRequestChain(requestChain *chain, hs []interface{}) { + mw := app.hooks.GetHooks() + for _, v := range mw { + requestChain.nodes = append(requestChain.nodes, v) + } + for _, v := range hs { + requestChain.nodes = append(requestChain.nodes, v) + } +} + // execute executes the first node in the chain, invoking it as either a Hook or Controller if applicable. func (cn *chain) execute(ctx *Context) { i := cn.getByIndex(0) diff --git a/go.mod b/go.mod index c603849..9a967b5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ replace git.smarteching.com/goffee/core/logger => ./logger replace git.smarteching.com/goffee/core/env => ./env -go 1.23.1 +go 1.24.1 require ( git.smarteching.com/zeni/go-chart/v2 v2.1.4 @@ -23,8 +23,10 @@ require ( ) require ( + git.smarteching.com/zeni/go-charts/v2 v2.6.11 // indirect github.com/SparkPost/gosparkpost v0.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-chi/chi/v5 v5.0.8 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect diff --git a/go.sum b/go.sum index 8b77517..9c1a556 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= +git.smarteching.com/zeni/go-charts/v2 v2.6.11 h1:9udzlv3uxGXszpplfkL5IaTUrgkNj++KwhbaN1vVEqI= +git.smarteching.com/zeni/go-charts/v2 v2.6.11/go.mod h1:3OpRPSXg7Qx4zcgsmwsC9ZFB9/wAkGSbnXf1wIbHYCg= github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -19,6 +21,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= @@ -100,6 +104,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= diff --git a/template/components/content_list.go b/template/components/content_list.go index 34bfb49..62927e6 100644 --- a/template/components/content_list.go +++ b/template/components/content_list.go @@ -1,10 +1,12 @@ package components type ContentList struct { + ID string Items []ContentListItem } type ContentListItem struct { + ID string Text string Description string EndElement string diff --git a/template/components/content_list.html b/template/components/content_list.html index 31af60b..351bcad 100644 --- a/template/components/content_list.html +++ b/template/components/content_list.html @@ -1,8 +1,10 @@ {{define "content_list"}} -
{{.Text}}
{{.EndElement}}