diff --git a/.github/testdata2/bruh.tmpl b/.github/testdata2/bruh.tmpl
new file mode 100644
index 00000000..28d75b49
--- /dev/null
+++ b/.github/testdata2/bruh.tmpl
@@ -0,0 +1 @@
+
I'm Bruh
\ No newline at end of file
diff --git a/app.go b/app.go
index ace88eb8..f725424e 100644
--- a/app.go
+++ b/app.go
@@ -18,7 +18,6 @@ import (
"strconv"
"strings"
"sync"
- "sync/atomic"
"time"
"encoding/json"
@@ -106,8 +105,6 @@ type App struct {
getBytes func(s string) (b []byte)
// Converts byte slice to a string
getString func(b []byte) string
- // Mounted and main apps
- appList map[string]*App
// Hooks
hooks *Hooks
// Latest route & group
@@ -115,6 +112,8 @@ type App struct {
latestGroup *Group
// TLS handler
tlsHandler *TLSHandler
+ // Mount fields
+ mountFields *mountFields
}
// Config is a struct holding the server settings.
@@ -485,7 +484,6 @@ func New(config ...Config) *App {
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
- appList: make(map[string]*App),
latestRoute: &Route{},
latestGroup: &Group{},
}
@@ -493,6 +491,9 @@ func New(config ...Config) *App {
// Define hooks
app.hooks = newHooks(app)
+ // Define mountFields
+ app.mountFields = newMountFields(app)
+
// Override config if provided
if len(config) > 0 {
app.config = config[0]
@@ -549,9 +550,6 @@ func New(config ...Config) *App {
// Override colors
app.config.ColorScheme = defaultColors(app.config.ColorScheme)
- // Init appList
- app.appList[""] = app
-
// Init app
app.init()
@@ -582,36 +580,6 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler) {
app.mutex.Unlock()
}
-// Mount attaches another app instance as a sub-router along a routing path.
-// It's very useful to split up a large API as many independent routers and
-// compose them as a single service using Mount. The fiber's error handler and
-// any of the fiber's sub apps are added to the application's error handlers
-// to be invoked on errors that happen within the prefix route.
-func (app *App) Mount(prefix string, fiber *App) Router {
- stack := fiber.Stack()
- prefix = strings.TrimRight(prefix, "/")
- if prefix == "" {
- prefix = "/"
- }
-
- for m := range stack {
- for r := range stack[m] {
- route := app.copyRoute(stack[m][r])
- app.addRoute(route.Method, app.addPrefixToRoute(prefix, route))
- }
- }
-
- // Support for configs of mounted-apps and sub-mounted-apps
- for mountedPrefixes, subApp := range fiber.appList {
- app.appList[prefix+mountedPrefixes] = subApp
- subApp.init()
- }
-
- atomic.AddUint32(&app.handlersCount, fiber.handlersCount)
-
- return app
-}
-
// Name Assign name to specific route.
func (app *App) Name(name string) Router {
app.mutex.Lock()
@@ -991,7 +959,7 @@ func (app *App) ErrorHandler(ctx *Ctx, err error) error {
mountedPrefixParts int
)
- for prefix, subApp := range app.appList {
+ for prefix, subApp := range app.mountFields.appList {
if prefix != "" && strings.HasPrefix(ctx.path, prefix) {
parts := len(strings.Split(prefix, "/"))
if mountedPrefixParts <= parts {
@@ -1041,7 +1009,17 @@ func (app *App) startupProcess() *App {
}
app.mutex.Lock()
+ defer app.mutex.Unlock()
+
+ // add routes of sub-apps
+ app.mountFields.subAppsRoutesAdded.Do(func() {
+ app.appendSubAppLists(app.mountFields.appList)
+ app.addSubAppsRoutes(app.mountFields.appList)
+ app.generateAppListKeys()
+ })
+
+ // build route tree stack
app.buildTree()
- app.mutex.Unlock()
+
return app
}
diff --git a/app_test.go b/app_test.go
index 6bbd8fcb..acfa8f9b 100644
--- a/app_test.go
+++ b/app_test.go
@@ -246,44 +246,6 @@ func Test_App_ErrorHandler_RouteStack(t *testing.T) {
utils.AssertEqual(t, "1: USE error", string(body))
}
-func Test_App_ErrorHandler_GroupMount(t *testing.T) {
- micro := New(Config{
- ErrorHandler: func(c *Ctx, err error) error {
- utils.AssertEqual(t, "0: GET error", err.Error())
- return c.Status(500).SendString("1: custom error")
- },
- })
- micro.Get("/doe", func(c *Ctx) error {
- return errors.New("0: GET error")
- })
-
- app := New()
- v1 := app.Group("/v1")
- v1.Mount("/john", micro)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
- testErrorResponse(t, err, resp, "1: custom error")
-}
-
-func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) {
- micro := New(Config{
- ErrorHandler: func(c *Ctx, err error) error {
- utils.AssertEqual(t, "0: GET error", err.Error())
- return c.Status(500).SendString("1: custom error")
- },
- })
- micro.Get("/john/doe", func(c *Ctx) error {
- return errors.New("0: GET error")
- })
-
- app := New()
- v1 := app.Group("/v1")
- v1.Mount("/", micro)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
- testErrorResponse(t, err, resp, "1: custom error")
-}
-
func Test_App_Nested_Params(t *testing.T) {
app := New()
@@ -307,22 +269,6 @@ func Test_App_Nested_Params(t *testing.T) {
utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
}
-// go test -run Test_App_Mount
-func Test_App_Mount(t *testing.T) {
- micro := New()
- micro.Get("/doe", func(c *Ctx) error {
- return c.SendStatus(StatusOK)
- })
-
- app := New()
- app.Mount("/john", micro)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil))
- utils.AssertEqual(t, nil, err, "app.Test(req)")
- utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
- utils.AssertEqual(t, uint32(2), app.handlersCount)
-}
-
func Test_App_Use_Params(t *testing.T) {
app := New()
@@ -1046,23 +992,6 @@ func Test_App_Group_Invalid(t *testing.T) {
New().Group("/").Use(1)
}
-// go test -run Test_App_Group_Mount
-func Test_App_Group_Mount(t *testing.T) {
- micro := New()
- micro.Get("/doe", func(c *Ctx) error {
- return c.SendStatus(StatusOK)
- })
-
- app := New()
- v1 := app.Group("/v1")
- v1.Mount("/john", micro)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
- utils.AssertEqual(t, nil, err, "app.Test(req)")
- utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
- utils.AssertEqual(t, uint32(2), app.handlersCount)
-}
-
func Test_App_Group(t *testing.T) {
dummyHandler := testEmptyHandler
@@ -1538,96 +1467,6 @@ func Test_App_DisablePreParseMultipartForm(t *testing.T) {
utils.AssertEqual(t, testString, string(body))
}
-func Test_App_UseMountedErrorHandler(t *testing.T) {
- app := New()
-
- fiber := New(Config{
- ErrorHandler: func(ctx *Ctx, err error) error {
- return ctx.Status(500).SendString("hi, i'm a custom error")
- },
- })
- fiber.Get("/", func(c *Ctx) error {
- return errors.New("something happened")
- })
-
- app.Mount("/api", fiber)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
- testErrorResponse(t, err, resp, "hi, i'm a custom error")
-}
-
-func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) {
- app := New()
-
- fiber := New(Config{
- ErrorHandler: func(ctx *Ctx, err error) error {
- return ctx.Status(500).SendString("hi, i'm a custom error")
- },
- })
- fiber.Get("/api", func(c *Ctx) error {
- return errors.New("something happened")
- })
-
- app.Mount("/", fiber)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
- testErrorResponse(t, err, resp, "hi, i'm a custom error")
-}
-
-func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) {
- app := New()
-
- tsf := func(ctx *Ctx, err error) error {
- return ctx.Status(200).SendString("hi, i'm a custom sub sub fiber error")
- }
- tripleSubFiber := New(Config{
- ErrorHandler: tsf,
- })
- tripleSubFiber.Get("/", func(c *Ctx) error {
- return errors.New("something happened")
- })
-
- sf := func(ctx *Ctx, err error) error {
- return ctx.Status(200).SendString("hi, i'm a custom sub fiber error")
- }
- subfiber := New(Config{
- ErrorHandler: sf,
- })
- subfiber.Get("/", func(c *Ctx) error {
- return errors.New("something happened")
- })
- subfiber.Mount("/third", tripleSubFiber)
-
- f := func(ctx *Ctx, err error) error {
- return ctx.Status(200).SendString("hi, i'm a custom error")
- }
- fiber := New(Config{
- ErrorHandler: f,
- })
- fiber.Get("/", func(c *Ctx) error {
- return errors.New("something happened")
- })
- fiber.Mount("/sub", subfiber)
-
- app.Mount("/api", fiber)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", nil))
- utils.AssertEqual(t, nil, err, "/api/sub req")
- utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
-
- b, err := ioutil.ReadAll(resp.Body)
- utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
- utils.AssertEqual(t, "hi, i'm a custom sub fiber error", string(b), "Response body")
-
- resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", nil))
- utils.AssertEqual(t, nil, err, "/api/sub/third req")
- utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
-
- b, err = ioutil.ReadAll(resp2.Body)
- utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
- utils.AssertEqual(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body")
-}
-
func Test_App_Test_no_timeout_infinitely(t *testing.T) {
var err error
c := make(chan int)
diff --git a/ctx.go b/ctx.go
index 327b3193..0fa466e5 100644
--- a/ctx.go
+++ b/ctx.go
@@ -1384,11 +1384,13 @@ func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
- // Pass-locals-to-views & bind
+ // Pass-locals-to-views, bind, appListKeys
c.renderExtensions(bind)
- rendered := false
- for prefix, app := range c.app.appList {
+ var rendered bool
+ for i := len(c.app.mountFields.appListKeys) - 1; i >= 0; i-- {
+ prefix := c.app.mountFields.appListKeys[i]
+ app := c.app.mountFields.appList[prefix]
if prefix == "" || strings.Contains(c.OriginalURL(), prefix) {
if len(layouts) == 0 && app.config.ViewsLayout != "" {
layouts = []string{
@@ -1454,6 +1456,10 @@ func (c *Ctx) renderExtensions(bind interface{}) {
})
}
}
+
+ if len(c.app.mountFields.appListKeys) == 0 {
+ c.app.generateAppListKeys()
+ }
}
// Route returns the matched Route struct.
diff --git a/ctx_test.go b/ctx_test.go
index ad975de9..f1ba1f6c 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -32,7 +32,6 @@ import (
"github.com/gofiber/fiber/v2/internal/bytebufferpool"
"github.com/gofiber/fiber/v2/internal/storage/memory"
- "github.com/gofiber/fiber/v2/internal/template/html"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
)
@@ -2792,58 +2791,6 @@ func Test_Ctx_Render(t *testing.T) {
utils.AssertEqual(t, false, err == nil)
}
-// go test -run Test_Ctx_Render_Mount
-func Test_Ctx_Render_Mount(t *testing.T) {
- t.Parallel()
-
- sub := New(Config{
- Views: html.New("./.github/testdata/template", ".gohtml"),
- })
-
- sub.Get("/:name", func(ctx *Ctx) error {
- return ctx.Render("hello_world", Map{
- "Name": ctx.Params("name"),
- })
- })
-
- app := New()
- app.Mount("/hello", sub)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", nil))
- utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
- utils.AssertEqual(t, nil, err, "app.Test(req)")
-
- body, err := ioutil.ReadAll(resp.Body)
- utils.AssertEqual(t, nil, err)
- utils.AssertEqual(t, "Hello a!
", string(body))
-}
-
-func Test_Ctx_Render_MountGroup(t *testing.T) {
- t.Parallel()
-
- micro := New(Config{
- Views: html.New("./.github/testdata/template", ".gohtml"),
- })
-
- micro.Get("/doe", func(c *Ctx) error {
- return c.Render("hello_world", Map{
- "Name": "doe",
- })
- })
-
- app := New()
- v1 := app.Group("/v1")
- v1.Mount("/john", micro)
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
- utils.AssertEqual(t, nil, err, "app.Test(req)")
- utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
-
- body, err := ioutil.ReadAll(resp.Body)
- utils.AssertEqual(t, nil, err)
- utils.AssertEqual(t, "Hello doe!
", string(body))
-}
-
func Test_Ctx_RenderWithoutLocals(t *testing.T) {
t.Parallel()
app := New(Config{
@@ -3149,6 +3096,7 @@ func Test_Ctx_RestartRoutingWithChangedPathAndCatchAll(t *testing.T) {
type testTemplateEngine struct {
templates *template.Template
+ path string
}
func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{}, layout ...string) error {
@@ -3160,7 +3108,10 @@ func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{},
}
func (t *testTemplateEngine) Load() error {
- t.templates = template.Must(template.ParseGlob("./.github/testdata/*.tmpl"))
+ if t.path == "" {
+ t.path = "testdata"
+ }
+ t.templates = template.Must(template.ParseGlob("./.github/" + t.path + "/*.tmpl"))
return nil
}
diff --git a/group.go b/group.go
index ab74b80e..a59eaed8 100644
--- a/group.go
+++ b/group.go
@@ -8,7 +8,6 @@ import (
"fmt"
"reflect"
"strings"
- "sync/atomic"
)
// Group struct
@@ -19,35 +18,6 @@ type Group struct {
Prefix string
}
-// Mount attaches another app instance as a sub-router along a routing path.
-// It's very useful to split up a large API as many independent routers and
-// compose them as a single service using Mount.
-func (grp *Group) Mount(prefix string, fiber *App) Router {
- stack := fiber.Stack()
- groupPath := getGroupPath(grp.Prefix, prefix)
- groupPath = strings.TrimRight(groupPath, "/")
- if groupPath == "" {
- groupPath = "/"
- }
-
- for m := range stack {
- for r := range stack[m] {
- route := grp.app.copyRoute(stack[m][r])
- grp.app.addRoute(route.Method, grp.app.addPrefixToRoute(groupPath, route))
- }
- }
-
- // Support for configs of mounted-apps and sub-mounted-apps
- for mountedPrefixes, subApp := range fiber.appList {
- grp.app.appList[groupPath+mountedPrefixes] = subApp
- subApp.init()
- }
-
- atomic.AddUint32(&grp.app.handlersCount, fiber.handlersCount)
-
- return grp
-}
-
// Name Assign name to specific route.
func (grp *Group) Name(name string) Router {
grp.app.mutex.Lock()
diff --git a/hooks.go b/hooks.go
index f614ef13..b2766a8c 100644
--- a/hooks.go
+++ b/hooks.go
@@ -8,6 +8,7 @@ type OnGroupNameHandler = OnGroupHandler
type OnListenHandler = func() error
type OnShutdownHandler = OnListenHandler
type OnForkHandler = func(int) error
+type OnMountHandler = func(*App) error
// Hooks is a struct to use it with App.
type Hooks struct {
@@ -22,6 +23,7 @@ type Hooks struct {
onListen []OnListenHandler
onShutdown []OnShutdownHandler
onFork []OnForkHandler
+ onMount []OnMountHandler
}
func newHooks(app *App) *Hooks {
@@ -34,6 +36,7 @@ func newHooks(app *App) *Hooks {
onListen: make([]OnListenHandler, 0),
onShutdown: make([]OnShutdownHandler, 0),
onFork: make([]OnForkHandler, 0),
+ onMount: make([]OnMountHandler, 0),
}
}
@@ -94,7 +97,22 @@ func (h *Hooks) OnFork(handler ...OnForkHandler) {
h.app.mutex.Unlock()
}
+// OnMount is a hook to execute user function after mounting process.
+// The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter.
+// It works for app and group mounting.
+func (h *Hooks) OnMount(handler ...OnMountHandler) {
+ h.app.mutex.Lock()
+ h.onMount = append(h.onMount, handler...)
+ h.app.mutex.Unlock()
+}
+
func (h *Hooks) executeOnRouteHooks(route Route) error {
+ // Check mounting
+ if h.app.mountFields.mountPath != "" {
+ route.path = h.app.mountFields.mountPath + route.path
+ route.Path = route.path
+ }
+
for _, v := range h.onRoute {
if err := v(route); err != nil {
return err
@@ -105,6 +123,12 @@ func (h *Hooks) executeOnRouteHooks(route Route) error {
}
func (h *Hooks) executeOnNameHooks(route Route) error {
+ // Check mounting
+ if h.app.mountFields.mountPath != "" {
+ route.path = h.app.mountFields.mountPath + route.path
+ route.Path = route.path
+ }
+
for _, v := range h.onName {
if err := v(route); err != nil {
return err
@@ -115,6 +139,11 @@ func (h *Hooks) executeOnNameHooks(route Route) error {
}
func (h *Hooks) executeOnGroupHooks(group Group) error {
+ // Check mounting
+ if h.app.mountFields.mountPath != "" {
+ group.Prefix = h.app.mountFields.mountPath + group.Prefix
+ }
+
for _, v := range h.onGroup {
if err := v(group); err != nil {
return err
@@ -125,6 +154,11 @@ func (h *Hooks) executeOnGroupHooks(group Group) error {
}
func (h *Hooks) executeOnGroupNameHooks(group Group) error {
+ // Check mounting
+ if h.app.mountFields.mountPath != "" {
+ group.Prefix = h.app.mountFields.mountPath + group.Prefix
+ }
+
for _, v := range h.onGroupName {
if err := v(group); err != nil {
return err
@@ -155,3 +189,13 @@ func (h *Hooks) executeOnForkHooks(pid int) {
_ = v(pid)
}
}
+
+func (h *Hooks) executeOnMountHooks(app *App) error {
+ for _, v := range h.onMount {
+ if err := v(app); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/hooks_test.go b/hooks_test.go
index 5ed9d821..e39d269c 100644
--- a/hooks_test.go
+++ b/hooks_test.go
@@ -33,6 +33,29 @@ func Test_Hook_OnRoute(t *testing.T) {
app.Mount("/sub", subApp)
}
+func Test_Hook_OnRoute_Mount(t *testing.T) {
+ t.Parallel()
+
+ app := New()
+ subApp := New()
+ app.Mount("/sub", subApp)
+
+ subApp.Hooks().OnRoute(func(r Route) error {
+ utils.AssertEqual(t, "/sub/test", r.Path)
+
+ return nil
+ })
+
+ app.Hooks().OnRoute(func(r Route) error {
+ utils.AssertEqual(t, "/", r.Path)
+
+ return nil
+ })
+
+ app.Get("/", testSimpleHandler).Name("x")
+ subApp.Get("/test", testSimpleHandler)
+}
+
func Test_Hook_OnName(t *testing.T) {
t.Parallel()
@@ -96,6 +119,24 @@ func Test_Hook_OnGroup(t *testing.T) {
utils.AssertEqual(t, "/x/x/a", buf.String())
}
+func Test_Hook_OnGroup_Mount(t *testing.T) {
+ t.Parallel()
+
+ app := New()
+ micro := New()
+ micro.Mount("/john", app)
+
+ app.Hooks().OnGroup(func(g Group) error {
+ utils.AssertEqual(t, "/john/v1", g.Prefix)
+ return nil
+ })
+
+ v1 := app.Group("/v1")
+ v1.Get("/doe", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+}
+
func Test_Hook_OnGroupName(t *testing.T) {
t.Parallel()
@@ -200,3 +241,21 @@ func Test_Hook_OnHook(t *testing.T) {
utils.AssertEqual(t, nil, app.prefork(NetworkTCP4, ":3000", nil))
}
+
+func Test_Hook_OnMount(t *testing.T) {
+ t.Parallel()
+
+ app := New()
+ app.Get("/", testSimpleHandler).Name("x")
+
+ subApp := New()
+ subApp.Get("/test", testSimpleHandler)
+
+ subApp.Hooks().OnMount(func(parent *App) error {
+ utils.AssertEqual(t, parent.mountFields.mountPath, "")
+
+ return nil
+ })
+
+ app.Mount("/sub", subApp)
+}
diff --git a/internal/template/html/html.go b/internal/template/html/html.go
index da9d8c52..71f6f02c 100644
--- a/internal/template/html/html.go
+++ b/internal/template/html/html.go
@@ -183,15 +183,6 @@ func (e *Engine) Load() error {
// Render will execute the template name along with the given values.
func (e *Engine) Render(out io.Writer, template string, binding interface{}, layout ...string) error {
- if !e.loaded || e.reload {
- if e.reload {
- e.loaded = false
- }
- if err := e.Load(); err != nil {
- return err
- }
- }
-
tmpl := e.Templates.Lookup(template)
if tmpl == nil {
return fmt.Errorf("render: template %s does not exist", template)
diff --git a/mount.go b/mount.go
new file mode 100644
index 00000000..f7df3a97
--- /dev/null
+++ b/mount.go
@@ -0,0 +1,146 @@
+// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
+// 🤖 Github Repository: https://github.com/gofiber/fiber
+// 📌 API Documentation: https://docs.gofiber.io
+
+package fiber
+
+import (
+ "sort"
+ "strings"
+ "sync"
+ "sync/atomic"
+)
+
+// Put fields related to mounting.
+type mountFields struct {
+ // Mounted and main apps
+ appList map[string]*App
+ // Ordered keys of apps (sorted by key length for Render)
+ appListKeys []string
+ // check added routes of sub-apps
+ subAppsRoutesAdded sync.Once
+ // Prefix of app if it was mounted
+ mountPath string
+}
+
+// Create empty mountFields instance
+func newMountFields(app *App) *mountFields {
+ return &mountFields{
+ appList: map[string]*App{"": app},
+ appListKeys: make([]string, 0),
+ }
+}
+
+// Mount attaches another app instance as a sub-router along a routing path.
+// It's very useful to split up a large API as many independent routers and
+// compose them as a single service using Mount. The fiber's error handler and
+// any of the fiber's sub apps are added to the application's error handlers
+// to be invoked on errors that happen within the prefix route.
+func (app *App) Mount(prefix string, fiber *App) Router {
+ prefix = strings.TrimRight(prefix, "/")
+ if prefix == "" {
+ prefix = "/"
+ }
+
+ // Support for configs of mounted-apps and sub-mounted-apps
+ for mountedPrefixes, subApp := range fiber.mountFields.appList {
+ subApp.mountFields.mountPath = prefix + mountedPrefixes
+ app.mountFields.appList[prefix+mountedPrefixes] = subApp
+ }
+
+ // Execute onMount hooks
+ if err := fiber.hooks.executeOnMountHooks(app); err != nil {
+ panic(err)
+ }
+
+ return app
+}
+
+// Mount attaches another app instance as a sub-router along a routing path.
+// It's very useful to split up a large API as many independent routers and
+// compose them as a single service using Mount.
+func (grp *Group) Mount(prefix string, fiber *App) Router {
+ groupPath := getGroupPath(grp.Prefix, prefix)
+ groupPath = strings.TrimRight(groupPath, "/")
+ if groupPath == "" {
+ groupPath = "/"
+ }
+
+ // Support for configs of mounted-apps and sub-mounted-apps
+ for mountedPrefixes, subApp := range fiber.mountFields.appList {
+ subApp.mountFields.mountPath = groupPath + mountedPrefixes
+ grp.app.mountFields.appList[groupPath+mountedPrefixes] = subApp
+ }
+
+ // Execute onMount hooks
+ if err := fiber.hooks.executeOnMountHooks(grp.app); err != nil {
+ panic(err)
+ }
+
+ return grp
+}
+
+// The MountPath property contains one or more path patterns on which a sub-app was mounted.
+func (app *App) MountPath() string {
+ return app.mountFields.mountPath
+}
+
+// generateAppListKeys generates app list keys for Render, should work after appendSubAppLists
+func (app *App) generateAppListKeys() {
+ for key := range app.mountFields.appList {
+ app.mountFields.appListKeys = append(app.mountFields.appListKeys, key)
+ }
+
+ sort.Slice(app.mountFields.appListKeys, func(i, j int) bool {
+ return len(app.mountFields.appListKeys[i]) < len(app.mountFields.appListKeys[j])
+ })
+}
+
+// appendSubAppLists supports nested for sub apps
+func (app *App) appendSubAppLists(appList map[string]*App, parent ...string) {
+ for prefix, subApp := range appList {
+ // skip real app
+ if prefix == "" {
+ continue
+ }
+
+ if len(parent) > 0 {
+ prefix = parent[0] + prefix
+ }
+
+ if _, ok := app.mountFields.appList[prefix]; !ok {
+ app.mountFields.appList[prefix] = subApp
+ }
+
+ // The first element of appList is always the app itself. If there are no other sub apps, we should skip appending nested apps.
+ if len(subApp.mountFields.appList) > 1 {
+ app.appendSubAppLists(subApp.mountFields.appList, prefix)
+ }
+
+ }
+}
+
+// addSubAppsRoutes adds routes of sub apps nestedly when to start the server
+func (app *App) addSubAppsRoutes(appList map[string]*App, parent ...string) {
+ for prefix, subApp := range appList {
+ // skip real app
+ if prefix == "" {
+ continue
+ }
+
+ if len(parent) > 0 {
+ prefix = parent[0] + prefix
+ }
+
+ // add routes
+ stack := subApp.stack
+ for m := range stack {
+ for r := range stack[m] {
+ route := app.copyRoute(stack[m][r])
+ app.addRoute(route.Method, app.addPrefixToRoute(prefix, route), true)
+ }
+ }
+
+ atomic.AddUint32(&app.handlersCount, subApp.handlersCount)
+ }
+}
diff --git a/mount_test.go b/mount_test.go
new file mode 100644
index 00000000..1fa91359
--- /dev/null
+++ b/mount_test.go
@@ -0,0 +1,351 @@
+// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
+// 🤖 Github Repository: https://github.com/gofiber/fiber
+// 📌 API Documentation: https://docs.gofiber.io
+
+package fiber
+
+import (
+ "errors"
+ "io/ioutil"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gofiber/fiber/v2/internal/template/html"
+ "github.com/gofiber/fiber/v2/utils"
+)
+
+// go test -run Test_App_Mount
+func Test_App_Mount(t *testing.T) {
+ micro := New()
+ micro.Get("/doe", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+
+ app := New()
+ app.Mount("/john", micro)
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, uint32(2), app.handlersCount)
+}
+
+// go test -run Test_App_Mount_Nested
+func Test_App_Mount_Nested(t *testing.T) {
+ app := New()
+ one := New()
+ two := New()
+ three := New()
+
+ two.Mount("/three", three)
+ app.Mount("/one", one)
+ one.Mount("/two", two)
+
+ one.Get("/doe", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+
+ two.Get("/nested", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+
+ three.Get("/test", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/one/doe", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/nested", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/three/test", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ utils.AssertEqual(t, uint32(6), app.handlersCount)
+}
+
+// go test -run Test_App_MountPath
+func Test_App_MountPath(t *testing.T) {
+ app := New()
+ one := New()
+ two := New()
+ three := New()
+
+ two.Mount("/three", three)
+ one.Mount("/two", two)
+ app.Mount("/one", one)
+
+ utils.AssertEqual(t, "/one", one.MountPath())
+ utils.AssertEqual(t, "/one/two", two.MountPath())
+ utils.AssertEqual(t, "/one/two/three", three.MountPath())
+ utils.AssertEqual(t, "", app.MountPath())
+}
+
+func Test_App_ErrorHandler_GroupMount(t *testing.T) {
+ micro := New(Config{
+ ErrorHandler: func(c *Ctx, err error) error {
+ utils.AssertEqual(t, "0: GET error", err.Error())
+ return c.Status(500).SendString("1: custom error")
+ },
+ })
+ micro.Get("/doe", func(c *Ctx) error {
+ return errors.New("0: GET error")
+ })
+
+ app := New()
+ v1 := app.Group("/v1")
+ v1.Mount("/john", micro)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
+ testErrorResponse(t, err, resp, "1: custom error")
+}
+
+func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) {
+ micro := New(Config{
+ ErrorHandler: func(c *Ctx, err error) error {
+ utils.AssertEqual(t, "0: GET error", err.Error())
+ return c.Status(500).SendString("1: custom error")
+ },
+ })
+ micro.Get("/john/doe", func(c *Ctx) error {
+ return errors.New("0: GET error")
+ })
+
+ app := New()
+ v1 := app.Group("/v1")
+ v1.Mount("/", micro)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
+ testErrorResponse(t, err, resp, "1: custom error")
+}
+
+// go test -run Test_App_Group_Mount
+func Test_App_Group_Mount(t *testing.T) {
+ micro := New()
+ micro.Get("/doe", func(c *Ctx) error {
+ return c.SendStatus(StatusOK)
+ })
+
+ app := New()
+ v1 := app.Group("/v1")
+ v1.Mount("/john", micro)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, uint32(2), app.handlersCount)
+}
+
+func Test_App_UseMountedErrorHandler(t *testing.T) {
+ app := New()
+
+ fiber := New(Config{
+ ErrorHandler: func(ctx *Ctx, err error) error {
+ return ctx.Status(500).SendString("hi, i'm a custom error")
+ },
+ })
+ fiber.Get("/", func(c *Ctx) error {
+ return errors.New("something happened")
+ })
+
+ app.Mount("/api", fiber)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
+ testErrorResponse(t, err, resp, "hi, i'm a custom error")
+}
+
+func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) {
+ app := New()
+
+ fiber := New(Config{
+ ErrorHandler: func(ctx *Ctx, err error) error {
+ return ctx.Status(500).SendString("hi, i'm a custom error")
+ },
+ })
+ fiber.Get("/api", func(c *Ctx) error {
+ return errors.New("something happened")
+ })
+
+ app.Mount("/", fiber)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil))
+ testErrorResponse(t, err, resp, "hi, i'm a custom error")
+}
+
+func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) {
+ app := New()
+
+ tsf := func(ctx *Ctx, err error) error {
+ return ctx.Status(200).SendString("hi, i'm a custom sub sub fiber error")
+ }
+ tripleSubFiber := New(Config{
+ ErrorHandler: tsf,
+ })
+ tripleSubFiber.Get("/", func(c *Ctx) error {
+ return errors.New("something happened")
+ })
+
+ sf := func(ctx *Ctx, err error) error {
+ return ctx.Status(200).SendString("hi, i'm a custom sub fiber error")
+ }
+ subfiber := New(Config{
+ ErrorHandler: sf,
+ })
+ subfiber.Get("/", func(c *Ctx) error {
+ return errors.New("something happened")
+ })
+ subfiber.Mount("/third", tripleSubFiber)
+
+ f := func(ctx *Ctx, err error) error {
+ return ctx.Status(200).SendString("hi, i'm a custom error")
+ }
+ fiber := New(Config{
+ ErrorHandler: f,
+ })
+ fiber.Get("/", func(c *Ctx) error {
+ return errors.New("something happened")
+ })
+ fiber.Mount("/sub", subfiber)
+
+ app.Mount("/api", fiber)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", nil))
+ utils.AssertEqual(t, nil, err, "/api/sub req")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ b, err := ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
+ utils.AssertEqual(t, "hi, i'm a custom sub fiber error", string(b), "Response body")
+
+ resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", nil))
+ utils.AssertEqual(t, nil, err, "/api/sub/third req")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ b, err = ioutil.ReadAll(resp2.Body)
+ utils.AssertEqual(t, nil, err, "iotuil.ReadAll()")
+ utils.AssertEqual(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body")
+}
+
+// go test -run Test_Ctx_Render_Mount
+func Test_Ctx_Render_Mount(t *testing.T) {
+ t.Parallel()
+
+ sub := New(Config{
+ Views: html.New("./.github/testdata/template", ".gohtml"),
+ })
+
+ sub.Get("/:name", func(ctx *Ctx) error {
+ return ctx.Render("hello_world", Map{
+ "Name": ctx.Params("name"),
+ })
+ })
+
+ app := New()
+ app.Mount("/hello", sub)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", nil))
+ utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+
+ body, err := ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err)
+ utils.AssertEqual(t, "Hello a!
", string(body))
+}
+
+// go test -run Test_Ctx_Render_Mount_ParentOrSubHasViews
+func Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) {
+ t.Parallel()
+
+ engine := &testTemplateEngine{}
+ err := engine.Load()
+ utils.AssertEqual(t, nil, err)
+
+ engine2 := &testTemplateEngine{path: "testdata2"}
+ err = engine2.Load()
+ utils.AssertEqual(t, nil, err)
+
+ sub := New(Config{
+ Views: html.New("./.github/testdata/template", ".gohtml"),
+ })
+
+ sub2 := New(Config{
+ Views: engine2,
+ })
+
+ app := New(Config{
+ Views: engine,
+ })
+
+ app.Get("/test", func(c *Ctx) error {
+ return c.Render("index.tmpl", Map{
+ "Title": "Hello, World!",
+ })
+ })
+
+ sub.Get("/world/:name", func(c *Ctx) error {
+ return c.Render("hello_world", Map{
+ "Name": c.Params("name"),
+ })
+ })
+
+ sub2.Get("/moment", func(c *Ctx) error {
+ return c.Render("bruh.tmpl", Map{})
+ })
+
+ sub.Mount("/bruh", sub2)
+ app.Mount("/hello", sub)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/world/a", nil))
+ utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+
+ body, err := ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err)
+ utils.AssertEqual(t, "Hello a!
", string(body))
+
+ resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil))
+ utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+
+ body, err = ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err)
+ utils.AssertEqual(t, "Hello, World!
", string(body))
+
+ resp, err = app.Test(httptest.NewRequest(MethodGet, "/hello/bruh/moment", nil))
+ utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+
+ body, err = ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err)
+ utils.AssertEqual(t, "I'm Bruh
", string(body))
+
+}
+
+func Test_Ctx_Render_MountGroup(t *testing.T) {
+ t.Parallel()
+
+ micro := New(Config{
+ Views: html.New("./.github/testdata/template", ".gohtml"),
+ })
+
+ micro.Get("/doe", func(c *Ctx) error {
+ return c.Render("hello_world", Map{
+ "Name": "doe",
+ })
+ })
+
+ app := New()
+ v1 := app.Group("/v1")
+ v1.Mount("/john", micro)
+
+ resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil))
+ utils.AssertEqual(t, nil, err, "app.Test(req)")
+ utils.AssertEqual(t, 200, resp.StatusCode, "Status code")
+
+ body, err := ioutil.ReadAll(resp.Body)
+ utils.AssertEqual(t, nil, err)
+ utils.AssertEqual(t, "Hello doe!
", string(body))
+}
diff --git a/router.go b/router.go
index ff51bb0d..642149d5 100644
--- a/router.go
+++ b/router.go
@@ -423,7 +423,13 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router {
return app
}
-func (app *App) addRoute(method string, route *Route) {
+func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
+ // Check mounted routes
+ var mounted bool
+ if len(isMounted) > 0 {
+ mounted = isMounted[0]
+ }
+
// Get unique HTTP method identifier
m := methodInt(method)
@@ -441,12 +447,15 @@ func (app *App) addRoute(method string, route *Route) {
app.routesRefreshed = true
}
- app.mutex.Lock()
- app.latestRoute = route
- if err := app.hooks.executeOnRouteHooks(*route); err != nil {
- panic(err)
+ // Execute onRoute hooks & change latestRoute if not adding mounted route
+ if !mounted {
+ app.mutex.Lock()
+ app.latestRoute = route
+ if err := app.hooks.executeOnRouteHooks(*route); err != nil {
+ panic(err)
+ }
+ app.mutex.Unlock()
}
- app.mutex.Unlock()
}
// buildTree build the prefix tree from the previously registered routes
@@ -454,6 +463,7 @@ func (app *App) buildTree() *App {
if !app.routesRefreshed {
return app
}
+
// loop all the methods and stacks and create the prefix tree
for m := range intMethod {
tsMap := make(map[string][]*Route)
@@ -467,6 +477,7 @@ func (app *App) buildTree() *App {
}
app.treeStack[m] = tsMap
}
+
// loop the methods and tree stacks and add global stack and sort everything
for m := range intMethod {
tsMap := app.treeStack[m]