Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"encoding/xml"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -780,6 +781,14 @@ func (app *App) Static(prefix, root string, config ...Static) Router {
return app
}

// StaticFilesystem Supports Serving Static Resources from an Embed.FS File System
//
// The filesystem parameter can directly accept an embed.FS instance.
Comment on lines +784 to +786
Copy link

Copilot AI May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The doc comment should follow Go conventions and include usage examples. For example:

// StaticFilesystem serves embedded static files from an fs.FS.
// Example:
//   app.StaticFilesystem("/assets", embeddedFiles)
Suggested change
// StaticFilesystem Supports Serving Static Resources from an Embed.FS File System
//
// The filesystem parameter can directly accept an embed.FS instance.
// StaticFilesystem serves static resources from an fs.FS, such as an embed.FS instance.
//
// Example:
// // Embed static files using embed.FS
// //go:embed static/*
// var embeddedFiles embed.FS
//
// app.StaticFilesystem("/static", embeddedFiles)

Copilot uses AI. Check for mistakes.

func (app *App) StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router {
app.registerStaticFS(prefix, filesystem, config...)
return app
}
Comment on lines +784 to +790
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Expose the new helper through the public Router interface

StaticFilesystem is now public on *App, but the Router interface (defined in router.go, lines 22-47) hasn’t been extended.
As a result:

var r fiber.Router = app // common pattern
r.StaticFilesystem("/assets", myFS) // ← does **not** compile

The omission also means *Group cannot forward the call, limiting feature parity with Static.

 type Router interface {
     …
     Static(prefix, root string, config ...Static) Router
+    StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router
     …
 }

After extending the interface, forwarders for *Group can be added with two lines:

func (g *Group) StaticFilesystem(p string, fs fs.FS, cfg ...Static) Router {
    return g.app.StaticFilesystem(getGroupPath(g.Prefix, p), fs, cfg...)
}

🏁 Script executed:

#!/bin/bash
set -e

# Locate the Router interface declaration and its methods
rg "type Router interface" -A 20

# Confirm if StaticFilesystem is already declared in router.go
rg "StaticFilesystem" -n router.go || echo "No StaticFilesystem in router.go"

# Inspect Group type and existing Static methods
rg "type Group" -A 10
rg "func .*Group\).*Static" -n

Length of output: 1560


Expose StaticFilesystem on the Router interface and Group

The new StaticFilesystem helper is public on *App but isn’t declared on the Router interface (router.go) or forwarded by *Group (group.go), preventing common patterns like:

var r fiber.Router = app
r.StaticFilesystem("/assets", myFS) // does not compile

Please add:

• router.go (within type Router interface):

 type Router interface {
     …
     Static(prefix, root string, config ...Static) Router
+    StaticFilesystem(prefix string, filesystem fs.FS, config ...Static) Router
     …
 }

• group.go (alongside func (grp *Group) Static):

func (grp *Group) StaticFilesystem(p string, fs fs.FS, cfg ...Static) Router {
    return grp.app.StaticFilesystem(getGroupPath(grp.Prefix, p), fs, cfg...)
}


// All will register the handler on all HTTP methods
func (app *App) All(path string, handlers ...Handler) Router {
for _, method := range app.config.RequestMethods {
Expand Down
120 changes: 120 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package fiber
import (
"fmt"
"html"
iofs "io/fs"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -444,6 +445,125 @@ func (app *App) registerStatic(prefix, root string, config ...Static) {
app.addRoute(MethodHead, &route)
}

func (app *App) registerStaticFS(prefix string, filesystem iofs.FS, config ...Static) {
if prefix == "" {
prefix = "/"
}
if prefix[0] != '/' {
prefix = "/" + prefix
}
// in case-sensitive routing, all to lowercase
if !app.config.CaseSensitive {
prefix = utils.ToLower(prefix)
}

prefixLen := len(prefix)
isStar := strings.Contains(prefix, "*")
if isStar {
prefix = strings.Split(prefix, "*")[0]
prefixLen = len(prefix)
}
isRoot := prefix == "/"

// add embed fs support
fsHandler := &fasthttp.FS{
FS: filesystem,
Root: ".",
GenerateIndexPages: false,
AcceptByteRange: false,
Compress: false,
CompressedFileSuffix: app.config.CompressedFileSuffix,
CacheDuration: 10 * time.Second,
IndexNames: []string{"index.html"},
PathRewrite: func(fctx *fasthttp.RequestCtx) []byte {
path := fctx.Path()
if len(path) >= prefixLen {
if isStar && app.getString(path[0:prefixLen]) == prefix {
path = append(path[0:0], '/')
} else {
path = path[prefixLen:]
if len(path) == 0 || path[len(path)-1] != '/' {
path = append(path, '/')
}
}
}
if len(path) > 0 && path[0] != '/' {
path = append([]byte("/"), path...)
}
return path
},
PathNotFound: func(fctx *fasthttp.RequestCtx) {
fctx.Response.SetStatusCode(StatusNotFound)
},
}

// Set config if provided
var cacheControlValue string
var modifyResponse Handler
if len(config) > 0 {
maxAge := config[0].MaxAge
if maxAge > 0 {
cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
}
fsHandler.CacheDuration = config[0].CacheDuration
fsHandler.Compress = config[0].Compress
fsHandler.AcceptByteRange = config[0].ByteRange
fsHandler.GenerateIndexPages = config[0].Browse
if config[0].Index != "" {
fsHandler.IndexNames = []string{config[0].Index}
}
modifyResponse = config[0].ModifyResponse
}

fileHandler := fsHandler.NewRequestHandler()
handler := func(c *Ctx) error {
// Don't execute middleware if Next returns true
if len(config) != 0 && config[0].Next != nil && config[0].Next(c) {
return c.Next()
}
// Serve file
fileHandler(c.fasthttp)
// Sets the response Content-Disposition header to attachment if the Download option is true
if len(config) > 0 && config[0].Download {
c.Attachment()
}
// Return request if found and not forbidden
status := c.fasthttp.Response.StatusCode()
if status != StatusNotFound && status != StatusForbidden {
if len(cacheControlValue) > 0 {
c.fasthttp.Response.Header.Set(HeaderCacheControl, cacheControlValue)
}
if modifyResponse != nil {
return modifyResponse(c)
}
return nil
}
// Reset response to default
c.fasthttp.SetContentType("") // Issue #420
c.fasthttp.Response.SetStatusCode(StatusOK)
c.fasthttp.Response.SetBodyString("")
// Next middleware
return c.Next()
}
Comment on lines +520 to +547
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing nil-filesystem validation

If a caller accidentally passes a nil fs.FS, the fast-http handler panics the first time it tries to open a file.

if filesystem == nil {
    panic("StaticFilesystem: filesystem cannot be nil")
}

Fail fast with a clear message rather than a runtime panic deep inside fasthttp.


// Create route metadata without pointer
route := Route{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use: true,
root: isRoot,
path: prefix,
// Public data
Method: MethodGet,
Path: prefix,
Handlers: []Handler{handler},
}
Comment on lines +551 to +558
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Route metadata sets use: true but only registers GET/HEAD

use: true traditionally means match all HTTP methods.
Because you later add the route only to GET & HEAD stacks, the flag is misleading for downstream tools that rely on it (e.g. route printers, metrics).

Set use: false or register the handler for all methods, mirroring the behaviour of app.Static.

// Increment global handler count
atomic.AddUint32(&app.handlersCount, 1)
// Add route to stack
app.addRoute(MethodGet, &route)
// Add HEAD route
app.addRoute(MethodHead, &route)
}

func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
// Check mounted routes
var mounted bool
Expand Down