1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-06 11:02:01 +00:00

middleware: add static middleware (#3006)

* middleware: add static middleware

* uncomment broken tests

* introduce isfile config property to fix file issues

* test

* add io/fs support to static mw

* add io/fs support to static mw

* remove filesystem and app.Static

* fix linter

* apply review

* support disablecache

* support multi indexes

* add an example for io/fs

* update whats new & apply reviews

* update

* use fasthttp from master

* Update .github/README.md

Co-authored-by: RW <rene@gofiber.io>

* update1

* apply reviews

* update

* update

* update examples

* add more examples

---------

Co-authored-by: RW <rene@gofiber.io>
This commit is contained in:
M. Efe Çetin 2024-05-28 10:29:25 +03:00 committed by GitHub
parent fca62c1853
commit 38fb8064c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1259 additions and 1705 deletions

10
.github/README.md vendored
View File

@ -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

53
app.go
View File

@ -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...)

View File

@ -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), "<!DOCTYPE html>"), "Response: "+string(body))
require.Contains(t, string(body), "TEST_BAR")
}
func Test_App_Group_Invalid(t *testing.T) {

View File

@ -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

View File

@ -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 |
|------------------------------------------------------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|
| <Reference id="compress">Compress</Reference> | `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 |
| <Reference id="byte_range">ByteRange</Reference> | `bool` | When set to true, enables byte range requests. | false |
| <Reference id="browse">Browse</Reference> | `bool` | When set to true, enables directory browsing. | false |
| <Reference id="download">Download</Reference> | `bool` | When set to true, enables direct download. | false |
| <Reference id="index">Index</Reference> | `string` | The name of the index file for serving a directory. | "index.html" |
| <Reference id="cache_duration">CacheDuration</Reference> | `time.Duration` | Expiration duration for inactive file handlers. Use a negative `time.Duration` to disable it. | 10 * time.Second |
| <Reference id="max_age">MaxAge</Reference> | `int` | The value for the `Cache-Control` HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 |
| <Reference id="modify_response">ModifyResponse</Reference> | `Handler` | ModifyResponse defines a function that allows you to alter the response. | nil |
| <Reference id="next">Next</Reference> | `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
<RoutingHandler />
@ -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
}
```

View File

@ -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.

View File

@ -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")
```

View File

@ -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://<server>/static/image.png`.
// Without `PathPrefix`, you have to access it via URL:
// `http://<server>/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"
"<Your go module>/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
_ "<Your go module>/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")
})
```

172
docs/middleware/static.md Normal file
View File

@ -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"))
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/hello.html
curl http://localhost:3000/css/style.css
```
</details>
### Serving files from a directory with Use
```go
app.Use("/", static.New("./public"))
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/hello.html
curl http://localhost:3000/css/style.css
```
</details>
### Serving a file
```go
app.Use("/static", static.New("./public/hello.html"))
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/static # will show hello.html
curl http://localhost:3000/static/john/doee # will show hello.html
```
</details>
### Serving files using os.DirFS
```go
app.Get("/files*", static.New("", static.Config{
FS: os.DirFS("files"),
Browse: true,
}))
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/files/css/style.css
curl http://localhost:3000/files/index.html
```
</details>
### 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,
}))
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/files/css/style.css
curl http://localhost:3000/files/index.html
```
</details>
### 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")
})
```
<details>
<summary>Test</summary>
```sh
curl http://localhost:3000/web/css/style.css
curl http://localhost:3000/web/index.html
curl http://localhost:3000/web
```
</details>
:::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.<br /><br />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.<br /><br />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.<br /><br />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,
}
```

View File

@ -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,
}))
```

View File

@ -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...)

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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, "<html><head><title>%s</title><style>.dir { font-weight: bold }</style></head><body>", basePathEscaped)
_, _ = fmt.Fprintf(c, "<h1>%s</h1>", basePathEscaped)
_, _ = fmt.Fprint(c, "<ul>")
if len(basePathEscaped) > 1 {
parentPathEscaped := html.EscapeString(strings.TrimRight(c.Path(), "/") + "/..")
_, _ = fmt.Fprintf(c, `<li><a href="%s" class="dir">..</a></li>`, 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, `<li><a href="%s" class="%s">%s</a>, %s, last modified %s</li>`,
pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime())
}
_, _ = fmt.Fprint(c, "</ul></body></html>")
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
}

View File

@ -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
}

175
middleware/static/static.go Normal file
View File

@ -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
}

View File

@ -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), "<h1>Hello, World!</h1>")
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)
})
}
}

View File

@ -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 {

141
router.go
View File

@ -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

View File

@ -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()