diff --git a/.github/README.md b/.github/README.md
index ee33a86a..40dcf7b1 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -203,15 +203,15 @@ func main() {
func main() {
app := fiber.New()
- app.Static("/", "./public")
+ app.Get("/*", static.New("./public"))
// => http://localhost:3000/js/script.js
// => http://localhost:3000/css/style.css
- app.Static("/prefix", "./public")
+ app.Get("/prefix*", static.New("./public"))
// => http://localhost:3000/prefix/js/script.js
// => http://localhost:3000/prefix/css/style.css
- app.Static("*", "./public/index.html")
+ app.Get("*", static.New("./public/index.html"))
// => http://localhost:3000/any/path/shows/index/html
log.Fatal(app.Listen(":3000"))
@@ -388,7 +388,7 @@ curl -H "Origin: http://example.com" --verbose http://localhost:3000
func main() {
app := fiber.New()
- app.Static("/", "./public")
+ app.Get("/", static.New("./public"))
app.Get("/demo", func(c fiber.Ctx) error {
return c.SendString("This is a demo!")
@@ -586,7 +586,6 @@ Here is a list of middleware that are included within the Fiber framework.
| [etag](https://github.com/gofiber/fiber/tree/main/middleware/etag) | Allows for caches to be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. |
| [expvar](https://github.com/gofiber/fiber/tree/main/middleware/expvar) | Serves via its HTTP server runtime exposed variants in the JSON format. |
| [favicon](https://github.com/gofiber/fiber/tree/main/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. |
-| [filesystem](https://github.com/gofiber/fiber/tree/main/middleware/filesystem) | FileSystem middleware for Fiber. |
| [healthcheck](https://github.com/gofiber/fiber/tree/main/middleware/healthcheck) | Liveness and Readiness probes for Fiber. |
| [helmet](https://github.com/gofiber/fiber/tree/main/middleware/helmet) | Helps secure your apps by setting various HTTP headers. |
| [idempotency](https://github.com/gofiber/fiber/tree/main/middleware/idempotency) | Allows for fault-tolerant APIs where duplicate requests do not erroneously cause the same action performed multiple times on the server-side. |
@@ -601,6 +600,7 @@ Here is a list of middleware that are included within the Fiber framework.
| [rewrite](https://github.com/gofiber/fiber/tree/main/middleware/rewrite) | Rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. |
| [session](https://github.com/gofiber/fiber/tree/main/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. |
| [skip](https://github.com/gofiber/fiber/tree/main/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. |
+| [static](https://github.com/gofiber/fiber/tree/main/middleware/static) | Static middleware for Fiber that serves static files such as **images**, **CSS,** and **JavaScript**. |
| [timeout](https://github.com/gofiber/fiber/tree/main/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. |
## 🧬 External Middleware
diff --git a/app.go b/app.go
index 5147e9e4..351b3a21 100644
--- a/app.go
+++ b/app.go
@@ -381,52 +381,6 @@ type Config struct {
EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"`
}
-// Static defines configuration options when defining static assets.
-type Static struct {
- // When set to true, the server tries minimizing CPU usage by caching compressed files.
- // This works differently than the github.com/gofiber/compression middleware.
- // Optional. Default value false
- Compress bool `json:"compress"`
-
- // When set to true, enables byte range requests.
- // Optional. Default value false
- ByteRange bool `json:"byte_range"`
-
- // When set to true, enables directory browsing.
- // Optional. Default value false.
- Browse bool `json:"browse"`
-
- // When set to true, enables direct download.
- // Optional. Default value false.
- Download bool `json:"download"`
-
- // The name of the index file for serving a directory.
- // Optional. Default value "index.html".
- Index string `json:"index"`
-
- // Expiration duration for inactive file handlers.
- // Use a negative time.Duration to disable it.
- //
- // Optional. Default value 10 * time.Second.
- CacheDuration time.Duration `json:"cache_duration"`
-
- // The value for the Cache-Control HTTP-header
- // that is set on the file response. MaxAge is defined in seconds.
- //
- // Optional. Default value 0.
- MaxAge int `json:"max_age"`
-
- // ModifyResponse defines a function that allows you to alter the response.
- //
- // Optional. Default: nil
- ModifyResponse Handler
-
- // Next defines a function to skip this middleware when returned true.
- //
- // Optional. Default: nil
- Next func(c Ctx) bool
-}
-
// RouteMessage is some message need to be print when server starts
type RouteMessage struct {
name string
@@ -780,13 +734,6 @@ func (app *App) Add(methods []string, path string, handler Handler, middleware .
return app
}
-// Static will create a file server serving static files
-func (app *App) Static(prefix, root string, config ...Static) Router {
- app.registerStatic(prefix, root, config...)
-
- return app
-}
-
// All will register the handler on all HTTP methods
func (app *App) All(path string, handler Handler, middleware ...Handler) Router {
return app.Add(app.config.RequestMethods, path, handler, middleware...)
diff --git a/app_test.go b/app_test.go
index 39890ead..6b493de1 100644
--- a/app_test.go
+++ b/app_test.go
@@ -901,314 +901,6 @@ func Test_App_ShutdownWithContext(t *testing.T) {
}
}
-// go test -run Test_App_Static_Index_Default
-func Test_App_Static_Index_Default(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/prefix", "./.github/workflows")
- app.Static("", "./.github/")
- app.Static("test", "", Static{Index: "index.html"})
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "Hello, World!")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/not-found", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Equal(t, "Cannot GET /not-found", string(body))
-}
-
-// go test -run Test_App_Static_Index
-func Test_App_Static_Direct(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/", "./.github")
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "Hello, World!")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/testdata/testRoutes.json", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMEApplicationJSON, resp.Header.Get("Content-Type"))
- require.Equal(t, "", resp.Header.Get(HeaderCacheControl), "CacheControl Control")
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "test_routes")
-}
-
-// go test -run Test_App_Static_MaxAge
-func Test_App_Static_MaxAge(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/", "./.github", Static{MaxAge: 100})
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType))
- require.Equal(t, "public, max-age=100", resp.Header.Get(HeaderCacheControl), "CacheControl Control")
-}
-
-// go test -run Test_App_Static_Custom_CacheControl
-func Test_App_Static_Custom_CacheControl(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/", "./.github", Static{ModifyResponse: func(c Ctx) error {
- if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") {
- c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
- }
- return nil
- }})
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(HeaderCacheControl), "CacheControl Control")
-
- normalResp, normalErr := app.Test(httptest.NewRequest(MethodGet, "/config.yml", nil))
- require.NoError(t, normalErr, "app.Test(req)")
- require.Equal(t, "", normalResp.Header.Get(HeaderCacheControl), "CacheControl Control")
-}
-
-// go test -run Test_App_Static_Download
-func Test_App_Static_Download(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/fiber.png", "./.github/testdata/fs/img/fiber.png", Static{Download: true})
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/fiber.png", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, "image/png", resp.Header.Get(HeaderContentType))
- require.Equal(t, `attachment`, resp.Header.Get(HeaderContentDisposition))
-}
-
-// go test -run Test_App_Static_Group
-func Test_App_Static_Group(t *testing.T) {
- t.Parallel()
- app := New()
-
- grp := app.Group("/v1", func(c Ctx) error {
- c.Set("Test-Header", "123")
- return c.Next()
- })
-
- grp.Static("/v2", "./.github/index.html")
-
- req := httptest.NewRequest(MethodGet, "/v1/v2", nil)
- resp, err := app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
- require.Equal(t, "123", resp.Header.Get("Test-Header"))
-
- grp = app.Group("/v2")
- grp.Static("/v3*", "./.github/index.html")
-
- req = httptest.NewRequest(MethodGet, "/v2/v3/john/doe", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-}
-
-func Test_App_Static_Wildcard(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("*", "./.github/index.html")
-
- req := httptest.NewRequest(MethodGet, "/yesyes/john/doe", nil)
- resp, err := app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "Test file")
-}
-
-func Test_App_Static_Prefix_Wildcard(t *testing.T) {
- t.Parallel()
- app := New()
-
- app.Static("/test/*", "./.github/index.html")
-
- req := httptest.NewRequest(MethodGet, "/test/john/doe", nil)
- resp, err := app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/my/nameisjohn*", "./.github/index.html")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/my/nameisjohn/no/its/not", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "Test file")
-}
-
-func Test_App_Static_Prefix(t *testing.T) {
- t.Parallel()
- app := New()
- app.Static("/john", "./.github")
-
- req := httptest.NewRequest(MethodGet, "/john/index.html", nil)
- resp, err := app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/prefix", "./.github/testdata")
-
- req = httptest.NewRequest(MethodGet, "/prefix/index.html", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/single", "./.github/testdata/testRoutes.json")
-
- req = httptest.NewRequest(MethodGet, "/single", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMEApplicationJSON, resp.Header.Get(HeaderContentType))
-}
-
-func Test_App_Static_Trailing_Slash(t *testing.T) {
- t.Parallel()
- app := New()
- app.Static("/john", "./.github")
-
- req := httptest.NewRequest(MethodGet, "/john/", nil)
- resp, err := app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/john_without_index", "./.github/testdata/fs/css")
-
- req = httptest.NewRequest(MethodGet, "/john_without_index/", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/john/", "./.github")
-
- req = httptest.NewRequest(MethodGet, "/john/", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- req = httptest.NewRequest(MethodGet, "/john", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- app.Static("/john_without_index/", "./.github/testdata/fs/css")
-
- req = httptest.NewRequest(MethodGet, "/john_without_index/", nil)
- resp, err = app.Test(req)
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))
-}
-
-func Test_App_Static_Next(t *testing.T) {
- t.Parallel()
- app := New()
- app.Static("/", ".github", Static{
- Next: func(c Ctx) bool {
- // If value of the header is any other from "skip"
- // c.Next() will be invoked
- return c.Get("X-Custom-Header") == "skip"
- },
- })
- app.Get("/", func(c Ctx) error {
- return c.SendString("You've skipped app.Static")
- })
-
- t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) {
- t.Parallel()
- req := httptest.NewRequest(MethodGet, "/", nil)
- req.Header.Set("X-Custom-Header", "skip")
- resp, err := app.Test(req)
- require.NoError(t, err)
- require.Equal(t, 200, resp.StatusCode)
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "You've skipped app.Static")
- })
-
- t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) {
- t.Parallel()
- req := httptest.NewRequest(MethodGet, "/", nil)
- req.Header.Set("X-Custom-Header", "don't skip")
- resp, err := app.Test(req)
- require.NoError(t, err)
- require.Equal(t, 200, resp.StatusCode)
- require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
- require.Equal(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType))
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- require.Contains(t, string(body), "Hello, World!")
- })
-}
-
// go test -run Test_App_Mixed_Routes_WithSameLen
func Test_App_Mixed_Routes_WithSameLen(t *testing.T) {
t.Parallel()
@@ -1220,7 +912,10 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) {
return c.Next()
})
// routes with the same length
- app.Static("/tesbar", "./.github")
+ app.Get("/tesbar", func(c Ctx) error {
+ c.Type("html")
+ return c.Send([]byte("TEST_BAR"))
+ })
app.Get("/foobar", func(c Ctx) error {
c.Type("html")
return c.Send([]byte("FOO_BAR"))
@@ -1246,12 +941,11 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) {
require.Equal(t, 200, resp.StatusCode, "Status code")
require.NotEmpty(t, resp.Header.Get(HeaderContentLength))
require.Equal(t, "TestValue", resp.Header.Get("TestHeader"))
- require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType))
+ require.Equal(t, "text/html", resp.Header.Get(HeaderContentType))
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
- require.Contains(t, string(body), "Hello, World!")
- require.True(t, strings.HasPrefix(string(body), ""), "Response: "+string(body))
+ require.Contains(t, string(body), "TEST_BAR")
}
func Test_App_Group_Invalid(t *testing.T) {
diff --git a/constants.go b/constants.go
index c8561839..6144dc76 100644
--- a/constants.go
+++ b/constants.go
@@ -20,6 +20,7 @@ const (
MIMETextHTML = "text/html"
MIMETextPlain = "text/plain"
MIMETextJavaScript = "text/javascript"
+ MIMETextCSS = "text/css"
MIMEApplicationXML = "application/xml"
MIMEApplicationJSON = "application/json"
// Deprecated: use MIMETextJavaScript instead
@@ -32,6 +33,7 @@ const (
MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8"
MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8"
MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8"
+ MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8"
MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8"
MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8"
// Deprecated: use MIMETextJavaScriptCharsetUTF8 instead
diff --git a/docs/api/app.md b/docs/api/app.md
index b9bd9772..164b1edc 100644
--- a/docs/api/app.md
+++ b/docs/api/app.md
@@ -11,70 +11,6 @@ import Reference from '@site/src/components/reference';
import RoutingHandler from './../partials/routing/handler.md';
-### Static
-
-Use the **Static** method to serve static files such as **images**, **CSS,** and **JavaScript**.
-
-:::info
-By default, **Static** will serve `index.html` files in response to a request on a directory.
-:::
-
-```go title="Signature"
-func (app *App) Static(prefix, root string, config ...Static) Router
-```
-
-Use the following code to serve files in a directory named `./public`
-
-```go title="Examples"
-// Serve files from multiple directories
-app.Static("/", "./public")
-
-// => http://localhost:3000/hello.html
-// => http://localhost:3000/js/jquery.js
-// => http://localhost:3000/css/style.css
-
-// Serve files from "./files" directory:
-app.Static("/", "./files")
-```
-
-You can use any virtual path prefix \(_where the path does not actually exist in the file system_\) for files that are served by the **Static** method, specify a prefix path for the static directory, as shown below:
-
-```go title="Examples"
-app.Static("/static", "./public")
-
-// => http://localhost:3000/static/hello.html
-// => http://localhost:3000/static/js/jquery.js
-// => http://localhost:3000/static/css/style.css
-```
-
-#### Config
-
-If you want to have a little bit more control regarding the settings for serving static files. You could use the `fiber.Static` struct to enable specific settings.
-
-| Property | Type | Description | Default |
-|------------------------------------------------------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|
-| Compress | `bool` | When set to true, the server tries minimizing CPU usage by caching compressed files. This works differently than the [compress](../middleware/compress.md) middleware. | false |
-| ByteRange | `bool` | When set to true, enables byte range requests. | false |
-| Browse | `bool` | When set to true, enables directory browsing. | false |
-| Download | `bool` | When set to true, enables direct download. | false |
-| Index | `string` | The name of the index file for serving a directory. | "index.html" |
-| CacheDuration | `time.Duration` | Expiration duration for inactive file handlers. Use a negative `time.Duration` to disable it. | 10 * time.Second |
-| MaxAge | `int` | The value for the `Cache-Control` HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 |
-| ModifyResponse | `Handler` | ModifyResponse defines a function that allows you to alter the response. | nil |
-| Next | `func(c Ctx) bool` | Next defines a function to skip this middleware when returned true. | nil |
-
-```go title="Example"
-// Custom config
-app.Static("/", "./public", fiber.Static{
- Compress: true,
- ByteRange: true,
- Browse: true,
- Index: "john.html",
- CacheDuration: 10 * time.Second,
- MaxAge: 3600,
-})
-```
-
### Route Handlers
@@ -181,8 +117,6 @@ type Register interface {
Add(methods []string, handler Handler, middleware ...Handler) Register
- Static(root string, config ...Static) Register
-
Route(path string) Register
}
```
diff --git a/docs/api/constants.md b/docs/api/constants.md
index fce36d36..a9ee6d5a 100644
--- a/docs/api/constants.md
+++ b/docs/api/constants.md
@@ -26,24 +26,27 @@ const (
```go
const (
- MIMETextXML = "text/xml"
- MIMETextHTML = "text/html"
- MIMETextPlain = "text/plain"
- MIMEApplicationXML = "application/xml"
- MIMEApplicationJSON = "application/json"
+ MIMETextXML = "text/xml"
+ MIMETextHTML = "text/html"
+ MIMETextPlain = "text/plain"
+ MIMETextJavaScript = "text/javascript"
+ MIMETextCSS = "text/css"
+ MIMEApplicationXML = "application/xml"
+ MIMEApplicationJSON = "application/json"
MIMEApplicationJavaScript = "application/javascript"
MIMEApplicationForm = "application/x-www-form-urlencoded"
MIMEOctetStream = "application/octet-stream"
MIMEMultipartForm = "multipart/form-data"
- MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8"
- MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8"
- MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8"
- MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8"
- MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8"
+ MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8"
+ MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8"
+ MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8"
+ MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8"
+ MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8"
+ MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8"
+ MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8"
MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8"
-)
-```
+)```
### HTTP status codes were copied from net/http.
diff --git a/docs/intro.md b/docs/intro.md
index 6dbc6ca7..d482df10 100644
--- a/docs/intro.md
+++ b/docs/intro.md
@@ -164,19 +164,15 @@ app.Get("/api/*", func(c fiber.Ctx) error {
### Static files
To serve static files such as **images**, **CSS**, and **JavaScript** files, replace your function handler with a file or directory string.
-
+You can check out [static middleware](./middleware/static.md) for more information.
Function signature:
-```go
-app.Static(prefix, root string, config ...Static)
-```
-
Use the following code to serve files in a directory named `./public`:
```go
app := fiber.New()
-app.Static("/", "./public")
+app.Get("/*", static.New("./public"))
app.Listen(":3000")
```
diff --git a/docs/middleware/filesystem.md b/docs/middleware/filesystem.md
deleted file mode 100644
index 01d50270..00000000
--- a/docs/middleware/filesystem.md
+++ /dev/null
@@ -1,300 +0,0 @@
----
-id: filesystem
----
-
-# FileSystem
-
-Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables you to serve files from a directory.
-
-:::caution
-**`:params` & `:optionals?` within the prefix path are not supported!**
-
-**To handle paths with spaces (or other url encoded values) make sure to set `fiber.Config{ UnescapePath: true }`**
-:::
-
-## Signatures
-
-```go
-func New(config Config) fiber.Handler
-```
-
-## Examples
-
-Import the middleware package that is part of the Fiber web framework
-
-```go
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-)
-```
-
-After you initiate your Fiber app, you can use the following possibilities:
-
-```go
-// Provide a minimal config
-app.Use(filesystem.New(filesystem.Config{
- Root: http.Dir("./assets"),
-}))
-
-// Or extend your config for customization
-app.Use(filesystem.New(filesystem.Config{
- Root: http.Dir("./assets"),
- Browse: true,
- Index: "index.html",
- NotFoundFile: "404.html",
- MaxAge: 3600,
-}))
-```
-
-
-> If your environment (Go 1.16+) supports it, we recommend using Go Embed instead of the other solutions listed as this one is native to Go and the easiest to use.
-
-## embed
-
-[Embed](https://golang.org/pkg/embed/) is the native method to embed files in a Golang excecutable. Introduced in Go 1.16.
-
-```go
-package main
-
-import (
- "embed"
- "io/fs"
- "log"
- "net/http"
-
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-)
-
-// Embed a single file
-//go:embed index.html
-var f embed.FS
-
-// Embed a directory
-//go:embed static/*
-var embedDirStatic embed.FS
-
-func main() {
- app := fiber.New()
-
- app.Use("/", filesystem.New(filesystem.Config{
- Root: http.FS(f),
- }))
-
- // Access file "image.png" under `static/` directory via URL: `http:///static/image.png`.
- // Without `PathPrefix`, you have to access it via URL:
- // `http:///static/static/image.png`.
- app.Use("/static", filesystem.New(filesystem.Config{
- Root: http.FS(embedDirStatic),
- PathPrefix: "static",
- Browse: true,
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## pkger
-
-[https://github.com/markbates/pkger](https://github.com/markbates/pkger)
-
-```go
-package main
-
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-
- "github.com/markbates/pkger"
-)
-
-func main() {
- app := fiber.New()
-
- app.Use("/assets", filesystem.New(filesystem.Config{
- Root: pkger.Dir("/assets"),
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## packr
-
-[https://github.com/gobuffalo/packr](https://github.com/gobuffalo/packr)
-
-```go
-package main
-
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-
- "github.com/gobuffalo/packr/v2"
-)
-
-func main() {
- app := fiber.New()
-
- app.Use("/assets", filesystem.New(filesystem.Config{
- Root: packr.New("Assets Box", "/assets"),
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## go.rice
-
-[https://github.com/GeertJohan/go.rice](https://github.com/GeertJohan/go.rice)
-
-```go
-package main
-
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-
- "github.com/GeertJohan/go.rice"
-)
-
-func main() {
- app := fiber.New()
-
- app.Use("/assets", filesystem.New(filesystem.Config{
- Root: rice.MustFindBox("assets").HTTPBox(),
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## fileb0x
-
-[https://github.com/UnnoTed/fileb0x](https://github.com/UnnoTed/fileb0x)
-
-```go
-package main
-
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-
- "/myEmbeddedFiles"
-)
-
-func main() {
- app := fiber.New()
-
- app.Use("/assets", filesystem.New(filesystem.Config{
- Root: myEmbeddedFiles.HTTP,
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## statik
-
-[https://github.com/rakyll/statik](https://github.com/rakyll/statik)
-
-```go
-package main
-
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-
- // Use blank to invoke init function and register data to statik
- _ "/statik"
- "github.com/rakyll/statik/fs"
-)
-
-func main() {
- statikFS, err := fs.New()
- if err != nil {
- panic(err)
- }
-
- app := fiber.New()
-
- app.Use("/", filesystem.New(filesystem.Config{
- Root: statikFS,
- }))
-
- log.Fatal(app.Listen(":3000"))
-}
-```
-
-## Config
-
-| Property | Type | Description | Default |
-|:-------------------|:------------------------|:------------------------------------------------------------------------------------------------------------|:-------------|
-| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
-| Root | `http.FileSystem` | Root is a FileSystem that provides access to a collection of files and directories. | `nil` |
-| PathPrefix | `string` | PathPrefix defines a prefix to be added to a filepath when reading a file from the FileSystem. | "" |
-| Browse | `bool` | Enable directory browsing. | `false` |
-| Index | `string` | Index file for serving a directory. | "index.html" |
-| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 |
-| NotFoundFile | `string` | File to return if the path is not found. Useful for SPA's. | "" |
-| ContentTypeCharset | `string` | The value for the Content-Type HTTP-header that is set on the file response. | "" |
-
-## Default Config
-
-```go
-var ConfigDefault = Config{
- Next: nil,
- Root: nil,
- PathPrefix: "",
- Browse: false,
- Index: "/index.html",
- MaxAge: 0,
- ContentTypeCharset: "",
-}
-```
-
-## Utils
-
-### SendFile
-
-Serves a file from an [HTTP file system](https://pkg.go.dev/net/http#FileSystem) at the specified path.
-
-```go title="Signature" title="Signature"
-func SendFile(c fiber.Ctx, filesystem http.FileSystem, path string) error
-```
-Import the middleware package that is part of the Fiber web framework
-
-```go
-import (
- "github.com/gofiber/fiber/v3"
- "github.com/gofiber/fiber/v3/middleware/filesystem"
-)
-```
-
-```go title="Example"
-// Define a route to serve a specific file
-app.Get("/download", func(c fiber.Ctx) error {
- // Serve the file using SendFile function
- err := filesystem.SendFile(c, http.Dir("your/filesystem/root"), "path/to/your/file.txt")
- if err != nil {
- // Handle the error, e.g., return a 404 Not Found response
- return c.Status(fiber.StatusNotFound).SendString("File not found")
- }
-
- return nil
-})
-```
-
-```go title="Example"
-// Serve static files from the "build" directory using Fiber's built-in middleware.
-app.Use("/", filesystem.New(filesystem.Config{
- Root: http.FS(f), // Specify the root directory for static files.
- PathPrefix: "build", // Define the path prefix where static files are served.
-}))
-
-// For all other routes (wildcard "*"), serve the "index.html" file from the "build" directory.
-app.Use("*", func(ctx fiber.Ctx) error {
- return filesystem.SendFile(ctx, http.FS(f), "build/index.html")
-})
-```
diff --git a/docs/middleware/static.md b/docs/middleware/static.md
new file mode 100644
index 00000000..6e292b08
--- /dev/null
+++ b/docs/middleware/static.md
@@ -0,0 +1,172 @@
+---
+id: static
+---
+
+# Static
+
+Static middleware for Fiber that serves static files such as **images**, **CSS,** and **JavaScript**.
+
+:::info
+By default, **Static** will serve `index.html` files in response to a request on a directory. You can change it from [Config](#config)`
+:::
+
+## Signatures
+
+```go
+func New(root string, cfg ...Config) fiber.Handler
+```
+
+## Examples
+
+Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework
+```go
+import(
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/middleware/static"
+)
+```
+
+### Serving files from a directory
+
+```go
+app.Get("/*", static.New("./public"))
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/hello.html
+curl http://localhost:3000/css/style.css
+```
+
+
+
+### Serving files from a directory with Use
+
+```go
+app.Use("/", static.New("./public"))
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/hello.html
+curl http://localhost:3000/css/style.css
+```
+
+
+
+### Serving a file
+
+```go
+app.Use("/static", static.New("./public/hello.html"))
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/static # will show hello.html
+curl http://localhost:3000/static/john/doee # will show hello.html
+```
+
+
+
+### Serving files using os.DirFS
+
+```go
+app.Get("/files*", static.New("", static.Config{
+ FS: os.DirFS("files"),
+ Browse: true,
+}))
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/files/css/style.css
+curl http://localhost:3000/files/index.html
+```
+
+
+
+### Serving files using embed.FS
+
+```go
+//go:embed path/to/files
+var myfiles embed.FS
+
+app.Get("/files*", static.New("", static.Config{
+ FS: myfiles,
+ Browse: true,
+}))
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/files/css/style.css
+curl http://localhost:3000/files/index.html
+```
+
+
+
+### SPA (Single Page Application)
+
+```go
+app.Use("/web", static.New("", static.Config{
+ FS: os.DirFS("dist"),
+}))
+
+app.Get("/web*", func(c fiber.Ctx) error {
+ return c.SendFile("dist/index.html")
+})
+```
+
+
+Test
+
+```sh
+curl http://localhost:3000/web/css/style.css
+curl http://localhost:3000/web/index.html
+curl http://localhost:3000/web
+```
+
+
+
+:::caution
+To define static routes using `Get`, append the wildcard (`*`) operator at the end of the route.
+:::
+
+## Config
+
+| Property | Type | Description | Default |
+|:-----------|:------------------------|:---------------------------------------------------------------------------------------------------------------------------|:-----------------------|
+| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
+| FS | `fs.FS` | FS is the file system to serve the static files from.
You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc. | `nil` |
+| Compress | `bool` | When set to true, the server tries minimizing CPU usage by caching compressed files.
This works differently than the github.com/gofiber/compression middleware. | `false` |
+| ByteRange | `bool` | When set to true, enables byte range requests. | `false` |
+| Browse | `bool` | When set to true, enables directory browsing. | `false` |
+| Download | `bool` | When set to true, enables direct download. | `false` |
+| IndexNames | `[]string` | The names of the index files for serving a directory. | `[]string{"index.html"}` |
+| CacheDuration | `string` | Expiration duration for inactive file handlers.
Use a negative time.Duration to disable it. | `10 * time.Second` |
+| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | `0` |
+| ModifyResponse | `fiber.Handler` | ModifyResponse defines a function that allows you to alter the response. | `nil` |
+| NotFoundHandler | `fiber.Handler` | NotFoundHandler defines a function to handle when the path is not found. | `nil` |
+
+:::info
+You can set `CacheDuration` config property to `-1` to disable caching.
+:::
+
+## Default Config
+
+```go
+var ConfigDefault = Config{
+ Index: []string{"index.html"},
+ CacheDuration: 10 * time.Second,
+}
+```
diff --git a/docs/whats_new.md b/docs/whats_new.md
index fe193485..ff8b834d 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -47,6 +47,7 @@ DRAFT section
We have made several changes to the Fiber app, including:
* Listen -> unified with config
+* Static -> has been removed and moved to [static middleware](./middleware/static.md)
* app.Config properties moved to listen config
* DisableStartupMessage
* EnablePrefork -> previously Prefork
@@ -270,9 +271,8 @@ DRAFT section
### Filesystem
-:::caution
-DRAFT section
-:::
+We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware.
+Now, static middleware can do everything that filesystem middleware and static do. You can check out [static middleware](./middleware/static.md) or [migration guide](#-migration-guide) to see what has been changed.
### Monitor
@@ -295,6 +295,34 @@ Monitor middleware is now in Contrib package.
### 🚀 App
+#### Static
+
+Since we've removed `app.Static()`, you need to move methods to static middleware like the example below:
+
+```go
+// Before
+app.Static("/", "./public")
+app.Static("/prefix", "./public")
+app.Static("/prefix", "./public", Static{
+ Index: "index.htm",
+})
+app.Static("*", "./public/index.html")
+```
+
+```go
+// After
+app.Get("/*", static.New("./public"))
+app.Get("/prefix*", static.New("./public"))
+app.Get("/prefix*", static.New("./public", static.Config{
+ IndexNames: []string{"index.htm", "index.html"},
+}))
+app.Get("*", static.New("./public/index.html"))
+```
+
+:::caution
+You have to put `*` to the end of the route if you don't define static route with `app.Use`.
+:::
+
### 🗺 Router
### 🧠Context
@@ -328,4 +356,35 @@ app.Use(cors.New(cors.Config{
ExposeHeaders: []string{"Content-Length"},
}))
```
-...
+
+#### Filesystem
+
+You need to move filesystem middleware to static middleware due to it has been removed from the core.
+
+```go
+// Before
+app.Use(filesystem.New(filesystem.Config{
+ Root: http.Dir("./assets"),
+}))
+
+app.Use(filesystem.New(filesystem.Config{
+ Root: http.Dir("./assets"),
+ Browse: true,
+ Index: "index.html",
+ MaxAge: 3600,
+}))
+```
+
+```go
+// After
+app.Use(static.New("", static.Config{
+ FS: os.DirFS("./assets"),
+}))
+
+app.Use(static.New("", static.Config{
+ FS: os.DirFS("./assets"),
+ Browse: true,
+ IndexNames: []string{"index.html"},
+ MaxAge: 3600,
+}))
+```
\ No newline at end of file
diff --git a/group.go b/group.go
index 2b8001d5..fe2ac97a 100644
--- a/group.go
+++ b/group.go
@@ -170,16 +170,6 @@ func (grp *Group) Add(methods []string, path string, handler Handler, middleware
return grp
}
-// Static will create a file server serving static files
-func (grp *Group) Static(prefix, root string, config ...Static) Router {
- grp.app.registerStatic(getGroupPath(grp.Prefix, prefix), root, config...)
- if !grp.anyRouteDefined {
- grp.anyRouteDefined = true
- }
-
- return grp
-}
-
// All will register the handler on all HTTP methods
func (grp *Group) All(path string, handler Handler, middleware ...Handler) Router {
_ = grp.Add(grp.app.config.RequestMethods, path, handler, middleware...)
diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go
deleted file mode 100644
index 62d4f4f6..00000000
--- a/middleware/filesystem/filesystem.go
+++ /dev/null
@@ -1,323 +0,0 @@
-package filesystem
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
-
- "github.com/gofiber/fiber/v3"
-)
-
-// Config defines the config for middleware.
-type Config struct {
- // Next defines a function to skip this middleware when returned true.
- //
- // Optional. Default: nil
- Next func(c fiber.Ctx) bool
-
- // Root is a FileSystem that provides access
- // to a collection of files and directories.
- //
- // Required. Default: nil
- Root fs.FS `json:"-"`
-
- // PathPrefix defines a prefix to be added to a filepath when
- // reading a file from the FileSystem.
- //
- // Optional. Default "."
- PathPrefix string `json:"path_prefix"`
-
- // Enable directory browsing.
- //
- // Optional. Default: false
- Browse bool `json:"browse"`
-
- // Index file for serving a directory.
- //
- // Optional. Default: "index.html"
- Index string `json:"index"`
-
- // When set to true, enables direct download for files.
- //
- // Optional. Default: false.
- Download bool `json:"download"`
-
- // The value for the Cache-Control HTTP-header
- // that is set on the file response. MaxAge is defined in seconds.
- //
- // Optional. Default value 0.
- MaxAge int `json:"max_age"`
-
- // File to return if path is not found. Useful for SPA's.
- //
- // Optional. Default: ""
- NotFoundFile string `json:"not_found_file"`
-
- // The value for the Content-Type HTTP-header
- // that is set on the file response
- //
- // Optional. Default: ""
- ContentTypeCharset string `json:"content_type_charset"`
-}
-
-// ConfigDefault is the default config
-var ConfigDefault = Config{
- Next: nil,
- Root: nil,
- PathPrefix: ".",
- Browse: false,
- Index: "/index.html",
- MaxAge: 0,
- ContentTypeCharset: "",
-}
-
-// New creates a new middleware handler.
-//
-// filesystem does not handle url encoded values (for example spaces)
-// on it's own. If you need that functionality, set "UnescapePath"
-// in fiber.Config
-func New(config ...Config) fiber.Handler {
- // Set default config
- cfg := ConfigDefault
-
- // Override config if provided
- if len(config) > 0 {
- cfg = config[0]
-
- // Set default values
- if cfg.Index == "" {
- cfg.Index = ConfigDefault.Index
- }
- if cfg.PathPrefix == "" {
- cfg.PathPrefix = ConfigDefault.PathPrefix
- }
- if !strings.HasPrefix(cfg.Index, "/") {
- cfg.Index = "/" + cfg.Index
- }
- if cfg.NotFoundFile != "" && !strings.HasPrefix(cfg.NotFoundFile, "/") {
- cfg.NotFoundFile = "/" + cfg.NotFoundFile
- }
- }
-
- if cfg.Root == nil {
- panic("filesystem: Root cannot be nil")
- }
-
- // PathPrefix configurations for io/fs compatibility.
- if cfg.PathPrefix != "." && !strings.HasPrefix(cfg.PathPrefix, "/") {
- cfg.PathPrefix = "./" + cfg.PathPrefix
- }
-
- if cfg.NotFoundFile != "" {
- cfg.NotFoundFile = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+cfg.NotFoundFile))
- }
-
- var once sync.Once
- var prefix string
- cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge)
-
- // Return new handler
- return func(c fiber.Ctx) error {
- // Don't execute middleware if Next returns true
- if cfg.Next != nil && cfg.Next(c) {
- return c.Next()
- }
-
- method := c.Method()
-
- // We only serve static assets on GET or HEAD methods
- if method != fiber.MethodGet && method != fiber.MethodHead {
- return c.Next()
- }
-
- // Set prefix once
- once.Do(func() {
- prefix = c.Route().Path
- })
-
- // Strip prefix
- path := strings.TrimPrefix(c.Path(), prefix)
- if !strings.HasPrefix(path, "/") {
- path = "/" + path
- }
-
- var (
- file fs.File
- stat os.FileInfo
- )
-
- // Add PathPrefix
- if cfg.PathPrefix != "" {
- // PathPrefix already has a "/" prefix
- path = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+path))
- }
-
- if len(path) > 1 {
- path = strings.TrimRight(path, "/")
- }
-
- file, err := openFile(cfg.Root, path)
-
- if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" {
- file, err = openFile(cfg.Root, cfg.NotFoundFile)
- }
-
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- return c.Status(fiber.StatusNotFound).Next()
- }
- return fmt.Errorf("failed to open: %w", err)
- }
-
- stat, err = file.Stat()
- if err != nil {
- return fmt.Errorf("failed to stat: %w", err)
- }
-
- // Serve index if path is directory
- if stat.IsDir() {
- indexPath := strings.TrimRight(path, "/") + cfg.Index
- indexPath = filepath.Join(cfg.PathPrefix, filepath.Clean("/"+indexPath))
-
- index, err := openFile(cfg.Root, indexPath)
- if err == nil {
- indexStat, err := index.Stat()
- if err == nil {
- file = index
- stat = indexStat
- }
- }
- }
-
- // Browse directory if no index found and browsing is enabled
- if stat.IsDir() {
- if cfg.Browse {
- return dirList(c, file)
- }
-
- return fiber.ErrForbidden
- }
-
- c.Status(fiber.StatusOK)
-
- modTime := stat.ModTime()
- contentLength := int(stat.Size())
-
- // Set Content Type header
- if cfg.ContentTypeCharset == "" {
- c.Type(getFileExtension(stat.Name()))
- } else {
- c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset)
- }
-
- // Set Last Modified header
- if !modTime.IsZero() {
- c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
- }
-
- // Sets the response Content-Disposition header to attachment if the Download option is true and if it's a file
- if cfg.Download && !stat.IsDir() {
- c.Attachment()
- }
-
- if method == fiber.MethodGet {
- if cfg.MaxAge > 0 {
- c.Set(fiber.HeaderCacheControl, cacheControlStr)
- }
- c.Response().SetBodyStream(file, contentLength)
- return nil
- }
- if method == fiber.MethodHead {
- c.Request().ResetBody()
- // Fasthttp should skipbody by default if HEAD?
- c.Response().SkipBody = true
- c.Response().Header.SetContentLength(contentLength)
- if err := file.Close(); err != nil {
- return fmt.Errorf("failed to close: %w", err)
- }
- return nil
- }
-
- return c.Next()
- }
-}
-
-// SendFile serves a file from an fs.FS filesystem at the specified path.
-// It handles content serving, sets appropriate headers, and returns errors when needed.
-// Usage: err := SendFile(ctx, fs, "/path/to/file.txt")
-func SendFile(c fiber.Ctx, filesystem fs.FS, path string) error {
- var (
- file fs.File
- stat os.FileInfo
- )
-
- path = filepath.Join(".", filepath.Clean("/"+path))
-
- file, err := openFile(filesystem, path)
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- return fiber.ErrNotFound
- }
- return fmt.Errorf("failed to open: %w", err)
- }
-
- stat, err = file.Stat()
- if err != nil {
- return fmt.Errorf("failed to stat: %w", err)
- }
-
- // Serve index if path is directory
- if stat.IsDir() {
- indexPath := strings.TrimRight(path, "/") + ConfigDefault.Index
- index, err := openFile(filesystem, indexPath)
- if err == nil {
- indexStat, err := index.Stat()
- if err == nil {
- file = index
- stat = indexStat
- }
- }
- }
-
- // Return forbidden if no index found
- if stat.IsDir() {
- return fiber.ErrForbidden
- }
-
- c.Status(fiber.StatusOK)
-
- modTime := stat.ModTime()
- contentLength := int(stat.Size())
-
- // Set Content Type header
- c.Type(getFileExtension(stat.Name()))
-
- // Set Last Modified header
- if !modTime.IsZero() {
- c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
- }
-
- method := c.Method()
- if method == fiber.MethodGet {
- c.Response().SetBodyStream(file, contentLength)
- return nil
- }
- if method == fiber.MethodHead {
- c.Request().ResetBody()
- // Fasthttp should skipbody by default if HEAD?
- c.Response().SkipBody = true
- c.Response().Header.SetContentLength(contentLength)
- if err := file.Close(); err != nil {
- return fmt.Errorf("failed to close: %w", err)
- }
- return nil
- }
-
- return nil
-}
diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go
deleted file mode 100644
index feccdd12..00000000
--- a/middleware/filesystem/filesystem_test.go
+++ /dev/null
@@ -1,252 +0,0 @@
-package filesystem
-
-import (
- "context"
- "net/http"
- "net/http/httptest"
- "os"
- "testing"
-
- "github.com/gofiber/fiber/v3"
- "github.com/stretchr/testify/require"
-)
-
-// go test -run Test_FileSystem
-func Test_FileSystem(t *testing.T) {
- t.Parallel()
- app := fiber.New()
-
- app.Use("/test", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- }))
-
- app.Use("/dir", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- Browse: true,
- }))
-
- app.Get("/", func(c fiber.Ctx) error {
- return c.SendString("Hello, World!")
- })
-
- app.Use("/spatest", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- Index: "index.html",
- NotFoundFile: "index.html",
- }))
-
- app.Use("/prefix", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- PathPrefix: "img",
- }))
-
- tests := []struct {
- name string
- url string
- statusCode int
- contentType string
- modifiedTime string
- }{
- {
- name: "Should be returns status 200 with suitable content-type",
- url: "/test/index.html",
- statusCode: 200,
- contentType: "text/html",
- },
- {
- name: "Should be returns status 200 with suitable content-type",
- url: "/test",
- statusCode: 200,
- contentType: "text/html",
- },
- {
- name: "Should be returns status 200 with suitable content-type",
- url: "/test/css/style.css",
- statusCode: 200,
- contentType: "text/css",
- },
- {
- name: "Should be returns status 404",
- url: "/test/nofile.js",
- statusCode: 404,
- },
- {
- name: "Should be returns status 404",
- url: "/test/nofile",
- statusCode: 404,
- },
- {
- name: "Should be returns status 200",
- url: "/",
- statusCode: 200,
- contentType: "text/plain; charset=utf-8",
- },
- {
- name: "Should be returns status 403",
- url: "/test/img",
- statusCode: 403,
- },
- {
- name: "Should list the directory contents",
- url: "/dir/img",
- statusCode: 200,
- contentType: "text/html",
- },
- {
- name: "Should list the directory contents",
- url: "/dir/img/",
- statusCode: 200,
- contentType: "text/html",
- },
- {
- name: "Should be returns status 200",
- url: "/dir/img/fiber.png",
- statusCode: 200,
- contentType: "image/png",
- },
- {
- name: "Should be return status 200",
- url: "/spatest/doesnotexist",
- statusCode: 200,
- contentType: "text/html",
- },
- {
- name: "PathPrefix should be applied",
- url: "/prefix/fiber.png",
- statusCode: 200,
- contentType: "image/png",
- },
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tt.url, nil))
- require.NoError(t, err)
- require.Equal(t, tt.statusCode, resp.StatusCode)
-
- if tt.contentType != "" {
- ct := resp.Header.Get("Content-Type")
- require.Equal(t, tt.contentType, ct)
- }
- })
- }
-}
-
-// go test -run Test_FileSystem_Next
-func Test_FileSystem_Next(t *testing.T) {
- t.Parallel()
- app := fiber.New()
- app.Use(New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- Next: func(_ fiber.Ctx) bool {
- return true
- },
- }))
-
- resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
- require.NoError(t, err)
- require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
-}
-
-// go test -run Test_FileSystem_Download
-func Test_FileSystem_Download(t *testing.T) {
- app := fiber.New()
- app.Use(New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- Download: true,
- }))
-
- resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/img/fiber.png", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
- require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
- require.Equal(t, "image/png", resp.Header.Get(fiber.HeaderContentType))
- require.Equal(t, "attachment", resp.Header.Get(fiber.HeaderContentDisposition))
-}
-
-func Test_FileSystem_NonGetAndHead(t *testing.T) {
- t.Parallel()
- app := fiber.New()
-
- app.Use("/test", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- }))
-
- resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil))
- require.NoError(t, err)
- require.Equal(t, 404, resp.StatusCode)
-}
-
-func Test_FileSystem_Head(t *testing.T) {
- t.Parallel()
- app := fiber.New()
-
- app.Use("/test", New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- }))
-
- req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/test", nil)
- require.NoError(t, err)
- resp, err := app.Test(req)
- require.NoError(t, err)
- require.Equal(t, 200, resp.StatusCode)
-}
-
-func Test_FileSystem_NoRoot(t *testing.T) {
- t.Parallel()
- defer func() {
- require.Equal(t, "filesystem: Root cannot be nil", recover())
- }()
-
- app := fiber.New()
- app.Use(New())
- _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
- require.NoError(t, err)
-}
-
-func Test_FileSystem_UsingParam(t *testing.T) {
- t.Parallel()
- app := fiber.New()
-
- app.Use("/:path", func(c fiber.Ctx) error {
- return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html")
- })
-
- req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/index", nil)
- require.NoError(t, err)
- resp, err := app.Test(req)
- require.NoError(t, err)
- require.Equal(t, 200, resp.StatusCode)
-}
-
-func Test_FileSystem_UsingParam_NonFile(t *testing.T) {
- t.Parallel()
- app := fiber.New()
-
- app.Use("/:path", func(c fiber.Ctx) error {
- return SendFile(c, os.DirFS("../../.github/testdata/fs"), c.Params("path")+".html")
- })
-
- req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/template", nil)
- require.NoError(t, err)
- resp, err := app.Test(req)
- require.NoError(t, err)
- require.Equal(t, 404, resp.StatusCode)
-}
-
-func Test_FileSystem_UsingContentTypeCharset(t *testing.T) {
- t.Parallel()
- app := fiber.New()
- app.Use(New(Config{
- Root: os.DirFS("../../.github/testdata/fs"),
- Index: "index.html",
- ContentTypeCharset: "UTF-8",
- }))
-
- resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
- require.NoError(t, err)
- require.Equal(t, 200, resp.StatusCode)
- require.Equal(t, "text/html; charset=UTF-8", resp.Header.Get("Content-Type"))
-}
diff --git a/middleware/filesystem/utils.go b/middleware/filesystem/utils.go
deleted file mode 100644
index 3c11acf1..00000000
--- a/middleware/filesystem/utils.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package filesystem
-
-import (
- "errors"
- "fmt"
- "html"
- "io/fs"
- "path"
- "path/filepath"
- "sort"
- "strings"
-
- "github.com/gofiber/fiber/v3"
-)
-
-// ErrDirListingNotSupported is returned from the filesystem middleware handler if
-// the given fs.FS does not support directory listing. This is uncommon and may
-// indicate an issue with the FS implementation.
-var ErrDirListingNotSupported = errors.New("failed to type-assert to fs.ReadDirFile")
-
-func getFileExtension(p string) string {
- n := strings.LastIndexByte(p, '.')
- if n < 0 {
- return ""
- }
- return p[n:]
-}
-
-func dirList(c fiber.Ctx, f fs.File) error {
- ff, ok := f.(fs.ReadDirFile)
- if !ok {
- return ErrDirListingNotSupported
- }
- fileinfos, err := ff.ReadDir(-1)
- if err != nil {
- return fmt.Errorf("failed to read dir: %w", err)
- }
-
- fm := make(map[string]fs.FileInfo, len(fileinfos))
- filenames := make([]string, 0, len(fileinfos))
- for _, fi := range fileinfos {
- name := fi.Name()
- info, err := fi.Info()
- if err != nil {
- return fmt.Errorf("failed to get file info: %w", err)
- }
-
- fm[name] = info
- filenames = append(filenames, name)
- }
-
- basePathEscaped := html.EscapeString(c.Path())
- _, _ = fmt.Fprintf(c, "%s", basePathEscaped)
- _, _ = fmt.Fprintf(c, "%s
", basePathEscaped)
- _, _ = fmt.Fprint(c, "")
-
- if len(basePathEscaped) > 1 {
- parentPathEscaped := html.EscapeString(strings.TrimRight(c.Path(), "/") + "/..")
- _, _ = fmt.Fprintf(c, `- ..
`, parentPathEscaped)
- }
-
- sort.Strings(filenames)
- for _, name := range filenames {
- pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name))
- fi := fm[name]
- auxStr := "dir"
- className := "dir"
- if !fi.IsDir() {
- auxStr = fmt.Sprintf("file, %d bytes", fi.Size())
- className = "file"
- }
- _, _ = fmt.Fprintf(c, `- %s, %s, last modified %s
`,
- pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime())
- }
- _, _ = fmt.Fprint(c, "
")
-
- c.Type("html")
-
- return nil
-}
-
-func openFile(filesystem fs.FS, name string) (fs.File, error) {
- name = filepath.ToSlash(name)
-
- file, err := filesystem.Open(name)
- if err != nil {
- return nil, fmt.Errorf("failed to open file: %w", err)
- }
-
- return file, nil
-}
diff --git a/middleware/static/config.go b/middleware/static/config.go
new file mode 100644
index 00000000..cc7a9357
--- /dev/null
+++ b/middleware/static/config.go
@@ -0,0 +1,98 @@
+package static
+
+import (
+ "io/fs"
+ "time"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// Config defines the config for middleware.
+type Config struct {
+ // Next defines a function to skip this middleware when returned true.
+ //
+ // Optional. Default: nil
+ Next func(c fiber.Ctx) bool
+
+ // FS is the file system to serve the static files from.
+ // You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.
+ //
+ // Optional. Default: nil
+ FS fs.FS
+
+ // When set to true, the server tries minimizing CPU usage by caching compressed files.
+ // This works differently than the github.com/gofiber/compression middleware.
+ //
+ // Optional. Default: false
+ Compress bool `json:"compress"`
+
+ // When set to true, enables byte range requests.
+ //
+ // Optional. Default: false
+ ByteRange bool `json:"byte_range"`
+
+ // When set to true, enables directory browsing.
+ //
+ // Optional. Default: false.
+ Browse bool `json:"browse"`
+
+ // When set to true, enables direct download.
+ //
+ // Optional. Default: false.
+ Download bool `json:"download"`
+
+ // The names of the index files for serving a directory.
+ //
+ // Optional. Default: []string{"index.html"}.
+ IndexNames []string `json:"index"`
+
+ // Expiration duration for inactive file handlers.
+ // Use a negative time.Duration to disable it.
+ //
+ // Optional. Default: 10 * time.Second.
+ CacheDuration time.Duration `json:"cache_duration"`
+
+ // The value for the Cache-Control HTTP-header
+ // that is set on the file response. MaxAge is defined in seconds.
+ //
+ // Optional. Default: 0.
+ MaxAge int `json:"max_age"`
+
+ // ModifyResponse defines a function that allows you to alter the response.
+ //
+ // Optional. Default: nil
+ ModifyResponse fiber.Handler
+
+ // NotFoundHandler defines a function to handle when the path is not found.
+ //
+ // Optional. Default: nil
+ NotFoundHandler fiber.Handler
+}
+
+// ConfigDefault is the default config
+var ConfigDefault = Config{
+ IndexNames: []string{"index.html"},
+ CacheDuration: 10 * time.Second,
+}
+
+// Helper function to set default values
+func configDefault(config ...Config) Config {
+ // Return default config if nothing provided
+ if len(config) < 1 {
+ return ConfigDefault
+ }
+
+ // Override default config
+ cfg := config[0]
+
+ // Set default values
+ if cfg.IndexNames == nil || len(cfg.IndexNames) == 0 {
+ cfg.IndexNames = ConfigDefault.IndexNames
+ }
+
+ if cfg.CacheDuration == 0 {
+ cfg.CacheDuration = ConfigDefault.CacheDuration
+ }
+
+ return cfg
+}
diff --git a/middleware/static/static.go b/middleware/static/static.go
new file mode 100644
index 00000000..1bb4e8cc
--- /dev/null
+++ b/middleware/static/static.go
@@ -0,0 +1,175 @@
+package static
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/gofiber/fiber/v3"
+ "github.com/gofiber/utils/v2"
+ "github.com/valyala/fasthttp"
+)
+
+// New creates a new middleware handler.
+// The root argument specifies the root directory from which to serve static assets.
+//
+// Note: Root has to be string or fs.FS, otherwise it will panic.
+func New(root string, cfg ...Config) fiber.Handler {
+ config := configDefault(cfg...)
+
+ var createFS sync.Once
+ var fileHandler fasthttp.RequestHandler
+ var cacheControlValue string
+
+ // adjustments for io/fs compatibility
+ if config.FS != nil && root == "" {
+ root = "."
+ }
+
+ return func(c fiber.Ctx) error {
+ // Don't execute middleware if Next returns true
+ if config.Next != nil && config.Next(c) {
+ return c.Next()
+ }
+
+ // We only serve static assets on GET or HEAD methods
+ method := c.Method()
+ if method != fiber.MethodGet && method != fiber.MethodHead {
+ return c.Next()
+ }
+
+ // Initialize FS
+ createFS.Do(func() {
+ prefix := c.Route().Path
+
+ // Is prefix a partial wildcard?
+ if strings.Contains(prefix, "*") {
+ // /john* -> /john
+ prefix = strings.Split(prefix, "*")[0]
+ }
+
+ prefixLen := len(prefix)
+ if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
+ // /john/ -> /john
+ prefixLen--
+ }
+
+ fs := &fasthttp.FS{
+ Root: root,
+ FS: config.FS,
+ AllowEmptyRoot: true,
+ GenerateIndexPages: config.Browse,
+ AcceptByteRange: config.ByteRange,
+ Compress: config.Compress,
+ CompressedFileSuffix: c.App().Config().CompressedFileSuffix,
+ CacheDuration: config.CacheDuration,
+ SkipCache: config.CacheDuration < 0,
+ IndexNames: config.IndexNames,
+ PathNotFound: func(fctx *fasthttp.RequestCtx) {
+ fctx.Response.SetStatusCode(fiber.StatusNotFound)
+ },
+ }
+
+ fs.PathRewrite = func(fctx *fasthttp.RequestCtx) []byte {
+ path := fctx.Path()
+
+ if len(path) >= prefixLen {
+ checkFile, err := isFile(root, fs.FS)
+ if err != nil {
+ return path
+ }
+
+ // If the root is a file, we need to reset the path to "/" always.
+ switch {
+ case checkFile && fs.FS == nil:
+ path = []byte("/")
+ case checkFile && fs.FS != nil:
+ path = utils.UnsafeBytes(root)
+ default:
+ 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
+ }
+
+ maxAge := config.MaxAge
+ if maxAge > 0 {
+ cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
+ }
+
+ fileHandler = fs.NewRequestHandler()
+ })
+
+ // Serve file
+ fileHandler(c.Context())
+
+ // Sets the response Content-Disposition header to attachment if the Download option is true
+ if config.Download {
+ c.Attachment()
+ }
+
+ // Return request if found and not forbidden
+ status := c.Context().Response.StatusCode()
+ if status != fiber.StatusNotFound && status != fiber.StatusForbidden {
+ if len(cacheControlValue) > 0 {
+ c.Context().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue)
+ }
+
+ if config.ModifyResponse != nil {
+ return config.ModifyResponse(c)
+ }
+
+ return nil
+ }
+
+ // Return custom 404 handler if provided.
+ if config.NotFoundHandler != nil {
+ return config.NotFoundHandler(c)
+ }
+
+ // Reset response to default
+ c.Context().SetContentType("") // Issue #420
+ c.Context().Response.SetStatusCode(fiber.StatusOK)
+ c.Context().Response.SetBodyString("")
+
+ // Next middleware
+ return c.Next()
+ }
+}
+
+// isFile checks if the root is a file.
+func isFile(root string, filesystem fs.FS) (bool, error) {
+ var file fs.File
+ var err error
+
+ if filesystem != nil {
+ file, err = filesystem.Open(root)
+ if err != nil {
+ return false, fmt.Errorf("static: %w", err)
+ }
+ } else {
+ file, err = os.Open(filepath.Clean(root))
+ if err != nil {
+ return false, fmt.Errorf("static: %w", err)
+ }
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ return false, fmt.Errorf("static: %w", err)
+ }
+
+ return stat.Mode().IsRegular(), nil
+}
diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go
new file mode 100644
index 00000000..bc0585a2
--- /dev/null
+++ b/middleware/static/static_test.go
@@ -0,0 +1,721 @@
+package static
+
+import (
+ "embed"
+ "io"
+ "io/fs"
+ "net/http/httptest"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/gofiber/fiber/v3"
+ "github.com/stretchr/testify/require"
+)
+
+// go test -run Test_Static_Index_Default
+func Test_Static_Index_Default(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/prefix", New("../../.github/workflows"))
+
+ app.Get("", New("../../.github/"))
+
+ app.Get("test", New("", Config{
+ IndexNames: []string{"index.html"},
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Hello, World!")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Equal(t, "Cannot GET /not-found", string(body))
+}
+
+// go test -run Test_Static_Index
+func Test_Static_Direct(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/*", New("../../.github"))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Hello, World!")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/index.html", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 405, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/testdata/testRoutes.json", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get("Content-Type"))
+ require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "test_routes")
+}
+
+// go test -run Test_Static_MaxAge
+func Test_Static_MaxAge(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/*", New("../../.github", Config{
+ MaxAge: 100,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(fiber.HeaderContentType))
+ require.Equal(t, "public, max-age=100", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+}
+
+// go test -run Test_Static_Custom_CacheControl
+func Test_Static_Custom_CacheControl(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/*", New("../../.github", Config{
+ ModifyResponse: func(c fiber.Ctx) error {
+ if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") {
+ c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ }
+ return nil
+ },
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+
+ normalResp, normalErr := app.Test(httptest.NewRequest(fiber.MethodGet, "/config.yml", nil))
+ require.NoError(t, normalErr, "app.Test(req)")
+ require.Equal(t, "", normalResp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+}
+
+func Test_Static_Disable_Cache(t *testing.T) {
+ // Skip on Windows. It's not possible to delete a file that is in use.
+ if runtime.GOOS == "windows" {
+ t.SkipNow()
+ }
+
+ t.Parallel()
+
+ app := fiber.New()
+
+ file, err := os.Create("../../.github/test.txt")
+ require.NoError(t, err)
+ _, err = file.WriteString("Hello, World!")
+ require.NoError(t, err)
+ require.NoError(t, file.Close())
+
+ // Remove the file even if the test fails
+ defer func() {
+ _ = os.Remove("../../.github/test.txt") //nolint:errcheck // not needed
+ }()
+
+ app.Get("/*", New("../../.github/", Config{
+ CacheDuration: -1,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Hello, World!")
+
+ require.NoError(t, os.Remove("../../.github/test.txt"))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, "", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Equal(t, "Cannot GET /test.txt", string(body))
+}
+
+func Test_Static_NotFoundHandler(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/*", New("../../.github", Config{
+ NotFoundHandler: func(c fiber.Ctx) error {
+ return c.SendString("Custom 404")
+ },
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Equal(t, "Custom 404", string(body))
+}
+
+// go test -run Test_Static_Download
+func Test_Static_Download(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/fiber.png", New("../../.github/testdata/fs/img/fiber.png", Config{
+ Download: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fiber.png", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, "image/png", resp.Header.Get(fiber.HeaderContentType))
+ require.Equal(t, `attachment`, resp.Header.Get(fiber.HeaderContentDisposition))
+}
+
+// go test -run Test_Static_Group
+func Test_Static_Group(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ grp := app.Group("/v1", func(c fiber.Ctx) error {
+ c.Set("Test-Header", "123")
+ return c.Next()
+ })
+
+ grp.Get("/v2*", New("../../.github/index.html"))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/v1/v2", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+ require.Equal(t, "123", resp.Header.Get("Test-Header"))
+
+ grp = app.Group("/v2")
+ grp.Get("/v3*", New("../../.github/index.html"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/v2/v3/john/doe", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+}
+
+func Test_Static_Wildcard(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("*", New("../../.github/index.html"))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/yesyes/john/doe", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Test file")
+}
+
+func Test_Static_Prefix_Wildcard(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/test*", New("../../.github/index.html"))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Get("/my/nameisjohn*", New("../../.github/index.html"))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/my/nameisjohn/no/its/not", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Test file")
+}
+
+func Test_Static_Prefix(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+ app.Get("/john*", New("../../.github"))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/john/index.html", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Get("/prefix*", New("../../.github/testdata"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/prefix/index.html", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Get("/single*", New("../../.github/testdata/testRoutes.json"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/single", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType))
+}
+
+func Test_Static_Trailing_Slash(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+ app.Get("/john*", New("../../.github"))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/john/", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Get("/john_without_index*", New("../../.github/testdata/fs/css"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Use("/john", New("../../.github"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/john/", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/john", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ app.Use("/john_without_index/", New("../../.github/testdata/fs/css"))
+
+ req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", nil)
+ resp, err = app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+}
+
+func Test_Static_Next(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/*", New("../../.github", Config{
+ Next: func(c fiber.Ctx) bool {
+ return c.Get("X-Custom-Header") == "skip"
+ },
+ }))
+
+ app.Get("/*", func(c fiber.Ctx) error {
+ return c.SendString("You've skipped app.Static")
+ })
+
+ t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) {
+ t.Parallel()
+ req := httptest.NewRequest(fiber.MethodGet, "/", nil)
+ req.Header.Set("X-Custom-Header", "skip")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode)
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "You've skipped app.Static")
+ })
+
+ t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) {
+ t.Parallel()
+ req := httptest.NewRequest(fiber.MethodGet, "/", nil)
+ req.Header.Set("X-Custom-Header", "don't skip")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode)
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Hello, World!")
+ })
+}
+
+func Test_Route_Static_Root(t *testing.T) {
+ t.Parallel()
+
+ dir := "../../.github/testdata/fs/css"
+ app := fiber.New()
+ app.Get("/*", New(dir, Config{
+ Browse: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+
+ app = fiber.New()
+ app.Get("/*", New(dir))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+}
+
+func Test_Route_Static_HasPrefix(t *testing.T) {
+ t.Parallel()
+
+ dir := "../../.github/testdata/fs/css"
+ app := fiber.New()
+ app.Get("/static*", New(dir, Config{
+ Browse: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+
+ app = fiber.New()
+ app.Get("/static/*", New(dir, Config{
+ Browse: true,
+ }))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+
+ app = fiber.New()
+ app.Get("/static*", New(dir))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+
+ app = fiber.New()
+ app.Get("/static*", New(dir))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 404, resp.StatusCode, "Status code")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+}
+
+func Test_Static_FS(t *testing.T) {
+ t.Parallel()
+
+ app := fiber.New()
+ app.Get("/*", New("", Config{
+ FS: os.DirFS("../../.github/testdata/fs"),
+ Browse: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+}
+
+/*func Test_Static_FS_DifferentRoot(t *testing.T) {
+ t.Parallel()
+
+ app := fiber.New()
+ app.Get("/*", New("fs", Config{
+ FS: os.DirFS("../../.github/testdata"),
+ IndexNames: []string{"index2.html"},
+ Browse: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "Hello, World!
")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+}*/
+
+//go:embed static.go config.go
+var fsTestFilesystem embed.FS
+
+func Test_Static_FS_Browse(t *testing.T) {
+ t.Parallel()
+
+ app := fiber.New()
+
+ app.Get("/embed*", New("", Config{
+ FS: fsTestFilesystem,
+ Browse: true,
+ }))
+
+ app.Get("/dirfs*", New("", Config{
+ FS: os.DirFS("../../.github/testdata/fs/css"),
+ Browse: true,
+ }))
+
+ resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "style.css")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs/style.css", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "color")
+
+ resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/embed", nil))
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err = io.ReadAll(resp.Body)
+ require.NoError(t, err, "app.Test(req)")
+ require.Contains(t, string(body), "static.go")
+}
+
+func Test_Static_FS_Prefix_Wildcard(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Get("/test*", New("index.html", Config{
+ FS: os.DirFS("../../.github"),
+ IndexNames: []string{"not_index.html"},
+ }))
+
+ req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", nil)
+ resp, err := app.Test(req)
+ require.NoError(t, err, "app.Test(req)")
+ require.Equal(t, 200, resp.StatusCode, "Status code")
+ require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
+ require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
+
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ require.Contains(t, string(body), "Test file")
+}
+
+func Test_isFile(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ path string
+ filesystem fs.FS
+ expected bool
+ gotError error
+ }{
+ {
+ name: "file",
+ path: "index.html",
+ filesystem: os.DirFS("../../.github"),
+ expected: true,
+ },
+ {
+ name: "file",
+ path: "index2.html",
+ filesystem: os.DirFS("../../.github"),
+ expected: false,
+ gotError: fs.ErrNotExist,
+ },
+ {
+ name: "directory",
+ path: ".",
+ filesystem: os.DirFS("../../.github"),
+ expected: false,
+ },
+ {
+ name: "directory",
+ path: "not_exists",
+ filesystem: os.DirFS("../../.github"),
+ expected: false,
+ gotError: fs.ErrNotExist,
+ },
+ {
+ name: "directory",
+ path: ".",
+ filesystem: os.DirFS("../../.github/testdata/fs/css"),
+ expected: false,
+ },
+ {
+ name: "file",
+ path: "../../.github/testdata/fs/css/style.css",
+ filesystem: nil,
+ expected: true,
+ },
+ {
+ name: "file",
+ path: "../../.github/testdata/fs/css/style2.css",
+ filesystem: nil,
+ expected: false,
+ gotError: fs.ErrNotExist,
+ },
+ {
+ name: "directory",
+ path: "../../.github/testdata/fs/css",
+ filesystem: nil,
+ expected: false,
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ c := c
+ t.Parallel()
+
+ actual, err := isFile(c.path, c.filesystem)
+ require.ErrorIs(t, err, c.gotError)
+ require.Equal(t, c.expected, actual)
+ })
+ }
+}
diff --git a/register.go b/register.go
index 60bc19ba..ab67447c 100644
--- a/register.go
+++ b/register.go
@@ -19,8 +19,6 @@ type Register interface {
Add(methods []string, handler Handler, middleware ...Handler) Register
- Static(root string, config ...Static) Register
-
Route(path string) Register
}
@@ -112,12 +110,6 @@ func (r *Registering) Add(methods []string, handler Handler, middleware ...Handl
return r
}
-// Static will create a file server serving static files
-func (r *Registering) Static(root string, config ...Static) Register {
- r.app.registerStatic(r.path, root, config...)
- return r
-}
-
// Route returns a new Register instance whose route path takes
// the path in the current instance as its prefix.
func (r *Registering) Route(path string) Register {
diff --git a/router.go b/router.go
index f65b4ece..26a2483f 100644
--- a/router.go
+++ b/router.go
@@ -9,10 +9,8 @@ import (
"fmt"
"html"
"sort"
- "strconv"
"strings"
"sync/atomic"
- "time"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
@@ -33,7 +31,6 @@ type Router interface {
Patch(path string, handler Handler, middleware ...Handler) Router
Add(methods []string, path string, handler Handler, middleware ...Handler) Router
- Static(prefix, root string, config ...Static) Router
All(path string, handler Handler, middleware ...Handler) Router
Group(prefix string, handlers ...Handler) Router
@@ -377,144 +374,6 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
}
}
-func (app *App) registerStatic(prefix, root string, config ...Static) {
- // For security, we want to restrict to the current work directory.
- if root == "" {
- root = "."
- }
- // Cannot have an empty prefix
- if prefix == "" {
- prefix = "/"
- }
- // Prefix always start with a '/' or '*'
- if prefix[0] != '/' {
- prefix = "/" + prefix
- }
- // in case-sensitive routing, all to lowercase
- if !app.config.CaseSensitive {
- prefix = utils.ToLower(prefix)
- }
- // Strip trailing slashes from the root path
- if len(root) > 0 && root[len(root)-1] == '/' {
- root = root[:len(root)-1]
- }
- // Is prefix a direct wildcard?
- isStar := prefix == "/*"
- // Is prefix a root slash?
- isRoot := prefix == "/"
- // Is prefix a partial wildcard?
- if strings.Contains(prefix, "*") {
- // /john* -> /john
- isStar = true
- prefix = strings.Split(prefix, "*")[0]
- // Fix this later
- }
- prefixLen := len(prefix)
- if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
- // /john/ -> /john
- prefixLen--
- prefix = prefix[:prefixLen]
- }
- const cacheDuration = 10 * time.Second
- // Fileserver settings
- fs := &fasthttp.FS{
- Root: root,
- AllowEmptyRoot: true,
- GenerateIndexPages: false,
- AcceptByteRange: false,
- Compress: false,
- CompressedFileSuffix: app.config.CompressedFileSuffix,
- CacheDuration: cacheDuration,
- 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)
- }
- fs.CacheDuration = config[0].CacheDuration
- fs.Compress = config[0].Compress
- fs.AcceptByteRange = config[0].ByteRange
- fs.GenerateIndexPages = config[0].Browse
- if config[0].Index != "" {
- fs.IndexNames = []string{config[0].Index}
- }
- modifyResponse = config[0].ModifyResponse
- }
- fileHandler := fs.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.Context())
- // 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.Context().Response.StatusCode()
- if status != StatusNotFound && status != StatusForbidden {
- if len(cacheControlValue) > 0 {
- c.Context().Response.Header.Set(HeaderCacheControl, cacheControlValue)
- }
- if modifyResponse != nil {
- return modifyResponse(c)
- }
- return nil
- }
- // Reset response to default
- c.Context().SetContentType("") // Issue #420
- c.Context().Response.SetStatusCode(StatusOK)
- c.Context().Response.SetBodyString("")
- // Next middleware
- return c.Next()
- }
-
- // Create route metadata without pointer
- route := Route{
- // Router booleans
- use: true,
- root: isRoot,
- path: prefix,
- // Public data
- Method: MethodGet,
- Path: prefix,
- Handlers: []Handler{handler},
- }
- // 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
diff --git a/router_test.go b/router_test.go
index 5d7c95c1..57ce9209 100644
--- a/router_test.go
+++ b/router_test.go
@@ -332,128 +332,6 @@ func Test_Router_Handler_Catch_Error(t *testing.T) {
require.Equal(t, StatusInternalServerError, c.Response.Header.StatusCode())
}
-func Test_Route_Static_Root(t *testing.T) {
- t.Parallel()
-
- dir := "./.github/testdata/fs/css"
- app := New()
- app.Static("/", dir, Static{
- Browse: true,
- })
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-
- app = New()
- app.Static("/", dir)
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-}
-
-func Test_Route_Static_HasPrefix(t *testing.T) {
- t.Parallel()
-
- dir := "./.github/testdata/fs/css"
- app := New()
- app.Static("/static", dir, Static{
- Browse: true,
- })
-
- resp, err := app.Test(httptest.NewRequest(MethodGet, "/static", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err := io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-
- app = New()
- app.Static("/static/", dir, Static{
- Browse: true,
- })
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-
- app = New()
- app.Static("/static", dir)
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-
- app = New()
- app.Static("/static/", dir)
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 404, resp.StatusCode, "Status code")
-
- resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil))
- require.NoError(t, err, "app.Test(req)")
- require.Equal(t, 200, resp.StatusCode, "Status code")
-
- body, err = io.ReadAll(resp.Body)
- require.NoError(t, err, "app.Test(req)")
- require.Contains(t, app.getString(body), "color")
-}
-
func Test_Router_NotFound(t *testing.T) {
t.Parallel()
app := New()