diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..6a4ec76843 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org +; This style originates from https://github.com/fewagency/best-practices +root = true + +[*] +charset = utf-8 +end_of_line = lf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..963a68ec2d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto eol=lf + +# Force batch scripts to always use CRLF line endings so that if a repo is accessed +# in Windows via a file share from Linux, the scripts will work. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf + +# Force bash scripts to always use LF line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 0452c7c5a4..84408f69f2 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -10,15 +10,15 @@ body: value: | ### Notice **This repository is not related to external or third-part Fiber modules. If you have a problem with them, open an issue under their repos. If you think the problem is related to Fiber, open the issue here.** - - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - - If you think Fiber doesn't have a nice feature that you think, open the issue with **✏️ Feature Request** template. + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Bug Description" description: "A clear and detailed description of what the bug is." - placeholder: "Explain your problem as clear and detailed." + placeholder: "Explain your problem clearly and in detail." validations: required: true - type: textarea @@ -39,7 +39,7 @@ body: id: expected-behavior attributes: label: Expected Behavior - description: "A clear and detailed description of what you think should happens." + description: "A clear and detailed description of what you think should happen." placeholder: "Tell us what Fiber should normally do." validations: required: true @@ -56,7 +56,7 @@ body: attributes: label: "Code Snippet (optional)" description: "For some issues, we need to know some parts of your code." - placeholder: "Share a code you think related to the issue." + placeholder: "Share a code snippet that you think is related to the issue." render: go value: | package main diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index 83bbcafdc8..91a05fd6cc 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -9,15 +9,15 @@ body: attributes: value: | ### Notice - - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - - If you think this is just a bug, open the issue with **☢️ Bug Report** template. + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Feature Description" - description: "A clear and detailed description of the feature we need to do." - placeholder: "Explain your feature as clear and detailed." + description: "A clear and detailed description of the feature you would like to see added." + placeholder: "Explain your feature clearly, and in detail." validations: required: true - type: textarea @@ -31,7 +31,7 @@ body: attributes: label: "Code Snippet (optional)" description: "Code snippet may be really helpful to describe some features." - placeholder: "Share a code to explain the feature better." + placeholder: "Share a code snippet to explain the feature better." render: go value: | package main diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml index 6e06c16788..d5df2a3d87 100644 --- a/.github/ISSUE_TEMPLATE/question.yaml +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -9,16 +9,16 @@ body: attributes: value: | ### Notice - - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - - If you think this is just a bug, open the issue with **☢️ Bug Report** template. - - If you think Fiber doesn't have a nice feature that you think, open the issue with **✏️ Feature Request** template. + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. + - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Question Description" description: "A clear and detailed description of the question." - placeholder: "Explain your question as clear and detailed." + placeholder: "Explain your question clearly, and in detail." validations: required: true - type: textarea @@ -26,7 +26,7 @@ body: attributes: label: "Code Snippet (optional)" description: "Code snippet may be really helpful to describe some features." - placeholder: "Share a code to explain the feature better." + placeholder: "Share a code snippet to explain the feature better." render: go value: | package main diff --git a/.github/README.md b/.github/README.md index 45f428f037..03c596440e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,6 +1,9 @@

- Fiber + + + Fiber +
@@ -58,6 +61,12 @@ + + + + + +
@@ -69,10 +78,10 @@ - + - + @@ -80,7 +89,7 @@ - +

Fiber is an Express inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. Designed to ease things up for fast development with zero memory allocation and performance in mind. @@ -109,13 +118,13 @@ func main() { These tests are performed by [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) and [Go Web](https://github.com/smallnest/go-web-framework-benchmark). If you want to see all the results, please visit our [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation -Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.14` or higher is required. +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: @@ -125,12 +134,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Features -- Robust [routing](https://docs.gofiber.io/routing) +- Robust [routing](https://docs.gofiber.io/guide/routing) - Serve [static files](https://docs.gofiber.io/api/app#static) - Extreme [performance](https://docs.gofiber.io/extra/benchmarks) - [Low memory](https://docs.gofiber.io/extra/benchmarks) footprint - [API endpoints](https://docs.gofiber.io/api/ctx) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) support +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) support - [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming - [Template engines](https://github.com/gofiber/template) - [WebSocket support](https://github.com/gofiber/websocket) @@ -148,12 +157,12 @@ Fiber is **inspired** by Express, the most popular web framework on the Internet We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord) _and all over the Internet_ to create a **fast**, **flexible** and **friendly** Go web framework for **any** task, **deadline** and developer **skill**! Just like Express does in the JavaScript world. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Examples -Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). +Listed below are some of the common examples. If you want to see more code examples, please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). #### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) @@ -437,7 +446,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -521,7 +530,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -579,7 +588,7 @@ func main() { app := fiber.New(fiber.Config{ EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range - ProxyHeader: fiber.HeaderXForwardedFor}, + ProxyHeader: fiber.HeaderXForwardedFor, }) // ... @@ -616,7 +625,12 @@ Here is a list of middleware that are included within the Fiber framework. | [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | ## 🧬 External Middleware @@ -624,12 +638,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -691,7 +700,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_az.md b/.github/README_az.md new file mode 100644 index 0000000000..f6486333eb --- /dev/null +++ b/.github/README_az.md @@ -0,0 +1,710 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber Go dili üçün ən sürətli HTTP mühərriki FasthttpExpress kitabxanasına bənzər arxitektura üzərində qurulmuş bir web framework-dür. Sıfır yaddaş ayrılması (zero-memory allocation) və performans səbəbilə development prosesini sürətləndirməkasanlaşdırmaq üçün tərtib edilmişdir. +

+ +## ⚡️ Sürətli Başlanğıc + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Performans Dəyərləri + +Bu testlər [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) və [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tərəfindən aparılıb. Bütün nəticələri görmək üçün [Wiki](https://docs.gofiber.io/extra/benchmarks) səhifəsinə keçid edə bilərsiniz. + +

+ + +

+ +## ⚙️ Quraşdırılması + +Go dilinin `1.17` və ya daha yuxarı versiyanın [yükləndiyindən](https://go.dev/dl/) əmin olun. + + +Bir qovluq yaratdıqdan sonra, `go mod init github.com/your/repo` komandasını eyni qovluğun daxilində işə salaraq layihənizi başladın ([go modulları haqqında əlavə bilgilər](https://go.dev/blog/using-go-modules)). Növbəti addım olaraq Fiber-i [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komandasını işlədərək yükləyin: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Özəllikləri + +- Güclü [routing](https://docs.gofiber.io/guide/routing) +- [Static faylların](https://docs.gofiber.io/api/app#static) təqdimatı +- Yüksək [performans](https://docs.gofiber.io/extra/benchmarks) +- [Daha az yaddaş istifadəsi](https://docs.gofiber.io/extra/benchmarks) +- [API son nöqtələri (endpoint)](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) dəstəyi +- [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server yönümlü proqramlaşdırma +- [Template mühərrikləri](https://github.com/gofiber/template) +- [WebSocket dəstəyi](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- [18 dildə](https://docs.gofiber.io/) mövcudluğu + +Daha ətraflı məlumat üçün [rəsmi sənədləşməyə](https://docs.gofiber.io/) baxış keçirə bilərsiniz. + +## 💡 Fəlsəfə + +[Node.js](https://nodejs.org/en/about/)-dən [Go](https://go.dev/doc/)-ya yeni keçən gopher-lər veb tətbiqlər və mikroservislər yazmadan öncə dilin özünəməxsus sintaksisini öyrənməklə məşğul olurlar. Fiber MinimalizmUNIX-in yaradılış prinsiplərinə uyğun şəkildə qurulmuş bir web framework-dür. Bu sahədə yeni olan gopher-lər Go dünyasında özlərini doğma və güvənli hiss edə biləcək şəkildə bir ab-hava ilə rastlaşa bilərlər. + +Fiber internet üzərində olan ən məşhur web framework-lərdən biri olan Express-dən ilhamlanaraq ərsəyə gəlmişdir. Biz Express-in rahatlıq və asanlıq xüsusiyyətlərini, Go-nun çiy performansı ilə birləşdirmişik; əgər əvvəldən Node.js üzərində (Express və ya bənzərləri) veb tətbiqi yaratmısınızsa, onda əksər metodlar və prinsiplər sizə tanış gələcəkdir. + +Biz istifadəçilərdən gələn [issue-a](https://github.com/gofiber/fiber/issues), Discord [kanalımıza](https://gofiber.io/discord) və bütün interneti əhatə edən vasitələrdən gələn rəyləri nəzərə alırıq. Bunun nəzdində, biz sürətli və rahat şəkildə hər bir tapşırığın səviyyəsinə uyğun olan — dostcasına bir Go web framework-ü olmağı hədəfləmişik (Express-in JavaScript dünyasında etdiyi kimi). + +## ⚠️ Limitlər +* Fiber unsafe prinsiplərə əsaslanaraq çalışdığından, o hər zaman Go-nun son versiyası ilə uyğunlaşmaya bilər. Buna görə də, Fiber 2.40.0 — Go 1.17 və 1.20 versiyaları ilə test edilərək saz vəziyyətə gətirilmişdir. +* Fiber net/http interfeysləri ilə uyğun deyil. Yəni gqlgen, go-swagger kimi net/http ekosisteminin parçası olan layihələri istifadə edə bilməzsiniz. + +## 👀 Misallar + +Aşağıda geniş istifadə olunan misallardan bəziləri siyahı şəklində qeyd olunub. Əgər daha çox koda dair misalları görmək istəyirsinizsə, onda [Əlavə misallardan ibarət github deposunu](https://github.com/gofiber/recipes) və ya [API sənədləşməni](https://docs.gofiber.io) nəzərdən keçirin. + +#### 📖 [**Sadə Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route-un Adlandırılması**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Static Fayl Təqdimatı**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Daha çox misalllar + +### Baxış mühərriki (View Engine) + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Mühərriklər](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber baxış mühərriki təyin edilmədikdə [html/template-in](https://pkg.go.dev/html/template/) default formasını alır. + +Əgər siz partial-ı və ya müxtəlif tipdə olan mühərrikləri istifadə etmək istəyirsinizsə, o zaman [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), [pug](https://github.com/Joker/jade) və s. kimi misallara baxa bilərsiniz. + +Çoxsaylı baxış mühərriklərini dəstəkləyən [template](https://github.com/gofiber/template) package-ə göstərilən link vasitəsilə nəzərdən keçirə bilərsiniz. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // Baxış mühərrikini tətbiqi başlatzmadan əvvəl quraşdıra bilərsiniz: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // Və indi `./views/home.pug` template-i bu şəkildə çağıra bilərsiniz: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Route-ın zəncirlərdə qruplaşdırılması + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware Logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +"Origin" başlığında istənilən domeni keçməklə CORS-un yoxlanması: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Sonuncu middleware-in hər şeyə uyğunlaşdırılması + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket-in təkminləşdirilməsi (upgrade) + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware-in Bərpası + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Etibarlı Proxy İstifadəsi + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Daxili Middleware + +Aşağıda Fiber-in daxilində olan middleware-lər siyahı şəklində göstərilmişdir. + +| Middleware | Açıqlama | +|:---------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Sadə bir auth middleware-dir və HTTP Basic Auth yaratmaq üçün istifadə olunur. Keçərli vəsiqə (credentials) bilgiləri üçün sonrakı handler-i, əksik və ya keçərsiz vəsiqə bilgiləri üçün 401 qaytarır. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Response-ı dayandırır və keşə yerləşdirir. | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Fiber üçün sıxışdırma (compression) middleware-dir. Default olaraq `deflate`, `gzip` və `brotli` dəstəkləyir. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Çeşidli seçimlərlə başlanğıclar arası mənbə paylaşımı (CORS) aktivləşdirir. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | CSRF exploit-dən qorunmasını təmin edir. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware-i cookie dəyərlərini şifrələyir. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Environment dəyərlərini göstərilən config-ə görə təyin edir. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Keşlərin daha səmərəli istifadəsinə və bant genişliyinə qənaət etməyə imkan verən ETag middleware-i; məzmun dəyişməyibsə veb serverin response-nı təkrar göndərməsinin qarşısını alır. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware, HTTP serverlərinin bəzi runtime dəyərlərini JSON formatında göstərir. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Əgər faylın yolu (path) göstərilmişdirsə, artıq loglarda olan favicon-u yox sayıb onu saxlanan depodan götürür. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Fiber üçün fayl sistem middleware-i. Alireza Salary-ə xüsusi təşəkkürlər. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Fiber üçün rate limitləyən middleware. Açıq API-ə və ya şifrə yeniləmə kimi endpoint-ə yönəlik təkrarlanan request-in qarşısını alır. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP istək/cavab (request/response) logger-i. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware-i serverin metriklərini report edər ("Express-status-monitor"-dan qaynaqlanıb). | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Matthew Lee-yə xüsusi təşəkkürlər \(@mthli\). | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Birdən çox server-ə proxy istəyi göndərməyiniz üçündür. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware-i stack chain-ni hər hansı bir yerindəki paniklərdən qurtulmasına kömək edir və kontrolu mərkəzləşdirilmiş [ErrorHandler-ə](https://docs.gofiber.io/guide/error-handling) ötürür.| +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Hər request üçün ayrı request id yaradır. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session üçün middleware. Qeyd: Bu middleware Fiber-in öz storage struktrunu istifadə edir. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware-i verilən şərt true olduğu halda handler-i görməyərək üstündən ötüb keçir. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Bir request üçün maksimum vaxt əlavə edir. Əgər arada fasilə yaranarsa, onda proses məhz ErrorHandler-ə göndərilərək icra edilir. | +| [keyauth](https://github.com/gofiber/keyauth) | Key giriş middleware-i, key əsaslı bir authentication metodudur. | +| [redirect](https://github.com/gofiber/redirect) | Yönləndirmə üçün middleware. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware-i verilən qanunlara əsasən URL yolunu (path) yenidən yazır. Geri dönüşün icrası üçün uyğunluq təşkil edən təsviri linklərin yaradılması üçün nəzərdə tutulmuşdur. | +| [adaptor](https://github.com/gofiber/adaptor) | Fiber request handler-dən net/http handler-ə çevirici. @arsmn-ə xüsusi təşəkkürlər! | +| [helmet](https://github.com/gofiber/helmet) | Fərqli HTTP header istifadə edərək tətbiqi daha təhlükəsiz saxlamağa kömək edir. | + +## 🧬 Xarici Middleware + +[Fiber komandası](https://github.com/orgs/gofiber/people) tərəfindən dəstəklənən və inkişaf etdirilən middleware-in siyahısı. + +| Middleware | Description | +| :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [jwt](https://github.com/gofiber/jwt) | JWT, JSON Web Token(JWT) girişi qaytaran bir middleware-dir. | +| [storage](https://github.com/gofiber/storage) | Fiber-in Storage arxitekturasını dəstəkləyən bir sıra storage driver verir. Bu sayədə storage-ə ehtiyac duyan Fiber middleware-də rahatlıqla istifadə oluna bilər. | +| [template](https://github.com/gofiber/template) | Bu paket, Fiber `v1.10.x`, Go versiyası 1.13 və ya daha yuxarı olduqda istifadə oluna bilər. 8 template mühərriki var. | +| [websocket](https://github.com/gofiber/websocket) | Yerlilərin dəstəyi ilə WebSocket-ə əsaslanan Fiber üçün Fasthttp. | + +## 🕶️ Möhtəşəm Siyahı + +Əlavə yazılar, middleware-lər, misallar, və alətlər üçün bizim [möhtəşəm siyahımıza](https://github.com/gofiber/awesome-fiber) göz atın. + +## 👍 Dəstək Nümayişi + +Əgər `Fiber`-ə dəstək olmaq və ya **təşəkkür etmək** istəyirsinizsə: + +1. Layihəni [GitHub Ulduzu](https://github.com/gofiber/fiber/stargazers) ilə işarələyin. +2. Layihə haqqında [şəxsi twitter hesabınızda](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) paylaşın. +3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) və ya şəxsi bloqunuz üzərindən bir incələmə və ya tədris yönümlü bir yazı dərc edin. +4. Bizim üçün, sadəcə bir [fincan kofe alın](https://buymeacoff.ee/fenny). + +## ☕ "Bir fincan kofe almaq" məsələsi + +Fiber açıq qaynaqlı bir layihə olduğu üçün, gəlirlərini yalnız ianələr vasitəsilə təmin edir və bu da domain adı, gitbook, netlify, serverless hosting xərcləri üçün istifadə olunur. Belə olduğu halda, Fiber-ə ən yaxşı dəstək elə bizim üçün ☕ [**bir kofe almaqdan gələ bilər**](https://buymeacoff.ee/fenny). + +| | İstifadəçi | İanə | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Koda Töhfə Verənlər + +Code Contributors + +## ⭐️ Layihəni Ulduzlayanlar + +Stargazers over time + +## ⚠️ Lisenziya Haqqında + +Müəllif Hüququ (c) 2019-bugün [Fenny](https://github.com/fenny) və [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` pulsuz və açıq qaynaqlı bir proqram təminatıdır və [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE) altında lisenziyalaşmışdır. Rəsmi loqo [Vic Shóstak](https://github.com/koddr) tərəfindən yaradılmış və [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) lisenziyası altında paylanmışdır (CC BY-SA 4.0 International). + +**Üçüncü Tərəf Kitabxana Lisenziyaları** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_ckb.md b/.github/README_ckb.md index 92fdba37d6..5358572752 100644 --- a/.github/README_ckb.md +++ b/.github/README_ckb.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { ئەم تاقیکردنەوانە لەلایەن [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ئەنجام دراون. دەتوانیت هەموو ئەنجامەکان [لێرە](https://docs.gofiber.io/extra/benchmarks) ببینیت.

- - + +

## ⚙️ دامەزراندن -دڵنیا بە لەوەی کە لەناو ئامێرەکەت Go دامەزراوە ([دای بگرە](https://go.dev/dl/)). دەبێت وەشانەکەشی `1.14` یان سەرووتر بێت. +دڵنیا بە لەوەی کە لەناو ئامێرەکەت Go دامەزراوە ([دای بگرە](https://go.dev/dl/)). دەبێت وەشانەکەشی `1.17` یان سەرووتر بێت. پڕۆژەکەت دەست پێ بکە بە دروستکردنی بوخچەیەک و کار پێ کردنی فەرمانی `go mod init github.com/your/repo` ([زیاتر](https://go.dev/blog/using-go-modules)) لەناو بوخچەکە. دواتریش بەم فەرمانەی خوارەوە فایبەر دامەزرێنە: @@ -121,12 +132,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 تایبەتمەندییەکان -- [ناونیشانی ئاڵۆز](https://docs.gofiber.io/routing) +- [ناونیشانی ئاڵۆز](https://docs.gofiber.io/guide/routing) - [فایلی جێگیر](https://docs.gofiber.io/api/app#static) - [خێراییەکی](https://docs.gofiber.io/extra/benchmarks) بێوێنە - بەکارهێنانی [میمۆریی کەم](https://docs.gofiber.io/extra/benchmarks) - توانای هەبوونی لقی [API](https://docs.gofiber.io/api/ctx) -- پشتگیریی [Middleware](https://docs.gofiber.io/middleware) و [Next](https://docs.gofiber.io/api/ctx#next) وەک Express +- پشتگیریی [Middleware](https://docs.gofiber.io/category/-middleware) و [Next](https://docs.gofiber.io/api/ctx#next) وەک Express - پڕۆگرامکردنی [خێرا](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497)ی ڕاژە - [داڕێژە](https://github.com/gofiber/template) - پشتگیریی [WebSocket](https://github.com/gofiber/websocket) @@ -436,7 +447,7 @@ func main() { ### وەڵامی JSON -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -520,7 +531,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -578,7 +589,7 @@ func main() { app := fiber.New(fiber.Config{ EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range - ProxyHeader: fiber.HeaderXForwardedFor}, + ProxyHeader: fiber.HeaderXForwardedFor, }) // ... @@ -616,6 +627,11 @@ func main() { | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 کاڵا دەرەکییەکان @@ -623,12 +639,7 @@ func main() { | کاڵا | دەربارە | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -690,7 +701,6 @@ For more articles, middlewares, examples or tools check our [awesome list](https - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_de.md b/.github/README_de.md index a2d432622d..8cddd01415 100644 --- a/.github/README_de.md +++ b/.github/README_de.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Diese Tests wurden von [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) und [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ausgeführt. Falls du alle Resultate sehen möchtest, besuche bitte unser [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation -Stelle sicher, dass du Go installiert hast ([Download hier](https://go.dev/dl/)). Version `1.14` oder neuer wird zu der Nutzung Fibers benötigt. +Stelle sicher, dass du Go installiert hast ([Download hier](https://go.dev/dl/)). Version `1.17` oder neuer wird zu der Nutzung Fibers benötigt. Erstelle ein neues Project, indem du zunächst einen neuen Ordner erstellst und dort in diesem Ordner `go mod init github.com/dein/repo` ausführst ([hier mehr dazu](https://go.dev/blog/using-go-modules)). Daraufhin kannst du Fiber mit dem [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) Kommandozeilenbefehl installieren: @@ -121,12 +132,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Eigenschaften -- Robustes [Routing](https://docs.gofiber.io/routing) +- Robustes [Routing](https://docs.gofiber.io/guide/routing) - Bereitstellen von [statischen Dateien](https://docs.gofiber.io/api/app#static) - Extreme [Performance](https://docs.gofiber.io/extra/benchmarks) - [Geringe Arbeitsspeichernutzung](https://docs.gofiber.io/extra/benchmarks) - Express [API Endpunkte](https://docs.gofiber.io/api/ctx) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) Support +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) Support - [Schnelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) serverseitige Programmierung - [Template engines](https://github.com/gofiber/template) - [WebSocket support](https://github.com/gofiber/websocket) @@ -142,7 +153,7 @@ Neue Gopher, welche von [Node.js](https://nodejs.org/en/about/) zu [Go](https:// Fiber ist **inspiriert** von Express.js, dem beliebtesten Web-Framework im Internet. Wir haben die **Leichtigkeit** von Express und die **Rohleistung** von Go kombiniert. Wenn du jemals eine Webanwendung mit Node.js implementiert hast (_mit Express.js oder ähnlichem_), werden dir viele Methoden und Prinzipien **sehr vertraut** vorkommen. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Beispiele @@ -431,7 +442,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -515,7 +526,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -586,6 +597,11 @@ Hier finden Sie eine Liste der Middleware, die im Fiber-Framework enthalten ist. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 External Middleware @@ -593,12 +609,7 @@ Liste der extern gehosteten Middleware-Module, die vom [Fiber team](https://gith | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. || [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -660,7 +671,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_es.md b/.github/README_es.md index fd6bd5ae3c..46b0927a9e 100644 --- a/.github/README_es.md +++ b/.github/README_es.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Estas pruebas son realizadas por [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) y [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si desea ver todos los resultados, visite nuestra [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Instalación -Asegúrese de tener instalado Go ([descargar](https://go.dev/dl/)). Versión `1.14` o superior. +Asegúrese de tener instalado Go ([descargar](https://go.dev/dl/)). Versión `1.17` o superior. Arranque su proyecto creando una nueva carpeta y ejecutando `go mod init github.com/your/repo` ([mas información](https://go.dev/blog/using-go-modules)) dentro del mismo directorio. Después instale Fiber mediante el comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Características -- [Enrutamiento](https://docs.gofiber.io/routing) robusto +- [Enrutamiento](https://docs.gofiber.io/guide/routing) robusto - Servir [archivos estáticos](https://docs.gofiber.io/api/app#static) - [Rendimiento](https://docs.gofiber.io/extra/benchmarks) extremo - [Bajo](https://docs.gofiber.io/extra/benchmarks) uso de [memoria](https://docs.gofiber.io/extra/benchmarks) @@ -142,7 +153,7 @@ Los nuevos gophers que hacen el cambio de [Node.js](https://nodejs.org/en/about/ Fiber está **inspirado** en Expressjs, el framework web más popular en Internet. Combinamos la **facilidad** de Express y **el rendimiento bruto** de Go. Si alguna vez ha implementado una aplicación web en Node.js ( _utilizando Express.js o similar_ ), muchos métodos y principios le parecerán **muy comunes** . ## ⚠️ Limitantes -* Debido a que Fiber utiliza unsafe, la biblioteca no siempre será compatible con la última versión de Go. Fiber 2.40.0 ha sido probado con las versiones de Go 1.16 a 1.19. +* Debido a que Fiber utiliza unsafe, la biblioteca no siempre será compatible con la última versión de Go. Fiber 2.40.0 ha sido probado con las versiones de Go 1.17 a 1.20. * Fiber no es compatible con interfaces net/http. Esto significa que no lo podrá usar en proyectos como qglgen, go-swagger, u otros que son parte del ecosistema net/http. ## 👀 Ejemplos @@ -431,7 +442,7 @@ func main() { ### Respuesta JSON -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -515,7 +526,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -586,6 +597,11 @@ Aquí está una lista del middleware incluido en el marco web Fiber. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 Middleware Externo @@ -593,12 +609,7 @@ Lista de módulos de middleware alojados externamente, y mantenidos por el [equi | Middleware | Descripción | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -660,7 +671,6 @@ Copyright (c) 2019-presente [Fenny](https://github.com/fenny) y [contribuyentes] - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_fa.md b/.github/README_fa.md index 8c8fad734c..7c6eec901f 100644 --- a/.github/README_fa.md +++ b/.github/README_fa.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -120,8 +131,8 @@ func main() {

- - + +


@@ -130,7 +141,7 @@ func main() {

-مطمئن شوید Go را نصب (دانلود) کرده اید. نسخه 1.14 یا بیشتر مورد نیاز است.
+مطمئن شوید Go را نصب (دانلود) کرده اید. نسخه 1.17 یا بیشتر مورد نیاز است.
پروژه خود را با ساختن یک پوشه و سپس اجرای go mod init github.com/your/repo داخل پوشه (یادگیری بیشتر) راه اندازی کنید. سپس Fiber را با دستور go get نصب کنید :

@@ -150,12 +161,12 @@ go get -u github.com/gofiber/fiber/v3
-- [مسیریابی](https://docs.gofiber.io/routing) قدرتمند +- [مسیریابی](https://docs.gofiber.io/guide/routing) قدرتمند - Serve [پرونده های ثابت](https://docs.gofiber.io/api/app#static) - حداکثر [عملکرد](https://docs.gofiber.io/extra/benchmarks) - مصرف [حافظه کم](https://docs.gofiber.io/extra/benchmarks) - قابلیت [API endpoints](https://docs.gofiber.io/api/ctx) -- پشتیبانی از [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) +- پشتیبانی از [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) - برنامه نویسی سمت سرور [سریع](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) - دارای [Template engines](https://github.com/gofiber/template) اختصاصی - [پشتیبانی از وب سوکت](https://github.com/gofiber/websocket) @@ -182,7 +193,7 @@ Fiber از Express الهام گرفته, که محبوب ترین فری

## ⚠️ محدودیت ها -* به دلیل استفاده ناامن از Fiber, ممکن است کتابخانه همیشه با آخرین نسخه Go سازگار نباشد. Fiber 2.40.0 با زبان گو نسخه 1.16 تا 1.19 تست شده است. +* به دلیل استفاده ناامن از Fiber, ممکن است کتابخانه همیشه با آخرین نسخه Go سازگار نباشد. Fiber 2.40.0 با زبان گو نسخه 1.17 تا 1.20 تست شده است. * فریمورک Fiber با پکیج net/http سازگار نیست. این بدان معناست شما نمی توانید از پکیج های مانند go-swagger, gqlgen یا سایر پروژه هایی که بخشی از اکوسیستم net/http هستند استفاده کنید.
@@ -527,7 +538,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json)
@@ -617,7 +628,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -663,6 +674,28 @@ func main() { } ``` +### Using Trusted Proxy + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +```
@@ -701,6 +734,11 @@ func main() { | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) |برای ذخیره و مدیریت شناسه کاربری یا session بازدید کنندگان استفاده .میشود| | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) |این میدلور میتواند با استفاده از شرط های تعیین شده درخواست هایی را نادیده بگیرد.| | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) |این میدلور محدودیت زمانی ای را برای درخواست ها تنظیم میکند، در صورتی که محدودیت به پایان برسد ErrorHandler صدا زده میشود.| +| [keyauth](https://github.com/gofiber/keyauth) | این میدلور احراز هویت مبتنی بر کلید را فراهم می کند. | +| [redirect](https://github.com/gofiber/redirect) | برای ریدایرکت کردن از این میدلور میتوانید استفاده کنید. | +| [rewrite](https://github.com/gofiber/rewrite) | مسیر URL را براساس قوانین مشخص شده بازنویسی می کند. این میتواند برای سازگاری با ورژن های قبلی یا برای ساخت لینک های تمیز تر و توصیفی تر مفید باشد. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | با استفاده از HTTP هدر های مختلف به ایمن سازی برنامه شما کمک می کند. |


@@ -717,19 +755,14 @@ func main() { | Middleware | توضیحات | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | ## 🕶️ Awesome List -For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + [awesome list](https://github.com/gofiber/awesome-fiber) برای مقاله، میدلور، مثال ها و ابزار های بیشتر لطفا از این لینک بازدید کنید
@@ -812,7 +845,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_fr.md b/.github/README_fr.md index 11d3db369d..6229d13b1e 100644 --- a/.github/README_fr.md +++ b/.github/README_fr.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Ces tests sont effectués par [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) et [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si vous voulez voir tous les résultats, n'hésitez pas à consulter notre [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installation -Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.14` or higher is required. +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Features -- [Routing](https://docs.gofiber.io/routing) robuste +- [Routing](https://docs.gofiber.io/guide/routing) robuste - Serve [static files](https://docs.gofiber.io/api/app#static) - [Performances](https://docs.gofiber.io/extra/benchmarks) extrêmes - [Faible empreinte mémoire](https://docs.gofiber.io/extra/benchmarks) @@ -142,7 +153,7 @@ Les nouveaux gophers qui passent de [Node.js](https://nodejs.org/en/about/) à [ Fiber est **inspiré** par Express, le framework web le plus populaire d'Internet. Nous avons combiné la **facilité** d'Express, et la **performance brute** de Go. Si vous avez déja développé une application web en Node.js (_en utilisant Express ou équivalent_), alors de nombreuses méthodes et principes vous sembleront **familiers**. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Exemples @@ -433,7 +444,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -517,7 +528,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -588,6 +599,11 @@ Here is a list of middleware that are included within the Fiber framework. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 External Middleware @@ -595,12 +611,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -662,7 +673,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_he.md b/.github/README_he.md index 622bc0141b..06f9dbe3cc 100644 --- a/.github/README_he.md +++ b/.github/README_he.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -122,15 +133,15 @@ func main() {

- - + +

## ⚙️ התקנה -Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.14` or higher is required. +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: @@ -144,14 +155,14 @@ go get -u github.com/gofiber/fiber/v3
-- [ניתוב](https://docs.gofiber.io/routing) רובסטי +- [ניתוב](https://docs.gofiber.io/guide/routing) רובסטי - הנגשת [קבצים סטטיים](https://docs.gofiber.io/api/app#static) - [ביצועים](https://docs.gofiber.io/extra/benchmarks) גבוהים במיוחד - צורך כמות [זכרון קטנה](https://docs.gofiber.io/extra/benchmarks) - [נקודות קצה עבור API](https://docs.gofiber.io/api/ctx) -- תמיכה ב-[Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) +- תמיכה ב-[Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) - תכנות [מהיר](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) של צד שרת -- [מנועי תבניות](https://docs.gofiber.io/middleware#template) +- [מנועי תבניות](https://docs.gofiber.io/category/-middleware#template) - [תמיכה ב-WebSocket](https://github.com/gofiber/websocket) - [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) - [הגבלת קצבים ובקשות](https://docs.gofiber.io/api/middleware/limiter) @@ -187,7 +198,7 @@ Fiber נוצרה **בהשראת** Express, ה-web framework הפופולרית
## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 דוגמאות @@ -523,7 +534,7 @@ func main() { ### תגובת JSON -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json)
@@ -617,7 +628,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -704,6 +715,11 @@ Here is a list of middleware that are included within the Fiber framework. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. |
@@ -723,12 +739,7 @@ Here is a list of middleware that are included within the Fiber framework. | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -837,7 +848,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_id.md b/.github/README_id.md index 84066b1ce8..4f31f2a7b5 100644 --- a/.github/README_id.md +++ b/.github/README_id.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Pengukuran ini dilakukan oleh [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) dan [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Apabila anda ingin melihat hasil lengkapnya, silahkan kunjungi halaman [Wiki](https://docs.gofiber.io/extra/benchmarks) kami.

- - + +

## ⚙️ Instalasi -Pastikan kamu sudah menginstalasi Golang ([unduh](https://go.dev/dl/)). Dengan versi `1.14` atau lebih tinggi [ Direkomendasikan ]. +Pastikan kamu sudah menginstalasi Golang ([unduh](https://go.dev/dl/)). Dengan versi `1.17` atau lebih tinggi [ Direkomendasikan ]. Inisialisasi proyek kamu dengan membuat folder lalu jalankan `go mod init github.com/nama-kamu/repo` ([belajar lebih banyak](https://go.dev/blog/using-go-modules)) di dalam folder. Kemudian instal Fiber dengan perintah [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Fitur -- Sistem [Routing](https://docs.gofiber.io/routing) yang padu +- Sistem [Routing](https://docs.gofiber.io/guide/routing) yang padu - Menyajikan [file statis](https://docs.gofiber.io/api/app#static) - [Kinerja](https://docs.gofiber.io/extra/benchmarks) ekstrim - [Penggunaan memori](https://docs.gofiber.io/extra/benchmarks) yang kecil @@ -141,11 +152,11 @@ Bagi yang baru yang beralih dari [Node.js](https://nodejs.org/en/about/) ke [Go] Fiber terinspirasi dari Express, salah satu kerangka kerja web yang paling terkenal di Internet. Kami menggabungkan **kemudahan** dari Express dan **kinerja luar biasa** dari Go. Apabila anda pernah membuat aplikasi dengan Node.js (_dengan Express atau yang lainnya_), maka banyak metode dan prinsip yang akan terasa **sangat umum** bagi anda. -Kami **mendengarkan** para pengguna di [GitHub Issues](https://github.com/gofiber/fiber/issues) (_dan berbagai platform lainnya_) untuk menciptakan kerangka kerja web yang **cepat**, **fleksibel** dan **bersahabat** untuk berbagai macam keperluan, **tenggat waktu** dan **keahlian** para pengguna! Sama halnya seperti yang dilakukan Express di dunia JavaScript. +Kami **mendengarkan** para pengguna di [GitHub Issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord), _dan berbagai platform lainnya_ untuk menciptakan kerangka kerja web yang **cepat**, **fleksibel** dan **bersahabat** untuk berbagai macam keperluan, **tenggat waktu** dan **keahlian** para pengguna! Sama halnya seperti yang dilakukan Express di dunia JavaScript. ## ⚠️ Limitasi -* Karena penggunaan Fiber yang tidak aman, perpustakaan mungkin tidak selalu kompatibel dengan versi Go terbaru. Fiber 2.40.0 telah diuji dengan Go versi 1.16 hingga 1.19. +* Karena penggunaan Fiber yang tidak aman, perpustakaan mungkin tidak selalu kompatibel dengan versi Go terbaru. Fiber 2.40.0 telah diuji dengan Go versi 1.17 hingga 1.20. * Fiber tidak kompatibel dengan antarmuka net/http. Ini berarti kamu tidak akan dapat menggunakan proyek seperti gqlgen, go-swagger, atau lainnya yang merupakan bagian dari ekosistem net/http. ## 👀 Contoh @@ -434,7 +445,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -518,7 +529,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -589,6 +600,11 @@ Kumpulan `middleware` yang ada didalam kerangka kerja Fiber. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 Middleware External @@ -596,12 +612,7 @@ Kumpulan `middleware` yang dihost external dan diurus oleh [Tim Fiber](https://g | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -663,7 +674,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_it.md b/.github/README_it.md index 233e7b216d..b5ecf26179 100644 --- a/.github/README_it.md +++ b/.github/README_it.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Questi test sono stati eseguiti da [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se vuoi vedere tutti i risultati, visita la nostra [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Installazione -Assicurati di avere Go ([per scaricalro](https://go.dev/dl/)) installato. Devi avere la versione `1.14` o superiore. +Assicurati di avere Go ([per scaricarlo](https://go.dev/dl/)) installato. Devi avere la versione `1.17` o superiore. Inizializza il tuo progetto creando una cartella e successivamente usando il comando `go mod init github.com/la-tua/repo` ([per maggiori informazioni](https://go.dev/blog/using-go-modules)) dentro la cartella. Dopodiche installa Fiber con il comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Caratteristiche -- [Routing](https://docs.gofiber.io/routing) solido +- [Routing](https://docs.gofiber.io/guide/routing) solido - Serve [file statici](https://docs.gofiber.io/api/app#static) - [Perfomance](https://docs.gofiber.io/extra/benchmarks) estreme - [Basso](https://docs.gofiber.io/extra/benchmarks) utilizzo di [memoria](https://docs.gofiber.io/extra/benchmarks) @@ -139,11 +150,11 @@ go get -u github.com/gofiber/fiber/v3 I nuovi gopher che passano da [Node.js](https://nodejs.org/en/about/) a [Go](https://go.dev/doc/) hanno a che fare con una curva di apprendimento prima di poter iniziare a creare le proprie applicazioni web o microservizi. Fiber, come **web framework** , è stato creato con l'idea di **minimalismo** e seguendo lo '**UNIX way**' , così i nuovi gopher posso entrare rapidamente nel mondo di Go con un caldo e fidato benvenuto. -Fiber è **ispirato** da Express, il web framework più popolare su internet. Abbiamo combiniamo la **facilità** di Express e **le prestazioni** di Go. Se hai mai implementato una applicazione web in Node.js (_utilizzando Express o simili_), allora i tanti metodi e principi ti saranno **molto familiari**. +Fiber è **ispirato** da Express, il web framework più popolare su internet. Abbiamo combinato la **facilità** di Express e **le prestazioni** di Go. Se hai mai implementato una applicazione web in Node.js (_utilizzando Express o simili_), allora i tanti metodi e principi ti saranno **molto familiari**. ## ⚠️ Limitazioni -* Dato che Fiber utilizza unsafe, la libreria non sempre potrebbe essere compatibile con l'ultima versione di Go. Fiber 2.40.0 è stato testato con la versioni 1.16 alla 1.19 di Go. +* Dato che Fiber utilizza unsafe, la libreria non sempre potrebbe essere compatibile con l'ultima versione di Go. Fiber 2.40.0 è stato testato con la versioni 1.17 alla 1.20 di Go. * Fiber non è compatibile con le interfacce net/http. Questo significa che non è possibile utilizzare progetti come qglgen, go-swagger, o altri che fanno parte dell'ecosistema net/http. ## 👀 Esempi @@ -432,7 +443,7 @@ func main() { ### Risposte JSON -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -516,7 +527,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -574,7 +585,7 @@ func main() { app := fiber.New(fiber.Config{ EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range - ProxyHeader: fiber.HeaderXForwardedFor}, + ProxyHeader: fiber.HeaderXForwardedFor, }) // ... @@ -591,27 +602,32 @@ Qui una lista dei middleware inclusi con Fiber. | Middleware | Descrizione | | :------------------------------------------------------------------------------------- |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Middleware basico di autenticazione usando http. Chiama il suo handler se le credenziali sono guiste e il codice 401 Unauthorized per credenziali mancanti o invailde. | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Middleware basico di autenticazione usando http. Chiama il suo handler se le credenziali sono giuste e il codice 401 Unauthorized per credenziali mancanti o invalide. | | [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercetta e mette nella cache la risposta | -| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Middlewere di compressione per Fiber, supporta `deflate`, `gzip` e `brotli` di default. | -| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Ti permette di usare cross-origin resource sharing \(CORS\) con tante optzioni. | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Middleware di compressione per Fiber, supporta `deflate`, `gzip` e `brotli` di default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Ti permette di usare cross-origin resource sharing \(CORS\) con tante opzioni. | | [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Ti protegge da attachi CSRF. | | [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Middleware che encrypta i valori dei cookie. | | [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Esporre le variabili di ambiente fornendo una configurazione facoltativa. | -| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middlewere che permette alle cache di essere piu efficienti e salvare banda, come un web server non deve rimandare il messagio pieno se il contenuto non è cambiato. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middleware che permette alle cache di essere più efficienti e salvare banda, come un web server che non deve rimandare il messagio pieno se il contenuto non è cambiato. | | [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Middleware che serve via il suo runtime server HTTP varianti esposte in formato JSON. | -| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignora favicon dai logs o serve dalla memoria se un filepath si dà. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignora favicon dai logs o serve dalla memoria se un filepath è specificato. | | [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Middleware per il FileSystem per Fiber, grazie tante e crediti a Alireza Salary | -| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Middelwere per Rate-limiting per Fiber. Usato per limitare richieste continue agli APIs publici e/o endpoints come un password reset. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Middleware per Rate-limiting per Fiber. Usato per limitare richieste continue agli APIs publici e/o endpoints come un password reset. | | [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Logger HTTP per richiesta/risposta. | | [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware per monitorare che riporta metriche server, ispirato da express-status-monitor | | [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Grazie tante a Matthew Lee \(@mthli\) | | [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Ti permette di fare richieste proxy a multipli server. | -| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware per recuperare dai attachi di panico da tutte le parti nella stack chain e da il controllo al centralizzato[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware per recuperare dagli attachi di panico da tutte le parti nella stack chain e affida il controllo al [ ErrorHandler](https://docs.gofiber.io/guide/error-handling) centralizzato. | | [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Aggiunge un requestid a ogni richiesta. | -| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middelwere per sessioni. NOTA: Questo middleware usa il nostro Storage package. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware per sessioni. NOTA: Questo middleware usa il nostro Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Middleware che salta un wrapped handler se un predicate è vero. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Aggiunge un tempo massimo per una richiesta e lo manda a ErrorHandler se si supera. | +| [keyauth](https://github.com/gofiber/keyauth) | Usa auth basato su chiavi. | +| [redirect](https://github.com/gofiber/redirect) | Middleware per reinderizzare | +| [rewrite](https://github.com/gofiber/rewrite) | Riscrive la path all URL con le regole date. Può essere di aiuto per compatibilità o per creare link puliti e più descrittivi. | +| [adaptor](https://github.com/gofiber/adaptor) | Converte gli handler net/http a/da i request handlers di Fiber, grazie tante a @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Aiuta a mettere sicurezza alla tua app usando vari header HTTP. | ## 🧬 Middleware Esterni @@ -619,12 +635,7 @@ La lista dei moduli middleware hostati esternamente e mantenuti dal [team di Fib | Middleware | Descrizione | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converte gli handler net/http a/da i request handlers di Fiber, grazie tante a @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Aiuta a mettere sicurezza alla tua app usando vari header HTTP. | | [jwt](https://github.com/gofiber/jwt) | Usa JSON Web Token \(JWT\) auth. | -| [keyauth](https://github.com/gofiber/keyauth) | Usa auth basato su chiavi. | -| [redirect](https://github.com/gofiber/redirect) | Middleware per reinderizzare | -| [rewrite](https://github.com/gofiber/rewrite) | Riscrive la path all URL con le regole date. Puo essere di aiuto per compatibilita o per creare link puliti e piu descrittivi. | | [storage](https://github.com/gofiber/storage) | Dirver di storage che implementa la interfaccia Storage, fatto per essere usato con vari Fiber middleware. | | [template](https://github.com/gofiber/template) | Questo pachetto contiene 8 motori template che possono essere usati con Fiber `v1.10.x`. Versione di go neccesaria: 1.13+. | | [websocket](https://github.com/gofiber/websocket) | Basato su Fasthttp WebSocket per Fiber con supporto per Locals! | @@ -686,7 +697,6 @@ Copyright (c) 2019-ora [Fenny](https://github.com/fenny) e [Contributors](https: - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_ja.md b/.github/README_ja.md index 007a0aa656..e6c350b5b2 100644 --- a/.github/README_ja.md +++ b/.github/README_ja.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -106,13 +117,13 @@ func main() { これらのテストは[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)および[Go Web](https://github.com/smallnest/go-web-framework-benchmark)によって計測を行っています 。すべての結果を表示するには、 [Wiki](https://docs.gofiber.io/extra/benchmarks)にアクセスしてください。

- - + +

## ⚙️ インストール -Go がインストールされていることを確認してください ([ダウンロード](https://go.dev/dl/)). バージョン `1.14` またはそれ以上であることが必要です。 +Go がインストールされていることを確認してください ([ダウンロード](https://go.dev/dl/)). バージョン `1.17` またはそれ以上であることが必要です。 フォルダを作成し、フォルダ内で `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) を実行してプロジェクトを初期化してください。その後、 Fiber を以下の [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) コマンドでインストールしてください。 @@ -122,7 +133,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 機能 -- 堅牢な[ルーティング](https://docs.gofiber.io/routing) +- 堅牢な[ルーティング](https://docs.gofiber.io/guide/routing) - [静的ファイル](https://docs.gofiber.io/api/app#static)のサポート - 究極の[パフォーマンス](https://docs.gofiber.io/extra/benchmarks) - [低メモリ](https://docs.gofiber.io/extra/benchmarks)フットプリント @@ -147,7 +158,7 @@ Fiber は人気の高い Web フレームワークである Expressjs に**イ ## ⚠️ 制限事項 -- Fiber は unsafe パッケージを使用しているため、最新の Go バージョンと互換性がない場合があります。Fiber 2.40.0 は、Go のバージョン 1.16 から 1.19 でテストされています。 +- Fiber は unsafe パッケージを使用しているため、最新の Go バージョンと互換性がない場合があります。Fiber 2.40.0 は、Go のバージョン 1.17 から 1.20 でテストされています。 - Fiber は net/http インターフェースと互換性がありません。つまり、gqlgen や go-swagger など、net/http のエコシステムの一部であるプロジェクトを使用することができません。 ## 👀 例 @@ -436,7 +447,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -591,6 +602,11 @@ func main() { | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 外部ミドルウェア @@ -598,12 +614,7 @@ func main() { | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -665,7 +676,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_ko.md b/.github/README_ko.md index d1123704f5..c6b2eed2c1 100644 --- a/.github/README_ko.md +++ b/.github/README_ko.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { 이 테스트들은 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)와 [Go Web](https://github.com/smallnest/go-web-framework-benchmark)을 통해 측정되었습니다. 만약 모든 결과를 보고 싶다면, [Wiki](https://docs.gofiber.io/extra/benchmarks)를 확인해 주세요.

- - + +

## ⚙️ 설치 -Go가 설치되어 있는 것을 확인해 주세요 ([download](https://go.dev/dl/)). 버전 1.14 또는 그 이상이어야 합니다. +Go가 설치되어 있는 것을 확인해 주세요 ([download](https://go.dev/dl/)). 버전 1.17 또는 그 이상이어야 합니다. 폴더를 생성하여 당신의 프로젝트를 초기화하고, 폴더 안에서 `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) 를 실행하세요. 그리고 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 명령어로 Fiber를 설치하세요: @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 특징 -- 견고한 [라우팅](https://docs.gofiber.io/routing) +- 견고한 [라우팅](https://docs.gofiber.io/guide/routing) - [정적 파일](https://docs.gofiber.io/api/app#static) 제공 - 뛰어난 [성능](https://docs.gofiber.io/extra/benchmarks) - [적은 메모리](https://docs.gofiber.io/extra/benchmarks) 공간 @@ -144,7 +155,7 @@ Fiber는 인터넷에서 가장 인기있는 웹 프레임워크인 Express에 우리는 **어떤한** 작업, **마감일정**, 개발자의 **기술**이던간에 **빠르고**, **유연하고**, **익숙한** Go 웹 프레임워크를 만들기 위해 사용자들의 [이슈들](https://github.com/gofiber/fiber/issues)을(그리고 모든 인터넷을 통해) **듣고 있습니다**! Express가 자바스크립트 세계에서 하는 것 처럼요. ## ⚠️ 한계점 -* Fiber는 unsafe 패키지를 사용하기 때문에 최신 Go버전과 호환되지 않을 수 있습니다.Fiber 2.40.0은 Go 버전 1.16에서 1.19로 테스트되고 있습니다. +* Fiber는 unsafe 패키지를 사용하기 때문에 최신 Go버전과 호환되지 않을 수 있습니다.Fiber 2.40.0은 Go 버전 1.17에서 1.20로 테스트되고 있습니다. * Fiber는 net/http 인터페이스와 호환되지 않습니다.즉, gqlgen이나 go-swagger 등 net/http 생태계의 일부인 프로젝트를 사용할 수 없습니다. ## 👀 예제 @@ -437,7 +448,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -521,7 +532,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -592,6 +603,11 @@ Fiber 프레임워크에 포함되는 미들웨어 목록입니다. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 External Middleware @@ -599,12 +615,7 @@ Fiber 프레임워크에 포함되는 미들웨어 목록입니다. | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -666,7 +677,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_nl.md b/.github/README_nl.md index 6451506a9f..5d58f24286 100644 --- a/.github/README_nl.md +++ b/.github/README_nl.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Deze tests zijn uitgevoerd door [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) en [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Bezoek onze [Wiki](https://fiber.wiki/benchmarks) voor alle benchmark resultaten.

- - + +

## ⚙️ Installatie -Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.14` or higher is required. +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: @@ -121,17 +132,17 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Features -- Robuuste [routing](https://docs.gofiber.io/routing) +- Robuuste [routing](https://docs.gofiber.io/guide/routing) - Serveer [statische bestanden](https://docs.gofiber.io/api/app#static) - Extreme [prestaties](https://docs.gofiber.io/extra/benchmarks) - [Weinig geheugenruimte](https://docs.gofiber.io/extra/benchmarks) - [API endpoints](https://docs.gofiber.io/api/ctx) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) ondersteuning +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) ondersteuning - [Snelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programmering - [Template engines](https://github.com/gofiber/template) - [WebSocket ondersteuning](https://github.com/gofiber/websocket) - [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) -- [Rate Limiter](https://docs.gofiber.io/middleware/limiter) +- [Rate Limiter](https://docs.gofiber.io/category/-middleware/limiter) - Vertaald in [18 talen](https://docs.gofiber.io/) - En nog veel meer, [ontdek Fiber](https://docs.gofiber.io/) @@ -144,7 +155,7 @@ Fiber is **geïnspireerd** door Express, het populairste webframework op interne We **luisteren** naar onze gebruikers in [issues](https://github.com/gofiber/fiber/issues) (_en overal op het internet_) om een **snelle**, **flexibele** en **vriendelijk** Go web framework te maken voor **elke** taak, **deadline** en ontwikkelaar **vaardigheid**! Net zoals Express dat doet in de JavaScript-wereld. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Voorbeelden @@ -437,7 +448,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -521,7 +532,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -592,6 +603,11 @@ Here is a list of middleware that are included within the Fiber framework. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | ## 🧬 External Middleware @@ -599,12 +615,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -666,7 +677,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_pt.md b/.github/README_pt.md index 8ebb91ccf2..f0d69774e2 100644 --- a/.github/README_pt.md +++ b/.github/README_pt.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Esses testes são realizados pelo [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se você quiser ver todos os resultados, visite nosso [Wiki](https://docs.gofiber.io/extra/benchmarks) .

- - + +

## ⚙️ Instalação -Certifique-se de ter o Go instalado ([download](https://go.dev/dl/)). Versão `1.14` ou superior é obrigatória. +Certifique-se de ter o Go instalado ([download](https://go.dev/dl/)). Versão `1.17` ou superior é obrigatória. Inicie seu projeto criando um diretório e então execute `go mod init github.com/your/repo` ([saiba mais](https://go.dev/blog/using-go-modules)) dentro dele. Então, instale o Fiber com o comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Recursos -- [Roteamento](https://docs.gofiber.io/routing) robusto +- [Roteamento](https://docs.gofiber.io/guide/routing) robusto - Servir [arquivos estáticos](https://docs.gofiber.io/api/app#static) - [Desempenho](https://docs.gofiber.io/extra/benchmarks) extremo - [Baixo consumo de memória](https://docs.gofiber.io/extra/benchmarks) @@ -142,7 +153,7 @@ Os novos gophers que mudaram do [Node.js](https://nodejs.org/en/about/) para o [ O Fiber é **inspirado** no Express, o framework web mais popular da Internet. Combinamos a **facilidade** do Express e com o **desempenho bruto** do Go. Se você já implementou um aplicativo web com Node.js ( _usando Express.js ou similar_ ), então muitos métodos e princípios parecerão **muito familiares** para você. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 Exemplos @@ -431,7 +442,7 @@ func main() { ### Resposta JSON -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -515,7 +526,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -586,6 +597,11 @@ Here is a list of middleware that are included within the Fiber framework. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 External Middleware @@ -593,12 +609,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -662,7 +673,6 @@ O logo oficial foi criado por [Vic Shóstak](https://github.com/koddr) e distrib - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_ru.md b/.github/README_ru.md index b7dfeebba9..c5f25be7be 100644 --- a/.github/README_ru.md +++ b/.github/README_ru.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Тестирование проводилось с помощью [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) и [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Если вы хотите увидеть все результаты, пожалуйста, посетите наш [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ Установка -Убедитесь, что Go установлен ([скачать](https://go.dev/dl/)). Требуется версия `1.14` или выше. +Убедитесь, что Go установлен ([скачать](https://go.dev/dl/)). Требуется версия `1.17` или выше. Инициализируйте проект, создав папку, а затем запустив `go mod init github.com/your/repo` ([подробнее](https://go.dev/blog/using-go-modules)) внутри этой папки. Далее, установите Fiber с помощью команды [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): @@ -121,12 +132,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Особенности -- Надежная [маршрутизация](https://docs.gofiber.io/routing) +- Надежная [маршрутизация](https://docs.gofiber.io/guide/routing) - Доступ к [статичным файлам](https://docs.gofiber.io/api/app#static) - Экстремальная [производительность](https://docs.gofiber.io/extra/benchmarks) - [Низкий объем потребления памяти](https://docs.gofiber.io/extra/benchmarks) - [Эндпоинты](https://docs.gofiber.io/context), как в [API](https://docs.gofiber.io/api/ctx) Express -- [Middleware](https://docs.gofiber.io/middleware) и поддержка [Next](https://docs.gofiber.io/api/ctx#next) +- [Middleware](https://docs.gofiber.io/category/-middleware) и поддержка [Next](https://docs.gofiber.io/api/ctx#next) - [Быстрое](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) программирование на стороне сервера - [Template engines](https://github.com/gofiber/template) - [Поддержка WebSocket](https://github.com/gofiber/websocket) @@ -143,9 +154,9 @@ Fiber **вдохновлен** Express, самым популярным веб Мы **прислушиваемся** к нашим пользователям в [issues](https://github.com/gofiber/fiber/issues), Discord [канале](https://gofiber.io/discord) _и в остальном Интернете_, чтобы создать **быстрый**, **гибкий** и **дружелюбный** веб фреймворк на Go для **любых** задач, **дедлайнов** и **уровней** разработчиков! Как это делает Express в мире JavaScript. -## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. -* Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. +## ⚠️ Ограничения +* Из-за того, что Fiber использует пакет unsafe, библиотека не всегда может быть совместима с последней версией Go. Fiber 2.40.0 был протестирован с версиями Go от 1.17 до 1.20. +* Fiber не совместим с интерфейсами net/http. Это означает, что вы не сможете использовать такие проекты, как gqlgen, go-swagger или любые другие, которые являются частью экосистемы net/http. ## 👀 Примеры @@ -433,7 +444,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -521,7 +532,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -593,6 +604,11 @@ func main() { | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 Внешние Middleware @@ -600,19 +616,14 @@ func main() { | Middleware | Описание | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | -## 🕶️ Awesome List +## 🕶️ Полезный список -For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). +Дополнительные статьи, middleware, примеры или инструменты смотри в нашем [полезном списке](https://github.com/gofiber/awesome-fiber). ## 👍 Помощь проекту @@ -658,9 +669,9 @@ Fiber — это проект с открытым исходным кодом, Stargazers over time -## ⚠️ License +## ⚠️ Лицензия -Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). +Copyright (c) 2019-настоящее время [Fenny](https://github.com/fenny) и [Контрибьютеры](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` - это свободное программное обсепечение с открытым исходным кодом лицензированное под [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Официальный логотип создан [Vic Shóstak](https://github.com/koddr) и распространяется под [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) лицензией (CC BY-SA 4.0 International). **Third-party library licenses** @@ -669,7 +680,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_sa.md b/.github/README_sa.md index e698bd1ee1..61ec729bee 100644 --- a/.github/README_sa.md +++ b/.github/README_sa.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -111,13 +122,13 @@ func main() { يتم تنفيذ هذه الاختبارات من قبل [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark). إذا كنت تريد رؤية جميع النتائج ، يرجى زيارة موقعنا [Wiki](https://docs.gofiber.io/extra/benchmarks).

- - + +

## ⚙️ تثبيت -تأكد من تثبيت Go ([تحميل](https://go.dev/dl/)). الإصدار `1.14` أو أعلى مطلوب. +تأكد من تثبيت Go ([تحميل](https://go.dev/dl/)). الإصدار `1.17` أو أعلى مطلوب. ابدأ مشروعك بإنشاء مجلد ثم تشغيله `go mod init github.com/your/repo` ([أعرف أكثر](https://go.dev/blog/using-go-modules)) داخل المجلد. ثم قم بتثبيت Fiber باستخدام ملف [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) أمر: @@ -134,12 +145,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 الميزات -- قوي [routing](https://docs.gofiber.io/routing) +- قوي [routing](https://docs.gofiber.io/guide/routing) - يقدم خدمة [static files](https://docs.gofiber.io/api/app#static) - أقصى [أداء](https://docs.gofiber.io/extra/benchmarks) - [ذاكرة منخفضة](https://docs.gofiber.io/extra/benchmarks) - [API endpoints](https://docs.gofiber.io/api/ctx) -- [Middleware](https://docs.gofiber.io/middleware) & [Next](https://docs.gofiber.io/api/ctx#next) مدعوم +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) مدعوم - [سريع](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming - [Template engines](https://github.com/gofiber/template) - [WebSocket دعم](https://github.com/gofiber/websocket) @@ -158,7 +169,7 @@ Fiber هو **مستوحى** من Express, إطار الويب الأكثر شع ** و تطوير **مهارات**! فقط مثل Express تفعل لـ JavaScript عالم. ## ⚠️ Limitations -* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.16 to 1.19. +* Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.20. * Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. ## 👀 أمثلة @@ -487,7 +498,7 @@ func main() { ### JSON Response -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json)
@@ -581,7 +592,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -657,6 +668,11 @@ Here is a list of middleware that are included within the Fiber framework. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | ## 🧬 External Middleware @@ -664,12 +680,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | Middleware | Description | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | | [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | | [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | @@ -731,7 +742,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_tr.md b/.github/README_tr.md index 4bd5136dfa..4b93d27a7b 100644 --- a/.github/README_tr.md +++ b/.github/README_tr.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -52,6 +57,12 @@ + + + + + +
@@ -63,10 +74,10 @@ - + - + @@ -105,13 +116,13 @@ func main() { Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tarafından gerçekleştirildi. Bütün sonuçları görmek için lütfen [Wiki](https://docs.gofiber.io/extra/benchmarks) sayfasını ziyaret ediniz.

- - + +

## ⚙️ Kurulum -Go'nun `1.14` sürümü ([indir](https://go.dev/dl/)) veya daha yüksek bir sürüm gerekli. +Go'nun `1.17` sürümü ([indir](https://go.dev/dl/)) veya daha yüksek bir sürüm gerekli. Bir dizin oluşturup dizinin içinde `go mod init github.com/your/repo` komutunu yazarak projenizi geliştirmeye başlayın ([daha fazla öğren](https://go.dev/blog/using-go-modules)). Ardından Fiber'ı kurmak için [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komutunu çalıştırın: @@ -121,7 +132,7 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 Özellikler -- Güçlü [routing](https://docs.gofiber.io/routing) +- Güçlü [routing](https://docs.gofiber.io/guide/routing) - [Statik dosya](https://docs.gofiber.io/api/app#static) sunumu - Olağanüstü [performans](https://docs.gofiber.io/extra/benchmarks) - [Düşük bellek](https://docs.gofiber.io/extra/benchmarks) kullanımı @@ -143,7 +154,7 @@ Fiber, internet üzerinde en popüler web framework'ü olan Express'ten **esinle ## ⚠️ Sınırlamalar -- Fiber unsafe kullanımı sebebiyle Go'nun son sürümüyle her zaman uyumlu olmayabilir. Fiber 2.40.0, Go 1.14 ile 1.19 sürümleriyle test edildi. +- Fiber unsafe kullanımı sebebiyle Go'nun son sürümüyle her zaman uyumlu olmayabilir. Fiber 2.40.0, Go 1.17 ile 1.20 sürümleriyle test edildi. - Fiber net/http arabirimiyle uyumlu değildir. Yani gqlgen veya go-swagger gibi net/http ekosisteminin parçası olan projeleri kullanamazsınız. ## 👀 Örnekler @@ -254,7 +265,7 @@ func main() { fmt.Println("🥇 İlk handler") return c.Next() }) - + // /api ile başlayan bütün routelara etki eder. app.Use("/api", func(c fiber.Ctx) error { fmt.Println("🥈 İkinci handler") @@ -431,7 +442,7 @@ func main() { ### JSON Yanıtları -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -515,7 +526,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -586,6 +597,11 @@ Fiber'a dahil edilen middlewareların bir listesi aşağıda verilmiştir. | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session için middleware. NOTE: Bu middleware Fiber'in Storage yapısını kullanır. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware'ı verilen koşul `true` olduğunda handler'ı atlar ve işlemez. | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Bir request için maksimum süre ekler ve aşılırsa ErrorHandler'a iletir. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware, key tabanlı bir authentication sağlar. | +| [redirect](https://github.com/gofiber/redirect) | Yönlendirme middleware 'ı. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware, sağlanan kurallara göre URL yolunu yeniden yazar. Geriye dönük uyumluluk için veya yalnızca daha temiz ve daha açıklayıcı bağlantılar oluşturmak için yardımcı olabilir. | +| [adaptor](https://github.com/gofiber/adaptor) | Fiber request handlerdan net/http handlerları için dönüştürücü, @arsmn'a özel teşekkürler! | +| [helmet](https://github.com/gofiber/helmet) | Çeşitli HTTP headerları ayarlayarak uygulamalarınızın güvenliğini sağlamaya yardımcı olur. | ## 🧬 Harici Middlewarelar @@ -593,14 +609,9 @@ Harici olarak barındırılan middlewareların modüllerinin listesi. Bu middlew | Middleware | Açıklama | | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [adaptor](https://github.com/gofiber/adaptor) | Fiber request handlerdan net/http handlerları için dönüştürücü, @arsmn'a özel teşekkürler! | -| [helmet](https://github.com/gofiber/helmet) | Çeşitli HTTP headerları ayarlayarak uygulamalarınızın güvenliğini sağlamaya yardımcı olur. | | [jwt](https://github.com/gofiber/jwt) | JWT, bir JSON Web Token \(JWT\) yetkilendirmesi döndüren middleware. | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware, key tabanlı bir authentication sağlar. | -| [redirect](https://github.com/gofiber/redirect) | Yönlendirme middleware 'ı. | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware, sağlanan kurallara göre URL yolunu yeniden yazar. Geriye dönük uyumluluk için veya yalnızca daha temiz ve daha açıklayıcı bağlantılar oluşturmak için yardımcı olabilir. | | [storage](https://github.com/gofiber/storage) | Fiber'in Storage yapısını destekleyen birçok storage driver'ı verir. Bu sayede depolama gerektiren Fiber middlewarelarında kolaylıkla kullanılabilir. | -| [template](https://github.com/gofiber/template) | Bu paket, Fiber `v2.x.x`, Go sürüm 1.14 veya üzeri gerekli olduğunda kullanılabilecek 9 template motoru içerir. | +| [template](https://github.com/gofiber/template) | Bu paket, Fiber `v2.x.x`, Go sürüm 1.17 veya üzeri gerekli olduğunda kullanılabilecek 9 template motoru içerir. | | [websocket](https://github.com/gofiber/websocket) | Yereller desteğiyle Fiber için Fasthttp WebSocket'a dayalıdır! | ## 🕶️ Awesome Listesi @@ -659,7 +670,6 @@ Telif (c) 2019-günümüz [Fenny](https://github.com/fenny) ve [katkıda bulunan - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_uk.md b/.github/README_uk.md new file mode 100644 index 0000000000..9556e924e1 --- /dev/null +++ b/.github/README_uk.md @@ -0,0 +1,715 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+ +

+ Fiber — це веб фреймворк, який був натхненний Express + і заснований на Fasthttp, найшвидшому HTTP-двигунові написаному на + Go. Фреймворк розроблено з метою спростити процес швидкої розробки + високопродуктивних веб-додатків з нульовим розподілом пам'яті. +

+ +## ⚡️ Швидкий старт + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Еталонні показники + +Тестування проводилося за допомогою [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) +та [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Якщо ви хочете побачити всі результати, будь ласка +відвідайте наш [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Встановлення + +Переконайтеся, що Go встановлено ([завантажити](https://go.dev/dl/)). Потрібна версія `1.17` або вища. + +Ініціалізуйте проект, створивши папку, а потім запустивши `go mod init github.com/your/repo` +([детальніше](https://go.dev/blog/using-go-modules)) всередині цієї папки. Далі встановіть Fiber за допомогою +команди [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Особливості + +- Надійна [маршрутизація](https://docs.gofiber.io/routing) +- Доступ до [статичних файлів](https://docs.gofiber.io/api/app#static) +- Екстремальна [продуктивність](https://docs.gofiber.io/extra/benchmarks) +- [Низький обсяг споживання пам'яті](https://docs.gofiber.io/extra/benchmarks) +- [Кінцеві точки API](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/middleware) та підтримка [Next](https://docs.gofiber.io/api/ctx#next) +- [Швидке](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) програмування на стороні сервера +- [Двигуни шаблонів](https://github.com/gofiber/template) +- [Підтримка WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) +- [Обмежувач швидкості](https://docs.gofiber.io/api/middleware/limiter) +- Документація доступна [18 мовами](https://docs.gofiber.io/) +- І багато іншого, [відвідайте наш Wiki](https://docs.gofiber.io/) + +## 💡 Філософія + +Нові програмісти, які переходять із [Node.js](https://nodejs.org/en/about/) на [Go](https://go.dev/doc/), мають справу зі звивистою кривою навчання, перш ніж можуть розпочати створення своїх веб-додатків або мікросервісів. Fiber, як **веб-фреймворк**, було створено з ідеєю **мінімалізму** та слідує **шляху UNIX**, щоб нові програмісти могли швидко увійти у світ Go з теплим та надійним прийомом. + +Fiber **натхненний** Express, найпопулярнішим веб-фреймворком в Інтернеті. Ми поєднали **легкість** Express і **чисту продуктивність** Go. Якщо ви коли-небудь реалізовували веб-додаток у Node.js (_з використанням Express або подібного_), то багато методів і принципів здадуться вам **дуже звичайними**. + +Ми **прислухаємося** до наших користувачів у [issues](https://github.com/gofiber/fiber/issues), Discord [сервері](https://gofiber.io/discord) та в інших місцях Інтернета, щоб створити **швидкий**, **гнучкий** та **доброзичливий** веб фреймворк на Go для **будь-яких** завдань, **дедлайнів** та **рівнів** розробників! Як це робить Express у світі JavaScript. + +## ⚠️ Обмеження + +- Через те, що Fiber використовує unsafe, бібліотека не завжди може бути сумісною з останньою версією Go. Fiber 2.40.0 було протестовано з Go версій 1.17 до 1.20. +- Fiber не сумісний з інтерфейсами net/http. Це означає, що ви не зможете використовувати такі проекти, як gqlgen, go-swagger або будь-які інші, які є частиною екосистеми net/http. + +## 👀 Приклади + +Нижче наведено деякі типові приклади. Якщо ви хочете переглянути більше прикладів коду, відвідайте наше [репозиторій рецептів](https://github.com/gofiber/recipes) або відвідайте нашу розміщену [документацію API](https://docs.gofiber.io). + +#### 📖 [**Основна маршрутизація**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Назви маршруту**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Обслуговування статичних файлів**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ 📚 Показати більше прикладів коду + +### Двигуни перегляду + +📖 [Конфігурація](https://docs.gofiber.io/api/fiber#config) +📖 [Двигуни](https://github.com/gofiber/template) +📖 [Рендер](https://docs.gofiber.io/api/ctx#render) + +Fiber за умовчанням використовує [html/template](https://pkg.go.dev/html/template/), якщо жодного двигуна не було вказано. + +Якщо ви хочете виконати частково або використовувати інший двигун, наприклад [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache]( https://github.com/cbroglie/mustache) або [jade](https://github.com/Joker/jade), тощо. + +Перегляньте наш пакет [Шаблон](https://github.com/gofiber/template), який підтримує кілька двигунів перегляду. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Групування маршрутів у ланцюги + +📖 [Група](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} +``` + +### Middleware логування + +📖 [Логування](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Спільне використання ресурсів між джерелами (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Перевірте CORS, передавши будь-який домен у заголовку `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Власна відповідь 404 + +📖 [HTTP Методи](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Відповідь + +📖 [JSON](https://docs.gofiber.io/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [Більше інформації](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Використання довіреного проксі + +📖 [Конфігурація](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Внутрішні Middleware + +Ось список middleware, яке входить до складу Fiber фреймворку. + +| Middleware | Опис | +|:---------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Middleware який забезпечує базову автентифікацію по HTTP. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Middleware який перехоплює та кешує відповіді | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | стиснення для Fiber, воно за замовчуванням підтримує `deflate`, `gzip` і `brotli`. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Middleware який вмикає перехресне використання ресурсів \(CORS\) із різними параметрами. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Захист від експлойтів CSRF. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Шифрування значень файлів cookie. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Middleware для відкриття змінних середевищ. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middleware яке робить кеш-пам’ять більш ефективним і заощаджує пропускну здатність, оскільки веб-серверу не потрібно повторно надсилати повну відповідь, якщо вміст не змінився. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Middleware який обслуговує доступні варіанти середовища виконання HTTP у форматі JSON. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ігнорування значка із журналів або обслуговувати з пам’яті, якщо вказано шлях до файлу. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Middleware файлової системи, особлива подяка та кредити Alireza Salary. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Ообмеження швидкості для Fiber. Використовуйте для обмеження повторних запитів до загальнодоступних API та/або кінцевих точок, таких як скидання пароля. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Реєстратор запитів/відповідей HTTP. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware який повідомляє показники сервера. | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Особлива подяка Метью Лі \(@mthli\) . | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Дозволяє надсилати проксі-запити до кількох серверів. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware який відновлює паніки будь-де в ланцюжку стека та передає керування централізованому [обробнику помилок](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | До кожного запиту додає ідентифікатор запиту. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware для сеансів. ПРИМІТКА: Цей middleware використовує наш пакет зберігання. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Middleware який пропускає упакований обробник, якщо предикат є істинним. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Додає максимальний час для запиту та пересилає до ErrorHandler, якщо його перевищено. | +| [keyauth](https://github.com/gofiber/keyauth) | Middleware для автентифікації по ключам. | +| [redirect](https://github.com/gofiber/redirect) | Middleware для перенаправлення. | +| [rewrite](https://github.com/gofiber/rewrite) | Middleware для перезапису URL-адреси на основі наданих правил. | +| [adaptor](https://github.com/gofiber/adaptor) | Конвентор для обробників net/http до/з обробників запитів Fiber, особлива подяка @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Допомагає захистити ваші програми, встановлюючи різні заголовки HTTP. | + +## 🧬 Зовнішні Middleware + +Список зовнішніх middleware модулів, які підтримуються [командою Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Опис | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT повертає middleware автентифікації JSON Web Token \(JWT\). | +| [storage](https://github.com/gofiber/storage) | Драйвер зберігання який може використовуватися в різних middleware. | +| [template](https://github.com/gofiber/template) | Цей пакет містить 8 модулів шаблонів, які можна використовувати з Fiber `v1.10.x` Потрібно версія Go 1.13 або новішу. | +| [websocket](https://github.com/gofiber/websocket) | На основі Fasthttp WebSocket для Fiber з підтримкою місцевих користувачів! | + +## 🕶️ Чудовий список + +Більше статей, middleware, прикладів або інструментів дивіться у нашому [чудовому списку](https://github.com/gofiber/awesome-fiber). + +## 👍 Внести свій внесок + +Якщо ви хочете сказати **дякую** та/або підтримати активний розвиток `Fiber`: + +1. Додайте [зірку GitHub](https://github.com/gofiber/fiber/stargazers) до проекту. +2. Напишіть про проект [у своєму Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Напишіть огляд або підручник на [Medium](https://medium.com/), [Dev.to](https://dev.to/) або особистому блогу. +4. Підтримайте проект, пожертвувавши [чашку кави](https://buymeacoff.ee/fenny). + +## ☕ Прихильники + +Fiber – це проект із відкритим вихідним кодом, який працює за рахунок пожертвувань для оплати рахунків, наприклад наше доменне ім’я, gitbook, netlify і безсерверний хостинг. Якщо ви хочете підтримати Fiber, ви можете ☕ [**купити каву тут**](https://buymeacoff.ee/fenny). + +| | Користувач | Пожертвування | +| :--------------------------------------------------------- | :----------------------------------------------- | :------------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Автори коду + +Code Contributors + +## ⭐️ Звіздарі + +Stargazers over time + +## ⚠️ Ліцензія + +Авторське право (c) 2019-дотепер [Fenny](https://github.com/fenny) та [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` це безкоштовне програмне забезпечення з відкритим вихідним кодом, ліцензоване згідно [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Офіційний логотип створено [Vic Shóstak](https://github.com/koddr) і поширюється під [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) ліцензією (CC BY-SA 4.0 International). + +**Ліцензії сторонніх бібліотек** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md index 4c7fd347e9..d9122531d9 100644 --- a/.github/README_zh-CN.md +++ b/.github/README_zh-CN.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,12 @@ + + + + + +
@@ -66,10 +77,10 @@ - + - + @@ -107,13 +118,13 @@ func main() { 这些测试由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web](https://github.com/smallnest/go-web-framework-benchmark) 完成。如果您想查看所有结果,请访问我们的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 。

- - + +

## ⚙️ 安装 -确保已安装 `1.14` 或更高版本的 Go ([下载](https://go.dev/dl/))。 +确保已安装 `1.17` 或更高版本的 Go ([下载](https://go.dev/dl/))。 通过创建文件夹并在文件夹内运行 `go mod init github.com/your/repo` ([了解更多](https://go.dev/blog/using-go-modules)) 来初始化项目,然后使用 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 命令安装 Fiber: @@ -123,12 +134,12 @@ go get -u github.com/gofiber/fiber/v3 ## 🎯 特点 -- 强大的[路由](https://docs.gofiber.io/routing) +- 强大的[路由](https://docs.gofiber.io/guide/routing) - [静态文件](https://docs.gofiber.io/api/app#static)服务 - 极致[性能](https://docs.gofiber.io/extra/benchmarks) - [低内存占用](https://docs.gofiber.io/extra/benchmarks) - [API 接口](https://docs.gofiber.io/api/ctx) -- 支持[中间件](https://docs.gofiber.io/middleware)和 [Next](https://docs.gofiber.io/api/ctx#next) +- 支持[中间件](https://docs.gofiber.io/category/-middleware)和 [Next](https://docs.gofiber.io/api/ctx#next) - [快速上手](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) - [模版引擎](https://github.com/gofiber/template) - [支持 WebSocket](https://github.com/gofiber/websocket) @@ -139,7 +150,7 @@ go get -u github.com/gofiber/fiber/v3 ## 💡 哲学 -从 [Node.js](https://nodejs.org/en/about/) 切换到 [Go](https://go.dev/doc/) 的新 `gopher` 在开始构建 `Web` +从 [Node.js](https://nodejs.org/en/about/) 切换到 [Go](https://go.dev/doc/) 的新 `gopher` 在开始构建 `Web` 应用程序或微服务之前需要经历一段艰难的学习过程。 而 `Fiber`,一个基于**极简主义**并且遵循 **UNIX 方式**创建的 **Web 框架**, 使新的 `gopher` 可以在热烈和可信赖的欢迎中迅速进入 `Go` 的世界。 @@ -149,7 +160,7 @@ go get -u github.com/gofiber/fiber/v3 以及在互联网上的所有诉求,为了创建一个能让有着任何技术栈的开发者都能在 deadline 前完成任务的**迅速**,**灵活**以及**友好**的 `Go web` 框架,就像 `Express` 在 `JavaScript` 世界中一样。 ## ⚠️ 限制 -* 由于 Fiber 使用了 unsafe 特性,导致其可能与最新的 Go 版本不兼容。Fiber 2.40.0 已经在 Go 1.16 到 1.19 上测试过。 +* 由于 Fiber 使用了 unsafe 特性,导致其可能与最新的 Go 版本不兼容。Fiber 2.40.0 已经在 Go 1.17 到 1.20 上测试过。 * Fiber 与 net/http 接口不兼容。也就是说你无法直接使用例如 gqlen,go-swagger 或者任何其他属于 net/http 生态的项目。 ## 👀 示例 @@ -249,7 +260,7 @@ func main() { ``` -#### 📖 [**中间件**](https://docs.gofiber.io/middleware)和 [**Next**](https://docs.gofiber.io/api/ctx#next) +#### 📖 [**中间件**](https://docs.gofiber.io/category/-middleware)和 [**Next**](https://docs.gofiber.io/api/ctx#next) ```go func main() { @@ -439,7 +450,7 @@ func main() { ### JSON 响应 -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -523,7 +534,7 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) @@ -594,6 +605,11 @@ func main() { | [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session 中间件. 注意: 此中间件使用了我们的存储包. | | [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip 中间件会在判断条为 true 时忽略此次请求 | | [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | 添加请求的最大时间,如果超时则发送给ErrorHandler 进行处理. | +| [adaptor](https://github.com/gofiber/adaptor) | net/http 处理程序与 Fiber 请求处理程序之间的转换器,特别感谢 @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | 通过设置各种 HTTP 头帮助保护您的应用程序 | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中间件提供基于密钥的身份验证 | +| [redirect](https://github.com/gofiber/redirect) | 用于重定向请求的中间件 | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite 中间件根据提供的规则重写URL路径。它有助于向后兼容或者创建更清晰、更具描述性的链接 | ## 🧬 外部中间件 @@ -601,12 +617,7 @@ func main() { | 中间件 | 描述 | |:--------------------------------------------------|:-------------------------------------------------------------------------------------------| -| [adaptor](https://github.com/gofiber/adaptor) | net/http 处理程序与 Fiber 请求处理程序之间的转换器,特别感谢 @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | 通过设置各种 HTTP 头帮助保护您的应用程序 | | [jwt](https://github.com/gofiber/jwt) | JWT 返回一个 JSON Web Token\(JWT\) 身份验证中间件 | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中间件提供基于密钥的身份验证 | -| [redirect](https://github.com/gofiber/redirect) | 用于重定向请求的中间件 | -| [rewrite](https://github.com/gofiber/rewrite) | Rewrite 中间件根据提供的规则重写URL路径。它有助于向后兼容或者创建更清晰、更具描述性的链接 | | [storage](https://github.com/gofiber/storage) | 包含实现 Storage 接口的数据库驱动,它的设计旨在配合 fiber 的其他中间件来进行使用 | | [template](https://github.com/gofiber/template) | 该中间件包含 8 个模板引擎,可与 Fiber `v1.10.x` Go 1.13或更高版本一起使用 | | [websocket](https://github.com/gofiber/websocket) | 基于 Fasthttp WebSocket for Fiber 实现,支持使用 [Locals](https://docs.gofiber.io/api/ctx#locals) ! | @@ -669,7 +680,6 @@ Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors]( - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/README_zh-TW.md b/.github/README_zh-TW.md index 1096fb2f47..84ea8c4f5c 100644 --- a/.github/README_zh-TW.md +++ b/.github/README_zh-TW.md @@ -1,6 +1,11 @@

- Fiber + + + + Fiber + +
@@ -55,6 +60,15 @@ + + + + + + + + +
@@ -66,10 +80,10 @@ - + - + @@ -77,9 +91,10 @@ +

- Fiber是移植NodeJS的Express框架改以Go語言編寫。本套件基於Fasthttp,Fasthttp有不分配記憶體空間Request Pool的特性,在網路效能方面有著顯著的效能。 + Fiber 是款啟發自 ExpressWeb 框架,建基於 Fasthttp——Go最快的 HTTP 引擎。設計旨在 減輕 快速開發的負擔,兼顧 零記憶體分配效能

## ⚡️ 快速入門 @@ -100,28 +115,35 @@ func main() { } ``` -## 🤖 效能 +## 🤖 效能評定結果 -本測試使用[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)和[Go Web 框架效能測試](https://github.com/smallnest/go-web-framework-benchmark)。如果要看全部的執行結果,請到[Wiki](https://docs.gofiber.io/extra/benchmarks) 。 +這些測試由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web 框架效能測試](https://github.com/smallnest/go-web-framework-benchmark) 完成。若需參閱所有結果,請參閱我們的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 資訊。

- - + +

## ⚙️ 安裝 -確保已安裝 Go 版本 `1.14` 或以上 ([下載](https://go.dev/dl/))。 +先確定您已經安裝 `1.17` 或更新版本的 Go([點此下載](https://go.dev/dl/))。 -建立文件夾並在文件夾內執行 `go mod init github.com/your/repo` ([了解更多](https://go.dev/blog/using-go-modules)) 指令建立專案,然後使用 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 指令下載 fiber : +要初始化專案,首先建立檔案夾,然後在檔案夾中執行 `go mod init github.com/名稱/儲存庫`([深入了解](https://go.dev/blog/using-go-modules))。接著,使用 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 命令安裝 Fiber: ```bash go get -u github.com/gofiber/fiber/v3 ``` -## 🎯 功能 +## 🎯 特色 -- 強大的[路由](https://docs.gofiber.io/routing) +- 強固的[路由系統](https://docs.gofiber.io/guide/routing) +- 可以寄存[靜態檔案](https://docs.gofiber.io/api/app#static) +- 疾速[效能](https://docs.gofiber.io/extra/benchmarks) +- 相當低的[記憶體使用量](https://docs.gofiber.io/extra/benchmarks) +- [API 端點](https://docs.gofiber.io/api/ctx) +- 支援 [中介模組](https://docs.gofiber.io/category/-middleware) 和 [接續函式 (Next)](https://docs.gofiber.io/api/ctx#next) +- [迅速開發](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) 伺服器端服務 +- 強大的[路由](https://docs.gofiber.io/guide/routing) - [靜態檔案](https://docs.gofiber.io/api/app#static)服務 - [超快速](https://docs.gofiber.io/extra/benchmarks) - [佔用很少記憶體](https://docs.gofiber.io/extra/benchmarks) @@ -129,31 +151,30 @@ go get -u github.com/gofiber/fiber/v3 - 支援中介器和[下一步](https://docs.gofiber.io/api/ctx#next) - [立即上手](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) - [樣板引擎](https://github.com/gofiber/template) -- 支援[WebSocket](https://github.com/gofiber/websocket) -- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) -- 支援[限速](https://docs.gofiber.io/api/middleware/limiter) -- 被翻譯成[18種語言](https://docs.gofiber.io/) -- 豐富的[文件](https://docs.gofiber.io/) +- [支援 WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) +- 支援[速率限制](https://docs.gofiber.io/api/middleware/limiter) +- 有 [18 門語言](https://docs.gofiber.io/)的翻譯 +- 還有很多功能,[開始探索 Fiber](https://docs.gofiber.io/) -## 💡 理念 +## 💡 設計哲學 -不少[Node.js](https://nodejs.org/en/about/)的工程師跳到[Go](https://go.dev/doc/)必須學習一些知識,因此做了一個跟 Express 一樣的 Fiber 省這些麻煩。設計還是照原本的**極簡主義**還有遵循**UNIX 慣例**,因此新手們可以**無痛**迅速進入 Go 的世界。 +從 [Node.js](https://nodejs.org/en/about/) 轉到 [Go](https://go.dev/doc/) 的新進 Go 開發者,得先面對 Go 的各種知識點,才能開始建構自己的 Web 應用程式或微服務。Fiber 作為一款 **Web 框架**,設計之初便以 **極簡主義** 為理念,並遵循 **UNIX 之道**,讓新進 Go 開發者能夠快速隨著友善且值得信賴的社群,進入 Go 的世界。 -Fiber **受到** 網路上最流行的 Web 框架 ExpressJS**啟發**,結合 Express 的**易用性**和 Go 的**高效能**。若你之前用過 Node.js 寫 Web 應用(_使用 ExpressJS/Koa 或類似工具_),那你已經**上手**了。 +Fiber **啟發自** Express——網際網路上最知名的 Web 框架,我們將 Express 的 **易用性** 和 Go 的 **原始效能** 結合在一起。如果您曾經在 Node.js(使用 Express 或類似框架)實作過 Web 應用程式,那麼許多方法和開發準則,將讓您感到 **無比熟悉**。 -有什麼問題請發[issues](https://github.com/gofiber/fiber/issues)或加入 Discord [channel](https://gofiber.io/discord)討論,我們想要創造**快速**、**彈性**、**友善**的社群給**任何人**使用!就像 Express 那樣。 +我們 **傾聽** 使用者在 [Issues](https://github.com/gofiber/fiber/issues)、Discord [群組](https://gofiber.io/discord) 和 **網路上任何角落** 的意見和建議,製作出 **快速**、**靈活** 且 **易於上手** 的 Go Web 框架,來應對**任何**工作、**繳件期限**以及開發者的**能力區間**——如同 Express 在 JavaScript 世界所扮演的角色一樣! -## 限制 -* 由於 Fiber 使用了 unsafe,該庫可能並不總是與最新的 Go 版本兼容。 Fiber 2.40.0 已經用 Go 版本 1.16 到 1.19 進行了測試。 -* Fiber 與 net/http 接口不兼容。 這意味著您將無法使用 gqlgen、go-swagger 或任何其他屬於 net/http 生態系統的項目。 +## ⚠️ 限制 -## 👀 範例 +- 由於 Fiber 有用到 Unsafe,本函式庫有時可能無法相容最新版的 Go 語言。Fiber 2.40.0 已在 Go 1.17 至 1.20 的版本測試過。 +- Fiber 不相容 net/http 的介面,意味著您無法使用像是 gqlgen、go-swagger 或其他任何屬於 net/http 生態系統的專案。 -以下是一些常見範例。 +## 👀 範例 -> 更多程式碼在[範例專案](https://github.com/gofiber/recipes)中或直接看[API 文件](https://docs.gofiber.io)。 +下方列出一些常見範例。如果您想查看更多程式碼範例,請參閱我們的 [Recipes 儲存庫](https://github.com/gofiber/recipes),或前往我們提供的 [API 文件](https://docs.gofiber.io)。 -#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) +#### 📖 [**基礎路由**](https://docs.gofiber.io/#basic-routing) ```go func main() { @@ -167,8 +188,8 @@ func main() { // GET /flights/LAX-SFO app.Get("/flights/:from-:to", func(c fiber.Ctx) error { - msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) - return c.SendString(msg) // => 💸 From: LAX, To: SFO + msg := fmt.Sprintf("💸 從:%s,到:%s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 從:LAX,到:SFO }) // GET /dictionary.txt @@ -179,14 +200,14 @@ func main() { // GET /john/75 app.Get("/:name/:age/:gender?", func(c fiber.Ctx) error { - msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) - return c.SendString(msg) // => 👴 john is 75 years old + msg := fmt.Sprintf("👴 %s 已經 %s 歲了", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john 已經 75 歲了 }) // GET /john - app.Get("/:name", func(c fiber.Ctx) error { - msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) - return c.SendString(msg) // => Hello john 👋! + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("哈囉,%s 👋!", c.Params("name")) + return c.SendString(msg) // => 哈囉,john 👋! }) log.Fatal(app.Listen(":3000")) @@ -194,7 +215,7 @@ func main() { ``` -#### 📖 [**路線命名**](https://docs.gofiber.io/api/app#name) +#### 📖 [**路由命名**](https://docs.gofiber.io/api/app#name) ```go func main() { @@ -208,7 +229,7 @@ func main() { data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") fmt.Print(string(data)) - // Prints: + // 會輸出: // { // "method": "GET", // "name": "api", @@ -224,7 +245,7 @@ func main() { ``` -#### 📖 [**提供靜態文件**](https://docs.gofiber.io/api/app#static) +#### 📖 [**寄存靜態檔案**](https://docs.gofiber.io/api/app#static) ```go func main() { @@ -246,47 +267,49 @@ func main() { ``` -#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) +#### 📖 [**中介模組和接續函式 (Next)**](https://docs.gofiber.io/api/ctx#next) ```go func main() { - app := fiber.New() - - // Match any route - app.Use(func(c fiber.Ctx) error { - fmt.Println("🥇 First handler") - return c.Next() - }) - - // Match all routes starting with /api - app.Use("/api", func(c fiber.Ctx) error { - fmt.Println("🥈 Second handler") - return c.Next() - }) - - // GET /api/register - app.Get("/api/list", func(c fiber.Ctx) error { - fmt.Println("🥉 Last handler") - return c.SendString("Hello, World 👋!") - }) - - log.Fatal(app.Listen(":3000")) + app := fiber.New() + + // 符合任何路由 + app.Use(func(c fiber.Ctx) error { + fmt.Println("🥇 第一個處理常式") + return c.Next() + }) + + // 符合所有 /api 開頭的路由 + app.Use("/api", func(c fiber.Ctx) error { + fmt.Println("🥈 第二個處理常式") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c fiber.Ctx) error { + fmt.Println("🥉 最後一個處理常式") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) } ```
- 📚 顯示更多範例 + 📚 展示更多程式碼範例 -### 界面引擎 +### 檢視引擎 -📖 [設定](https://docs.gofiber.io/api/fiber#config) +📖 [組態設定](https://docs.gofiber.io/api/fiber#config) 📖 [引擎](https://github.com/gofiber/template) -📖 [渲染](https://docs.gofiber.io/api/ctx#render) +📖 [轉譯 (render)](https://docs.gofiber.io/api/ctx#render) -當不指定樣板引擎時 Fiber 預設用[html/template](https://pkg.go.dev/html/template/)。 +若未指定檢視引擎,Fiber 預設採用 [html/template](https://pkg.go.dev/html/template/)。 -如果你想要執行部份或用別的樣板引擎[amber](https://github.com/eknkc/amber)、[handlebars](https://github.com/aymerick/raymond)、[mustache](https://github.com/cbroglie/mustache)、[pug](https://github.com/Joker/jade)之類…請參考符合多樣板引擎的[樣板](https://github.com/gofiber/template)套件。 +如果您想執行部分檢視 (partials),或者是使用如 [amber](https://github.com/eknkc/amber)、[handlebars](https://github.com/aymerick/raymond)、[mustache](https://github.com/cbroglie/mustache) 或 [pug](https://github.com/Joker/jade) 等等不同的引擎…… + +請參考我們的 [Template](https://github.com/gofiber/template) 套件——它支援多種檢視引擎。 ```go package main @@ -297,12 +320,12 @@ import ( ) func main() { - // You can setup Views engine before initiation app: + // 您可以在 app 初始化前設定檢視 (Views) 引擎: app := fiber.New(fiber.Config{ Views: pug.New("./views", ".pug"), }) - // And now, you can call template `./views/home.pug` like this: + // 現在,您可以用下面這種方式呼叫 `./views/home.pug` 樣板: app.Get("/", func(c fiber.Ctx) error { return c.Render("home", fiber.Map{ "title": "Homepage", @@ -312,16 +335,15 @@ func main() { log.Fatal(app.Listen(":3000")) } - ``` -### 鏈式路線分組 +### 組合路由鏈 -📖 [Group](https://docs.gofiber.io/api/app#group) +📖 [分組](https://docs.gofiber.io/api/app#group) ```go func middleware(c fiber.Ctx) error { - fmt.Println("Don't mind me!") + fmt.Println("不要理我!") return c.Next() } @@ -332,15 +354,15 @@ func handler(c fiber.Ctx) error { func main() { app := fiber.New() - // Root API route + // 根 API 路由 api := app.Group("/api", middleware) // /api - // API v1 routes + // API v1 路由 v1 := api.Group("/v1", middleware) // /api/v1 v1.Get("/list", handler) // /api/v1/list v1.Get("/user", handler) // /api/v1/user - // API v2 routes + // API v2 路由 v2 := api.Group("/v2", middleware) // /api/v2 v2.Get("/list", handler) // /api/v2/list v2.Get("/user", handler) // /api/v2/user @@ -350,9 +372,9 @@ func main() { ``` -### 中介器 logger +### 中介模組記錄器 -📖 [Logger](https://docs.gofiber.io/api/middleware/logger) +📖 [記錄器](https://docs.gofiber.io/api/middleware/logger) ```go package main @@ -375,7 +397,7 @@ func main() { } ``` -### 跨網域資源共享 (CORS) +### 跨原始來源資源共用 (CORS) 📖 [CORS](https://docs.gofiber.io/api/middleware/cors) @@ -398,15 +420,15 @@ func main() { } ``` -在`Origin` header 中放網域來檢查 CORS: +在 `Origin` 標頭傳入任何網域來檢查 CORS 的效果: ```bash curl -H "Origin: http://example.com" --verbose http://localhost:3000 ``` -### 客制 404 回應 +### 自訂 404 回應 -📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) +📖 [HTTP 方法](https://docs.gofiber.io/api/ctx#status) ```go func main() { @@ -422,7 +444,7 @@ func main() { return c.SendString("Welcome!") }) - // Last middleware to match anything + // 最後一個中介模組,符合所有條件 app.Use(func(c fiber.Ctx) error { return c.SendStatus(404) // => 404 "Not Found" @@ -434,7 +456,7 @@ func main() { ### JSON 回應 -📖 [JSON](https://docs.gofiber.io/ctx#json) +📖 [JSON](https://docs.gofiber.io/api/ctx#json) ```go type User struct { @@ -462,7 +484,7 @@ func main() { } ``` -### WebSocket 升級 +### WebSocket 升級 (Upgrade) 📖 [Websocket](https://github.com/gofiber/websocket) @@ -498,7 +520,7 @@ func main() { ### Server-Sent Events -📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) +📖 [更多資訊](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) ```go import ( @@ -518,11 +540,11 @@ func main() { c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { fmt.Println("WRITER") var i int - + for { i++ - msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) - fmt.Fprintf(w, "data: Message: %s\n\n", msg) + msg := fmt.Sprintf("%d - 目前時間為 %v", i, time.Now()) + fmt.Fprintf(w, "data: 訊息: %s\n\n", msg) fmt.Println(msg) w.Flush() @@ -537,132 +559,157 @@ func main() { } ``` -### Recover 中介器 +### Recover 中介模組 📖 [Recover](https://docs.gofiber.io/api/middleware/recover) ```go import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/recover" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/recover" ) func main() { - app := fiber.New() + app := fiber.New() - app.Use(recover.New()) + app.Use(recover.New()) - app.Get("/", func(c fiber.Ctx) error { - panic("normally this would crash your app") - }) + app.Get("/", func(c fiber.Ctx) error { + panic("正常來說,這會導致 app 當機") + }) + + log.Fatal(app.Listen(":3000")) +} +``` - log.Fatal(app.Listen(":3000")) +
+ +### 使用信任的代理伺服器 + +📖 [組態設定](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP 地址或 IP 地址區間 + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) } ``` -## 🧬 内部中間件 -以下為包含在Fiber框架中的中間件列表. - -| 中間件 | 描述 | -| :------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | 基本身份驗證中間件提供 HTTP 基本身份驗證。 它為有效憑據調用下一個處理程序,並為丟失或無效憑據調用 401 Unauthorized。 | -| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | 攔截和緩存響應 | -| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | CFiber的壓縮中間件,默認支持`deflate`、`gzip`和`brotli`。 | -| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | 使用各種選項啟用跨域資源共享 \(CORS\)。 | -| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | 防止 CSRF 漏洞利用。 | -| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | -| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | -| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches 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/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | -| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | 如果提供了文件路徑,則忽略日誌中的網站圖標或從內存中提供服務。 | -| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | 用於 Fiber 的 FileSystem 中間件,特別感謝 Alireza Salary | -| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Fiber 的限速中間件。 用於限制對公共 API 和/或端點的重複請求,例如密碼重置。 | -| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP 請求/響應 logger. | -| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | -| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | 特別感謝 Matthew Lee \(@mthli\) | -| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | 允許您將請求代理到多個服務器 | -| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | 恢復中間件從堆棧鏈中任何地方的恐慌中恢復,並將控制權交給集中式[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | -| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | 為每個請求添加一個 requestid。 | -| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | -| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | -| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | 添加請求的最大時間,如果超過則轉發給 ErrorHandler。 | - -## 🧬 外部中間件 - -由 [Fiber 團隊] (https://github.com/orgs/gofiber/people) 維護的外部託管中間件模塊列表。 - -| Middleware | Description | -| :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [adaptor](https://github.com/gofiber/adaptor) | net/http 處理程序與 Fiber 請求處理程序之間的轉換器,特別感謝 @arsmn! | -| [helmet](https://github.com/gofiber/helmet) | 通過設置各種 HTTP 標頭來幫助保護您的應用程序。 | -| [jwt](https://github.com/gofiber/jwt) | JWT 返回一個 JSON Web Token \(JWT\) 身份驗證中間件。 | -| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中間件提供基於密鑰的身份驗證。 | -| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | -| [rewrite](https://github.com/gofiber/rewrite) | 重寫中間件根據提供的規則重寫 URL 路徑。 它有助於向後兼容或只是創建更清晰和更具描述性的鏈接。 | -| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | -| [template](https://github.com/gofiber/template) |該軟件包包含 8 個模板引擎,可用於 Fiber `v1.10.x` Go 版本 1.13 或更高版本。 | -| [websocket](https://github.com/gofiber/websocket) | 基於 Fasthttp WebSocket for Fiber,支持 Locals! | +## 🧬 內建中介模組 + +這裡列出了 Fiber 框架內建的中介模組。 + +| 中介模組 | 描述 | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | 提供 HTTP Basic 認證的基本認證中介模組。如果憑證有效,則會呼叫接續函式 (next);如果沒有憑證或失效,則回傳 401 Unauthorized。 | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | 攔截並快取回應。 | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | 適用於 Fiber 的壓縮中介模組。預設支援 `deflate`、`gzip` 和 `brotli`。 | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | 啟用跨來源資源共用 (CORS),可調整多種選項。 | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | 保護資源防止 CSRF 利用。 | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | 加密中介模組,會將 Cookie 的值進行加密。 | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | 公開環境變數,並提供可調整設定。 | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag 中介模組,讓快取更高效,同時因為 Web 伺服器不需要在內容未更動時重新傳送完整請求,因此可以減少流量使用。 | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar 中介模組,透過其 HTTP 伺服器執行階段,提供 JSON 格式的公用變數。 | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | 不輸出 Favicons 請求記錄;若有提供檔案路徑,則從記憶體提供圖示。 | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | 適用於 Fiber 的檔案系統中介模組。特別銘謝 Alireza Salary! | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | 適用於 Fiber 的速率限制中介模組。用來限制傳入公開 API 或者(以及)端點(如密碼重設)的重複請求。 | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP 請求/回應記錄工具。 | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | 監控中介模組,用來回報伺服器指標。啟發自 express-status-monitor。 | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | 特別感謝 Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | 讓您可以將請求代理 (proxy) 至多台伺服器。 | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover 中介模組:可以從呼叫堆疊鏈中任何部分的當機 (panic) 中復原,並將控制權交由中央的 [錯誤處理常式 (ErrorHandler)](https://docs.gofiber.io/guide/error-handling) 處理。 | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | 為每個請求加上 requestid。 | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | 連線階段中介模組。注意:這個中介模組有用到我們的 Storage 套件。 | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | 略過中介模組,會在條件成立時略過封裝過的處理常式。 | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | 為請求加上最長時限,並在逾時後轉送至錯誤處理常式 (ErrorHandler)。 | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中介模組提供以金鑰為基礎的認證模式。 | +| [redirect](https://github.com/gofiber/redirect) | 用來重新導向的中介模組。 | +| [rewrite](https://github.com/gofiber/rewrite) | 重寫 (Rewrite) 中介模組:根據提供規則重寫 URL 路徑,適合用來向後相容,或者是製作更乾淨且更好懂的連結。 | +| [adaptor](https://github.com/gofiber/adaptor) | 將 net/http 處理常式轉換至 Fiber 處理常式,或者是反著做。特別感謝 @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | 透過設定多種 HTTP 標頭,協助保護您應用程式的安全。 | + +## 🧬 外掛中介模組 + +這裡列出由 [Fiber 團隊](https://github.com/orgs/gofiber/people) 維護、存放在外部的中介模組。 + +| 中介模組 | 描述 | +| :------------------------------------------------ | :----------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT 回傳 JSON Web Token \(JWT\) 認證中介模組。 | +| [storage](https://github.com/gofiber/storage) | 已經做好,實作 Storage 介面的儲存區驅動模組,設計用來與各種 Fiber 中介模組搭配使用。 | +| [template](https://github.com/gofiber/template) | 本套件包含 8 種樣板引擎,可以和 Fiber `v1.10.x` 一起使用。需要 Go 1.13 或更新版本。 | +| [websocket](https://github.com/gofiber/websocket) | 適用於 Fiber,建基於 Fasthttp 的 WebSocket。支援本機空間 (Locals)! | ## 🕶️ Awesome List -For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). +更多文章、中介模組、範例或工具,請參考我們的 [Awesome List](https://github.com/gofiber/awesome-fiber)。 ## 👍 貢獻 -如果您要說聲**謝謝**或支援`Fiber`的積極發展: +如果您想和我們 **道謝**,或者是支持 `Fiber` 繼續積極開發下去(也可以兩個都做): -1. 點擊[GitHub Star](https://github.com/gofiber/fiber/stargazers)關注本專案。 -2. 在[Twitter](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)轉[推](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)。 -3. 在[Medium](https://medium.com/)、[Dev.to](https://dev.to/)、部落格上發表意見或教學。 -4. 贊助我們[一杯咖啡](https://buymeacoff.ee/fenny)。 +1. 送給專案一顆 [GitHub 星星](https://github.com/gofiber/fiber/stargazers)。 +2. [在您的 Twitter 上](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)發出關於本專案的推文。 +3. 在 [Medium](https://medium.com/)、[Dev.to](https://dev.to/) 或者是個人部落格上寫下評論或教學。 +4. 捐專案 [一杯咖啡的費用](https://buymeacoff.ee/fenny) 以示支持。 ## ☕ 支持者 -Fiber 是一個以贊助維生的開源專案,像是: 網域、gitbook、netlify、serverless 伺服器。如果你想贊助也可以 ☕ [**買杯咖啡**](https://buymeacoff.ee/fenny) - -| | User | Donation | -| :--------------------------------------------------------- | :----------------------------------------------- | :------- | -| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | -| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | -| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | -| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | -| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | - -## ‎‍💻 貢獻者 +Fiber 是個仰賴捐款的開放原始碼專案——用來支付如域名、Gitbook、Netlify 和無服務器運算服務的費用。如果您想支持 Fiber,可以在 ☕ [**這裡捐杯咖啡**](https://buymeacoff.ee/fenny)。 + +| | 使用者 | 捐款 | +| :--------------------------------------------------------- | :----------------------------------------------- | :------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 程式碼貢獻者 Code Contributors -## ⭐️ Stargazers +## ⭐️ 收藏數 Stargazers over time -## ⚠️ 授權 +## ⚠️ 授權條款 -版權所有 (c) 2019 年至今 [Fenny](https://github.com/fenny) 和 [貢獻者](https://github.com/gofiber/fiber/graphs/contributors)。 `Fiber` 是根據 [MIT 許可證] (https://github.com/gofiber/fiber/blob/master/LICENSE) 許可的免費開源軟件。 官方徽標由 [Vic Shóstak](https://github.com/koddr) 創建並在 [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) 許可下分發 (CC BY- SA 4.0 國際)。 +著作權所有 (c) 2019-現在 [Fenny](https://github.com/fenny) 和[貢獻者](https://github.com/gofiber/fiber/graphs/contributors)。`Fiber` 是款依照 [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE) 授權,免費且開放原始碼的軟體。官方圖示 (logo) 由 [Vic Shóstak](https://github.com/koddr) 製作,並依據 [創用 CC](https://creativecommons.org/licenses/by-sa/4.0/) 授權條款散佈 (CC BY-SA 4.0 International)。 -**Third-party library licenses** +**第三方函式庫的授權條款** - [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) - [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) - [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) - [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) - [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) -- [dictpool](https://github.com/savsgio/dictpool/blob/master/LICENSE) - [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) - [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) - [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 30d08a5cf0..9d4826fe02 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -20,7 +20,7 @@ The table below shows the supported versions for Fiber which include security up **DO NOT CREATE AN ISSUE** to report a security problem. Instead, please send us an e-mail at `team@gofiber.io` or join our discord server via -[this invite link](https://discord.gg/bSnH7db) and send a private message +[this invite link](https://gofiber.io/discord) and send a private message to Fenny or any of the maintainers. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..ea3e61d5ba --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,20 @@ +version: v1 +labels: + - label: '📒 Documentation' + matcher: + title: '\b(docs|doc:|\[doc\]|README|typos|comment|documentation)\b' + - label: '☢️ Bug' + matcher: + title: '\b(fix|race|bug|missing|correct)\b' + - label: '🧹 Updates' + matcher: + title: '\b(improve|update|refactor|deprecated|remove|unused|test)\b' + - label: '🤖 Dependencies' + matcher: + title: '\b(bumb|bdependencies)\b' + - label: '✏️ Feature' + matcher: + title: '\b(feature|feat|create|implement|add)\b' + - label: '🤔 Question' + matcher: + title: '\b(question|how)\b' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5c34c3748c..8d259296da 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,7 +19,7 @@ Please delete options that are not relevant. - [ ] For new functionalities I follow the inspiration of the express js framework and built them similar in usage - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation - https://github.com/gofiber/docs for https://docs.gofiber.io/ +- [ ] I have made corresponding changes to the documentation - /docs/ directory for https://docs.gofiber.io/ - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] If new dependencies exist, I have checked that they are really necessary and agreed with the maintainers/community (we want to have as few dependencies as possible) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6df748953c..134447d7bc 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -32,61 +32,6 @@ version-resolver: - '🤖 Dependencies' - '🧹 Updates' default: patch -autolabeler: - - label: '📒 Documentation' - title: - - '/docs/i' - - '/doc:/i' - - '/\[doc\]/i' - - '/README/i' - - '/typos/i' - - '/comment/i' - - '/📚/i' - - '/📒/i' - - '/📝/i' - - '/documentation/i' - - label: '☢️ Bug' - title: - - '/fix/i' - - '/race/i' - - '/bug/i' - - '/missing/i' - - '/correct/i' - - '/🐛/i' - - '/☢/i' - - '/🩹/i' - - '/🚨/i' - - label: '🧹 Updates' - title: - - '/improve/i' - - '/update/i' - - '/refactor/i' - - '/deprecated/i' - - '/remove/i' - - '/unused/i' - - '/test/i' - - '/⚡/i' - - '/👷/i' - - '/🚧/i' - - '/♻️/i' - - '/🎨/i' - - '/🧪/i' - - '/🧹/i' - - label: '🤖 Dependencies' - title: - - '/bumb/i' - - '/dependencies/i' - - '/📦/i' - - '/🤖/i' - - label: '✏️ Feature' - title: - - '/feature/i' - - '/create/i' - - '/implement/i' - - '/add/i' - - '/🚀/i' - - '/✨/i' - - '/🔥/i' template: | $CHANGES diff --git a/.github/scripts/sync_docs.sh b/.github/scripts/sync_docs.sh new file mode 100755 index 0000000000..4b58a1e128 --- /dev/null +++ b/.github/scripts/sync_docs.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +# Some env variables +BRANCH="master" +REPO_URL="github.com/gofiber/docs.git" +AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com" +AUTHOR_USERNAME="github-actions[bot]" +VERSION_FILE="versions.json" +REPO_DIR="core" +COMMIT_URL="https://github.com/gofiber/fiber" +DOCUSAURUS_COMMAND="npm run docusaurus -- docs:version" + +# Set commit author +git config --global user.email "${AUTHOR_EMAIL}" +git config --global user.name "${AUTHOR_USERNAME}" + +git clone https://${TOKEN}@${REPO_URL} fiber-docs + +# Handle push event +if [ "$EVENT" == "push" ]; then + latest_commit=$(git rev-parse --short HEAD) + log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) + if [[ $log_output != "" ]]; then + cp -a docs/* fiber-docs/docs/${REPO_DIR} + fi + +# Handle release event +elif [ "$EVENT" == "release" ]; then + major_version="${TAG_NAME%%.*}" + + # Form new version name + new_version="${major_version}.x" + + cd fiber-docs/ || true + npm ci + + # Check if contrib_versions.json exists and modify it if required + if [[ -f $VERSION_FILE ]]; then + jq --arg new_version "$new_version" 'del(.[] | select(. == $new_version))' $VERSION_FILE >temp.json && mv temp.json $VERSION_FILE + jq -S . ${VERSION_FILE} >temp.json && mv temp.json ${VERSION_FILE} + fi + + # Run docusaurus versioning command + $DOCUSAURUS_COMMAND "${new_version}" +fi + +# Push changes +cd fiber-docs/ || true +git add . +if [[ $EVENT == "push" ]]; then + git commit -m "Add docs from ${COMMIT_URL}/commit/${latest_commit}" +elif [[ $EVENT == "release" ]]; then + git commit -m "Sync docs for release ${COMMIT_URL}/releases/tag/${TAG_NAME}" +fi + +MAX_RETRIES=5 +DELAY=5 +retry=0 + +while ((retry < MAX_RETRIES)); do + git push https://${TOKEN}@${REPO_URL} && break + retry=$((retry + 1)) + git pull --rebase + sleep $DELAY +done + +if ((retry == MAX_RETRIES)); then + echo "Failed to push after $MAX_RETRIES attempts. Exiting with 1." + exit 1 +fi diff --git a/.github/testdata/testRoutes.json b/.github/testdata/testRoutes.json index 8e08aef162..0503d1802e 100644 --- a/.github/testdata/testRoutes.json +++ b/.github/testdata/testRoutes.json @@ -1,6 +1,5 @@ { - "testRoutes": [ - { + "test_routes": [{ "method": "GET", "path": "/authorizations" }, @@ -957,8 +956,7 @@ "path": "/user/keys/1337" } ], - "githubAPI": [ - { + "github_api": [{ "method": "GET", "path": "/authorizations" }, @@ -1915,4 +1913,4 @@ "path": "/user/keys/:id" } ] -} \ No newline at end of file +} diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml new file mode 100644 index 0000000000..ef7299117e --- /dev/null +++ b/.github/workflows/auto-labeler.yml @@ -0,0 +1,21 @@ +name: Auto labeler +on: + issues: + types: [ opened, edited, milestoned ] + pull_request_target: + types: [ opened ] +permissions: + contents: read + issues: write + pull-requests: write + statuses: write + checks: write +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - name: Check Labels + id: labeler + uses: fuxingloh/multi-labeler@v2 + with: + github-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9cc328eed5..a751eae918 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,13 +1,26 @@ -on: [push] +on: + push: + branches: + - master + - main + paths: + - '**' + - '!docs/**' + - '!**.md' + pull_request: + paths: + - '**' + - '!docs/**' + - '!**.md' name: Benchmark jobs: Compare: runs-on: ubuntu-latest steps: - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.19.x + go-version: 1.20.x - name: Fetch Repository uses: actions/checkout@v3 - name: Run Benchmark @@ -19,12 +32,17 @@ jobs: # TODO: reactivate it later -> when v3 is the stable one key: ${{ runner.os }}-benchmark-v3 - name: Save Benchmark Results - uses: rhysd/github-action-benchmark@v1 + uses: benchmark-action/github-action-benchmark@v1.16.2 with: tool: 'go' output-file-path: output.txt github-token: ${{ secrets.BENCHMARK_TOKEN }} + benchmark-data-dir-path: 'benchmarks' fail-on-alert: true - comment-on-alert: true + comment-on-alert: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + # Enable Job Summary for PRs - deactivated because of issues + #summary-always: ${{ github.event_name != 'push' && github.event_name != 'workflow_dispatch' }} # TODO: reactivate it later -> when v3 is the stable one + #auto-push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} auto-push: false + save-data-file: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 54aa7ba8d4..e95e715d50 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,10 +2,19 @@ name: "CodeQL" on: push: - branches: [v3-beta, ] + branches: + - master + - main + - v3-beta + paths: + - '**' + - '!docs/**' + - '!**.md' pull_request: - # The branches below must be a subset of the branches above - branches: [v3-beta] + paths: + - '**' + - '!docs/**' + - '!**.md' schedule: - cron: '0 3 * * 6' diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e40a67086c..fa43748571 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,16 +1,27 @@ +# Adapted from https://github.com/golangci/golangci-lint-action/blob/b56f6f529003f1c81d4d759be6bd5f10bf9a0fa0/README.md#how-to-use + +name: golangci-lint on: - push: - branches: - - v3-beta - pull_request: -name: Linter + push: + branches: + #- master + #- main + - v3-beta + pull_request: +permissions: + contents: read jobs: - Golint: + golangci: + name: lint runs-on: ubuntu-latest steps: - - name: Fetch Repository - uses: actions/checkout@v3 - - name: Run Golint - uses: reviewdog/action-golangci-lint@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: - golangci_lint_flags: "--tests=false" + # NOTE: Keep this in sync with the version from go.mod + go-version: 1.20.x + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # NOTE: Keep this in sync with the version from .golangci.yml + version: v1.51.0 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 1669279191..f4a5cea982 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,10 +5,7 @@ on: # branches to consider in the event; optional, defaults to all branches: - master - # pull_request event is required only for autolabeler - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [opened, reopened, synchronize] + - main jobs: update_release_draft: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index b5c495747e..0000000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,16 +0,0 @@ -on: - push: - branches: - - v3-beta - pull_request: -name: Security -jobs: - Gosec: - runs-on: ubuntu-latest - steps: - - name: Fetch Repository - uses: actions/checkout@v3 - - name: Run Gosec - uses: securego/gosec@master - with: - args: -exclude-dir=internal/*/ ./... diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml new file mode 100644 index 0000000000..c7f895de6a --- /dev/null +++ b/.github/workflows/sync-docs.yml @@ -0,0 +1,35 @@ +name: 'Sync docs' + +on: + push: + branches: + - master + - main + paths: + - 'docs/**' + release: + types: [ published ] + +jobs: + sync-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 2 + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install JQ + run: sudo apt-get install jq + + - name: Sync docs + run: ./.github/scripts/sync_docs.sh + env: + EVENT: ${{ github.event_name }} + TAG_NAME: ${{ github.ref_name }} + TOKEN: ${{ secrets.DOC_SYNC_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ffb54108b..da1c72d31a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,42 +1,36 @@ on: push: branches: + - master + - main - v3-beta + paths: + - '**' + - '!docs/**' + - '!**.md' pull_request: + paths: + - '**' + - '!docs/**' + - '!**.md' name: Test jobs: Build: strategy: matrix: - go-version: [1.18.x, 1.19.x] + go-version: [1.19.x, 1.20.x] platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: + - name: Fetch Repository + uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - - name: Setup Golang caches - uses: actions/cache@v3 - with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) - path: | - ~/go/pkg/mod - ~/.cache/go-build - ~/Library/Caches/go-build - ~\AppData\Local\go-build - key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ matrix.go-version }}- - - name: Fetch Repository - uses: actions/checkout@v3 - name: Run Test uses: nick-fields/retry@v2 with: max_attempts: 3 timeout_minutes: 15 - command: go test ./... -v -race + command: go test ./... -v -race -count=1 diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index bddf020f91..4c13423020 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -1,26 +1,35 @@ +name: Run govulncheck + on: push: branches: - master - main + paths: + - '**' + - '!docs/**' + - '!**.md' pull_request: -name: Vulnerability Check + paths: + - '**' + - '!docs/**' + - '!**.md' + jobs: - Security: + govulncheck-check: runs-on: ubuntu-latest + env: + GO111MODULE: on steps: + - name: Fetch Repository + uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: 'stable' check-latest: true - - name: Fetch Repository - uses: actions/checkout@v3 + cache: false - name: Install Govulncheck - run: | - export GO111MODULE=on - export PATH=${PATH}:`go env GOPATH`/bin - go install golang.org/x/vuln/cmd/govulncheck@latest + run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run Govulncheck - run: "`go env GOPATH`/bin/govulncheck ./..." - + run: govulncheck ./... \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000..30b28dc8b7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,207 @@ +# Created based on v1.51.0 +# NOTE: Keep this in sync with the version in .github/workflows/linter.yml + +run: + modules-download-mode: readonly + skip-dirs-use-default: false + skip-dirs: + - internal + +output: + sort-results: true + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + disable-default-exclusions: true + + errchkjson: + report-no-exported: true + + exhaustive: + default-signifies-exhaustive: true + + forbidigo: + forbid: + - ^(fmt\.Print(|f|ln)|print|println)$ + - 'http\.Default(Client|Transport)' + # TODO: Eventually enable these patterns + # - 'time\.Sleep' + # - 'panic' + + gocritic: + disabled-checks: + - ifElseChain + + gofumpt: + module-path: github.com/gofiber/fiber + extra-rules: true + + gosec: + config: + global: + audit: true + + depguard: + include-go-root: true + packages: + - flag + - io/ioutil + packages-with-error-message: + - flag: '`flag` package is only allowed in main.go' + - io/ioutil: '`io/ioutil` package is deprecated, use the `io` and `os` package instead' + + govet: + check-shadowing: true + enable-all: true + disable: + - shadow + - fieldalignment + - loopclosure + + grouper: + import-require-single-import: true + import-require-grouping: true + + misspell: + locale: US + + nolintlint: + require-explanation: true + require-specific: true + + nonamedreturns: + report-error-in-defer: true + + predeclared: + q: true + + promlinter: + strict: true + + revive: + enable-all-rules: true + rules: + # Provided by gomnd linter + - name: add-constant + disabled: true + - name: argument-limit + disabled: true + # Provided by bidichk + - name: banned-characters + disabled: true + - name: cognitive-complexity + disabled: true + - name: cyclomatic + disabled: true + - name: early-return + severity: warning + disabled: true + - name: exported + disabled: true + - name: file-header + disabled: true + - name: function-result-limit + disabled: true + - name: function-length + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: modifies-parameter + disabled: true + - name: nested-structs + disabled: true + - name: package-comments + disabled: true + + stylecheck: + checks: + - all + - -ST1000 + - -ST1020 + - -ST1021 + - -ST1022 + + tagliatelle: + case: + rules: + json: snake + + tenv: + all: true + + #unparam: + # check-exported: true + + wrapcheck: + ignorePackageGlobs: + - github.com/gofiber/fiber/* + - github.com/valyala/fasthttp + +issues: + exclude-use-default: false + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - depguard + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - execinquery + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + - goconst + - gocritic + - gofmt + - gofumpt + - goimports + - gomoddirectives + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - loggercheck + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - stylecheck + - tagliatelle + # - testpackage # TODO: Enable once https://github.com/gofiber/fiber/issues/2252 is implemented + - thelper + # - tparallel # TODO: Enable once https://github.com/gofiber/fiber/issues/2254 is implemented + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - wastedassign + - whitespace + - wrapcheck + - tenv diff --git a/addon/retry/config.go b/addon/retry/config.go index a2dcd27122..2a8bfabc19 100644 --- a/addon/retry/config.go +++ b/addon/retry/config.go @@ -1,6 +1,8 @@ package retry -import "time" +import ( + "time" +) // Config defines the config for addon. type Config struct { diff --git a/app.go b/app.go index 4e30d71373..51aad01c8f 100644 --- a/app.go +++ b/app.go @@ -9,6 +9,7 @@ package fiber import ( "bufio" + "context" "encoding/json" "encoding/xml" "errors" @@ -22,7 +23,9 @@ import ( "sync" "time" + "github.com/gofiber/fiber/v2/log" "github.com/gofiber/utils/v2" + "github.com/valyala/fasthttp" ) @@ -297,7 +300,7 @@ type Config struct { // FEATURE: v2.3.x // The router executes the same handler by default if StrictRouting or CaseSensitive is disabled. - // Enabling RedirectFixedPath will change this behaviour into a client redirect to the original route path. + // Enabling RedirectFixedPath will change this behavior into a client redirect to the original route path. // Using the status code 301 for GET requests and 308 for all other request methods. // // Default: false @@ -471,7 +474,7 @@ var DefaultMethods = []string{ } // DefaultErrorHandler that process return errors from handlers -var DefaultErrorHandler = func(c Ctx, err error) error { +func DefaultErrorHandler(c Ctx, err error) error { code := StatusInternalServerError var e *Error if errors.As(err, &e) { @@ -588,12 +591,11 @@ func New(config ...Config) *App { func (app *App) handleTrustedProxy(ipAddress string) { if strings.Contains(ipAddress, "/") { _, ipNet, err := net.ParseCIDR(ipAddress) - if err != nil { - fmt.Printf("[Warning] IP range `%s` could not be parsed. \n", ipAddress) + log.Warnf("IP range %q could not be parsed: %v", ipAddress, err) + } else { + app.config.trustedProxyRanges = append(app.config.trustedProxyRanges, ipNet) } - - app.config.trustedProxyRanges = append(app.config.trustedProxyRanges, ipNet) } else { app.config.trustedProxiesMap[ipAddress] = struct{}{} } @@ -616,18 +618,23 @@ func (app *App) SetTLSHandler(tlsHandler *TLSHandler) { // Name Assign name to specific route. func (app *App) Name(name string) Router { app.mutex.Lock() + defer app.mutex.Unlock() - latestGroup := app.latestRoute.group - if latestGroup != nil { - app.latestRoute.Name = latestGroup.name + name - } else { - app.latestRoute.Name = name + for _, routes := range app.stack { + for _, route := range routes { + if route.Path == app.latestRoute.path { + route.Name = name + + if route.group != nil { + route.Name = route.group.name + route.Name + } + } + } } if err := app.hooks.executeOnNameHooks(*app.latestRoute); err != nil { panic(err) } - app.mutex.Unlock() return app } @@ -778,12 +785,16 @@ func (app *App) Patch(path string, handler Handler, middleware ...Handler) Route // Add allows you to specify multiple HTTP methods to register a route. func (app *App) Add(methods []string, path string, handler Handler, middleware ...Handler) Router { - return app.register(methods, path, nil, handler, middleware...) + app.register(methods, path, nil, handler, middleware...) + + return app } // Static will create a file server serving static files func (app *App) Static(prefix, root string, config ...Static) Router { - return app.registerStatic(prefix, root, config...) + app.registerStatic(prefix, root, config...) + + return app } // All will register the handler on all HTTP methods @@ -839,10 +850,10 @@ func (app *App) Config() Config { } // Handler returns the server handler. -func (app *App) Handler() fasthttp.RequestHandler { +func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476 // prepare the server for the start app.startupProcess() - return app.handler + return app.requestHandler } // Stack returns the raw router stack. @@ -856,12 +867,34 @@ func (app *App) HandlersCount() uint32 { } // Shutdown gracefully shuts down the server without interrupting any active connections. -// Shutdown works by first closing all open listeners and then waiting indefinitely for all connections to return to idle and then shut down. +// Shutdown works by first closing all open listeners and then waiting indefinitely for all connections to return to idle before shutting down. // // Make sure the program doesn't exit and waits instead for Shutdown to return. // // Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) Shutdown() error { + return app.ShutdownWithContext(context.Background()) +} + +// ShutdownWithTimeout gracefully shuts down the server without interrupting any active connections. However, if the timeout is exceeded, +// ShutdownWithTimeout will forcefully close any active connections. +// ShutdownWithTimeout works by first closing all open listeners and then waiting for all connections to return to idle before shutting down. +// +// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return. +// +// ShutdownWithTimeout does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. +func (app *App) ShutdownWithTimeout(timeout time.Duration) error { + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + return app.ShutdownWithContext(ctx) +} + +// ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. +// +// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return. +// +// ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. +func (app *App) ShutdownWithContext(ctx context.Context) error { if app.hooks != nil { // TODO: check should be defered? app.hooks.executeOnShutdownHooks() @@ -872,7 +905,7 @@ func (app *App) Shutdown() error { if app.server == nil { return fmt.Errorf("shutdown: server is not running") } - return app.server.Shutdown() + return app.server.ShutdownWithContext(ctx) } // Server returns the underlying fasthttp server @@ -887,11 +920,11 @@ func (app *App) Hooks() *Hooks { // Test is used for internal debugging by passing a *http.Request. // Timeout is optional and defaults to 1s, -1 will disable it completely. -func (app *App) Test(req *http.Request, timeout ...time.Duration) (resp *http.Response, err error) { +func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) { // Set timeout - to := 1 * time.Second - if len(timeout) > 0 { - to = timeout[0] + timeout := 1000 + if len(msTimeout) > 0 { + timeout = msTimeout[0] } // Add Content-Length if not provided with body @@ -902,15 +935,15 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (resp *http.Re // Dump raw http request dump, err := httputil.DumpRequest(req, true) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to dump request: %w", err) } // Create test connection conn := new(testConn) // Write raw http request - if _, err = conn.r.Write(dump); err != nil { - return nil, err + if _, err := conn.r.Write(dump); err != nil { + return nil, fmt.Errorf("failed to write: %w", err) } // prepare the server for the start app.startupProcess() @@ -930,12 +963,12 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (resp *http.Re }() // Wait for callback - if to >= 0 { + if timeout >= 0 { // With timeout select { case err = <-channel: - case <-time.After(to): - return nil, fmt.Errorf("test: timeout error after %s", to) + case <-time.After(time.Duration(timeout) * time.Millisecond): + return nil, fmt.Errorf("test: timeout error %vms", timeout) } } else { // Without timeout @@ -943,7 +976,7 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (resp *http.Re } // Check for errors - if err != nil && err != fasthttp.ErrGetOnly { + if err != nil && !errors.Is(err, fasthttp.ErrGetOnly) { return nil, err } @@ -951,12 +984,17 @@ func (app *App) Test(req *http.Request, timeout ...time.Duration) (resp *http.Re buffer := bufio.NewReader(&conn.w) // Convert raw http response to *http.Response - return http.ReadResponse(buffer, req) + res, err := http.ReadResponse(buffer, req) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + return res, nil } type disableLogger struct{} -func (dl *disableLogger) Printf(_ string, _ ...any) { +func (*disableLogger) Printf(_ string, _ ...any) { // fmt.Println(fmt.Sprintf(format, args...)) } @@ -967,7 +1005,7 @@ func (app *App) init() *App { // Only load templates if a view engine is specified if app.config.Views != nil { if err := app.config.Views.Load(); err != nil { - fmt.Printf("views: %v\n", err) + log.Warnf("failed to load views: %v", err) } } @@ -979,7 +1017,7 @@ func (app *App) init() *App { } // fasthttp server settings - app.server.Handler = app.handler + app.server.Handler = app.requestHandler app.server.Name = app.config.ServerHeader app.server.Concurrency = app.config.Concurrency app.server.NoDefaultDate = app.config.DisableDefaultDate @@ -1039,48 +1077,59 @@ func (app *App) ErrorHandler(ctx Ctx, err error) error { // errors before calling the application's error handler method. func (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) { // Acquire Ctx with fasthttp request from pool - c := app.AcquireCtx().(*DefaultCtx) + c, ok := app.AcquireCtx().(*DefaultCtx) + if !ok { + panic(fmt.Errorf("failed to type-assert to *DefaultCtx")) + } c.Reset(fctx) - if _, ok := err.(*fasthttp.ErrSmallBuffer); ok { + defer app.ReleaseCtx(c) + + var ( + errNetOP *net.OpError + netErr net.Error + ) + + switch { + case errors.As(err, new(*fasthttp.ErrSmallBuffer)): err = ErrRequestHeaderFieldsTooLarge - } else if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { + case errors.As(err, &errNetOP) && errNetOP.Timeout(): err = ErrRequestTimeout - } else if err == fasthttp.ErrBodyTooLarge { + case errors.As(err, &netErr): + err = ErrBadGateway + case errors.Is(err, fasthttp.ErrBodyTooLarge): err = ErrRequestEntityTooLarge - } else if err == fasthttp.ErrGetOnly { + case errors.Is(err, fasthttp.ErrGetOnly): err = ErrMethodNotAllowed - } else if strings.Contains(err.Error(), "timeout") { + case strings.Contains(err.Error(), "timeout"): err = ErrRequestTimeout - } else { - err = ErrBadRequest + default: + err = NewError(StatusBadRequest, err.Error()) } if catch := app.ErrorHandler(c, err); catch != nil { - _ = c.SendStatus(StatusInternalServerError) + log.Errorf("serverErrorHandler: failed to call ErrorHandler: %v", catch) + _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here + return } - - app.ReleaseCtx(c) } // startupProcess Is the method which executes all the necessary processes just before the start of the server. func (app *App) startupProcess() *App { - if err := app.hooks.executeOnListenHooks(); err != nil { - panic(err) - } - app.mutex.Lock() defer app.mutex.Unlock() - // add routes of sub-apps - app.mountFields.subAppsRoutesAdded.Do(func() { - app.appendSubAppLists(app.mountFields.appList) - app.addSubAppsRoutes(app.mountFields.appList) - app.generateAppListKeys() - }) + app.mountStartupProcess() // build route tree stack app.buildTree() return app } + +// Run onListen hooks. If they return an error, panic. +func (app *App) runOnListenHooks(listenData ListenData) { + if err := app.hooks.executeOnListenHooks(listenData); err != nil { + panic(err) + } +} diff --git a/app_test.go b/app_test.go index 8044bf8e86..c50df64ae0 100644 --- a/app_test.go +++ b/app_test.go @@ -2,10 +2,12 @@ // 🤖 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package fiber import ( "bytes" + "context" "crypto/tls" "errors" "fmt" @@ -21,15 +23,18 @@ import ( "testing" "time" + "github.com/gofiber/utils/v2" + "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" ) -var testEmptyHandler = func(c Ctx) error { +func testEmptyHandler(_ Ctx) error { return nil } -func testStatus200(t *testing.T, app *App, url string, method string) { +func testStatus200(t *testing.T, app *App, url, method string) { t.Helper() req := httptest.NewRequest(method, url, nil) @@ -40,6 +45,8 @@ func testStatus200(t *testing.T, app *App, url string, method string) { } func testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBodyError string) { + t.Helper() + require.NoError(t, err, "app.Test(req)") require.Equal(t, 500, resp.StatusCode, "Status code") @@ -49,6 +56,7 @@ func testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBod } func Test_App_MethodNotAllowed(t *testing.T) { + t.Parallel() app := New() app.Use(func(c Ctx) error { @@ -100,6 +108,7 @@ func Test_App_MethodNotAllowed(t *testing.T) { } func Test_App_Custom_Middleware_404_Should_Not_SetMethodNotAllowed(t *testing.T) { + t.Parallel() app := New() app.Use(func(c Ctx) error { @@ -124,6 +133,7 @@ func Test_App_Custom_Middleware_404_Should_Not_SetMethodNotAllowed(t *testing.T) } func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { + t.Parallel() expectedError := regexp.MustCompile( `error when reading request headers: small read buffer\. Increase ReadBufferSize\. Buffer size=4096, contents: "GET / HTTP/1.1\\r\\nHost: example\.com\\r\\nVery-Long-Header: -+`, ) @@ -137,7 +147,6 @@ func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { logHeaderSlice := make([]string, 5000) request.Header.Set("Very-Long-Header", strings.Join(logHeaderSlice, "-")) _, err := app.Test(request) - if err == nil { t.Error("Expect an error at app.Test(request)") } @@ -146,6 +155,7 @@ func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { } func Test_App_Errors(t *testing.T) { + t.Parallel() app := New(Config{ BodyLimit: 4, }) @@ -169,6 +179,7 @@ func Test_App_Errors(t *testing.T) { } func Test_App_ErrorHandler_Custom(t *testing.T) { + t.Parallel() app := New(Config{ ErrorHandler: func(c Ctx, err error) error { return c.Status(200).SendString("hi, i'm an custom error") @@ -189,6 +200,7 @@ func Test_App_ErrorHandler_Custom(t *testing.T) { } func Test_App_ErrorHandler_HandlerStack(t *testing.T) { + t.Parallel() app := New(Config{ ErrorHandler: func(c Ctx, err error) error { require.Equal(t, "1: USE error", err.Error()) @@ -218,6 +230,7 @@ func Test_App_ErrorHandler_HandlerStack(t *testing.T) { } func Test_App_ErrorHandler_RouteStack(t *testing.T) { + t.Parallel() app := New(Config{ ErrorHandler: func(c Ctx, err error) error { require.Equal(t, "1: USE error", err.Error()) @@ -238,11 +251,37 @@ func Test_App_ErrorHandler_RouteStack(t *testing.T) { require.Equal(t, 500, resp.StatusCode, "Status code") body, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "1: USE error", string(body)) } +func Test_App_serverErrorHandler_Internal_Error(t *testing.T) { + t.Parallel() + app := New() + msg := "test err" + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + app.serverErrorHandler(c.fasthttp, errors.New(msg)) + require.Equal(t, string(c.fasthttp.Response.Body()), msg) + require.Equal(t, c.fasthttp.Response.StatusCode(), StatusBadRequest) +} + +func Test_App_serverErrorHandler_Network_Error(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + app.serverErrorHandler(c.fasthttp, &net.DNSError{ + Err: "test error", + Name: "test host", + IsTimeout: false, + }) + require.Equal(t, string(c.fasthttp.Response.Body()), utils.StatusMessage(StatusBadGateway)) + require.Equal(t, c.fasthttp.Response.StatusCode(), StatusBadGateway) +} + func Test_App_Nested_Params(t *testing.T) { + t.Parallel() app := New() app.Get("/test", func(c Ctx) error { @@ -266,6 +305,7 @@ func Test_App_Nested_Params(t *testing.T) { } func Test_App_Use_Params(t *testing.T) { + t.Parallel() app := New() app.Use("/prefix/:param", func(c Ctx) error { @@ -308,6 +348,7 @@ func Test_App_Use_Params(t *testing.T) { } func Test_App_Use_UnescapedPath(t *testing.T) { + t.Parallel() app := New(Config{UnescapePath: true, CaseSensitive: true}) app.Use("/cRéeR/:param", func(c Ctx) error { @@ -336,6 +377,7 @@ func Test_App_Use_UnescapedPath(t *testing.T) { } func Test_App_Use_CaseSensitive(t *testing.T) { + t.Parallel() app := New(Config{CaseSensitive: true}) app.Use("/abc", func(c Ctx) error { @@ -366,6 +408,7 @@ func Test_App_Use_CaseSensitive(t *testing.T) { } func Test_App_Not_Use_StrictRouting(t *testing.T) { + t.Parallel() app := New() app.Use("/abc", func(c Ctx) error { @@ -399,6 +442,7 @@ func Test_App_Not_Use_StrictRouting(t *testing.T) { } func Test_App_Use_MultiplePrefix(t *testing.T) { + t.Parallel() app := New() app.Use([]string{"/john", "/doe"}, func(c Ctx) error { @@ -441,10 +485,10 @@ func Test_App_Use_MultiplePrefix(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "/test/doe", string(body)) - } func Test_App_Use_StrictRouting(t *testing.T) { + t.Parallel() app := New(Config{StrictRouting: true}) app.Get("/abc", func(c Ctx) error { @@ -478,13 +522,14 @@ func Test_App_Use_StrictRouting(t *testing.T) { } func Test_App_Add_Method_Test(t *testing.T) { + t.Parallel() defer func() { if err := recover(); err != nil { require.Equal(t, "add: invalid http method JANE\n", fmt.Sprintf("%v", err)) } }() - methods := append(DefaultMethods, "JOHN") + methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here app := New(Config{ RequestMethods: methods, }) @@ -508,6 +553,7 @@ func Test_App_Add_Method_Test(t *testing.T) { // go test -run Test_App_GETOnly func Test_App_GETOnly(t *testing.T) { + t.Parallel() app := New(Config{ GETOnly: true, }) @@ -523,6 +569,7 @@ func Test_App_GETOnly(t *testing.T) { } func Test_App_Use_Params_Group(t *testing.T) { + t.Parallel() app := New() group := app.Group("/prefix/:param/*") @@ -541,6 +588,7 @@ func Test_App_Use_Params_Group(t *testing.T) { } func Test_App_Chaining(t *testing.T) { + t.Parallel() n := func(c Ctx) error { return c.Next() } @@ -569,20 +617,27 @@ func Test_App_Chaining(t *testing.T) { } func Test_App_Order(t *testing.T) { + t.Parallel() app := New() app.Get("/test", func(c Ctx) error { - c.Write([]byte("1")) + _, err := c.Write([]byte("1")) + require.NoError(t, err) + return c.Next() }) app.All("/test", func(c Ctx) error { - c.Write([]byte("2")) + _, err := c.Write([]byte("2")) + require.NoError(t, err) + return c.Next() }) app.Use(func(c Ctx) error { - c.Write([]byte("3")) + _, err := c.Write([]byte("3")) + require.NoError(t, err) + return nil }) @@ -598,6 +653,7 @@ func Test_App_Order(t *testing.T) { } func Test_App_Methods(t *testing.T) { + t.Parallel() dummyHandler := testEmptyHandler app := New() @@ -637,6 +693,7 @@ func Test_App_Methods(t *testing.T) { } func Test_App_Route_Naming(t *testing.T) { + t.Parallel() app := New() handler := func(c Ctx) error { return c.SendStatus(StatusOK) @@ -667,6 +724,7 @@ func Test_App_Route_Naming(t *testing.T) { } func Test_App_New(t *testing.T) { + t.Parallel() app := New() app.Get("/", testEmptyHandler) @@ -677,6 +735,7 @@ func Test_App_New(t *testing.T) { } func Test_App_Config(t *testing.T) { + t.Parallel() app := New(Config{ StrictRouting: true, }) @@ -684,12 +743,15 @@ func Test_App_Config(t *testing.T) { } func Test_App_Shutdown(t *testing.T) { + t.Parallel() t.Run("success", func(t *testing.T) { + t.Parallel() app := New() require.True(t, app.Shutdown() == nil) }) t.Run("no server", func(t *testing.T) { + t.Parallel() app := &App{} if err := app.Shutdown(); err != nil { if err.Error() != "shutdown: server is not running" { @@ -699,6 +761,93 @@ func Test_App_Shutdown(t *testing.T) { }) } +func Test_App_ShutdownWithTimeout(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c Ctx) error { + time.Sleep(5 * time.Second) + return c.SendString("body") + }) + ln := fasthttputil.NewInmemoryListener() + go func() { + require.NoError(t, app.Listener(ln)) + }() + time.Sleep(1 * time.Second) + go func() { + conn, err := ln.Dial() + if err != nil { + t.Errorf("unexepcted error: %v", err) + } + + if _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")); err != nil { + t.Errorf("unexpected error: %v", err) + } + }() + time.Sleep(1 * time.Second) + + shutdownErr := make(chan error) + go func() { + shutdownErr <- app.ShutdownWithTimeout(1 * time.Second) + }() + + timer := time.NewTimer(time.Second * 5) + select { + case <-timer.C: + t.Fatal("idle connections not closed on shutdown") + case err := <-shutdownErr: + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) + } + } +} + +func Test_App_ShutdownWithContext(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/", func(ctx Ctx) error { + time.Sleep(5 * time.Second) + return ctx.SendString("body") + }) + + ln := fasthttputil.NewInmemoryListener() + + go func() { + require.Equal(t, nil, app.Listener(ln)) + }() + + time.Sleep(1 * time.Second) + + go func() { + conn, err := ln.Dial() + if err != nil { + t.Errorf("unexepcted error: %v", err) + } + + if _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")); err != nil { + t.Errorf("unexpected error: %v", err) + } + }() + + time.Sleep(1 * time.Second) + + shutdownErr := make(chan error) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() + + select { + case <-time.After(5 * time.Second): + t.Fatal("idle connections not closed on shutdown") + case err := <-shutdownErr: + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) + } + } +} + // go test -run Test_App_Static_Index_Default func Test_App_Static_Index_Default(t *testing.T) { app := New() @@ -753,7 +902,7 @@ func Test_App_Static_Direct(t *testing.T) { body, err = io.ReadAll(resp.Body) require.NoError(t, err) - require.True(t, strings.Contains(string(body), "testRoutes")) + require.True(t, strings.Contains(string(body), "test_routes")) } // go test -run Test_App_Static_MaxAge @@ -762,7 +911,7 @@ func Test_App_Static_MaxAge(t *testing.T) { app.Static("/", "./.github", Static{MaxAge: 100}) - resp, err := app.Test(httptest.NewRequest("GET", "/index.html", nil)) + 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.False(t, resp.Header.Get(HeaderContentLength) == "") @@ -775,19 +924,19 @@ func Test_App_Static_Custom_CacheControl(t *testing.T) { app := New() app.Static("/", "./.github", Static{ModifyResponse: func(c Ctx) error { - if strings.Contains(string(c.GetRespHeader("Content-Type")), "text/html") { + 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("GET", "/index.html", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(HeaderCacheControl), "CacheControl Control") - normal_resp, normal_err := app.Test(httptest.NewRequest("GET", "/config.yml", nil)) - require.Equal(t, nil, normal_err, "app.Test(req)") - require.Equal(t, "", normal_resp.Header.Get(HeaderCacheControl), "CacheControl Control") + normalResp, normalErr := app.Test(httptest.NewRequest(MethodGet, "/config.yml", nil)) + require.Equal(t, nil, normalErr, "app.Test(req)") + require.Equal(t, "", normalResp.Header.Get(HeaderCacheControl), "CacheControl Control") } // go test -run Test_App_Static_Download @@ -796,7 +945,7 @@ func Test_App_Static_Download(t *testing.T) { app.Static("/fiber.png", "./.github/testdata/fs/img/fiber.png", Static{Download: true}) - resp, err := app.Test(httptest.NewRequest("GET", "/fiber.png", nil)) + 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.False(t, resp.Header.Get(HeaderContentLength) == "") @@ -966,7 +1115,7 @@ func Test_App_Static_Next(t *testing.T) { }) t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(MethodGet, "/", nil) req.Header.Set("X-Custom-Header", "skip") resp, err := app.Test(req) require.NoError(t, err) @@ -980,7 +1129,7 @@ func Test_App_Static_Next(t *testing.T) { }) t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(MethodGet, "/", nil) req.Header.Set("X-Custom-Header", "don't skip") resp, err := app.Test(req) require.NoError(t, err) @@ -1039,6 +1188,7 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { } func Test_App_Group_Invalid(t *testing.T) { + t.Parallel() defer func() { if err := recover(); err != nil { require.Equal(t, "use: invalid handler int\n", fmt.Sprintf("%v", err)) @@ -1048,6 +1198,7 @@ func Test_App_Group_Invalid(t *testing.T) { } func Test_App_Group(t *testing.T) { + t.Parallel() dummyHandler := testEmptyHandler app := New() @@ -1108,6 +1259,7 @@ func Test_App_Group(t *testing.T) { } func Test_App_Route(t *testing.T) { + t.Parallel() dummyHandler := testEmptyHandler app := New() @@ -1152,10 +1304,10 @@ func Test_App_Route(t *testing.T) { resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/v1/v2/v3", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") - } func Test_App_Deep_Group(t *testing.T) { + t.Parallel() runThroughCount := 0 dummyHandler := func(c Ctx) error { runThroughCount++ @@ -1176,6 +1328,7 @@ func Test_App_Deep_Group(t *testing.T) { // go test -run Test_App_Next_Method func Test_App_Next_Method(t *testing.T) { + t.Parallel() app := New() app.Use(func(c Ctx) error { @@ -1204,12 +1357,13 @@ func Benchmark_AcquireCtx(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_NewError -benchmem -count=4 func Benchmark_NewError(b *testing.B) { for n := 0; n < b.N; n++ { - NewError(200, "test") + NewError(200, "test") //nolint:errcheck // not needed } } // go test -run Test_NewError func Test_NewError(t *testing.T) { + t.Parallel() e := NewError(StatusForbidden, "permission denied") require.Equal(t, StatusForbidden, e.Code) require.Equal(t, "permission denied", e.Message) @@ -1217,6 +1371,7 @@ func Test_NewError(t *testing.T) { // go test -run Test_Test_Timeout func Test_Test_Timeout(t *testing.T) { + t.Parallel() app := New() app.Get("/", testEmptyHandler) @@ -1230,7 +1385,7 @@ func Test_Test_Timeout(t *testing.T) { return nil }) - _, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), 20*time.Millisecond) + _, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), 20) require.True(t, err != nil, "app.Test(req)") } @@ -1242,17 +1397,19 @@ func (errorReader) Read([]byte) (int, error) { // go test -run Test_Test_DumpError func Test_Test_DumpError(t *testing.T) { + t.Parallel() app := New() app.Get("/", testEmptyHandler) resp, err := app.Test(httptest.NewRequest(MethodGet, "/", errorReader(0))) require.True(t, resp == nil) - require.Equal(t, "errorReader", err.Error()) + require.Equal(t, "failed to dump request: errorReader", err.Error()) } // go test -run Test_App_Handler func Test_App_Handler(t *testing.T) { + t.Parallel() h := New().Handler() require.Equal(t, "fasthttp.RequestHandler", reflect.TypeOf(h).String()) } @@ -1261,10 +1418,11 @@ type invalidView struct{} func (invalidView) Load() error { return errors.New("invalid view") } -func (i invalidView) Render(io.Writer, string, any, ...string) error { panic("implement me") } +func (invalidView) Render(io.Writer, string, any, ...string) error { panic("implement me") } // go test -run Test_App_Init_Error_View func Test_App_Init_Error_View(t *testing.T) { + t.Parallel() app := New(Config{Views: invalidView{}}) defer func() { @@ -1272,11 +1430,14 @@ func Test_App_Init_Error_View(t *testing.T) { require.Equal(t, "implement me", fmt.Sprintf("%v", err)) } }() - _ = app.config.Views.Render(nil, "", nil) + + err := app.config.Views.Render(nil, "", nil) + require.NoError(t, err) } // go test -run Test_App_Stack func Test_App_Stack(t *testing.T) { + t.Parallel() app := New() app.Use("/path0", testEmptyHandler) @@ -1300,6 +1461,7 @@ func Test_App_Stack(t *testing.T) { // go test -run Test_App_HandlersCount func Test_App_HandlersCount(t *testing.T) { + t.Parallel() app := New() app.Use("/path0", testEmptyHandler) @@ -1312,6 +1474,7 @@ func Test_App_HandlersCount(t *testing.T) { // go test -run Test_App_ReadTimeout func Test_App_ReadTimeout(t *testing.T) { + t.Parallel() app := New(Config{ ReadTimeout: time.Nanosecond, IdleTimeout: time.Minute, @@ -1350,6 +1513,7 @@ func Test_App_ReadTimeout(t *testing.T) { // go test -run Test_App_BadRequest func Test_App_BadRequest(t *testing.T) { + t.Parallel() app := New() app.Get("/bad-request", func(c Ctx) error { @@ -1383,6 +1547,7 @@ func Test_App_BadRequest(t *testing.T) { // go test -run Test_App_SmallReadBuffer func Test_App_SmallReadBuffer(t *testing.T) { + t.Parallel() app := New(Config{ ReadBufferSize: 1, }) @@ -1393,11 +1558,12 @@ func Test_App_SmallReadBuffer(t *testing.T) { go func() { time.Sleep(500 * time.Millisecond) - resp, err := http.Get("http://127.0.0.1:4006/small-read-buffer") - if resp != nil { - require.Equal(t, 431, resp.StatusCode) - } + req, err := http.NewRequestWithContext(context.Background(), MethodGet, "http://127.0.0.1:4006/small-read-buffer", http.NoBody) + require.NoError(t, err) + var client http.Client + resp, err := client.Do(req) require.NoError(t, err) + require.Equal(t, 431, resp.StatusCode) require.Nil(t, app.Shutdown()) }() @@ -1405,12 +1571,14 @@ func Test_App_SmallReadBuffer(t *testing.T) { } func Test_App_Server(t *testing.T) { + t.Parallel() app := New() require.False(t, app.Server() == nil) } func Test_App_Error_In_Fasthttp_Server(t *testing.T) { + t.Parallel() app := New() app.config.ErrorHandler = func(c Ctx, err error) error { return errors.New("fake error") @@ -1424,28 +1592,30 @@ func Test_App_Error_In_Fasthttp_Server(t *testing.T) { // go test -race -run Test_App_New_Test_Parallel func Test_App_New_Test_Parallel(t *testing.T) { + t.Parallel() t.Run("Test_App_New_Test_Parallel_1", func(t *testing.T) { t.Parallel() app := New(Config{Immutable: true}) - _, err := app.Test(httptest.NewRequest("GET", "/", nil)) - require.Equal(t, nil, err) + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.NoError(t, err) }) t.Run("Test_App_New_Test_Parallel_2", func(t *testing.T) { t.Parallel() app := New(Config{Immutable: true}) - _, err := app.Test(httptest.NewRequest("GET", "/", nil)) - require.Equal(t, nil, err) + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + require.NoError(t, err) }) } func Test_App_ReadBodyStream(t *testing.T) { + t.Parallel() app := New(Config{StreamRequestBody: true}) app.Post("/", func(c Ctx) error { // Calling c.Body() automatically reads the entire stream. return c.SendString(fmt.Sprintf("%v %s", c.Request().IsBodyStream(), c.Body())) }) testString := "this is a test" - resp, err := app.Test(httptest.NewRequest("POST", "/", bytes.NewBufferString(testString))) + resp, err := app.Test(httptest.NewRequest(MethodPost, "/", bytes.NewBufferString(testString))) require.NoError(t, err, "app.Test(req)") body, err := io.ReadAll(resp.Body) require.NoError(t, err, "io.ReadAll(resp.Body)") @@ -1453,6 +1623,7 @@ func Test_App_ReadBodyStream(t *testing.T) { } func Test_App_DisablePreParseMultipartForm(t *testing.T) { + t.Parallel() // Must be used with both otherwise there is no point. testString := "this is a test" @@ -1468,12 +1639,12 @@ func Test_App_DisablePreParseMultipartForm(t *testing.T) { } file, err := mpf.File["test"][0].Open() if err != nil { - return err + return fmt.Errorf("failed to open: %w", err) } buffer := make([]byte, len(testString)) n, err := file.Read(buffer) if err != nil { - return err + return fmt.Errorf("failed to read: %w", err) } if n != len(testString) { return fmt.Errorf("bad read length") @@ -1489,7 +1660,7 @@ func Test_App_DisablePreParseMultipartForm(t *testing.T) { require.Equal(t, len(testString), n, "writer n") require.Nil(t, w.Close(), "w.Close()") - req := httptest.NewRequest("POST", "/", b) + req := httptest.NewRequest(MethodPost, "/", b) req.Header.Set("Content-Type", w.FormDataContentType()) resp, err := app.Test(req) require.NoError(t, err, "app.Test(req)") @@ -1500,6 +1671,7 @@ func Test_App_DisablePreParseMultipartForm(t *testing.T) { } func Test_App_Test_no_timeout_infinitely(t *testing.T) { + t.Parallel() var err error c := make(chan int) @@ -1511,7 +1683,7 @@ func Test_App_Test_no_timeout_infinitely(t *testing.T) { return nil }) - req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + req := httptest.NewRequest(MethodGet, "/", http.NoBody) _, err = app.Test(req, -1) }() @@ -1532,6 +1704,7 @@ func Test_App_Test_no_timeout_infinitely(t *testing.T) { } func Test_App_SetTLSHandler(t *testing.T) { + t.Parallel() tlsHandler := &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{ ServerName: "example.golang", }} @@ -1546,7 +1719,8 @@ func Test_App_SetTLSHandler(t *testing.T) { } func Test_App_AddCustomRequestMethod(t *testing.T) { - methods := append(DefaultMethods, "TEST") + t.Parallel() + methods := append(DefaultMethods, "TEST") //nolint:gocritic // We want a new slice here app := New(Config{ RequestMethods: methods, }) @@ -1559,6 +1733,7 @@ func Test_App_AddCustomRequestMethod(t *testing.T) { } func TestApp_GetRoutes(t *testing.T) { + t.Parallel() app := New() app.Use(func(c Ctx) error { return c.Next() @@ -1586,3 +1761,62 @@ func TestApp_GetRoutes(t *testing.T) { require.Equal(t, name, route.Name) } } + +func Test_Middleware_Route_Naming_With_Use(t *testing.T) { + named := "named" + app := New() + + app.Get("/unnamed", func(c Ctx) error { + return c.Next() + }) + + app.Post("/named", func(c Ctx) error { + return c.Next() + }).Name(named) + + app.Use(func(c Ctx) error { + return c.Next() + }) // no name - logging MW + + app.Use(func(c Ctx) error { + return c.Next() + }).Name("corsMW") + + app.Use(func(c Ctx) error { + return c.Next() + }).Name("compressMW") + + app.Use(func(c Ctx) error { + return c.Next() + }) // no name - cache MW + + grp := app.Group("/pages").Name("pages.") + grp.Use(func(c Ctx) error { + return c.Next() + }).Name("csrfMW") + + grp.Get("/home", func(c Ctx) error { + return c.Next() + }).Name("home") + + grp.Get("/unnamed", func(c Ctx) error { + return c.Next() + }) + + for _, route := range app.GetRoutes() { + switch route.Path { + case "/": + require.Equal(t, "compressMW", route.Name) + case "/unnamed": + require.Equal(t, "", route.Name) + case "named": + require.Equal(t, named, route.Name) + case "/pages": + require.Equal(t, "pages.csrfMW", route.Name) + case "/pages/home": + require.Equal(t, "pages.home", route.Name) + case "/pages/unnamed": + require.Equal(t, "", route.Name) + } + } +} diff --git a/bind_test.go b/bind_test.go index 1dcd1b9e28..dd239f460d 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1,3 +1,4 @@ +//nolint:wrapcheck,tagliatelle,bodyclose // We must not wrap errors in tests package fiber import ( diff --git a/client.go b/client.go index b217f39b8f..4fb177da06 100644 --- a/client.go +++ b/client.go @@ -8,11 +8,9 @@ import ( "fmt" "io" "mime/multipart" - "net" "os" "path/filepath" "strconv" - "strings" "sync" "time" @@ -79,50 +77,50 @@ type Client struct { JSONDecoder utils.JSONUnmarshal } -// Get returns a agent with http method GET. +// Get returns an agent with http method GET. func Get(url string) *Agent { return defaultClient.Get(url) } -// Get returns a agent with http method GET. +// Get returns an agent with http method GET. func (c *Client) Get(url string) *Agent { return c.createAgent(MethodGet, url) } -// Head returns a agent with http method HEAD. +// Head returns an agent with http method HEAD. func Head(url string) *Agent { return defaultClient.Head(url) } -// Head returns a agent with http method GET. +// Head returns an agent with http method GET. func (c *Client) Head(url string) *Agent { return c.createAgent(MethodHead, url) } -// Post sends POST request to the given url. +// Post sends POST request to the given URL. func Post(url string) *Agent { return defaultClient.Post(url) } -// Post sends POST request to the given url. +// Post sends POST request to the given URL. func (c *Client) Post(url string) *Agent { return c.createAgent(MethodPost, url) } -// Put sends PUT request to the given url. +// Put sends PUT request to the given URL. func Put(url string) *Agent { return defaultClient.Put(url) } -// Put sends PUT request to the given url. +// Put sends PUT request to the given URL. func (c *Client) Put(url string) *Agent { return c.createAgent(MethodPut, url) } -// Patch sends PATCH request to the given url. +// Patch sends PATCH request to the given URL. func Patch(url string) *Agent { return defaultClient.Patch(url) } -// Patch sends PATCH request to the given url. +// Patch sends PATCH request to the given URL. func (c *Client) Patch(url string) *Agent { return c.createAgent(MethodPatch, url) } -// Delete sends DELETE request to the given url. +// Delete sends DELETE request to the given URL. func Delete(url string) *Agent { return defaultClient.Delete(url) } -// Delete sends DELETE request to the given url. +// Delete sends DELETE request to the given URL. func (c *Client) Delete(url string) *Agent { return c.createAgent(MethodDelete, url) } @@ -188,11 +186,11 @@ func (a *Agent) Parse() error { uri := a.req.URI() - isTLS := false + var isTLS bool scheme := uri.Scheme() - if bytes.Equal(scheme, strHTTPS) { + if bytes.Equal(scheme, []byte(schemeHTTPS)) { isTLS = true - } else if !bytes.Equal(scheme, strHTTP) { + } else if !bytes.Equal(scheme, []byte(schemeHTTP)) { return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme) } @@ -202,7 +200,7 @@ func (a *Agent) Parse() error { } a.HostClient = &fasthttp.HostClient{ - Addr: addMissingPort(string(uri.Host()), isTLS), + Addr: fasthttp.AddMissingPort(string(uri.Host()), isTLS), Name: name, NoDefaultUserAgentHeader: a.NoDefaultUserAgentHeader, IsTLS: isTLS, @@ -211,18 +209,6 @@ func (a *Agent) Parse() error { return nil } -func addMissingPort(addr string, isTLS bool) string { - n := strings.Index(addr, ":") - if n >= 0 { - return addr - } - port := 80 - if isTLS { - port = 443 - } - return net.JoinHostPort(addr, strconv.Itoa(port)) -} - /************************** Header Setting **************************/ // Set sets the given 'key: value' header. @@ -255,7 +241,7 @@ func (a *Agent) SetBytesV(k string, v []byte) *Agent { // SetBytesKV sets the given 'key: value' header. // // Use AddBytesKV for setting multiple header values under the same key. -func (a *Agent) SetBytesKV(k []byte, v []byte) *Agent { +func (a *Agent) SetBytesKV(k, v []byte) *Agent { a.req.Header.SetBytesKV(k, v) return a @@ -295,7 +281,7 @@ func (a *Agent) AddBytesV(k string, v []byte) *Agent { // // Multiple headers with the same key may be added with this function. // Use SetBytesKV for setting a single header for the given key. -func (a *Agent) AddBytesKV(k []byte, v []byte) *Agent { +func (a *Agent) AddBytesKV(k, v []byte) *Agent { a.req.Header.AddBytesKV(k, v) return a @@ -393,7 +379,7 @@ func (a *Agent) ContentTypeBytes(contentType []byte) *Agent { /************************** URI Setting **************************/ -// Host sets host for the uri. +// Host sets host for the URI. func (a *Agent) Host(host string) *Agent { a.req.URI().SetHost(host) @@ -666,10 +652,8 @@ func (a *Agent) Reuse() *Agent { // certificate chain and host name. func (a *Agent) InsecureSkipVerify() *Agent { if a.HostClient.TLSConfig == nil { - /* #nosec G402 */ - a.HostClient.TLSConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 + a.HostClient.TLSConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We explicitly let the user set insecure mode here } else { - /* #nosec G402 */ a.HostClient.TLSConfig.InsecureSkipVerify = true } @@ -742,14 +726,14 @@ func (a *Agent) RetryIf(retryIf RetryIfFunc) *Agent { // Bytes returns the status code, bytes body and errors of url. // // it's not safe to use Agent after calling [Agent.Bytes] -func (a *Agent) Bytes() (code int, body []byte, errs []error) { +func (a *Agent) Bytes() (int, []byte, []error) { defer a.release() return a.bytes() } -func (a *Agent) bytes() (code int, body []byte, errs []error) { +func (a *Agent) bytes() (code int, body []byte, errs []error) { //nolint:nonamedreturns,revive // We want to overwrite the body in a deferred func. TODO: Check if we really need to do this. We eventually want to get rid of all named returns. if errs = append(errs, a.errs...); len(errs) > 0 { - return + return code, body, errs } var ( @@ -774,7 +758,7 @@ func (a *Agent) bytes() (code int, body []byte, errs []error) { code = resp.StatusCode() } - body = append(a.dest, resp.Body()...) + body = append(a.dest, resp.Body()...) //nolint:gocritic // We want to append to the returned slice here if nilResp { ReleaseResponse(resp) @@ -784,25 +768,25 @@ func (a *Agent) bytes() (code int, body []byte, errs []error) { if a.timeout > 0 { if err := a.HostClient.DoTimeout(req, resp, a.timeout); err != nil { errs = append(errs, err) - return + return code, body, errs } } else if a.maxRedirectsCount > 0 && (string(req.Header.Method()) == MethodGet || string(req.Header.Method()) == MethodHead) { if err := a.HostClient.DoRedirects(req, resp, a.maxRedirectsCount); err != nil { errs = append(errs, err) - return + return code, body, errs } } else if err := a.HostClient.Do(req, resp); err != nil { errs = append(errs, err) } - return + return code, body, errs } func printDebugInfo(req *Request, resp *Response, w io.Writer) { msg := fmt.Sprintf("Connected to %s(%s)\r\n\r\n", req.URI().Host(), resp.RemoteAddr()) - _, _ = w.Write(utils.UnsafeBytes(msg)) - _, _ = req.WriteTo(w) - _, _ = resp.WriteTo(w) + _, _ = w.Write(utils.UnsafeBytes(msg)) //nolint:errcheck // This will never fail + _, _ = req.WriteTo(w) //nolint:errcheck // This will never fail + _, _ = resp.WriteTo(w) //nolint:errcheck // This will never fail } // String returns the status code, string body and errors of url. @@ -811,20 +795,24 @@ func printDebugInfo(req *Request, resp *Response, w io.Writer) { func (a *Agent) String() (int, string, []error) { defer a.release() code, body, errs := a.bytes() + // TODO: There might be a data race here on body. Maybe use utils.CopyBytes on it? return code, utils.UnsafeString(body), errs } -// Struct returns the status code, bytes body and errors of url. +// Struct returns the status code, bytes body and errors of URL. // And bytes body will be unmarshalled to given v. // // it's not safe to use Agent after calling [Agent.Struct] -func (a *Agent) Struct(v any) (code int, body []byte, errs []error) { +func (a *Agent) Struct(v any) (int, []byte, []error) { defer a.release() - if code, body, errs = a.bytes(); len(errs) > 0 { - return + + code, body, errs := a.bytes() + if len(errs) > 0 { + return code, body, errs } + // TODO: This should only be done once if a.jsonDecoder == nil { a.jsonDecoder = json.Unmarshal } @@ -833,7 +821,7 @@ func (a *Agent) Struct(v any) (code int, body []byte, errs []error) { errs = append(errs, err) } - return + return code, body, errs } func (a *Agent) release() { @@ -891,12 +879,16 @@ func AcquireClient() *Client { if v == nil { return &Client{} } - return v.(*Client) + c, ok := v.(*Client) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Client")) + } + return c } // ReleaseClient returns c acquired via AcquireClient to client pool. // -// It is forbidden accessing req and/or its' members after returning +// It is forbidden accessing req and/or it's members after returning // it to client pool. func ReleaseClient(c *Client) { c.UserAgent = "" @@ -913,12 +905,16 @@ func ReleaseClient(c *Client) { // no longer needed. This allows Agent recycling, reduces GC pressure // and usually improves performance. func AcquireAgent() *Agent { - return agentPool.Get().(*Agent) + a, ok := agentPool.Get().(*Agent) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Agent")) + } + return a } -// ReleaseAgent returns a acquired via AcquireAgent to Agent pool. +// ReleaseAgent returns an acquired via AcquireAgent to Agent pool. // -// It is forbidden accessing req and/or its' members after returning +// It is forbidden accessing req and/or it's members after returning // it to Agent pool. func ReleaseAgent(a *Agent) { a.reset() @@ -936,12 +932,16 @@ func AcquireResponse() *Response { if v == nil { return &Response{} } - return v.(*Response) + r, ok := v.(*Response) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Response")) + } + return r } // ReleaseResponse return resp acquired via AcquireResponse to response pool. // -// It is forbidden accessing resp and/or its' members after returning +// It is forbidden accessing resp and/or it's members after returning // it to response pool. // Copy from fasthttp func ReleaseResponse(resp *Response) { @@ -959,7 +959,11 @@ func AcquireArgs() *Args { if v == nil { return &Args{} } - return v.(*Args) + a, ok := v.(*Args) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Args")) + } + return a } // ReleaseArgs returns the object acquired via AcquireArgs to the pool. @@ -980,7 +984,11 @@ func AcquireFormFile() *FormFile { if v == nil { return &FormFile{} } - return v.(*FormFile) + ff, ok := v.(*FormFile) + if !ok { + panic(fmt.Errorf("failed to type-assert to *FormFile")) + } + return ff } // ReleaseFormFile returns the object acquired via AcquireFormFile to the pool. @@ -995,9 +1003,7 @@ func ReleaseFormFile(ff *FormFile) { formFilePool.Put(ff) } -var ( - strHTTP = []byte("http") - strHTTPS = []byte("https") +const ( defaultUserAgent = "fiber" ) diff --git a/client_test.go b/client_test.go index 36b1ccba84..4eec890e72 100644 --- a/client_test.go +++ b/client_test.go @@ -1,3 +1,4 @@ +//nolint:wrapcheck // We must not wrap errors in tests package fiber import ( @@ -61,7 +62,6 @@ func Test_Client_Unsupported_Protocol(t *testing.T) { require.Equal(t, 1, len(errs)) require.Equal(t, `unsupported protocol "ftp". http and https are supported`, errs[0].Error()) - } func Test_Client_Get(t *testing.T) { @@ -288,6 +288,7 @@ func Test_Client_UserAgent(t *testing.T) { }() t.Run("default", func(t *testing.T) { + t.Parallel() for i := 0; i < 5; i++ { a := Get("http://example.com") @@ -302,6 +303,7 @@ func Test_Client_UserAgent(t *testing.T) { }) t.Run("custom", func(t *testing.T) { + t.Parallel() for i := 0; i < 5; i++ { c := AcquireClient() c.UserAgent = "ua" @@ -321,11 +323,14 @@ func Test_Client_UserAgent(t *testing.T) { } func Test_Client_Agent_Set_Or_Add_Headers(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { c.Request().Header.VisitAll(func(key, value []byte) { if k := string(key); k == "K1" || k == "K2" { - _, _ = c.Write(key) - _, _ = c.Write(value) + _, err := c.Write(key) + require.NoError(t, err) + _, err = c.Write(value) + require.NoError(t, err) } }) return nil @@ -346,6 +351,7 @@ func Test_Client_Agent_Set_Or_Add_Headers(t *testing.T) { } func Test_Client_Agent_Connection_Close(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { if c.Request().Header.ConnectionClose() { return c.SendString("close") @@ -361,6 +367,7 @@ func Test_Client_Agent_Connection_Close(t *testing.T) { } func Test_Client_Agent_UserAgent(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Header.UserAgent()) } @@ -374,6 +381,7 @@ func Test_Client_Agent_UserAgent(t *testing.T) { } func Test_Client_Agent_Cookie(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.SendString( c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4")) @@ -391,6 +399,7 @@ func Test_Client_Agent_Cookie(t *testing.T) { } func Test_Client_Agent_Referer(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Header.Referer()) } @@ -404,6 +413,7 @@ func Test_Client_Agent_Referer(t *testing.T) { } func Test_Client_Agent_ContentType(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Header.ContentType()) } @@ -449,6 +459,7 @@ func Test_Client_Agent_Host(t *testing.T) { } func Test_Client_Agent_QueryString(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().URI().QueryString()) } @@ -462,6 +473,7 @@ func Test_Client_Agent_QueryString(t *testing.T) { } func Test_Client_Agent_BasicAuth(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { // Get authorization header auth := c.Get(HeaderAuthorization) @@ -481,6 +493,7 @@ func Test_Client_Agent_BasicAuth(t *testing.T) { } func Test_Client_Agent_BodyString(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Body()) } @@ -493,6 +506,7 @@ func Test_Client_Agent_BodyString(t *testing.T) { } func Test_Client_Agent_Body(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Body()) } @@ -505,6 +519,7 @@ func Test_Client_Agent_Body(t *testing.T) { } func Test_Client_Agent_BodyStream(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.Send(c.Request().Body()) } @@ -575,6 +590,7 @@ func Test_Client_Agent_Dest(t *testing.T) { }() t.Run("small dest", func(t *testing.T) { + t.Parallel() dest := []byte("de") a := Get("http://example.com") @@ -590,6 +606,7 @@ func Test_Client_Agent_Dest(t *testing.T) { }) t.Run("enough dest", func(t *testing.T) { + t.Parallel() dest := []byte("foobar") a := Get("http://example.com") @@ -610,25 +627,34 @@ type readErrorConn struct { net.Conn } -func (r *readErrorConn) Read(p []byte) (int, error) { +func (*readErrorConn) Read(_ []byte) (int, error) { return 0, fmt.Errorf("error") } -func (r *readErrorConn) Write(p []byte) (int, error) { +func (*readErrorConn) Write(p []byte) (int, error) { return len(p), nil } -func (r *readErrorConn) Close() error { +func (*readErrorConn) Close() error { return nil } -func (r *readErrorConn) LocalAddr() net.Addr { +func (*readErrorConn) LocalAddr() net.Addr { return nil } -func (r *readErrorConn) RemoteAddr() net.Addr { +func (*readErrorConn) RemoteAddr() net.Addr { return nil } + +func (*readErrorConn) SetReadDeadline(_ time.Time) error { + return nil +} + +func (*readErrorConn) SetWriteDeadline(_ time.Time) error { + return nil +} + func Test_Client_Agent_RetryIf(t *testing.T) { t.Parallel() @@ -670,6 +696,7 @@ func Test_Client_Agent_RetryIf(t *testing.T) { } func Test_Client_Agent_Json(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { require.Equal(t, MIMEApplicationJSON, string(c.Request().Header.ContentType())) @@ -684,6 +711,7 @@ func Test_Client_Agent_Json(t *testing.T) { } func Test_Client_Agent_Json_Error(t *testing.T) { + t.Parallel() a := Get("http://example.com"). JSONEncoder(json.Marshal). JSON(complex(1, 1)) @@ -696,6 +724,7 @@ func Test_Client_Agent_Json_Error(t *testing.T) { } func Test_Client_Agent_XML(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { require.Equal(t, MIMEApplicationXML, string(c.Request().Header.ContentType())) @@ -710,6 +739,7 @@ func Test_Client_Agent_XML(t *testing.T) { } func Test_Client_Agent_XML_Error(t *testing.T) { + t.Parallel() a := Get("http://example.com"). XML(complex(1, 1)) @@ -721,6 +751,7 @@ func Test_Client_Agent_XML_Error(t *testing.T) { } func Test_Client_Agent_Form(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { require.Equal(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) @@ -815,7 +846,10 @@ func Test_Client_Agent_MultipartForm_SendFiles(t *testing.T) { buf := make([]byte, fh1.Size) f, err := fh1.Open() require.NoError(t, err) - defer func() { _ = f.Close() }() + defer func() { + err := f.Close() + require.NoError(t, err) + }() _, err = f.Read(buf) require.NoError(t, err) require.Equal(t, "form file", string(buf)) @@ -867,13 +901,16 @@ func checkFormFile(t *testing.T, fh *multipart.FileHeader, filename string) { basename := filepath.Base(filename) require.Equal(t, fh.Filename, basename) - b1, err := os.ReadFile(filename) + b1, err := os.ReadFile(filename) //nolint:gosec // We're in a test so reading user-provided files by name is fine require.NoError(t, err) b2 := make([]byte, fh.Size) f, err := fh.Open() require.NoError(t, err) - defer func() { _ = f.Close() }() + defer func() { + err := f.Close() + require.NoError(t, err) + }() _, err = f.Read(b2) require.NoError(t, err) require.Equal(t, b1, b2) @@ -912,6 +949,7 @@ func Test_Client_Agent_SendFile_Error(t *testing.T) { } func Test_Client_Debug(t *testing.T) { + t.Parallel() handler := func(c Ctx) error { return c.SendString("debug") } @@ -926,7 +964,7 @@ func Test_Client_Debug(t *testing.T) { str := output.String() - require.True(t, strings.Contains(str, "Connected to example.com(pipe)")) + require.True(t, strings.Contains(str, "Connected to example.com(InmemoryListener)")) require.True(t, strings.Contains(str, "GET / HTTP/1.1")) require.True(t, strings.Contains(str, "User-Agent: fiber")) require.True(t, strings.Contains(str, "Host: example.com\r\n\r\n")) @@ -1005,6 +1043,7 @@ func Test_Client_Agent_InsecureSkipVerify(t *testing.T) { cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") require.NoError(t, err) + //nolint:gosec // We're in a test so using old ciphers is fine serverTLSConf := &tls.Config{ Certificates: []tls.Certificate{cer}, } @@ -1092,6 +1131,7 @@ func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { }() t.Run("success", func(t *testing.T) { + t.Parallel() a := Get("http://example.com?foo"). MaxRedirectsCount(1) @@ -1105,6 +1145,7 @@ func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { }) t.Run("error", func(t *testing.T) { + t.Parallel() a := Get("http://example.com"). MaxRedirectsCount(1) @@ -1173,6 +1214,7 @@ func Test_Client_Agent_Struct(t *testing.T) { }) t.Run("error", func(t *testing.T) { + t.Parallel() a := Get("http://example.com/error") a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } @@ -1188,11 +1230,12 @@ func Test_Client_Agent_Struct(t *testing.T) { }) t.Run("nil jsonDecoder", func(t *testing.T) { + t.Parallel() a := AcquireAgent() defer ReleaseAgent(a) defer a.ConnectionClose() request := a.Request() - request.Header.SetMethod("GET") + request.Header.SetMethod(MethodGet) request.SetRequestURI("http://example.com") err := a.Parse() require.NoError(t, err) @@ -1214,13 +1257,8 @@ func Test_Client_Agent_Parse(t *testing.T) { require.Nil(t, a.Parse()) } -func Test_AddMissingPort_TLS(t *testing.T) { - addr := addMissingPort("example.com", true) - require.Equal(t, "example.com:443", addr) -} - func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), excepted string, count ...int) { - t.Parallel() + t.Helper() ln := fasthttputil.NewInmemoryListener() @@ -1262,8 +1300,8 @@ type errorMultipartWriter struct { count int } -func (e *errorMultipartWriter) Boundary() string { return "myBoundary" } -func (e *errorMultipartWriter) SetBoundary(_ string) error { return nil } +func (*errorMultipartWriter) Boundary() string { return "myBoundary" } +func (*errorMultipartWriter) SetBoundary(_ string) error { return nil } func (e *errorMultipartWriter) CreateFormFile(_, _ string) (io.Writer, error) { if e.count == 0 { e.count++ @@ -1271,8 +1309,8 @@ func (e *errorMultipartWriter) CreateFormFile(_, _ string) (io.Writer, error) { } return errorWriter{}, nil } -func (e *errorMultipartWriter) WriteField(_, _ string) error { return errors.New("WriteField error") } -func (e *errorMultipartWriter) Close() error { return errors.New("Close error") } +func (*errorMultipartWriter) WriteField(_, _ string) error { return errors.New("WriteField error") } +func (*errorMultipartWriter) Close() error { return errors.New("Close error") } type errorWriter struct{} diff --git a/ctx.go b/ctx.go index f91aa7b0c9..579310af46 100644 --- a/ctx.go +++ b/ctx.go @@ -26,6 +26,11 @@ import ( "github.com/valyala/fasthttp" ) +const ( + schemeHTTP = "http" + schemeHTTPS = "https" +) + // maxParams defines the maximum number of parameters per route. const maxParams = 30 @@ -60,9 +65,10 @@ type TLSHandler struct { } // GetClientInfo Callback function to set CHI +// TODO: Why is this a getter which sets stuff? func (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate, error) { t.clientHelloInfo = info - return nil, nil + return nil, nil //nolint:nilnil // Not returning anything useful here is probably fine } // Range data for c.Range @@ -96,73 +102,22 @@ type Views interface { // Accepts checks if the specified extensions or content types are acceptable. func (c *DefaultCtx) Accepts(offers ...string) string { - if len(offers) == 0 { - return "" - } - header := c.Get(HeaderAccept) - if header == "" { - return offers[0] - } - - spec, commaPos := "", 0 - for len(header) > 0 && commaPos != -1 { - commaPos = strings.IndexByte(header, ',') - if commaPos != -1 { - spec = strings.TrimLeft(header[:commaPos], " ") - } else { - spec = strings.TrimLeft(header, " ") - } - if factorSign := strings.IndexByte(spec, ';'); factorSign != -1 { - spec = spec[:factorSign] - } - - var mimetype string - for _, offer := range offers { - if len(offer) == 0 { - continue - // Accept: */* - } else if spec == "*/*" { - return offer - } - - if strings.IndexByte(offer, '/') != -1 { - mimetype = offer // MIME type - } else { - mimetype = utils.GetMIME(offer) // extension - } - - if spec == mimetype { - // Accept: / - return offer - } - - s := strings.IndexByte(mimetype, '/') - // Accept: /* - if strings.HasPrefix(spec, mimetype[:s]) && (spec[s:] == "/*" || mimetype[s:] == "/*") { - return offer - } - } - if commaPos != -1 { - header = header[commaPos+1:] - } - } - - return "" + return getOffer(c.Get(HeaderAccept), acceptsOfferType, offers...) } // AcceptsCharsets checks if the specified charset is acceptable. func (c *DefaultCtx) AcceptsCharsets(offers ...string) string { - return getOffer(c.Get(HeaderAcceptCharset), offers...) + return getOffer(c.Get(HeaderAcceptCharset), acceptsOffer, offers...) } // AcceptsEncodings checks if the specified encoding is acceptable. func (c *DefaultCtx) AcceptsEncodings(offers ...string) string { - return getOffer(c.Get(HeaderAcceptEncoding), offers...) + return getOffer(c.Get(HeaderAcceptEncoding), acceptsOffer, offers...) } // AcceptsLanguages checks if the specified language is acceptable. func (c *DefaultCtx) AcceptsLanguages(offers ...string) string { - return getOffer(c.Get(HeaderAcceptLanguage), offers...) + return getOffer(c.Get(HeaderAcceptLanguage), acceptsOffer, offers...) } // App returns the *App reference to the instance of the Fiber application @@ -230,8 +185,8 @@ func (c *DefaultCtx) Body() []byte { var body []byte // faster than peek c.Request().Header.VisitAll(func(key, value []byte) { - if utils.UnsafeString(key) == HeaderContentEncoding { - encoding = utils.UnsafeString(value) + if c.app.getString(key) == HeaderContentEncoding { + encoding = c.app.getString(value) } }) @@ -399,6 +354,7 @@ func (c *DefaultCtx) FormFile(key string) (*multipart.FileHeader, error) { } // FormValue returns the first value by key from a MultipartForm. +// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order. // Defaults to the empty string "" if the form value doesn't exist. // If a default value is given, it will return that value if the form value does not exist. // Returned value is only valid within the handler. Do not store any references. @@ -504,8 +460,11 @@ func (c *DefaultCtx) Hostname() string { // Port returns the remote port of the request. func (c *DefaultCtx) Port() string { - port := c.fasthttp.RemoteAddr().(*net.TCPAddr).Port - return strconv.Itoa(port) + tcpaddr, ok := c.fasthttp.RemoteAddr().(*net.TCPAddr) + if !ok { + panic(fmt.Errorf("failed to type-assert to *net.TCPAddr")) + } + return strconv.Itoa(tcpaddr.Port) } // IP returns the remote IP address of the request. @@ -522,13 +481,16 @@ func (c *DefaultCtx) IP() string { // extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear. // When IP validation is enabled, any invalid IPs will be omitted. func (c *DefaultCtx) extractIPsFromHeader(header string) []string { + // TODO: Reuse the c.extractIPFromHeader func somehow in here + headerValue := c.Get(header) // We can't know how many IPs we will return, but we will try to guess with this constant division. // Counting ',' makes function slower for about 50ns in general case. - estimatedCount := len(headerValue) / 8 - if estimatedCount > 8 { - estimatedCount = 8 // Avoid big allocation on big header + const maxEstimatedCount = 8 + estimatedCount := len(headerValue) / maxEstimatedCount + if estimatedCount > maxEstimatedCount { + estimatedCount = maxEstimatedCount // Avoid big allocation on big header } ipsFound := make([]string, 0, estimatedCount) @@ -538,8 +500,7 @@ func (c *DefaultCtx) extractIPsFromHeader(header string) []string { iploop: for { - v4 := false - v6 := false + var v4, v6 bool // Manually splitting string without allocating slice, working with parts directly i, j = j+1, j+2 @@ -589,8 +550,9 @@ func (c *DefaultCtx) extractIPFromHeader(header string) string { iploop: for { - v4 := false - v6 := false + var v4, v6 bool + + // Manually splitting string without allocating slice, working with parts directly i, j = j+1, j+2 if j > len(headerValue) { @@ -624,14 +586,14 @@ func (c *DefaultCtx) extractIPFromHeader(header string) string { return c.fasthttp.RemoteIP().String() } - // default behaviour if IP validation is not enabled is just to return whatever value is + // default behavior if IP validation is not enabled is just to return whatever value is // in the proxy header. Even if it is empty or invalid return c.Get(c.app.config.ProxyHeader) } // IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header. // When IP validation is enabled, only valid IPs are returned. -func (c *DefaultCtx) IPs() (ips []string) { +func (c *DefaultCtx) IPs() []string { return c.extractIPsFromHeader(HeaderXForwardedFor) } @@ -670,7 +632,7 @@ func (c *DefaultCtx) JSON(data any) error { func (c *DefaultCtx) JSONP(data any, callback ...string) error { raw, err := json.Marshal(data) if err != nil { - return err + return fmt.Errorf("failed to marshal: %w", err) } var result, cb string @@ -708,11 +670,11 @@ func (c *DefaultCtx) Links(link ...string) { bb := bytebufferpool.Get() for i := range link { if i%2 == 0 { - _ = bb.WriteByte('<') - _, _ = bb.WriteString(link[i]) - _ = bb.WriteByte('>') + _ = bb.WriteByte('<') //nolint:errcheck // This will never fail + _, _ = bb.WriteString(link[i]) //nolint:errcheck // This will never fail + _ = bb.WriteByte('>') //nolint:errcheck // This will never fail } else { - _, _ = bb.WriteString(`; rel="` + link[i] + `",`) + _, _ = bb.WriteString(`; rel="` + link[i] + `",`) //nolint:errcheck // This will never fail } } c.setCanonical(HeaderLink, strings.TrimRight(c.app.getString(bb.Bytes()), ",")) @@ -721,7 +683,7 @@ func (c *DefaultCtx) Links(link ...string) { // Locals makes it possible to pass any values under keys scoped to the request // and therefore available to all following routes that match the request. -func (c *DefaultCtx) Locals(key any, value ...any) (val any) { +func (c *DefaultCtx) Locals(key any, value ...any) any { if len(value) == 0 { return c.fasthttp.UserValue(key) } @@ -764,10 +726,11 @@ func (c *DefaultCtx) ClientHelloInfo() *tls.ClientHelloInfo { } // Next executes the next method in the stack that matches the current route. -func (c *DefaultCtx) Next() (err error) { +func (c *DefaultCtx) Next() error { // Increment handler index c.indexHandler++ - // Did we executed all route handlers? + var err error + // Did we execute all route handlers? if c.indexHandler < len(c.route.Handlers) { // Continue route stack err = c.route.Handlers[c.indexHandler](c) @@ -782,7 +745,7 @@ func (c *DefaultCtx) Next() (err error) { return err } -// RestartRouting instead of going to the next handler. This may be usefull after +// RestartRouting instead of going to the next handler. This may be useful after // changing the request path. Note that handlers might be executed again. func (c *DefaultCtx) RestartRouting() error { var err error @@ -838,9 +801,8 @@ func (c *DefaultCtx) ParamsInt(key string, defaultValue ...int) (int, error) { if err != nil { if len(defaultValue) > 0 { return defaultValue[0], nil - } else { - return 0, err } + return 0, fmt.Errorf("failed to convert: %w", err) } return value, nil @@ -865,15 +827,16 @@ func (c *DefaultCtx) Path(override ...string) string { // Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. func (c *DefaultCtx) Scheme() string { if c.fasthttp.IsTLS() { - return "https" + return schemeHTTPS } if !c.IsProxyTrusted() { - return "http" + return schemeHTTP } - scheme := "http" + scheme := schemeHTTP + const lenXHeaderName = 12 c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { - if len(key) < 12 { + if len(key) < lenXHeaderName { return // Neither "X-Forwarded-" nor "X-Url-Scheme" } switch { @@ -888,7 +851,7 @@ func (c *DefaultCtx) Scheme() string { scheme = v } } else if bytes.Equal(key, []byte(HeaderXForwardedSsl)) && bytes.Equal(val, []byte("on")) { - scheme = "https" + scheme = schemeHTTPS } case bytes.Equal(key, []byte(HeaderXUrlScheme)): @@ -912,25 +875,116 @@ func (c *DefaultCtx) Query(key string, defaultValue ...string) string { return defaultString(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), defaultValue) } +// Queries returns a map of query parameters and their values. +// +// GET /?name=alex&wanna_cake=2&id= +// Queries()["name"] == "alex" +// Queries()["wanna_cake"] == "2" +// Queries()["id"] == "" +// +// GET /?field1=value1&field1=value2&field2=value3 +// Queries()["field1"] == "value2" +// Queries()["field2"] == "value3" +// +// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3 +// Queries()["list_a"] == "3" +// Queries()["list_b[]"] == "3" +// Queries()["list_c"] == "1,2,3" +// +// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending +// Queries()["filters.author.name"] == "John" +// Queries()["filters.category.name"] == "Technology" +// Queries()["filters[customer][name]"] == "Alice" +// Queries()["filters[status]"] == "pending" +func (c *DefaultCtx) Queries() map[string]string { + m := make(map[string]string, c.Context().QueryArgs().Len()) + c.Context().QueryArgs().VisitAll(func(key, value []byte) { + m[c.app.getString(key)] = c.app.getString(value) + }) + return m +} + +// QueryInt returns integer value of key string parameter in the url. +// Default to empty or invalid key is 0. +// +// GET /?name=alex&wanna_cake=2&id= +// QueryInt("wanna_cake", 1) == 2 +// QueryInt("name", 1) == 1 +// QueryInt("id", 1) == 1 +// QueryInt("id") == 0 +func (c *DefaultCtx) QueryInt(key string, defaultValue ...int) int { + // Use Atoi to convert the param to an int or return zero and an error + value, err := strconv.Atoi(c.app.getString(c.fasthttp.QueryArgs().Peek(key))) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + + return value +} + +// QueryBool returns bool value of key string parameter in the url. +// Default to empty or invalid key is true. +// +// Get /?name=alex&want_pizza=false&id= +// QueryBool("want_pizza") == false +// QueryBool("want_pizza", true) == false +// QueryBool("name") == false +// QueryBool("name", true) == true +// QueryBool("id") == false +// QueryBool("id", true) == true +func (c *DefaultCtx) QueryBool(key string, defaultValue ...bool) bool { + value, err := strconv.ParseBool(c.app.getString(c.fasthttp.QueryArgs().Peek(key))) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + return value +} + +// QueryFloat returns float64 value of key string parameter in the url. +// Default to empty or invalid key is 0. +// +// GET /?name=alex&amount=32.23&id= +// QueryFloat("amount") = 32.23 +// QueryFloat("amount", 3) = 32.23 +// QueryFloat("name", 1) = 1 +// QueryFloat("name") = 0 +// QueryFloat("id", 3) = 3 +func (c *DefaultCtx) QueryFloat(key string, defaultValue ...float64) float64 { + // use strconv.ParseFloat to convert the param to a float or return zero and an error. + value, err := strconv.ParseFloat(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), 64) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + return value +} + // Range returns a struct containing the type and a slice of ranges. -func (c *DefaultCtx) Range(size int) (rangeData Range, err error) { +func (c *DefaultCtx) Range(size int) (Range, error) { + var rangeData Range rangeStr := c.Get(HeaderRange) if rangeStr == "" || !strings.Contains(rangeStr, "=") { - err = ErrRangeMalformed - return + return rangeData, ErrRangeMalformed } data := strings.Split(rangeStr, "=") - if len(data) != 2 { - err = ErrRangeMalformed - return + const expectedDataParts = 2 + if len(data) != expectedDataParts { + return rangeData, ErrRangeMalformed } rangeData.Type = data[0] arr := strings.Split(data[1], ",") for i := 0; i < len(arr); i++ { item := strings.Split(arr[i], "-") if len(item) == 1 { - err = ErrRangeMalformed - return + return rangeData, ErrRangeMalformed } start, startErr := strconv.Atoi(item[0]) end, endErr := strconv.Atoi(item[1]) @@ -955,11 +1009,10 @@ func (c *DefaultCtx) Range(size int) (rangeData Range, err error) { }) } if len(rangeData.Ranges) < 1 { - err = ErrRangeUnsatisfiable - return + return rangeData, ErrRangeUnsatisfiable } - return + return rangeData, nil } // Redirect returns the Redirect reference. @@ -982,7 +1035,6 @@ func (c *DefaultCtx) BindVars(vars Map) error { for k, v := range vars { c.viewBindMap.Store(k, v) } - return nil } @@ -993,7 +1045,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro if !segment.IsParam { _, err := buf.WriteString(segment.Const) if err != nil { - return "", err + return "", fmt.Errorf("failed to write string: %w", err) } continue } @@ -1004,7 +1056,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro if isSame || isGreedy { _, err := buf.WriteString(utils.ToString(val)) if err != nil { - return "", err + return "", fmt.Errorf("failed to write string: %w", err) } } } @@ -1023,7 +1075,6 @@ func (c *DefaultCtx) GetRouteURL(routeName string, params Map) (string, error) { // Render a template with data and sends a text/html response. // We support the following engines: https://github.com/gofiber/template func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error { - var err error // Get new buffer from pool buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -1045,7 +1096,7 @@ func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error { // Render template from Views if app.config.Views != nil { if err := app.config.Views.Render(buf, name, bind, layouts...); err != nil { - return err + return fmt.Errorf("failed to render: %w", err) } rendered = true @@ -1057,17 +1108,18 @@ func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error { if !rendered { // Render raw template using 'name' as filepath if no engine is set var tmpl *template.Template - if _, err = readContent(buf, name); err != nil { + if _, err := readContent(buf, name); err != nil { return err } // Parse template - if tmpl, err = template.New("").Parse(c.app.getString(buf.Bytes())); err != nil { - return err + tmpl, err := template.New("").Parse(c.app.getString(buf.Bytes())) + if err != nil { + return fmt.Errorf("failed to parse: %w", err) } buf.Reset() // Render template - if err = tmpl.Execute(buf, bind); err != nil { - return err + if err := tmpl.Execute(buf, bind); err != nil { + return fmt.Errorf("failed to execute: %w", err) } } @@ -1075,28 +1127,35 @@ func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error { c.fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8) // Set rendered template to body c.fasthttp.Response.SetBody(buf.Bytes()) - // Return err if exist - return err -} -func (c *DefaultCtx) renderExtensions(bind Map) { - // Bind view map - c.viewBindMap.Range(func(key, value any) bool { - bind[key.(string)] = value - - return true - }) + return nil +} - // Check if the PassLocalsToViews option is enabled (by default it is disabled) - if c.app.config.PassLocalsToViews { - // Loop through each local and set it in the map - c.fasthttp.VisitUserValues(func(key []byte, val any) { - // check if bindMap doesn't contain the key - if _, ok := bind[utils.UnsafeString(key)]; !ok { - // Set the key and value in the bindMap - bind[utils.UnsafeString(key)] = val +func (c *DefaultCtx) renderExtensions(bind interface{}) { + if bindMap, ok := bind.(Map); ok { + // Bind view map + c.viewBindMap.Range(func(key, value interface{}) bool { + keyValue, ok := key.(string) + if !ok { + return true + } + if _, ok := bindMap[keyValue]; !ok { + bindMap[keyValue] = value } + return true }) + + // Check if the PassLocalsToViews option is enabled (by default it is disabled) + if c.app.config.PassLocalsToViews { + // Loop through each local and set it in the map + c.fasthttp.VisitUserValues(func(key []byte, val interface{}) { + // check if bindMap doesn't contain the key + if _, ok := bindMap[c.app.getString(key)]; !ok { + // Set the key and value in the bindMap + bindMap[c.app.getString(key)] = val + } + }) + } } if len(c.app.mountFields.appListKeys) == 0 { @@ -1120,28 +1179,32 @@ func (c *DefaultCtx) Route() *Route { } // SaveFile saves any multipart file to disk. -func (c *DefaultCtx) SaveFile(fileheader *multipart.FileHeader, path string) error { +func (*DefaultCtx) SaveFile(fileheader *multipart.FileHeader, path string) error { return fasthttp.SaveMultipartFile(fileheader, path) } // SaveFileToStorage saves any multipart file to an external storage system. -func (c *DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error { +func (*DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error { file, err := fileheader.Open() if err != nil { - return err + return fmt.Errorf("failed to open: %w", err) } content, err := io.ReadAll(file) if err != nil { - return err + return fmt.Errorf("failed to read: %w", err) + } + + if err := storage.Set(path, content, 0); err != nil { + return fmt.Errorf("failed to store: %w", err) } - return storage.Set(path, content, 0) + return nil } // Secure returns whether a secure connection was established. func (c *DefaultCtx) Secure() bool { - return c.Protocol() == "https" + return c.Protocol() == schemeHTTPS } // Send sets the HTTP response body without copying it. @@ -1167,6 +1230,7 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error { // https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134 sendFileOnce.Do(func() { + const cacheDuration = 10 * time.Second sendFileFS = &fasthttp.FS{ Root: "", AllowEmptyRoot: true, @@ -1174,7 +1238,7 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error { AcceptByteRange: true, Compress: true, CompressedFileSuffix: c.app.config.CompressedFileSuffix, - CacheDuration: 10 * time.Second, + CacheDuration: cacheDuration, IndexNames: []string{"index.html"}, PathNotFound: func(ctx *fasthttp.RequestCtx) { ctx.Response.SetStatusCode(StatusNotFound) @@ -1198,7 +1262,7 @@ func (c *DefaultCtx) SendFile(file string, compress ...bool) error { var err error file = filepath.FromSlash(file) if file, err = filepath.Abs(file); err != nil { - return err + return fmt.Errorf("failed to determine abs file path: %w", err) } if hasTrailingSlash { file += "/" @@ -1263,11 +1327,11 @@ func (c *DefaultCtx) SendStream(stream io.Reader, size ...int) error { } // Set sets the response's HTTP header field to the specified key, value. -func (c *DefaultCtx) Set(key string, val string) { +func (c *DefaultCtx) Set(key, val string) { c.fasthttp.Response.Header.Set(key, val) } -func (c *DefaultCtx) setCanonical(key string, val string) { +func (c *DefaultCtx) setCanonical(key, val string) { c.fasthttp.Response.Header.SetCanonical(utils.UnsafeBytes(key), utils.UnsafeBytes(val)) } @@ -1338,6 +1402,7 @@ func (c *DefaultCtx) Write(p []byte) (int, error) { // Writef appends f & a into response body writer. func (c *DefaultCtx) Writef(f string, a ...any) (int, error) { + //nolint:wrapcheck // This must not be wrapped return fmt.Fprintf(c.fasthttp.Response.BodyWriter(), f, a...) } @@ -1350,7 +1415,7 @@ func (c *DefaultCtx) WriteString(s string) (int, error) { // XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest, // indicating that the request was issued by a client library (such as jQuery). func (c *DefaultCtx) XHR() bool { - return utils.EqualFold(c.Get(HeaderXRequestedWith), "xmlhttprequest") + return utils.EqualFold(c.app.getBytes(c.Get(HeaderXRequestedWith)), []byte("xmlhttprequest")) } // configDependentPaths set paths for route recognition and prepared paths for the user, @@ -1379,8 +1444,9 @@ func (c *DefaultCtx) configDependentPaths() { // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes c.treePath = c.treePath[0:0] - if len(c.detectionPath) >= 3 { - c.treePath = c.detectionPath[:3] + const maxDetectionPaths = 3 + if len(c.detectionPath) >= maxDetectionPaths { + c.treePath = c.detectionPath[:maxDetectionPaths] } } @@ -1392,13 +1458,14 @@ func (c *DefaultCtx) IsProxyTrusted() bool { return true } - _, trusted := c.app.config.trustedProxiesMap[c.fasthttp.RemoteIP().String()] - if trusted { - return trusted + ip := c.fasthttp.RemoteIP() + + if _, trusted := c.app.config.trustedProxiesMap[ip.String()]; trusted { + return true } for _, ipNet := range c.app.config.trustedProxyRanges { - if ipNet.Contains(c.fasthttp.RemoteIP()) { + if ipNet.Contains(ip) { return true } } @@ -1407,7 +1474,7 @@ func (c *DefaultCtx) IsProxyTrusted() bool { } // IsLocalHost will return true if address is a localhost address. -func (c *DefaultCtx) isLocalHost(address string) bool { +func (*DefaultCtx) isLocalHost(address string) bool { localHosts := []string{"127.0.0.1", "0.0.0.0", "::1"} for _, h := range localHosts { if strings.Contains(address, h) { diff --git a/ctx_interface.go b/ctx_interface.go index 8388659b8d..604f123b68 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -7,6 +7,7 @@ package fiber import ( "context" "crypto/tls" + "fmt" "io" "mime/multipart" "sync" @@ -196,7 +197,7 @@ type Ctx interface { // Next executes the next method in the stack that matches the current route. Next() (err error) - // RestartRouting instead of going to the next handler. This may be usefull after + // RestartRouting instead of going to the next handler. This may be useful after // changing the request path. Note that handlers might be executed again. RestartRouting() error @@ -237,6 +238,39 @@ type Ctx interface { // Make copies or use the Immutable setting to use the value outside the Handler. Query(key string, defaultValue ...string) string + // QueryInt returns integer value of key string parameter in the url. + // Default to empty or invalid key is 0. + // + // GET /?name=alex&wanna_cake=2&id= + // QueryInt("wanna_cake", 1) == 2 + // QueryInt("name", 1) == 1 + // QueryInt("id", 1) == 1 + // QueryInt("id") == 0 + QueryInt(key string, defaultValue ...int) int + + // QueryBool returns bool value of key string parameter in the url. + // Default to empty or invalid key is true. + // + // Get /?name=alex&want_pizza=false&id= + // QueryBool("want_pizza") == false + // QueryBool("want_pizza", true) == false + // QueryBool("name") == false + // QueryBool("name", true) == true + // QueryBool("id") == false + // QueryBool("id", true) == true + QueryBool(key string, defaultValue ...bool) bool + + // QueryFloat returns float64 value of key string parameter in the url. + // Default to empty or invalid key is 0. + // + // GET /?name=alex&amount=32.23&id= + // QueryFloat("amount") = 32.23 + // QueryFloat("amount", 3) = 32.23 + // QueryFloat("name", 1) = 1 + // QueryFloat("name") = 0 + // QueryFloat("id", 3) = 3 + QueryFloat(key string, defaultValue ...float64) float64 + // Range returns a struct containing the type and a slice of ranges. Range(size int) (rangeData Range, err error) @@ -290,7 +324,7 @@ type Ctx interface { SendStream(stream io.Reader, size ...int) error // Set sets the response's HTTP header field to the specified key, value. - Set(key string, val string) + Set(key, val string) // Subdomains returns a string slice of subdomains in the domain name of the request. // The subdomain offset, which defaults to 2, is used for determining the beginning of the subdomain segments. @@ -418,7 +452,11 @@ func (app *App) NewCtx(fctx *fasthttp.RequestCtx) Ctx { // AcquireCtx retrieves a new Ctx from the pool. func (app *App) AcquireCtx() Ctx { - return app.pool.Get().(Ctx) + ctx, ok := app.pool.Get().(Ctx) + if !ok { + panic(fmt.Errorf("failed to type-assert to Ctx")) + } + return ctx } // ReleaseCtx releases the ctx back into the pool. diff --git a/ctx_test.go b/ctx_test.go index 8b0aebdfee..f2eca403a2 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -2,6 +2,7 @@ // 🤖 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package fiber import ( @@ -42,6 +43,10 @@ func Test_Ctx_Accepts(t *testing.T) { require.Equal(t, "", c.Accepts()) require.Equal(t, ".xml", c.Accepts(".xml")) require.Equal(t, "", c.Accepts(".john")) + require.Equal(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type") + + c.Request().Header.Set(HeaderAccept, "application/json, text/plain, */*;q=0") + require.Equal(t, "", c.Accepts("html"), "must treat */*;q=0 as not acceptable") c.Request().Header.Set(HeaderAccept, "text/*, application/json") require.Equal(t, "html", c.Accepts("html")) @@ -61,23 +66,36 @@ func Test_Ctx_Accepts(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Accepts -benchmem -count=4 func Benchmark_Ctx_Accepts(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9") - var res string - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - res = c.Accepts(".xml") + acceptHeader := "text/html,application/xhtml+xml,application/xml;q=0.9" + c.Request().Header.Set("Accept", acceptHeader) + acceptValues := [][]string{ + {".xml"}, + {"json", "xml"}, + {"application/json", "application/xml"}, + } + expectedResults := []string{".xml", "xml", "application/xml"} + + for i := 0; i < len(acceptValues); i++ { + b.Run(fmt.Sprintf("run-%#v", acceptValues[i]), func(bb *testing.B) { + var res string + bb.ReportAllocs() + bb.ResetTimer() + + for n := 0; n < bb.N; n++ { + res = c.Accepts(acceptValues[i]...) + } + require.Equal(bb, expectedResults[i], res) + }) } - require.Equal(b, ".xml", res) } type customCtx struct { DefaultCtx } -func (c *customCtx) Params(key string, defaultValue ...string) string { +func (c *customCtx) Params(key string, defaultValue ...string) string { //revive:disable-line:unused-parameter // We need defaultValue for some cases return "prefix_" + c.DefaultCtx.Params(key) } @@ -96,7 +114,7 @@ func Test_Ctx_CustomCtx(t *testing.T) { app.Get("/:id", func(c Ctx) error { return c.SendString(c.Params("id")) }) - resp, err := app.Test(httptest.NewRequest("GET", "/v3", &bytes.Buffer{})) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v3", &bytes.Buffer{})) require.NoError(t, err, "app.Test(req)") body, err := io.ReadAll(resp.Body) require.NoError(t, err, "io.ReadAll(resp.Body)") @@ -139,7 +157,7 @@ func Test_Ctx_AcceptsCharsets(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4 func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") var res string @@ -165,7 +183,7 @@ func Test_Ctx_AcceptsEncodings(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4 func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") var res string @@ -190,7 +208,7 @@ func Test_Ctx_AcceptsLanguages(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4 func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") var res string @@ -251,7 +269,7 @@ func Test_Ctx_Append(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 func Benchmark_Ctx_Append(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -284,7 +302,7 @@ func Test_Ctx_Attachment(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 func Benchmark_Ctx_Attachment(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -310,7 +328,7 @@ func Test_Ctx_BaseURL(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem func Benchmark_Ctx_BaseURL(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().SetHost("google.com:1337") c.Request().URI().SetPath("/haha/oke/lol") @@ -404,6 +422,7 @@ func Test_Ctx_UserContext(t *testing.T) { // go test -run Test_Ctx_SetUserContext func Test_Ctx_SetUserContext(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -416,6 +435,7 @@ func Test_Ctx_SetUserContext(t *testing.T) { // go test -run Test_Ctx_UserContext_Multiple_Requests func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { + t.Parallel() testKey := struct{}{} testValue := "foobar-value" @@ -492,12 +512,20 @@ func Test_Ctx_Cookie(t *testing.T) { cookie.MaxAge = 10000 c.Cookie(cookie) require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; path=/; secure; SameSite=None" + // should remove expires and max-age headers when no expire and no MaxAge (default time) + cookie.SessionOnly = false + cookie.Expires = time.Time{} + cookie.MaxAge = 0 + c.Cookie(cookie) + require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) } // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 func Benchmark_Ctx_Cookie(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -528,35 +556,43 @@ func Test_Ctx_Format(t *testing.T) { c := app.NewCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderAccept, MIMETextPlain) - c.Format([]byte("Hello, World!")) + err := c.Format([]byte("Hello, World!")) + require.NoError(t, err) require.Equal(t, "Hello, World!", string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMETextHTML) - c.Format("Hello, World!") + err = c.Format("Hello, World!") + require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON) - c.Format("Hello, World!") + err = c.Format("Hello, World!") + require.NoError(t, err) require.Equal(t, `"Hello, World!"`, string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMETextPlain) - c.Format(complex(1, 1)) + err = c.Format(complex(1, 1)) + require.NoError(t, err) require.Equal(t, "(1+1i)", string(c.Response().Body())) c.Request().Header.Set(HeaderAccept, MIMEApplicationXML) - c.Format("Hello, World!") + err = c.Format("Hello, World!") + require.NoError(t, err) require.Equal(t, `Hello, World!`, string(c.Response().Body())) - err := c.Format(complex(1, 1)) - require.True(t, err != nil) + err = c.Format(complex(1, 1)) + require.Error(t, err) c.Request().Header.Set(HeaderAccept, MIMETextPlain) - c.Format(Map{}) + err = c.Format(Map{}) + require.NoError(t, err) require.Equal(t, "map[]", string(c.Response().Body())) type broken string c.Request().Header.Set(HeaderAccept, "broken/accept") - c.Format(broken("Hello, World!")) + require.NoError(t, err) + err = c.Format(broken("Hello, World!")) + require.NoError(t, err) require.Equal(t, `Hello, World!`, string(c.Response().Body())) } @@ -641,12 +677,13 @@ func Test_Ctx_FormFile(t *testing.T) { f, err := fh.Open() require.NoError(t, err) + defer func() { + require.Equal(t, nil, f.Close()) + }() b := new(bytes.Buffer) _, err = io.Copy(b, f) require.NoError(t, err) - - f.Close() require.Equal(t, "hello world", b.String()) return nil }) @@ -659,8 +696,7 @@ func Test_Ctx_FormFile(t *testing.T) { _, err = ioWriter.Write([]byte("hello world")) require.NoError(t, err) - - writer.Close() + require.NoError(t, writer.Close()) req := httptest.NewRequest(MethodPost, "/test", body) req.Header.Set(HeaderContentType, writer.FormDataContentType()) @@ -683,10 +719,9 @@ func Test_Ctx_FormValue(t *testing.T) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) - require.Nil(t, writer.WriteField("name", "john")) + require.Nil(t, writer.Close()) - writer.Close() req := httptest.NewRequest(MethodPost, "/test", body) req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())) req.Header.Set("Content-Length", strconv.Itoa(len(body.Bytes()))) @@ -867,6 +902,97 @@ func Benchmark_Ctx_Host(b *testing.B) { require.Equal(b, "google.com", host) } +// go test -run Test_Ctx_IsProxyTrusted +func Test_Ctx_IsProxyTrusted(t *testing.T) { + t.Parallel() + + { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + require.True(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: false, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.True(t, c.IsProxyTrusted()) + } + + { + app := New(Config{ + EnableTrustedProxyCheck: true, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{}, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "127.0.0.1", + }, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "127.0.0.1/8", + }, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.0", + }, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.True(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.1/31", + }, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.True(t, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.1/31junk", + }, + }) + c := app.NewCtx(&fasthttp.RequestCtx{}) + require.False(t, c.IsProxyTrusted()) + } +} + // go test -run Test_Ctx_Hostname func Test_Ctx_Hostname(t *testing.T) { t.Parallel() @@ -986,7 +1112,7 @@ func Test_Ctx_IP(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - // default behaviour will return the remote IP from the stack + // default behavior will return the remote IP from the stack require.Equal(t, "0.0.0.0", c.IP()) // X-Forwarded-For is set, but it is ignored because proxyHeader is not set @@ -998,7 +1124,7 @@ func Test_Ctx_IP(t *testing.T) { func Test_Ctx_IP_ProxyHeader(t *testing.T) { t.Parallel() - // make sure that the same behaviour exists for different proxy header names + // make sure that the same behavior exists for different proxy header names proxyHeaderNames := []string{"Real-Ip", HeaderXForwardedFor} for _, proxyHeaderName := range proxyHeaderNames { @@ -1030,7 +1156,7 @@ func Test_Ctx_IP_ProxyHeader(t *testing.T) { func Test_Ctx_IP_ProxyHeader_With_IP_Validation(t *testing.T) { t.Parallel() - // make sure that the same behaviour exists for different proxy header names + // make sure that the same behavior exists for different proxy header names proxyHeaderNames := []string{"Real-Ip", HeaderXForwardedFor} for _, proxyHeaderName := range proxyHeaderNames { @@ -1297,6 +1423,7 @@ func Benchmark_Ctx_Is(b *testing.B) { // go test -run Test_Ctx_Locals func Test_Ctx_Locals(t *testing.T) { + t.Parallel() app := New() app.Use(func(c Ctx) error { c.Locals("john", "doe") @@ -1357,35 +1484,45 @@ func Test_Ctx_ClientHelloInfo(t *testing.T) { }) // Test without TLS handler - resp, _ := app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) - body, _ := io.ReadAll(resp.Body) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) + require.NoError(t, err) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) require.Equal(t, []byte("ClientHelloInfo is nil"), body) // Test with TLS Handler const ( - PSSWithSHA256 = 0x0804 - VersionTLS13 = 0x0304 + pssWithSHA256 = 0x0804 + versionTLS13 = 0x0304 ) app.tlsHandler = &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{ ServerName: "example.golang", - SignatureSchemes: []tls.SignatureScheme{PSSWithSHA256}, - SupportedVersions: []uint16{VersionTLS13}, + SignatureSchemes: []tls.SignatureScheme{pssWithSHA256}, + SupportedVersions: []uint16{versionTLS13}, }} // Test ServerName - resp, _ = app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) - body, _ = io.ReadAll(resp.Body) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) + require.NoError(t, err) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) require.Equal(t, []byte("example.golang"), body) // Test SignatureSchemes - resp, _ = app.Test(httptest.NewRequest(MethodGet, "/SignatureSchemes", nil)) - body, _ = io.ReadAll(resp.Body) - require.Equal(t, "["+strconv.Itoa(PSSWithSHA256)+"]", string(body)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/SignatureSchemes", nil)) + require.NoError(t, err) + + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "["+strconv.Itoa(pssWithSHA256)+"]", string(body)) // Test SupportedVersions - resp, _ = app.Test(httptest.NewRequest(MethodGet, "/SupportedVersions", nil)) - body, _ = io.ReadAll(resp.Body) - require.Equal(t, "["+strconv.Itoa(VersionTLS13)+"]", string(body)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/SupportedVersions", nil)) + require.NoError(t, err) + body, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "["+strconv.Itoa(versionTLS13)+"]", string(body)) } // go test -run Test_Ctx_InvalidMethod @@ -1422,8 +1559,8 @@ func Test_Ctx_MultipartForm(t *testing.T) { writer := multipart.NewWriter(body) require.Nil(t, writer.WriteField("name", "john")) + require.NoError(t, writer.Close()) - writer.Close() req := httptest.NewRequest(MethodPost, "/test", body) req.Header.Set(HeaderContentType, fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())) req.Header.Set(HeaderContentLength, strconv.Itoa(len(body.Bytes()))) @@ -1438,8 +1575,8 @@ func Benchmark_Ctx_MultipartForm(b *testing.B) { app := New() app.Post("/", func(c Ctx) error { - _, _ = c.MultipartForm() - return nil + _, err := c.MultipartForm() + return err }) c := &fasthttp.RequestCtx{} @@ -1535,14 +1672,14 @@ func Test_Ctx_Params_Case_Sensitive(t *testing.T) { require.Equal(t, StatusOK, resp.StatusCode, "Status code") resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/first/second", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, StatusOK, resp.StatusCode, "Status code") } // go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4 func Benchmark_Ctx_Params(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.route = &Route{ Params: []string{ @@ -1590,6 +1727,7 @@ func Test_Ctx_Path(t *testing.T) { // go test -run Test_Ctx_Protocol func Test_Ctx_Protocol(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -1625,23 +1763,31 @@ func Test_Ctx_Scheme(t *testing.T) { c := app.NewCtx(freq) - c.Request().Header.Set(HeaderXForwardedProto, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProto, "https, http") + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXForwardedProtocol, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProtocol, "https, http") + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() c.Request().Header.Set(HeaderXForwardedSsl, "on") - require.Equal(t, "https", c.Scheme()) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXUrlScheme, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) } // go test -v -run=^$ -bench=Benchmark_Ctx_Scheme -benchmem -count=4 @@ -1664,23 +1810,23 @@ func Test_Ctx_Scheme_TrustedProxy(t *testing.T) { app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}}) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Set(HeaderXForwardedProto, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXForwardedProtocol, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() c.Request().Header.Set(HeaderXForwardedSsl, "on") - require.Equal(t, "https", c.Scheme()) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXUrlScheme, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) } // go test -run Test_Ctx_Scheme_TrustedProxyRange @@ -1689,23 +1835,23 @@ func Test_Ctx_Scheme_TrustedProxyRange(t *testing.T) { app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Set(HeaderXForwardedProto, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXForwardedProtocol, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() c.Request().Header.Set(HeaderXForwardedSsl, "on") - require.Equal(t, "https", c.Scheme()) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXUrlScheme, "https") - require.Equal(t, "https", c.Scheme()) + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + require.Equal(t, schemeHTTPS, c.Scheme()) c.Request().Header.Reset() - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) } // go test -run Test_Ctx_Scheme_UntrustedProxyRange @@ -1714,23 +1860,23 @@ func Test_Ctx_Scheme_UntrustedProxyRange(t *testing.T) { app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.1.1.1/30"}}) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Set(HeaderXForwardedProto, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXForwardedProtocol, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() c.Request().Header.Set(HeaderXForwardedSsl, "on") - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXUrlScheme, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) } // go test -run Test_Ctx_Scheme_UnTrustedProxy @@ -1739,23 +1885,23 @@ func Test_Ctx_Scheme_UnTrustedProxy(t *testing.T) { app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}}) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Request().Header.Set(HeaderXForwardedProto, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXForwardedProtocol, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() c.Request().Header.Set(HeaderXForwardedSsl, "on") - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - c.Request().Header.Set(HeaderXUrlScheme, "https") - require.Equal(t, "http", c.Scheme()) + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + require.Equal(t, schemeHTTP, c.Scheme()) c.Request().Header.Reset() - require.Equal(t, "http", c.Scheme()) + require.Equal(t, schemeHTTP, c.Scheme()) } // go test -run Test_Ctx_Query @@ -1770,6 +1916,50 @@ func Test_Ctx_Query(t *testing.T) { require.Equal(t, "default", c.Query("unknown", "default")) } +func Test_Ctx_QueryInt(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().URI().SetQueryString("search=john&age=20&id=") + require.Equal(t, 0, c.QueryInt("foo")) + require.Equal(t, 20, c.QueryInt("age", 12)) + require.Equal(t, 0, c.QueryInt("search")) + require.Equal(t, 1, c.QueryInt("search", 1)) + require.Equal(t, 0, c.QueryInt("id")) + require.Equal(t, 2, c.QueryInt("id", 2)) +} + +func Test_Ctx_QueryBool(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().URI().SetQueryString("name=alex&want_pizza=false&id=") + + require.Equal(t, false, c.QueryBool("want_pizza")) + require.Equal(t, false, c.QueryBool("want_pizza", true)) + require.Equal(t, false, c.QueryBool("name")) + require.Equal(t, true, c.QueryBool("name", true)) + require.Equal(t, false, c.QueryBool("id")) + require.Equal(t, true, c.QueryBool("id", true)) +} + +func Test_Ctx_QueryFloat(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().URI().SetQueryString("name=alex&amount=32.23&id=") + + require.Equal(t, 32.23, c.QueryFloat("amount")) + require.Equal(t, 32.23, c.QueryFloat("amount", 3.123)) + require.Equal(t, 87.123, c.QueryFloat("name", 87.123)) + require.Equal(t, float64(0), c.QueryFloat("name")) + require.Equal(t, 12.87, c.QueryFloat("id", 12.87)) + require.Equal(t, float64(0), c.QueryFloat("id")) +} + // go test -run Test_Ctx_Range func Test_Ctx_Range(t *testing.T) { t.Parallel() @@ -1856,7 +2046,12 @@ func Test_Ctx_SaveFile(t *testing.T) { tempFile, err := os.CreateTemp(os.TempDir(), "test-") require.NoError(t, err) - defer os.Remove(tempFile.Name()) + defer func(file *os.File) { + err := file.Close() + require.NoError(t, err) + err = os.Remove(file.Name()) + require.NoError(t, err) + }(tempFile) err = c.SaveFile(fh, tempFile.Name()) require.NoError(t, err) @@ -1874,7 +2069,7 @@ func Test_Ctx_SaveFile(t *testing.T) { _, err = ioWriter.Write([]byte("hello world")) require.NoError(t, err) - writer.Close() + require.NoError(t, writer.Close()) req := httptest.NewRequest(MethodPost, "/test", body) req.Header.Set("Content-Type", writer.FormDataContentType()) @@ -1916,7 +2111,7 @@ func Test_Ctx_SaveFileToStorage(t *testing.T) { _, err = ioWriter.Write([]byte("hello world")) require.NoError(t, err) - writer.Close() + require.NoError(t, writer.Close()) req := httptest.NewRequest(MethodPost, "/test", body) req.Header.Set("Content-Type", writer.FormDataContentType()) @@ -2001,7 +2196,9 @@ func Test_Ctx_Download(t *testing.T) { f, err := os.Open("./ctx.go") require.NoError(t, err) - defer f.Close() + defer func() { + require.NoError(t, f.Close()) + }() expect, err := io.ReadAll(f) require.NoError(t, err) @@ -2020,7 +2217,9 @@ func Test_Ctx_SendFile(t *testing.T) { // fetch file content f, err := os.Open("./ctx.go") require.NoError(t, err) - defer f.Close() + defer func() { + require.NoError(t, nil, f.Close()) + }() expectFileContent, err := io.ReadAll(f) require.NoError(t, err) // fetch file info for the not modified test case @@ -2066,7 +2265,7 @@ func Test_Ctx_SendFile_404(t *testing.T) { return err }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, StatusNotFound, resp.StatusCode) } @@ -2104,11 +2303,11 @@ func Test_Ctx_SendFile_Immutable(t *testing.T) { for _, endpoint := range endpointsForTest { t.Run(endpoint, func(t *testing.T) { // 1st try - resp, err := app.Test(httptest.NewRequest("GET", endpoint, nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, endpoint, nil)) require.NoError(t, err) require.Equal(t, StatusOK, resp.StatusCode) // 2nd try - resp, err = app.Test(httptest.NewRequest("GET", endpoint, nil)) + resp, err = app.Test(httptest.NewRequest(MethodGet, endpoint, nil)) require.NoError(t, err) require.Equal(t, StatusOK, resp.StatusCode) }) @@ -2126,9 +2325,9 @@ func Test_Ctx_SendFile_RestoreOriginalURL(t *testing.T) { return err }) - _, err1 := app.Test(httptest.NewRequest("GET", "/?test=true", nil)) + _, err1 := app.Test(httptest.NewRequest(MethodGet, "/?test=true", nil)) // second request required to confirm with zero allocation - _, err2 := app.Test(httptest.NewRequest("GET", "/?test=true", nil)) + _, err2 := app.Test(httptest.NewRequest(MethodGet, "/?test=true", nil)) require.Nil(t, err1) require.Nil(t, err2) @@ -2213,7 +2412,7 @@ func Test_Ctx_JSONP(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4 func Benchmark_Ctx_JSONP(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed type SomeStruct struct { Name string @@ -2238,7 +2437,7 @@ func Benchmark_Ctx_JSONP(b *testing.B) { func Test_Ctx_XML(t *testing.T) { t.Parallel() app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed require.True(t, c.JSON(complex(1, 1)) != nil) @@ -2271,7 +2470,7 @@ func Test_Ctx_XML(t *testing.T) { // go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4 func Benchmark_Ctx_XML(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed type SomeStruct struct { Name string `xml:"Name"` Age uint8 `xml:"Age"` @@ -2310,7 +2509,7 @@ func Test_Ctx_Links(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 func Benchmark_Ctx_Links(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -2334,6 +2533,7 @@ func Test_Ctx_Location(t *testing.T) { // go test -run Test_Ctx_Next func Test_Ctx_Next(t *testing.T) { + t.Parallel() app := New() app.Use("/", func(c Ctx) error { return c.Next() @@ -2350,6 +2550,7 @@ func Test_Ctx_Next(t *testing.T) { // go test -run Test_Ctx_Next_Error func Test_Ctx_Next_Error(t *testing.T) { + t.Parallel() app := New() app.Use("/", func(c Ctx) error { c.Set("X-Next-Result", "Works") @@ -2371,12 +2572,12 @@ func Test_Ctx_Render(t *testing.T) { err := c.Render("./.github/testdata/index.tmpl", Map{ "Title": "Hello, World!", }) + require.NoError(t, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail defer bytebufferpool.Put(buf) - require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(c.Response().Body())) err = c.Render("./.github/testdata/template-non-exists.html", nil) @@ -2396,12 +2597,12 @@ func Test_Ctx_RenderWithoutLocals(t *testing.T) { c.Locals("Title", "Hello, World!") err := c.Render("./.github/testdata/index.tmpl", Map{}) + require.NoError(t, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail defer bytebufferpool.Put(buf) - require.NoError(t, err) require.Equal(t, "

", string(c.Response().Body())) } @@ -2415,14 +2616,13 @@ func Test_Ctx_RenderWithLocals(t *testing.T) { c.Locals("Title", "Hello, World!") err := c.Render("./.github/testdata/index.tmpl", Map{}) + require.NoError(t, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail defer bytebufferpool.Put(buf) - require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(c.Response().Body())) - } func Test_Ctx_RenderWithBindVars(t *testing.T) { @@ -2431,24 +2631,45 @@ func Test_Ctx_RenderWithBindVars(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.BindVars(Map{ + err := c.BindVars(Map{ "Title": "Hello, World!", }) + require.NoError(t, err) - err := c.Render("./.github/testdata/index.tmpl", Map{}) + err = c.Render("./.github/testdata/index.tmpl", Map{}) require.NoError(t, err) buf := bytebufferpool.Get() - _, _ = buf.WriteString("overwrite") + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail defer bytebufferpool.Put(buf) require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(c.Response().Body())) +} + +func Test_Ctx_RenderWithOverwrittenBind(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + err := c.BindVars(Map{ + "Title": "Hello, World!", + }) + require.NoError(t, err) + err = c.Render("./.github/testdata/index.tmpl", Map{ + "Title": "Hello from Fiber!", + }) + require.NoError(t, err) + + buf := bytebufferpool.Get() + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail + defer bytebufferpool.Put(buf) + + require.Equal(t, "

Hello from Fiber!

", string(c.Response().Body())) } func Test_Ctx_RenderWithBindVarsLocals(t *testing.T) { t.Parallel() - app := New(Config{ PassLocalsToViews: true, }) @@ -2466,6 +2687,7 @@ func Test_Ctx_RenderWithBindVarsLocals(t *testing.T) { require.NoError(t, err) require.Equal(t, "

Hello, World! Test

", string(c.Response().Body())) + require.Equal(t, "

Hello, World! Test

", string(c.Response().Body())) } func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) { @@ -2500,10 +2722,10 @@ func Benchmark_Ctx_RenderWithLocalsAndBindVars(b *testing.B) { }) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.BindVars(Map{ + err = c.BindVars(Map{ "Title": "Hello, World!", }) - require.Equal(b, nil, err) + require.NoError(b, err) c.Locals("Summary", "Test") b.ReportAllocs() @@ -2548,10 +2770,10 @@ func Benchmark_Ctx_RenderBindVars(b *testing.B) { app.config.Views = engine c := app.NewCtx(&fasthttp.RequestCtx{}) - c.BindVars(Map{ + err = c.BindVars(Map{ "Title": "Hello, World!", }) - require.Equal(b, nil, err) + require.NoError(b, err) b.ReportAllocs() b.ResetTimer() @@ -2566,6 +2788,7 @@ func Benchmark_Ctx_RenderBindVars(b *testing.B) { // go test -run Test_Ctx_RestartRouting func Test_Ctx_RestartRouting(t *testing.T) { + t.Parallel() app := New() calls := 0 app.Get("/", func(c Ctx) error { @@ -2583,9 +2806,9 @@ func Test_Ctx_RestartRouting(t *testing.T) { // go test -run Test_Ctx_RestartRoutingWithChangedPath func Test_Ctx_RestartRoutingWithChangedPath(t *testing.T) { + t.Parallel() app := New() - executedOldHandler := false - executedNewHandler := false + var executedOldHandler, executedNewHandler bool app.Get("/old", func(c Ctx) error { c.Path("/new") @@ -2609,6 +2832,7 @@ func Test_Ctx_RestartRoutingWithChangedPath(t *testing.T) { // go test -run Test_Ctx_RestartRoutingWithChangedPathAnd404 func Test_Ctx_RestartRoutingWithChangedPathAndCatchAll(t *testing.T) { + t.Parallel() app := New() app.Get("/new", func(c Ctx) error { return nil @@ -2634,10 +2858,18 @@ type testTemplateEngine struct { func (t *testTemplateEngine) Render(w io.Writer, name string, bind any, layout ...string) error { if len(layout) == 0 { - return t.templates.ExecuteTemplate(w, name, bind) + if err := t.templates.ExecuteTemplate(w, name, bind); err != nil { + return fmt.Errorf("failed to execute template without layout: %w", err) + } + return nil + } + if err := t.templates.ExecuteTemplate(w, name, bind); err != nil { + return fmt.Errorf("failed to execute template: %w", err) } - _ = t.templates.ExecuteTemplate(w, name, bind) - return t.templates.ExecuteTemplate(w, layout[0], bind) + if err := t.templates.ExecuteTemplate(w, layout[0], bind); err != nil { + return fmt.Errorf("failed to execute template with layout: %w", err) + } + return nil } func (t *testTemplateEngine) Load() error { @@ -2650,6 +2882,7 @@ func (t *testTemplateEngine) Load() error { // go test -run Test_Ctx_Render_Engine func Test_Ctx_Render_Engine(t *testing.T) { + t.Parallel() engine := &testTemplateEngine{} require.Equal(t, nil, engine.Load()) app := New() @@ -2665,6 +2898,7 @@ func Test_Ctx_Render_Engine(t *testing.T) { // go test -run Test_Ctx_Render_Engine_With_View_Layout func Test_Ctx_Render_Engine_With_View_Layout(t *testing.T) { + t.Parallel() engine := &testTemplateEngine{} require.Equal(t, nil, engine.Load()) app := New(Config{ViewsLayout: "main.tmpl"}) @@ -2701,7 +2935,7 @@ func Benchmark_Ctx_Render_Engine(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4 func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed app.Get("/user/:name", func(c Ctx) error { return c.SendString(c.Params("name")) @@ -2712,14 +2946,17 @@ func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) { for n := 0; n < b.N; n++ { location, err = c.getLocationFromRoute(app.GetRoute("User"), Map{"name": "fiber"}) } + require.Equal(b, "/user/fiber", location) require.Equal(b, nil, err) - } // go test -run Test_Ctx_Get_Location_From_Route_name func Test_Ctx_Get_Location_From_Route_name(t *testing.T) { + t.Parallel() + t.Run("case insensitive", func(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) app.Get("/user/:name", func(c Ctx) error { @@ -2736,6 +2973,7 @@ func Test_Ctx_Get_Location_From_Route_name(t *testing.T) { }) t.Run("case sensitive", func(t *testing.T) { + t.Parallel() app := New(Config{CaseSensitive: true}) c := app.NewCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) @@ -2755,6 +2993,7 @@ func Test_Ctx_Get_Location_From_Route_name(t *testing.T) { // go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy func Test_Ctx_Get_Location_From_Route_name_Optional_greedy(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -2773,6 +3012,7 @@ func Test_Ctx_Get_Location_From_Route_name_Optional_greedy(t *testing.T) { // go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param func Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -2790,14 +3030,15 @@ func Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param(t *testing. type errorTemplateEngine struct{} -func (t errorTemplateEngine) Render(w io.Writer, name string, bind any, layout ...string) error { +func (errorTemplateEngine) Render(_ io.Writer, _ string, _ any, _ ...string) error { return errors.New("errorTemplateEngine") } -func (t errorTemplateEngine) Load() error { return nil } +func (errorTemplateEngine) Load() error { return nil } // go test -run Test_Ctx_Render_Engine_Error func Test_Ctx_Render_Engine_Error(t *testing.T) { + t.Parallel() app := New() app.config.Views = errorTemplateEngine{} c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -2809,10 +3050,12 @@ func Test_Ctx_Render_Engine_Error(t *testing.T) { // go test -run Test_Ctx_Render_Go_Template func Test_Ctx_Render_Go_Template(t *testing.T) { t.Parallel() - file, err := os.CreateTemp(os.TempDir(), "fiber") require.NoError(t, err) - defer os.Remove(file.Name()) + defer func() { + err := os.Remove(file.Name()) + require.NoError(t, err) + }() _, err = file.Write([]byte("template")) require.NoError(t, err) @@ -2887,13 +3130,16 @@ func Test_Ctx_SendStream(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.SendStream(bytes.NewReader([]byte("Don't crash please"))) + err := c.SendStream(bytes.NewReader([]byte("Don't crash please"))) + require.NoError(t, err) require.Equal(t, "Don't crash please", string(c.Response().Body())) - c.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) + err = c.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) + require.NoError(t, err) require.Equal(t, "Don't crash please", string(c.Response().Body())) - c.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) + err = c.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) + require.NoError(t, err) require.Equal(t, "Hello bufio", string(c.Response().Body())) } @@ -2989,7 +3235,7 @@ func Benchmark_Ctx_Type(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4 func Benchmark_Ctx_Type_Charset(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -3014,7 +3260,7 @@ func Test_Ctx_Vary(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 func Benchmark_Ctx_Vary(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() @@ -3029,8 +3275,10 @@ func Test_Ctx_Write(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Write([]byte("Hello, ")) - c.Write([]byte("World!")) + _, err := c.Write([]byte("Hello, ")) + require.NoError(t, err) + _, err = c.Write([]byte("World!")) + require.NoError(t, err) require.Equal(t, "Hello, World!", string(c.Response().Body())) } @@ -3065,7 +3313,7 @@ func Test_Ctx_Writef(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4 func Benchmark_Ctx_Writef(b *testing.B) { app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed world := "World!" b.ReportAllocs() @@ -3084,8 +3332,10 @@ func Test_Ctx_WriteString(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.WriteString("Hello, ") - c.WriteString("World!") + _, err := c.WriteString("Hello, ") + require.NoError(t, err) + _, err = c.WriteString("World!") + require.NoError(t, err) require.Equal(t, "Hello, World!", string(c.Response().Body())) } @@ -3134,7 +3384,6 @@ func Benchmark_Ctx_SendString_B(b *testing.B) { // go test -run Test_Ctx_BodyStreamWriter func Test_Ctx_BodyStreamWriter(t *testing.T) { t.Parallel() - ctx := &fasthttp.RequestCtx{} ctx.SetBodyStreamWriter(func(w *bufio.Writer) { @@ -3185,7 +3434,6 @@ func Benchmark_Ctx_BodyStreamWriter(b *testing.B) { func Test_Ctx_String(t *testing.T) { t.Parallel() - app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) @@ -3194,8 +3442,8 @@ func Test_Ctx_String(t *testing.T) { func TestCtx_ParamsInt(t *testing.T) { // Create a test context and set some strings (or params) - // create a fake app to be used within this test + t.Parallel() app := New() // Create some test endpoints @@ -3281,20 +3529,21 @@ func TestCtx_ParamsInt(t *testing.T) { }) _, err := app.Test(httptest.NewRequest(MethodGet, "/test/1111", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) _, err = app.Test(httptest.NewRequest(MethodGet, "/testnoint/xd", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) _, err = app.Test(httptest.NewRequest(MethodGet, "/testignoredefault/2222", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) _, err = app.Test(httptest.NewRequest(MethodGet, "/testdefault/xd", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) } // go test -run Test_Ctx_GetRespHeader func Test_Ctx_GetRespHeader(t *testing.T) { + t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) diff --git a/docs/api/_category_.json b/docs/api/_category_.json new file mode 100644 index 0000000000..c0fc66388a --- /dev/null +++ b/docs/api/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "API", + "position": 2, + "link": { + "type": "generated-index", + "description": "API documentation for Fiber." + } +} diff --git a/docs/api/app.md b/docs/api/app.md new file mode 100644 index 0000000000..5f788ca356 --- /dev/null +++ b/docs/api/app.md @@ -0,0 +1,657 @@ +--- +id: app +title: 🚀 App +description: The app instance conventionally denotes the Fiber application. +sidebar_position: 2 +--- + +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 +app.Static("/", "./public") + +// => http://localhost:3000/hello.html +// => http://localhost:3000/js/jquery.js +// => http://localhost:3000/css/style.css +``` + +```go title="Examples" +// Serve files from multiple directories +app.Static("/", "./public") + +// 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 +``` + +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. + +```go title="fiber.Static{}" +// 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 +} +``` + +```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 + + + +## Mount + +You can Mount Fiber instance by creating a `*Mount` + +```go title="Signature" +func (a *App) Mount(prefix string, app *App) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + micro := fiber.New() + app.Mount("/john", micro) // GET /john/doe -> 200 OK + + micro.Get("/doe", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +## MountPath + +The `MountPath` property contains one or more path patterns on which a sub-app was mounted. + +```go title="Signature" +func (app *App) MountPath() string +``` + +```go title="Examples" +func main() { + app := fiber.New() + one := fiber.New() + two := fiber.New() + three := fiber.New() + + two.Mount("/three", three) + one.Mount("/two", two) + app.Mount("/one", one) + + one.MountPath() // "/one" + two.MountPath() // "/one/two" + three.MountPath() // "/one/two/three" + app.MountPath() // "" +} +``` + +:::caution +Mounting order is important for MountPath. If you want to get mount paths properly, you should start mounting from the deepest app. +::: + +## Group + +You can group routes by creating a `*Group` struct. + +```go title="Signature" +func (app *App) Group(prefix string, handlers ...Handler) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + + api := app.Group("/api", handler) // /api + + v1 := api.Group("/v1", handler) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", handler) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +## Route + +You can define routes with a common prefix inside the common function. + +```go title="Signature" +func (app *App) Route(prefix string, fn func(router Router), name ...string) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + + app.Route("/test", func(api fiber.Router) { + api.Get("/foo", handler).Name("foo") // /test/foo (name: test.foo) + api.Get("/bar", handler).Name("bar") // /test/bar (name: test.bar) + }, "test.") + + log.Fatal(app.Listen(":3000")) +} +``` + +## Server + +Server returns the underlying [fasthttp server](https://godoc.org/github.com/valyala/fasthttp#Server) + +```go title="Signature" +func (app *App) Server() *fasthttp.Server +``` + +```go title="Examples" +func main() { + app := fiber.New() + + app.Server().MaxConnsPerIP = 1 + + // ... +} +``` + +## Server Shutdown + +Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners and then waits indefinitely for all connections to return to idle before shutting down. + +ShutdownWithTimeout will forcefully close any active connections after the timeout expires. + +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. + +```go +func (app *App) Shutdown() error +func (app *App) ShutdownWithTimeout(timeout time.Duration) error +func (app *App) ShutdownWithContext(ctx context.Context) error +``` + +## HandlersCount + +This method returns the amount of registered handlers. + +```go title="Signature" +func (app *App) HandlersCount() uint32 +``` + +## Stack + +This method returns the original router stack + +```go title="Signature" +func (app *App) Stack() [][]*Route +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/john/:age", handler) + app.Post("/register", handler) + + data, _ := json.MarshalIndent(app.Stack(), "", " ") + fmt.Println(string(data)) + + app.Listen(":3000") +} +``` + +```javascript title="Result" +[ + [ + { + "method": "GET", + "path": "/john/:age", + "params": [ + "age" + ] + } + ], + [ + { + "method": "HEAD", + "path": "/john/:age", + "params": [ + "age" + ] + } + ], + [ + { + "method": "POST", + "path": "/register", + "params": null + } + ] +] +``` + +## Name + +This method assigns the name of latest created route. + +```go title="Signature" +func (app *App) Name(name string) Router +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/", handler) + app.Name("index") + + app.Get("/doe", handler).Name("home") + + app.Trace("/tracer", handler).Name("tracert") + + app.Delete("/delete", handler).Name("delete") + + a := app.Group("/a") + a.Name("fd.") + + a.Get("/test", handler).Name("test") + + data, _ := json.MarshalIndent(app.Stack(), "", " ") + fmt.Print(string(data)) + + app.Listen(":3000") + +} +``` + +```javascript title="Result" +[ + [ + { + "method": "GET", + "name": "index", + "path": "/", + "params": null + }, + { + "method": "GET", + "name": "home", + "path": "/doe", + "params": null + }, + { + "method": "GET", + "name": "fd.test", + "path": "/a/test", + "params": null + } + ], + [ + { + "method": "HEAD", + "name": "", + "path": "/", + "params": null + }, + { + "method": "HEAD", + "name": "", + "path": "/doe", + "params": null + }, + { + "method": "HEAD", + "name": "", + "path": "/a/test", + "params": null + } + ], + null, + null, + [ + { + "method": "DELETE", + "name": "delete", + "path": "/delete", + "params": null + } + ], + null, + null, + [ + { + "method": "TRACE", + "name": "tracert", + "path": "/tracer", + "params": null + } + ], + null +] +``` + +## GetRoute + +This method gets the route by name. + +```go title="Signature" +func (app *App) GetRoute(name string) Route +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/", handler).Name("index") + + data, _ := json.MarshalIndent(app.GetRoute("index"), "", " ") + fmt.Print(string(data)) + + + app.Listen(":3000") + +} +``` + +```javascript title="Result" +{ + "method": "GET", + "name": "index", + "path": "/", + "params": null +} +``` + +## GetRoutes + +This method gets all routes. + +```go title="Signature" +func (app *App) GetRoutes(filterUseOption ...bool) []Route +``` + +When filterUseOption equal to true, it will filter the routes registered by the middleware. +```go title="Examples" +func main() { + app := fiber.New() + app.Post("/", func (c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }).Name("index") + data, _ := json.MarshalIndent(app.GetRoutes(true), "", " ") + fmt.Print(string(data)) +} +``` + +```javascript title="Result" +[ + { + "method": "POST", + "name": "index", + "path": "/", + "params": null + } +] +``` + +## Config + +Config returns the app config as value \( read-only \). + +```go title="Signature" +func (app *App) Config() Config +``` + +## Handler + +Handler returns the server handler that can be used to serve custom \*fasthttp.RequestCtx requests. + +```go title="Signature" +func (app *App) Handler() fasthttp.RequestHandler +``` + +## Listen + +Listen serves HTTP requests from the given address. + +```go title="Signature" +func (app *App) Listen(addr string) error +``` + +```go title="Examples" +// Listen on port :8080 +app.Listen(":8080") + +// Custom host +app.Listen("127.0.0.1:8080") +``` + +## ListenTLS + +ListenTLS serves HTTPs requests from the given address using certFile and keyFile paths to as TLS certificate and key file. + +```go title="Signature" +func (app *App) ListenTLS(addr, certFile, keyFile string) error +``` + +```go title="Examples" +app.ListenTLS(":443", "./cert.pem", "./cert.key"); +``` + +Using `ListenTLS` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenTLSWithCertificate + +```go title="Signature" +func (app *App) ListenTLS(addr string, cert tls.Certificate) error +``` + +```go title="Examples" +app.ListenTLSWithCertificate(":443", cert); +``` + +Using `ListenTLSWithCertificate` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenMutualTLS + +ListenMutualTLS serves HTTPs requests from the given address using certFile, keyFile and clientCertFile are the paths to TLS certificate and key file + +```go title="Signature" +func (app *App) ListenMutualTLS(addr, certFile, keyFile, clientCertFile string) error +``` + +```go title="Examples" +app.ListenMutualTLS(":443", "./cert.pem", "./cert.key", "./ca-chain-cert.pem"); +``` + +Using `ListenMutualTLS` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenMutualTLSWithCertificate + +ListenMutualTLSWithCertificate serves HTTPs requests from the given address using certFile, keyFile and clientCertFile are the paths to TLS certificate and key file + +```go title="Signature" +func (app *App) ListenMutualTLSWithCertificate(addr string, cert tls.Certificate, clientCertPool *x509.CertPool) error +``` + +```go title="Examples" +app.ListenMutualTLSWithCertificate(":443", cert, clientCertPool); +``` + +Using `ListenMutualTLSWithCertificate` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## Listener + +You can pass your own [`net.Listener`](https://pkg.go.dev/net/#Listener) using the `Listener` method. This method can be used to enable **TLS/HTTPS** with a custom tls.Config. + +```go title="Signature" +func (app *App) Listener(ln net.Listener) error +``` + +```go title="Examples" +ln, _ := net.Listen("tcp", ":3000") + +cer, _:= tls.LoadX509KeyPair("server.crt", "server.key") + +ln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cer}}) + +app.Listener(ln) +``` + +## Test + +Testing your application is done with the **Test** method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s` if you want to disable a timeout altogether, pass `-1` as a second argument. + +```go title="Signature" +func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) +``` + +```go title="Examples" +// Create route with GET method for test: +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println(c.BaseURL()) // => http://google.com + fmt.Println(c.Get("X-Custom-Header")) // => hi + + return c.SendString("hello, World!") +}) + +// http.Request +req := httptest.NewRequest("GET", "http://google.com", nil) +req.Header.Set("X-Custom-Header", "hi") + +// http.Response +resp, _ := app.Test(req) + +// Do something with results: +if resp.StatusCode == fiber.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + fmt.Println(string(body)) // => Hello, World! +} +``` + +## Hooks + +Hooks is a method to return [hooks](../guide/hooks.md) property. + +```go title="Signature" +func (app *App) Hooks() *Hooks +``` diff --git a/docs/api/client.md b/docs/api/client.md new file mode 100644 index 0000000000..c3ed4b11a6 --- /dev/null +++ b/docs/api/client.md @@ -0,0 +1,607 @@ +--- +id: client +title: 🌎 Client +description: The Client struct represents the Fiber HTTP Client. +sidebar_position: 5 +--- + +## Start request + +Start a http request with http method and url. + +```go title="Signatures" +// Client http methods +func (c *Client) Get(url string) *Agent +func (c *Client) Head(url string) *Agent +func (c *Client) Post(url string) *Agent +func (c *Client) Put(url string) *Agent +func (c *Client) Patch(url string) *Agent +func (c *Client) Delete(url string) *Agent +``` + +Here we present a brief example demonstrating the simulation of a proxy using our `*fiber.Agent` methods. +```go +// Get something +func getSomething(c *fiber.Ctx) (err error) { + agent := fiber.Get("") + statusCode, body, errs := agent.Bytes() + if len(errs) > 0 { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "errs": errs, + }) + } + + var something fiber.Map + err = json.Unmarshal(body, &something) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "err": err, + }) + } + + return c.Status(statusCode).JSON(something) +} + +// Post something +func createSomething(c *fiber.Ctx) (err error) { + agent := fiber.Post("") + agent.Body(c.Body()) // set body received by request + statusCode, body, errs := agent.Bytes() + if len(errs) > 0 { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "errs": errs, + }) + } + + // pass status code and body received by the proxy + return c.Status(statusCode).Send(body) +} +``` +Based on this short example, we can perceive that using the `*fiber.Client` is very straightforward and intuitive. + + +## ✨ Agent +`Agent` is built on top of FastHTTP's [`HostClient`](https://github.com/valyala/fasthttp/blob/master/client.go#L603) which has lots of convenient helper methods such as dedicated methods for request methods. + +### Parse + +Parse initializes a HostClient. + +```go title="Parse" +a := AcquireAgent() +req := a.Request() +req.Header.SetMethod(MethodGet) +req.SetRequestURI("http://example.com") + +if err := a.Parse(); err != nil { + panic(err) +} + +code, body, errs := a.Bytes() // ... +``` + +### Set + +Set sets the given `key: value` header. + +```go title="Signature" +func (a *Agent) Set(k, v string) *Agent +func (a *Agent) SetBytesK(k []byte, v string) *Agent +func (a *Agent) SetBytesV(k string, v []byte) *Agent +func (a *Agent) SetBytesKV(k []byte, v []byte) *Agent +``` + +```go title="Example" +agent.Set("k1", "v1"). + SetBytesK([]byte("k1"), "v1"). + SetBytesV("k1", []byte("v1")). + SetBytesKV([]byte("k2"), []byte("v2")) +// ... +``` + +### Add + +Add adds the given `key: value` header. Multiple headers with the same key may be added with this function. + +```go title="Signature" +func (a *Agent) Add(k, v string) *Agent +func (a *Agent) AddBytesK(k []byte, v string) *Agent +func (a *Agent) AddBytesV(k string, v []byte) *Agent +func (a *Agent) AddBytesKV(k []byte, v []byte) *Agent +``` + +```go title="Example" +agent.Add("k1", "v1"). + AddBytesK([]byte("k1"), "v1"). + AddBytesV("k1", []byte("v1")). + AddBytesKV([]byte("k2"), []byte("v2")) +// Headers: +// K1: v1 +// K1: v1 +// K1: v1 +// K2: v2 +``` + +### ConnectionClose + +ConnectionClose adds the `Connection: close` header. + +```go title="Signature" +func (a *Agent) ConnectionClose() *Agent +``` + +```go title="Example" +agent.ConnectionClose() +// ... +``` + +### UserAgent + +UserAgent sets `User-Agent` header value. + +```go title="Signature" +func (a *Agent) UserAgent(userAgent string) *Agent +func (a *Agent) UserAgentBytes(userAgent []byte) *Agent +``` + +```go title="Example" +agent.UserAgent("fiber") +// ... +``` + +### Cookie + +Cookie sets a cookie in `key: value` form. `Cookies` can be used to set multiple cookies. + +```go title="Signature" +func (a *Agent) Cookie(key, value string) *Agent +func (a *Agent) CookieBytesK(key []byte, value string) *Agent +func (a *Agent) CookieBytesKV(key, value []byte) *Agent +func (a *Agent) Cookies(kv ...string) *Agent +func (a *Agent) CookiesBytesKV(kv ...[]byte) *Agent +``` + +```go title="Example" +agent.Cookie("k", "v") +agent.Cookies("k1", "v1", "k2", "v2") +// ... +``` + +### Referer + +Referer sets the Referer header value. + +```go title="Signature" +func (a *Agent) Referer(referer string) *Agent +func (a *Agent) RefererBytes(referer []byte) *Agent +``` + +```go title="Example" +agent.Referer("https://docs.gofiber.io") +// ... +``` + +### ContentType + +ContentType sets Content-Type header value. + +```go title="Signature" +func (a *Agent) ContentType(contentType string) *Agent +func (a *Agent) ContentTypeBytes(contentType []byte) *Agent +``` + +```go title="Example" +agent.ContentType("custom-type") +// ... +``` + +### Host + +Host sets the Host header. + +```go title="Signature" +func (a *Agent) Host(host string) *Agent +func (a *Agent) HostBytes(host []byte) *Agent +``` + +```go title="Example" +agent.Host("example.com") +// ... +``` + +### QueryString + +QueryString sets the URI query string. + +```go title="Signature" +func (a *Agent) QueryString(queryString string) *Agent +func (a *Agent) QueryStringBytes(queryString []byte) *Agent +``` + +```go title="Example" +agent.QueryString("foo=bar") +// ... +``` + +### BasicAuth + +BasicAuth sets the URI username and password using HTTP Basic Auth. + +```go title="Signature" +func (a *Agent) BasicAuth(username, password string) *Agent +func (a *Agent) BasicAuthBytes(username, password []byte) *Agent +``` + +```go title="Example" +agent.BasicAuth("foo", "bar") +// ... +``` + +### Body + +There are several ways to set request body. + +```go title="Signature" +func (a *Agent) BodyString(bodyString string) *Agent +func (a *Agent) Body(body []byte) *Agent + +// BodyStream sets request body stream and, optionally body size. +// +// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes +// before returning io.EOF. +// +// If bodySize < 0, then bodyStream is read until io.EOF. +// +// bodyStream.Close() is called after finishing reading all body data +// if it implements io.Closer. +// +// Note that GET and HEAD requests cannot have body. +func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent +``` + +```go title="Example" +agent.BodyString("foo=bar") +agent.Body([]byte("bar=baz")) +agent.BodyStream(strings.NewReader("body=stream"), -1) +// ... +``` + +### JSON + +JSON sends a JSON request by setting the Content-Type header to `application/json`. + +```go title="Signature" +func (a *Agent) JSON(v interface{}) *Agent +``` + +```go title="Example" +agent.JSON(fiber.Map{"success": true}) +// ... +``` + +### XML + +XML sends an XML request by setting the Content-Type header to `application/xml`. + +```go title="Signature" +func (a *Agent) XML(v interface{}) *Agent +``` + +```go title="Example" +agent.XML(fiber.Map{"success": true}) +// ... +``` + +### Form + +Form sends a form request by setting the Content-Type header to `application/x-www-form-urlencoded`. + +```go title="Signature" +// Form sends form request with body if args is non-nil. +// +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) Form(args *Args) *Agent +``` + +```go title="Example" +args := AcquireArgs() +args.Set("foo", "bar") + +agent.Form(args) +// ... +ReleaseArgs(args) +``` + +### MultipartForm + +MultipartForm sends multipart form request by setting the Content-Type header to `multipart/form-data`. These requests can include key-value's and files. + +```go title="Signature" +// MultipartForm sends multipart form request with k-v and files. +// +// It is recommended to obtain args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) MultipartForm(args *Args) *Agent +``` + +```go title="Example" +args := AcquireArgs() +args.Set("foo", "bar") + +agent.MultipartForm(args) +// ... +ReleaseArgs(args) +``` + +Fiber provides several methods for sending files. Note that they must be called before `MultipartForm`. + +#### Boundary + +Boundary sets boundary for multipart form request. + +```go title="Signature" +func (a *Agent) Boundary(boundary string) *Agent +``` + +```go title="Example" +agent.Boundary("myBoundary") + .MultipartForm(nil) +// ... +``` + +#### SendFile\(s\) + +SendFile read a file and appends it to a multipart form request. Sendfiles can be used to append multiple files. + +```go title="Signature" +func (a *Agent) SendFile(filename string, fieldname ...string) *Agent +func (a *Agent) SendFiles(filenamesAndFieldnames ...string) *Agent +``` + +```go title="Example" +agent.SendFile("f", "field name") + .SendFiles("f1", "field name1", "f2"). + .MultipartForm(nil) +// ... +``` + +#### FileData + +FileData appends file data for multipart form request. + +```go +// FormFile represents multipart form file +type FormFile struct { + // Fieldname is form file's field name + Fieldname string + // Name is form file's name + Name string + // Content is form file's content + Content []byte +} +``` + +```go title="Signature" +// FileData appends files for multipart form request. +// +// It is recommended obtaining formFile via AcquireFormFile and release it +// manually in performance-critical code. +func (a *Agent) FileData(formFiles ...*FormFile) *Agent +``` + +```go title="Example" +ff1 := &FormFile{"filename1", "field name1", []byte("content")} +ff2 := &FormFile{"filename2", "field name2", []byte("content")} +agent.FileData(ff1, ff2). + MultipartForm(nil) +// ... +``` + +### Debug + +Debug mode enables logging request and response detail to `io.writer`\(default is `os.Stdout`\). + +```go title="Signature" +func (a *Agent) Debug(w ...io.Writer) *Agent +``` + +```go title="Example" +agent.Debug() +// ... +``` + +### Timeout + +Timeout sets request timeout duration. + +```go title="Signature" +func (a *Agent) Timeout(timeout time.Duration) *Agent +``` + +```go title="Example" +agent.Timeout(time.Second) +// ... +``` + +### Reuse + +Reuse enables the Agent instance to be used again after one request. If agent is reusable, then it should be released manually when it is no longer used. + +```go title="Signature" +func (a *Agent) Reuse() *Agent +``` + +```go title="Example" +agent.Reuse() +// ... +``` + +### InsecureSkipVerify + +InsecureSkipVerify controls whether the Agent verifies the server certificate chain and host name. + +```go title="Signature" +func (a *Agent) InsecureSkipVerify() *Agent +``` + +```go title="Example" +agent.InsecureSkipVerify() +// ... +``` + +### TLSConfig + +TLSConfig sets tls config. + +```go title="Signature" +func (a *Agent) TLSConfig(config *tls.Config) *Agent +``` + +```go title="Example" +// Create tls certificate +cer, _ := tls.LoadX509KeyPair("pem", "key") + +config := &tls.Config{ + Certificates: []tls.Certificate{cer}, +} + +agent.TLSConfig(config) +// ... +``` + +### MaxRedirectsCount + +MaxRedirectsCount sets max redirect count for GET and HEAD. + +```go title="Signature" +func (a *Agent) MaxRedirectsCount(count int) *Agent +``` + +```go title="Example" +agent.MaxRedirectsCount(7) +// ... +``` + +### JSONEncoder + +JSONEncoder sets custom json encoder. + +```go title="Signature" +func (a *Agent) JSONEncoder(jsonEncoder utils.JSONMarshal) *Agent +``` + +```go title="Example" +agent.JSONEncoder(json.Marshal) +// ... +``` + +### JSONDecoder + +JSONDecoder sets custom json decoder. + +```go title="Signature" +func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent +``` + +```go title="Example" +agent.JSONDecoder(json.Unmarshal) +// ... +``` + +### Request + +Request returns Agent request instance. + +```go title="Signature" +func (a *Agent) Request() *Request +``` + +```go title="Example" +req := agent.Request() +// ... +``` + +### SetResponse + +SetResponse sets custom response for the Agent instance. It is recommended obtaining custom response via AcquireResponse and release it manually in performance-critical code. + +```go title="Signature" +func (a *Agent) SetResponse(customResp *Response) *Agent +``` + +```go title="Example" +resp := AcquireResponse() +agent.SetResponse(resp) +// ... +ReleaseResponse(resp) +``` + +### Dest + +Dest sets custom dest. The contents of dest will be replaced by the response body, if the dest is too small a new slice will be allocated. + +```go title="Signature" +func (a *Agent) Dest(dest []byte) *Agent { +``` + +```go title="Example" +agent.Dest(nil) +// ... +``` + +### Bytes + +Bytes returns the status code, bytes body and errors of url. + +```go title="Signature" +func (a *Agent) Bytes() (code int, body []byte, errs []error) +``` + +```go title="Example" +code, body, errs := agent.Bytes() +// ... +``` + +### String + +String returns the status code, string body and errors of url. + +```go title="Signature" +func (a *Agent) String() (int, string, []error) +``` + +```go title="Example" +code, body, errs := agent.String() +// ... +``` + +### Struct + +Struct returns the status code, bytes body and errors of url. And bytes body will be unmarshalled to given v. + +```go title="Signature" +func (a *Agent) Struct(v interface{}) (code int, body []byte, errs []error) +``` + +```go title="Example" +var d data +code, body, errs := agent.Struct(&d) +// ... +``` + +### RetryIf + +RetryIf controls whether a retry should be attempted after an error. +By default, will use isIdempotent function from fasthttp + +```go title="Signature" +func (a *Agent) RetryIf(retryIf RetryIfFunc) *Agent +``` + +```go title="Example" +agent.Get("https://example.com").RetryIf(func (req *fiber.Request) bool { + return req.URI() == "https://example.com" +}) +// ... +``` diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 0000000000..8a436a9f3b --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,291 @@ +--- +id: constants +title: 📋 Constants +description: Some constants for Fiber. +sidebar_position: 4 +--- + +HTTP methods were copied from net/http. + +```go +const ( + MethodGet = "GET" // RFC 7231, 4.3.1 + MethodHead = "HEAD" // RFC 7231, 4.3.2 + MethodPost = "POST" // RFC 7231, 4.3.3 + MethodPut = "PUT" // RFC 7231, 4.3.4 + MethodPatch = "PATCH" // RFC 5789 + MethodDelete = "DELETE" // RFC 7231, 4.3.5 + MethodConnect = "CONNECT" // RFC 7231, 4.3.6 + MethodOptions = "OPTIONS" // RFC 7231, 4.3.7 + MethodTrace = "TRACE" // RFC 7231, 4.3.8 + methodUse = "USE" +) +``` + +MIME types that are commonly used + +```go +const ( + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + 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" + MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" +) +``` + +HTTP status codes were copied from net/http. + +```go +const ( + StatusContinue = 100 // RFC 7231, 6.2.1 + StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 + StatusProcessing = 102 // RFC 2518, 10.1 + StatusEarlyHints = 103 // RFC 8297 + StatusOK = 200 // RFC 7231, 6.3.1 + StatusCreated = 201 // RFC 7231, 6.3.2 + StatusAccepted = 202 // RFC 7231, 6.3.3 + StatusNonAuthoritativeInformation = 203 // RFC 7231, 6.3.4 + StatusNoContent = 204 // RFC 7231, 6.3.5 + StatusResetContent = 205 // RFC 7231, 6.3.6 + StatusPartialContent = 206 // RFC 7233, 4.1 + StatusMultiStatus = 207 // RFC 4918, 11.1 + StatusAlreadyReported = 208 // RFC 5842, 7.1 + StatusIMUsed = 226 // RFC 3229, 10.4.1 + StatusMultipleChoices = 300 // RFC 7231, 6.4.1 + StatusMovedPermanently = 301 // RFC 7231, 6.4.2 + StatusFound = 302 // RFC 7231, 6.4.3 + StatusSeeOther = 303 // RFC 7231, 6.4.4 + StatusNotModified = 304 // RFC 7232, 4.1 + StatusUseProxy = 305 // RFC 7231, 6.4.5 + StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 + StatusPermanentRedirect = 308 // RFC 7538, 3 + StatusBadRequest = 400 // RFC 7231, 6.5.1 + StatusUnauthorized = 401 // RFC 7235, 3.1 + StatusPaymentRequired = 402 // RFC 7231, 6.5.2 + StatusForbidden = 403 // RFC 7231, 6.5.3 + StatusNotFound = 404 // RFC 7231, 6.5.4 + StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5 + StatusNotAcceptable = 406 // RFC 7231, 6.5.6 + StatusProxyAuthRequired = 407 // RFC 7235, 3.2 + StatusRequestTimeout = 408 // RFC 7231, 6.5.7 + StatusConflict = 409 // RFC 7231, 6.5.8 + StatusGone = 410 // RFC 7231, 6.5.9 + StatusLengthRequired = 411 // RFC 7231, 6.5.10 + StatusPreconditionFailed = 412 // RFC 7232, 4.2 + StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11 + StatusRequestURITooLong = 414 // RFC 7231, 6.5.12 + StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13 + StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 + StatusExpectationFailed = 417 // RFC 7231, 6.5.14 + StatusTeapot = 418 // RFC 7168, 2.3.3 + StatusMisdirectedRequest = 421 // RFC 7540, 9.1.2 + StatusUnprocessableEntity = 422 // RFC 4918, 11.2 + StatusLocked = 423 // RFC 4918, 11.3 + StatusFailedDependency = 424 // RFC 4918, 11.4 + StatusTooEarly = 425 // RFC 8470, 5.2. + StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 + StatusPreconditionRequired = 428 // RFC 6585, 3 + StatusTooManyRequests = 429 // RFC 6585, 4 + StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 + StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 + StatusInternalServerError = 500 // RFC 7231, 6.6.1 + StatusNotImplemented = 501 // RFC 7231, 6.6.2 + StatusBadGateway = 502 // RFC 7231, 6.6.3 + StatusServiceUnavailable = 503 // RFC 7231, 6.6.4 + StatusGatewayTimeout = 504 // RFC 7231, 6.6.5 + StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6 + StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 + StatusInsufficientStorage = 507 // RFC 4918, 11.5 + StatusLoopDetected = 508 // RFC 5842, 7.2 + StatusNotExtended = 510 // RFC 2774, 7 + StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 +) +``` + +Errors + +```go +var ( + ErrBadRequest = NewError(StatusBadRequest) // RFC 7231, 6.5.1 + ErrUnauthorized = NewError(StatusUnauthorized) // RFC 7235, 3.1 + ErrPaymentRequired = NewError(StatusPaymentRequired) // RFC 7231, 6.5.2 + ErrForbidden = NewError(StatusForbidden) // RFC 7231, 6.5.3 + ErrNotFound = NewError(StatusNotFound) // RFC 7231, 6.5.4 + ErrMethodNotAllowed = NewError(StatusMethodNotAllowed) // RFC 7231, 6.5.5 + ErrNotAcceptable = NewError(StatusNotAcceptable) // RFC 7231, 6.5.6 + ErrProxyAuthRequired = NewError(StatusProxyAuthRequired) // RFC 7235, 3.2 + ErrRequestTimeout = NewError(StatusRequestTimeout) // RFC 7231, 6.5.7 + ErrConflict = NewError(StatusConflict) // RFC 7231, 6.5.8 + ErrGone = NewError(StatusGone) // RFC 7231, 6.5.9 + ErrLengthRequired = NewError(StatusLengthRequired) // RFC 7231, 6.5.10 + ErrPreconditionFailed = NewError(StatusPreconditionFailed) // RFC 7232, 4.2 + ErrRequestEntityTooLarge = NewError(StatusRequestEntityTooLarge) // RFC 7231, 6.5.11 + ErrRequestURITooLong = NewError(StatusRequestURITooLong) // RFC 7231, 6.5.12 + ErrUnsupportedMediaType = NewError(StatusUnsupportedMediaType) // RFC 7231, 6.5.13 + ErrRequestedRangeNotSatisfiable = NewError(StatusRequestedRangeNotSatisfiable) // RFC 7233, 4.4 + ErrExpectationFailed = NewError(StatusExpectationFailed) // RFC 7231, 6.5.14 + ErrTeapot = NewError(StatusTeapot) // RFC 7168, 2.3.3 + ErrMisdirectedRequest = NewError(StatusMisdirectedRequest) // RFC 7540, 9.1.2 + ErrUnprocessableEntity = NewError(StatusUnprocessableEntity) // RFC 4918, 11.2 + ErrLocked = NewError(StatusLocked) // RFC 4918, 11.3 + ErrFailedDependency = NewError(StatusFailedDependency) // RFC 4918, 11.4 + ErrTooEarly = NewError(StatusTooEarly) // RFC 8470, 5.2. + ErrUpgradeRequired = NewError(StatusUpgradeRequired) // RFC 7231, 6.5.15 + ErrPreconditionRequired = NewError(StatusPreconditionRequired) // RFC 6585, 3 + ErrTooManyRequests = NewError(StatusTooManyRequests) // RFC 6585, 4 + ErrRequestHeaderFieldsTooLarge = NewError(StatusRequestHeaderFieldsTooLarge) // RFC 6585, 5 + ErrUnavailableForLegalReasons = NewError(StatusUnavailableForLegalReasons) // RFC 7725, 3 + ErrInternalServerError = NewError(StatusInternalServerError) // RFC 7231, 6.6.1 + ErrNotImplemented = NewError(StatusNotImplemented) // RFC 7231, 6.6.2 + ErrBadGateway = NewError(StatusBadGateway) // RFC 7231, 6.6.3 + ErrServiceUnavailable = NewError(StatusServiceUnavailable) // RFC 7231, 6.6.4 + ErrGatewayTimeout = NewError(StatusGatewayTimeout) // RFC 7231, 6.6.5 + ErrHTTPVersionNotSupported = NewError(StatusHTTPVersionNotSupported) // RFC 7231, 6.6.6 + ErrVariantAlsoNegotiates = NewError(StatusVariantAlsoNegotiates) // RFC 2295, 8.1 + ErrInsufficientStorage = NewError(StatusInsufficientStorage) // RFC 4918, 11.5 + ErrLoopDetected = NewError(StatusLoopDetected) // RFC 5842, 7.2 + ErrNotExtended = NewError(StatusNotExtended) // RFC 2774, 7 + ErrNetworkAuthenticationRequired = NewError(StatusNetworkAuthenticationRequired) // RFC 6585, 6 +) +``` + +HTTP Headers were copied from net/http. + +```go +const ( + HeaderAuthorization = "Authorization" + HeaderProxyAuthenticate = "Proxy-Authenticate" + HeaderProxyAuthorization = "Proxy-Authorization" + HeaderWWWAuthenticate = "WWW-Authenticate" + HeaderAge = "Age" + HeaderCacheControl = "Cache-Control" + HeaderClearSiteData = "Clear-Site-Data" + HeaderExpires = "Expires" + HeaderPragma = "Pragma" + HeaderWarning = "Warning" + HeaderAcceptCH = "Accept-CH" + HeaderAcceptCHLifetime = "Accept-CH-Lifetime" + HeaderContentDPR = "Content-DPR" + HeaderDPR = "DPR" + HeaderEarlyData = "Early-Data" + HeaderSaveData = "Save-Data" + HeaderViewportWidth = "Viewport-Width" + HeaderWidth = "Width" + HeaderETag = "ETag" + HeaderIfMatch = "If-Match" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderLastModified = "Last-Modified" + HeaderVary = "Vary" + HeaderConnection = "Connection" + HeaderKeepAlive = "Keep-Alive" + HeaderAccept = "Accept" + HeaderAcceptCharset = "Accept-Charset" + HeaderAcceptEncoding = "Accept-Encoding" + HeaderAcceptLanguage = "Accept-Language" + HeaderCookie = "Cookie" + HeaderExpect = "Expect" + HeaderMaxForwards = "Max-Forwards" + HeaderSetCookie = "Set-Cookie" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" + HeaderAccessControlRequestMethod = "Access-Control-Request-Method" + HeaderOrigin = "Origin" + HeaderTimingAllowOrigin = "Timing-Allow-Origin" + HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + HeaderDNT = "DNT" + HeaderTk = "Tk" + HeaderContentDisposition = "Content-Disposition" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentLength = "Content-Length" + HeaderContentLocation = "Content-Location" + HeaderContentType = "Content-Type" + HeaderForwarded = "Forwarded" + HeaderVia = "Via" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXForwardedHost = "X-Forwarded-Host" + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXForwardedProtocol = "X-Forwarded-Protocol" + HeaderXForwardedSsl = "X-Forwarded-Ssl" + HeaderXUrlScheme = "X-Url-Scheme" + HeaderLocation = "Location" + HeaderFrom = "From" + HeaderHost = "Host" + HeaderReferer = "Referer" + HeaderReferrerPolicy = "Referrer-Policy" + HeaderUserAgent = "User-Agent" + HeaderAllow = "Allow" + HeaderServer = "Server" + HeaderAcceptRanges = "Accept-Ranges" + HeaderContentRange = "Content-Range" + HeaderIfRange = "If-Range" + HeaderRange = "Range" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" + HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" + HeaderExpectCT = "Expect-CT" + HeaderFeaturePolicy = "Feature-Policy" + HeaderPublicKeyPins = "Public-Key-Pins" + HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXDownloadOptions = "X-Download-Options" + HeaderXFrameOptions = "X-Frame-Options" + HeaderXPoweredBy = "X-Powered-By" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderLastEventID = "Last-Event-ID" + HeaderNEL = "NEL" + HeaderPingFrom = "Ping-From" + HeaderPingTo = "Ping-To" + HeaderReportTo = "Report-To" + HeaderTE = "TE" + HeaderTrailer = "Trailer" + HeaderTransferEncoding = "Transfer-Encoding" + HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" + HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" + HeaderSecWebSocketKey = "Sec-WebSocket-Key" + HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" + HeaderSecWebSocketVersion = "Sec-WebSocket-Version" + HeaderAcceptPatch = "Accept-Patch" + HeaderAcceptPushPolicy = "Accept-Push-Policy" + HeaderAcceptSignature = "Accept-Signature" + HeaderAltSvc = "Alt-Svc" + HeaderDate = "Date" + HeaderIndex = "Index" + HeaderLargeAllocation = "Large-Allocation" + HeaderLink = "Link" + HeaderPushPolicy = "Push-Policy" + HeaderRetryAfter = "Retry-After" + HeaderServerTiming = "Server-Timing" + HeaderSignature = "Signature" + HeaderSignedHeaders = "Signed-Headers" + HeaderSourceMap = "SourceMap" + HeaderUpgrade = "Upgrade" + HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" + HeaderXPingback = "X-Pingback" + HeaderXRequestID = "X-Request-ID" + HeaderXRequestedWith = "X-Requested-With" + HeaderXRobotsTag = "X-Robots-Tag" + HeaderXUACompatible = "X-UA-Compatible" +) +``` \ No newline at end of file diff --git a/docs/api/ctx.md b/docs/api/ctx.md new file mode 100644 index 0000000000..aa48c60929 --- /dev/null +++ b/docs/api/ctx.md @@ -0,0 +1,2046 @@ +--- +id: ctx +title: 🧠 Ctx +description: >- + The Ctx struct represents the Context which hold the HTTP request and + response. It has methods for the request query string, parameters, body, HTTP + headers, and so on. +sidebar_position: 3 +--- + +## Accepts + +Checks, if the specified **extensions** or **content** **types** are acceptable. + +:::info +Based on the request’s [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. +::: + +```go title="Signature" +func (c *Ctx) Accepts(offers ...string) string +func (c *Ctx) AcceptsCharsets(offers ...string) string +func (c *Ctx) AcceptsEncodings(offers ...string) string +func (c *Ctx) AcceptsLanguages(offers ...string) string +``` + +```go title="Example" +// Accept: text/html, application/json; q=0.8, text/plain; q=0.5; charset="utf-8" + +app.Get("/", func(c *fiber.Ctx) error { + c.Accepts("html") // "html" + c.Accepts("text/html") // "text/html" + c.Accepts("json", "text") // "json" + c.Accepts("application/json") // "application/json" + c.Accepts("text/plain", "application/json") // "application/json", due to quality + c.Accepts("image/png") // "" + c.Accepts("png") // "" + // ... +}) +``` + +```go title="Example 2" +// Accept: text/html, text/*, application/json, */*; q=0 + +app.Get("/", func(c *fiber.Ctx) error { + c.Accepts("text/plain", "application/json") // "application/json", due to specificity + c.Accepts("application/json", "text/html") // "text/html", due to first match + c.Accepts("image/png") // "", due to */* without q factor 0 is Not Acceptable + // ... +}) +``` + +Fiber provides similar functions for the other accept headers. + +```go +// Accept-Charset: utf-8, iso-8859-1;q=0.2 +// Accept-Encoding: gzip, compress;q=0.2 +// Accept-Language: en;q=0.8, nl, ru + +app.Get("/", func(c *fiber.Ctx) error { + c.AcceptsCharsets("utf-16", "iso-8859-1") + // "iso-8859-1" + + c.AcceptsEncodings("compress", "br") + // "compress" + + c.AcceptsLanguages("pt", "nl", "ru") + // "nl" + // ... +}) +``` + +## AllParams + +Params is used to get all route parameters. +Using Params method to get params. + +```go title="Signature" +func (c *Ctx) AllParams() map[string]string +``` + +```go title="Example" +// GET http://example.com/user/fenny +app.Get("/user/:name", func(c *fiber.Ctx) error { + c.AllParams() // "{"name": "fenny"}" + + // ... +}) + +// GET http://example.com/user/fenny/123 +app.Get("/user/*", func(c *fiber.Ctx) error { + c.AllParams() // "{"*1": "fenny/123"}" + + // ... +}) +``` + +## App + +Returns the [\*App](ctx.md) reference so you could easily access all application settings. + +```go title="Signature" +func (c *Ctx) App() *App +``` + +```go title="Example" +app.Get("/stack", func(c *fiber.Ctx) error { + return c.JSON(c.App().Stack()) +}) +``` + +## Append + +Appends the specified **value** to the HTTP response header field. + +:::caution +If the header is **not** already set, it creates the header with the specified value. +::: + +```go title="Signature" +func (c *Ctx) Append(field string, values ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Append("Link", "http://google.com", "http://localhost") + // => Link: http://localhost, http://google.com + + c.Append("Link", "Test") + // => Link: http://localhost, http://google.com, Test + + // ... +}) +``` + +## Attachment + +Sets the HTTP response [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header field to `attachment`. + +```go title="Signature" +func (c *Ctx) Attachment(filename ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Attachment() + // => Content-Disposition: attachment + + c.Attachment("./upload/images/logo.png") + // => Content-Disposition: attachment; filename="logo.png" + // => Content-Type: image/png + + // ... +}) +``` + +## BaseURL + +Returns the base URL \(**protocol** + **host**\) as a `string`. + +```go title="Signature" +func (c *Ctx) BaseURL() string +``` + +```go title="Example" +// GET https://example.com/page#chapter-1 + +app.Get("/", func(c *fiber.Ctx) error { + c.BaseURL() // https://example.com + // ... +}) +``` + +## Bind +Add vars to default view var map binding to template engine. +Variables are read by the Render method and may be overwritten. + +```go title="Signature" +func (c *Ctx) Bind(vars Map) error +``` + +```go title="Example" +app.Use(func(c *fiber.Ctx) error { + c.Bind(fiber.Map{ + "Title": "Hello, World!", + }) +}) + +app.Get("/", func(c *fiber.Ctx) error { + return c.Render("xxx.tmpl", fiber.Map{}) // Render will use Title variable +}) +``` + +## Body + +Returns the raw request **body**. + +```go title="Signature" +func (c *Ctx) Body() []byte +``` + +```go title="Example" +// curl -X POST http://localhost:8080 -d user=john + +app.Post("/", func(c *fiber.Ctx) error { + // Get raw body from POST request: + return c.Send(c.Body()) // []byte("user=john") +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## BodyParser + +Binds the request body to a struct. + +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called Pass, you would use a struct field of `json:"pass"`. + +| content-type | struct tag | +|---|---| +| `application/x-www-form-urlencoded` | form | +| `multipart/form-data` | form | +| `application/json` | json | +| `application/xml` | xml | +| `text/xml` | xml | + +```go title="Signature" +func (c *Ctx) BodyParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `json:"name" xml:"name" form:"name"` + Pass string `json:"pass" xml:"pass" form:"pass"` +} + +app.Post("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.BodyParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + + // ... +}) + +// Run tests with the following curl commands + +// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 + +// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 + +// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 + +// curl -X POST -F name=john -F pass=doe http://localhost:3000 + +// curl -X POST "http://localhost:3000/?name=john&pass=doe" +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## ClearCookie + +Expire a client cookie \(_or all cookies if left empty\)_ + +```go title="Signature" +func (c *Ctx) ClearCookie(key ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Clears all cookies: + c.ClearCookie() + + // Expire specific cookie by name: + c.ClearCookie("user") + + // Expire multiple cookies by names: + c.ClearCookie("token", "session", "track_id", "version") + // ... +}) +``` + +:::caution +Web browsers and other compliant clients will only clear the cookie if the given options are identical to those when creating the cookie, excluding expires and maxAge. ClearCookie will not set these values for you - a technique similar to the one shown below should be used to ensure your cookie is deleted. +::: + +```go title="Example" +app.Get("/set", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + Value: "randomvalue", + Expires: time.Now().Add(24 * time.Hour), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) + +app.Get("/delete", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + // Set expiry date to the past + Expires: time.Now().Add(-(time.Hour * 2)), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) +``` + +## ClientHelloInfo + +ClientHelloInfo contains information from a ClientHello message in order to guide application logic in the GetCertificate and GetConfigForClient callbacks. +You can refer to the [ClientHelloInfo](https://golang.org/pkg/crypto/tls/#ClientHelloInfo) struct documentation for more information on the returned struct. + +```go title="Signature" +func (c *Ctx) ClientHelloInfo() *tls.ClientHelloInfo +``` + +```go title="Example" +// GET http://example.com/hello +app.Get("/hello", func(c *fiber.Ctx) error { + chi := c.ClientHelloInfo() + // ... +}) +``` + +## Context + +Returns [\*fasthttp.RequestCtx](https://godoc.org/github.com/valyala/fasthttp#RequestCtx) that is compatible with the context.Context interface that requires a deadline, a cancellation signal, and other values across API boundaries. + +```go title="Signature" +func (c *Ctx) Context() *fasthttp.RequestCtx +``` + +:::info +Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. +::: + +## Cookie + +Set cookie + +```go title="Signature" +func (c *Ctx) Cookie(cookie *Cookie) +``` + +```go +type Cookie struct { + Name string `json:"name"` + Value string `json:"value"` + Path string `json:"path"` + Domain string `json:"domain"` + MaxAge int `json:"max_age"` + Expires time.Time `json:"expires"` + Secure bool `json:"secure"` + HTTPOnly bool `json:"http_only"` + SameSite string `json:"same_site"` + SessionOnly bool `json:"session_only"` +} +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Create cookie + cookie := new(fiber.Cookie) + cookie.Name = "john" + cookie.Value = "doe" + cookie.Expires = time.Now().Add(24 * time.Hour) + + // Set cookie + c.Cookie(cookie) + // ... +}) +``` + +## Cookies + +Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist. + +```go title="Signature" +func (c *Ctx) Cookies(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Get cookie by key: + c.Cookies("name") // "john" + c.Cookies("empty", "doe") // "doe" + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Download + +Transfers the file from path as an `attachment`. + +Typically, browsers will prompt the user to download. By default, the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header `filename=` parameter is the file path \(_this typically appears in the browser dialog_\). + +Override this default with the **filename** parameter. + +```go title="Signature" +func (c *Ctx) Download(file string, filename ...string) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.Download("./files/report-12345.pdf"); + // => Download report-12345.pdf + + return c.Download("./files/report-12345.pdf", "report.pdf"); + // => Download report.pdf +}) +``` + +## Format + +Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format. + +:::info +If the header is **not** specified or there is **no** proper format, **text/plain** is used. +::: + +```go title="Signature" +func (c *Ctx) Format(body interface{}) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Accept: text/plain + c.Format("Hello, World!") + // => Hello, World! + + // Accept: text/html + c.Format("Hello, World!") + // =>

Hello, World!

+ + // Accept: application/json + c.Format("Hello, World!") + // => "Hello, World!" + // .. +}) +``` + +## FormFile + +MultipartForm files can be retrieved by name, the **first** file from the given key is returned. + +```go title="Signature" +func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Get first file from form field "document": + file, err := c.FormFile("document") + + // Save file to root directory: + return c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)) +}) +``` + +## FormValue + +Any form values can be retrieved by name, the **first** value from the given key is returned. + +```go title="Signature" +func (c *Ctx) FormValue(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Get first value from form field "name": + c.FormValue("name") + // => "john" or "" if not exist + + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Fresh + +When the response is still **fresh** in the client's cache **true** is returned, otherwise **false** is returned to indicate that the client cache is now stale and the full response should be sent. + +When a client sends the Cache-Control: no-cache request header to indicate an end-to-end reload request, `Fresh` will return false to make handling these requests transparent. + +Read more on [https://expressjs.com/en/4x/api.html\#req.fresh](https://expressjs.com/en/4x/api.html#req.fresh) + +```go title="Signature" +func (c *Ctx) Fresh() bool +``` + +## Get + +Returns the HTTP request header specified by the field. + +:::tip +The match is **case-insensitive**. +::: + +```go title="Signature" +func (c *Ctx) Get(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Get("Content-Type") // "text/plain" + c.Get("CoNtEnT-TypE") // "text/plain" + c.Get("something", "john") // "john" + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetReqHeaders + +Returns the HTTP request headers. + +```go title="Signature" +func (c *Ctx) GetReqHeaders() map[string]string +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRespHeader + +Returns the HTTP response header specified by the field. + +:::tip +The match is **case-insensitive**. +::: + +```go title="Signature" +func (c *Ctx) GetRespHeader(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.GetRespHeader("X-Request-Id") // "8d7ad5e3-aaf3-450b-a241-2beb887efd54" + c.GetRespHeader("Content-Type") // "text/plain" + c.GetRespHeader("something", "john") // "john" + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRespHeaders + +Returns the HTTP response headers. + +```go title="Signature" +func (c *Ctx) GetRespHeaders() map[string]string +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRouteURL + +Generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" + +```go title="Signature" +func (c *Ctx) GetRouteURL(routeName string, params Map) (string, error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Home page") +}).Name("home") + +app.Get("/user/:id", func(c *fiber.Ctx) error { + return c.SendString(c.Params("id")) +}).Name("user.show") + +app.Get("/test", func(c *fiber.Ctx) error { + location, _ := c.GetRouteURL("user.show", fiber.Map{"id": 1}) + return c.SendString(location) +}) + +// /test returns "/user/1" +``` + +## Hostname + +Returns the hostname derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header. + +```go title="Signature" +func (c *Ctx) Hostname() string +``` + +```go title="Example" +// GET http://google.com/search + +app.Get("/", func(c *fiber.Ctx) error { + c.Hostname() // "google.com" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## IP + +Returns the remote IP address of the request. + +```go title="Signature" +func (c *Ctx) IP() string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.IP() // "127.0.0.1" + + // ... +}) +``` + +When registering the proxy request header in the fiber app, the ip address of the header is returned [(Fiber configuration)](fiber.md#config) + +```go +app := fiber.New(fiber.Config{ + ProxyHeader: fiber.HeaderXForwardedFor, +}) +``` + +## IPs + +Returns an array of IP addresses specified in the [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) request header. + +```go title="Signature" +func (c *Ctx) IPs() []string +``` + +```go title="Example" +// X-Forwarded-For: proxy1, 127.0.0.1, proxy3 + +app.Get("/", func(c *fiber.Ctx) error { + c.IPs() // ["proxy1", "127.0.0.1", "proxy3"] + + // ... +}) +``` + +:::caution +Improper use of the X-Forwarded-For header can be a security risk. For details, see the [Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns) section. +::: + +## Is + +Returns the matching **content type**, if the incoming request’s [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header field matches the [MIME type](https://developer.mozilla.org/ru/docs/Web/HTTP/Basics_of_HTTP/MIME_types) specified by the type parameter. + +:::info +If the request has **no** body, it returns **false**. +::: + +```go title="Signature" +func (c *Ctx) Is(extension string) bool +``` + +```go title="Example" +// Content-Type: text/html; charset=utf-8 + +app.Get("/", func(c *fiber.Ctx) error { + c.Is("html") // true + c.Is(".html") // true + c.Is("json") // false + + // ... +}) +``` + +## IsFromLocal + +Returns true if request came from localhost +```go title="Signature" +func (c *Ctx) IsFromLocal() bool { +``` + +```go title="Example" + +app.Get("/", func(c *fiber.Ctx) error { + // If request came from localhost, return true else return false + c.IsFromLocal() + + // ... +}) +``` + +## JSON + +Converts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package. + +:::info +JSON also sets the content header to **application/json**. +::: + +```go title="Signature" +func (c *Ctx) JSON(data interface{}) error +``` + +```go title="Example" +type SomeStruct struct { + Name string + Age uint8 +} + +app.Get("/json", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.JSON(data) + // => Content-Type: application/json + // => "{"Name": "Grame", "Age": 20}" + + return c.JSON(fiber.Map{ + "name": "Grame", + "age": 20, + }) + // => Content-Type: application/json + // => "{"name": "Grame", "age": 20}" +}) +``` + +## JSONP + +Sends a JSON response with JSONP support. This method is identical to [JSON](ctx.md#json), except that it opts-in to JSONP callback support. By default, the callback name is simply callback. + +Override this by passing a **named string** in the method. + +```go title="Signature" +func (c *Ctx) JSONP(data interface{}, callback ...string) error +``` + +```go title="Example" +type SomeStruct struct { + name string + age uint8 +} + +app.Get("/", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + name: "Grame", + age: 20, + } + + return c.JSONP(data) + // => callback({"name": "Grame", "age": 20}) + + return c.JSONP(data, "customFunc") + // => customFunc({"name": "Grame", "age": 20}) +}) +``` + +## Links + +Joins the links followed by the property to populate the response’s [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) HTTP header field. + +```go title="Signature" +func (c *Ctx) Links(link ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + // Link: ; rel="next", + // ; rel="last" + + // ... +}) +``` + +## Locals + +A method that stores variables scoped to the request and, therefore, are available only to the routes that match the request. + +:::tip +This is useful if you want to pass some **specific** data to the next middleware. +::: + +```go title="Signature" +func (c *Ctx) Locals(key interface{}, value ...interface{}) interface{} +``` + +```go title="Example" +app.Use(func(c *fiber.Ctx) error { + c.Locals("user", "admin") + return c.Next() +}) + +app.Get("/admin", func(c *fiber.Ctx) error { + if c.Locals("user") == "admin" { + return c.Status(fiber.StatusOK).SendString("Welcome, admin!") + } + return c.SendStatus(fiber.StatusForbidden) + +}) +``` + +## Location + +Sets the response [Location](https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter. + +```go title="Signature" +func (c *Ctx) Location(path string) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + c.Location("http://example.com") + + c.Location("/foo/bar") + + return nil +}) +``` + +## Method + +Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. +Optionally, you could override the method by passing a string. + +```go title="Signature" +func (c *Ctx) Method(override ...string) string +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + c.Method() // "POST" + + c.Method("GET") + c.Method() // GET + + // ... +}) +``` + +## MultipartForm + +To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `map[string][]string`, so given a key, the value will be a string slice. + +```go title="Signature" +func (c *Ctx) MultipartForm() (*multipart.Form, error) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + if token := form.Value["token"]; len(token) > 0 { + // Get key value: + fmt.Println(token[0]) + } + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to disk: + if err := c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)); err != nil { + return err + } + } + } + + return err +}) +``` + +## Next + +When **Next** is called, it executes the next method in the stack that matches the current route. You can pass an error struct within the method that will end the chaining and call the [error handler](https://docs.gofiber.io/guide/error-handling). + +```go title="Signature" +func (c *Ctx) Next() error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println("1st route!") + return c.Next() +}) + +app.Get("*", func(c *fiber.Ctx) error { + fmt.Println("2nd route!") + return c.Next() +}) + +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println("3rd route!") + return c.SendString("Hello, World!") +}) +``` + +## OriginalURL + +Returns the original request URL. + +```go title="Signature" +func (c *Ctx) OriginalURL() string +``` + +```go title="Example" +// GET http://example.com/search?q=something + +app.Get("/", func(c *fiber.Ctx) error { + c.OriginalURL() // "/search?q=something" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Params + +Method can be used to get the route parameters, you could pass an optional default value that will be returned if the param key does not exist. + +:::info +Defaults to empty string \(`""`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) Params(key string, defaultValue ...string) string +``` + +```go title="Example" +// GET http://example.com/user/fenny +app.Get("/user/:name", func(c *fiber.Ctx) error { + c.Params("name") // "fenny" + + // ... +}) + +// GET http://example.com/user/fenny/123 +app.Get("/user/*", func(c *fiber.Ctx) error { + c.Params("*") // "fenny/123" + c.Params("*1") // "fenny/123" + + // ... +}) +``` + +Unnamed route parameters\(\*, +\) can be fetched by the **character** and the **counter** in the route. + +```go title="Example" +// ROUTE: /v1/*/shop/* +// GET: /v1/brand/4/shop/blue/xs +c.Params("*1") // "brand/4" +c.Params("*2") // "blue/xs" +``` + +For reasons of **downward compatibility**, the first parameter segment for the parameter character can also be accessed without the counter. + +```go title="Example" +app.Get("/v1/*/shop/*", func(c *fiber.Ctx) error { + c.Params("*") // outputs the values of the first wildcard segment +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## ParamsInt + +Method can be used to get an integer from the route parameters. +Please note if that parameter is not in the request, zero +will be returned. If the parameter is NOT a number, zero and an error +will be returned + +:::info +Defaults to the integer zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) ParamsInt(key string) (int, error) +``` + +```go title="Example" +// GET http://example.com/user/123 +app.Get("/user/:id", func(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") // int 123 and no error + + // ... +}) + +``` + +This method is equivalent of using `atoi` with ctx.Params + +## ParamsParser +This method is similar to BodyParser, but for path parameters. It is important to use the struct tag "params". For example, if you want to parse a path parameter with a field called Pass, you would use a struct field of params:"pass" + +```go title="Signature" +func (c *Ctx) ParamsParser(out interface{}) error +``` + +```go title="Example" +// GET http://example.com/user/111 +app.Get("/user/:id", func(c *fiber.Ctx) error { + param := struct {ID uint `params:"id"`}{} + + c.ParamsParser(¶m) // "{"id": 111}" + + // ... +}) + +``` + +## Path + +Contains the path part of the request URL. Optionally, you could override the path by passing a string. For internal redirects, you might want to call [RestartRouting](ctx.md#restartrouting) instead of [Next](ctx.md#next). + +```go title="Signature" +func (c *Ctx) Path(override ...string) string +``` + +```go title="Example" +// GET http://example.com/users?sort=desc + +app.Get("/users", func(c *fiber.Ctx) error { + c.Path() // "/users" + + c.Path("/john") + c.Path() // "/john" + + // ... +}) +``` + +## Protocol + +Contains the request protocol string: `http` or `https` for **TLS** requests. + +```go title="Signature" +func (c *Ctx) Protocol() string +``` + +```go title="Example" +// GET http://example.com + +app.Get("/", func(c *fiber.Ctx) error { + c.Protocol() // "http" + + // ... +}) +``` + +## Queries + +Queries is a function that returns an object containing a property for each query string parameter in the route. + +```go title="Signature" +func (c *Ctx) Queries() map[string]string +``` + +```go title="Example" +// GET http://example.com/?name=alex&want_pizza=false&id= + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["name"] // "alex" + m["want_pizza"] // "false" + m["id"] // "" + // ... +}) +``` + +```go title="Example" +// GET http://example.com/?field1=value1&field1=value2&field2=value3 + +app.Get("/", func (c *fiber.Ctx) error { + m := c.Queries() + m["field1"] // "value2" + m["field2"] // value3 +}) +``` + +```go title="Example" +// GET http://example.com/?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3 + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["list_a"] // "3" + m["list_b[]"] // "3" + m["list_c"] // "1,2,3" +}) +``` + +```go title="Example" +// GET /api/posts?filters.author.name=John&filters.category.name=Technology + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["filters.author.name"] // John + m["filters.category.name"] // Technology +}) +``` + +```go title="Example" +// GET /api/posts?tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["tags"] // apple,orange,banana + m["filters[tags]"] // apple,orange,banana + m["filters[category][name]"] // fruits + m["filters.tags"] // apple,orange,banana + m["filters.category.name"] // fruits +}) +``` + +## Query + +This property is an object containing a property for each query string parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::info +If there is **no** query string, it returns an **empty string**. +::: + +```go title="Signature" +func (c *Ctx) Query(key string, defaultValue ...string) string +``` + +```go title="Example" +// GET http://example.com/?order=desc&brand=nike + +app.Get("/", func(c *fiber.Ctx) error { + c.Query("order") // "desc" + c.Query("brand") // "nike" + c.Query("empty", "nike") // "nike" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## QueryBool + +This property is an object containing a property for each query boolean parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + + +:::caution +Please note if that parameter is not in the request, false will be returned. +If the parameter is not a boolean, it is still tried to be converted and usually returned as false. +::: + +```go title="Signature" +func (c *Ctx) QueryBool(key string, defaultValue ...bool) bool +``` + +```go title="Example" +// GET http://example.com/?name=alex&want_pizza=false&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryBool("want_pizza") // false + c.QueryBool("want_pizza", true) // false + c.QueryBool("name") // false + c.QueryBool("name", true) // true + c.QueryBool("id") // false + c.QueryBool("id", true) // true + + // ... +}) +``` + +## QueryFloat + +This property is an object containing a property for each query float64 parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::caution +Please note if that parameter is not in the request, zero will be returned. +If the parameter is not a number, it is still tried to be converted and usually returned as 1. +::: + +:::info +Defaults to the float64 zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) QueryFloat(key string, defaultValue ...float64) float64 +``` + +```go title="Example" +// GET http://example.com/?name=alex&amount=32.23&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryFloat("amount") // 32.23 + c.QueryFloat("amount", 3) // 32.23 + c.QueryFloat("name", 1) // 1 + c.QueryFloat("name") // 0 + c.QueryFloat("id", 3) // 3 + + // ... +}) +``` + + +## QueryInt + +This property is an object containing a property for each query integer parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + + +:::caution +Please note if that parameter is not in the request, zero will be returned. +If the parameter is not a number, it is still tried to be converted and usually returned as 1. +::: + +:::info +Defaults to the integer zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) QueryInt(key string, defaultValue ...int) int +``` + +```go title="Example" +// GET http://example.com/?name=alex&wanna_cake=2&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryInt("wanna_cake", 1) // 2 + c.QueryInt("name", 1) // 1 + c.QueryInt("id", 1) // 1 + c.QueryInt("id") // 0 + + // ... +}) +``` + +## QueryParser + +This method is similar to [BodyParser](ctx.md#bodyparser), but for query parameters. +It is important to use the struct tag "query". For example, if you want to parse a query parameter with a field called Pass, you would use a struct field of `query:"pass"`. + +```go title="Signature" +func (c *Ctx) QueryParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `query:"name"` + Pass string `query:"pass"` + Products []string `query:"products"` +} + +app.Get("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.QueryParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Products) // [shoe, hat] + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" +``` + +## Range + +A struct containing the type and a slice of ranges will be returned. + +```go title="Signature" +func (c *Ctx) Range(size int) (Range, error) +``` + +```go title="Example" +// Range: bytes=500-700, 700-900 +app.Get("/", func(c *fiber.Ctx) error { + b := c.Range(1000) + if b.Type == "bytes" { + for r := range r.Ranges { + fmt.Println(r) + // [500, 700] + } + } +}) +``` + +## Redirect + +Redirects to the URL derived from the specified path, with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +```go title="Signature" +func (c *Ctx) Redirect(location string, status ...int) error +``` + +```go title="Example" +app.Get("/coffee", func(c *fiber.Ctx) error { + return c.Redirect("/teapot") +}) + +app.Get("/teapot", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).Send("🍵 short and stout 🍵") +}) +``` + +```go title="More examples" +app.Get("/", func(c *fiber.Ctx) error { + return c.Redirect("/foo/bar") + return c.Redirect("../login") + return c.Redirect("http://example.com") + return c.Redirect("http://example.com", 301) +}) +``` + +## RedirectToRoute + +Redirects to the specific route along with the parameters and with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +:::info +If you want to send queries to route, you must add **"queries"** key typed as **map[string]string** to params. +::: + +```go title="Signature" +func (c *Ctx) RedirectToRoute(routeName string, params fiber.Map, status ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // /user/fiber + return c.RedirectToRoute("user", fiber.Map{ + "name": "fiber" + }) +}) + +app.Get("/with-queries", func(c *fiber.Ctx) error { + // /user/fiber?data[0][name]=john&data[0][age]=10&test=doe + return c.RedirectToRoute("user", fiber.Map{ + "name": "fiber", + "queries": map[string]string{"data[0][name]": "john", "data[0][age]": "10", "test": "doe"}, + }) +}) + +app.Get("/user/:name", func(c *fiber.Ctx) error { + return c.SendString(c.Params("name")) +}).Name("user") +``` + +## RedirectBack + +Redirects back to refer URL. It redirects to fallback URL if refer header doesn't exists, with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +```go title="Signature" +func (c *Ctx) RedirectBack(fallback string, status ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Home page") +}) +app.Get("/test", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/html") + return c.SendString(`Back`) +}) + +app.Get("/back", func(c *fiber.Ctx) error { + return c.RedirectBack("/") +}) +``` + +## Render + +Renders a view with data and sends a `text/html` response. By default `Render` uses the default [**Go Template engine**](https://pkg.go.dev/html/template/). If you want to use another View engine, please take a look at our [**Template middleware**](https://docs.gofiber.io/template). + +```go title="Signature" +func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error +``` + +## Request + +Request return the [\*fasthttp.Request](https://godoc.org/github.com/valyala/fasthttp#Request) pointer + +```go title="Signature" +func (c *Ctx) Request() *fasthttp.Request +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Request().Header.Method() + // => []byte("GET") +}) +``` + +## ReqHeaderParser + +This method is similar to [BodyParser](ctx.md#bodyparser), but for request headers. +It is important to use the struct tag "reqHeader". For example, if you want to parse a request header with a field called Pass, you would use a struct field of `reqHeader:"pass"`. + +```go title="Signature" +func (c *Ctx) ReqHeaderParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `reqHeader:"name"` + Pass string `reqHeader:"pass"` + Products []string `reqHeader:"products"` +} + +app.Get("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.ReqHeaderParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Products) // [shoe, hat] + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" +``` + +## Response + +Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer + +```go title="Signature" +func (c *Ctx) Response() *fasthttp.Response +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Response().BodyWriter().Write([]byte("Hello, World!")) + // => "Hello, World!" + return nil +}) +``` + +## RestartRouting + +Instead of executing the next method when calling [Next](ctx.md#next), **RestartRouting** restarts execution from the first method that matches the current route. This may be helpful after overriding the path, i. e. an internal redirect. Note that handlers might be executed again which could result in an infinite loop. + +```go title="Signature" +func (c *Ctx) RestartRouting() error +``` + +```go title="Example" +app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("From /new") +}) + +app.Get("/old", func(c *fiber.Ctx) error { + c.Path("/new") + return c.RestartRouting() +}) +``` + +## Route + +Returns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct. + +```go title="Signature" +func (c *Ctx) Route() *Route +``` + +```go title="Example" +// http://localhost:8080/hello + + +app.Get("/hello/:name", func(c *fiber.Ctx) error { + r := c.Route() + fmt.Println(r.Method, r.Path, r.Params, r.Handlers) + // GET /hello/:name handler [name] + + // ... +}) +``` + +:::caution +Do not rely on `c.Route()` in middlewares **before** calling `c.Next()` - `c.Route()` returns the **last executed route**. +::: + +```go title="Example" +func MyMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + beforeNext := c.Route().Path // Will be '/' + err := c.Next() + afterNext := c.Route().Path // Will be '/hello/:name' + return err + } +} +``` + +## SaveFile + +Method is used to save **any** multipart file to disk. + +```go title="Signature" +func (c *Ctx) SaveFile(fh *multipart.FileHeader, path string) error +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to disk: + if err := c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)); err != nil { + return err + } + } + return err + } +}) +``` + +## SaveFileToStorage + +Method is used to save **any** multipart file to an external storage system. + +```go title="Signature" +func (c *Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error +``` + +```go title="Example" +storage := memory.New() + +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to storage: + if err := c.SaveFileToStorage(file, fmt.Sprintf("./%s", file.Filename), storage); err != nil { + return err + } + } + return err + } +}) +``` + +## Secure + +A boolean property that is `true` , if a **TLS** connection is established. + +```go title="Signature" +func (c *Ctx) Secure() bool +``` + +```go title="Example" +// Secure() method is equivalent to: +c.Protocol() == "https" +``` + +## Send + +Sets the HTTP response body. + +```go title="Signature" +func (c *Ctx) Send(body []byte) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.Send([]byte("Hello, World!")) // => "Hello, World!" +}) +``` + +Fiber also provides `SendString` and `SendStream` methods for raw inputs. + +:::tip +Use this if you **don't need** type assertion, recommended for **faster** performance. +::: + +```go title="Signature" +func (c *Ctx) SendString(body string) error +func (c *Ctx) SendStream(stream io.Reader, size ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + // => "Hello, World!" + + return c.SendStream(bytes.NewReader([]byte("Hello, World!"))) + // => "Hello, World!" +}) +``` + +## SendFile + +Transfers the file from the given path. Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response HTTP header field based on the **filenames** extension. + +:::caution +Method doesn´t use **gzipping** by default, set it to **true** to enable. +::: + +```go title="Signature" title="Signature" +func (c *Ctx) SendFile(file string, compress ...bool) error +``` + +```go title="Example" +app.Get("/not-found", func(c *fiber.Ctx) error { + return c.SendFile("./public/404.html"); + + // Disable compression + return c.SendFile("./static/index.html", false); +}) +``` + +:::info +If the file contains an url specific character you have to escape it before passing the file path into the `sendFile` function. +::: + +```go title="Example" +app.Get("/file-with-url-chars", func(c *fiber.Ctx) error { + return c.SendFile(url.PathEscape("hash_sign_#.txt")) +}) +``` + +## SendStatus + +Sets the status code and the correct status message in the body, if the response body is **empty**. + +:::tip +You can find all used status codes and messages [here](https://github.com/gofiber/fiber/blob/dffab20bcdf4f3597d2c74633a7705a517d2c8c2/utils.go#L183-L244). +::: + +```go title="Signature" +func (c *Ctx) SendStatus(status int) error +``` + +```go title="Example" +app.Get("/not-found", func(c *fiber.Ctx) error { + return c.SendStatus(415) + // => 415 "Unsupported Media Type" + + c.SendString("Hello, World!") + return c.SendStatus(415) + // => 415 "Hello, World!" +}) +``` + +## Set + +Sets the response’s HTTP header field to the specified `key`, `value`. + +```go title="Signature" +func (c *Ctx) Set(key string, val string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/plain") + // => "Content-type: text/plain" + + // ... +}) +``` + +## SetParserDecoder + +Allow you to config BodyParser/QueryParser decoder, base on schema's options, providing possibility to add custom type for parsing. + +```go title="Signature" +func SetParserDecoder(parserConfig fiber.ParserConfig{ + IgnoreUnknownKeys bool, + ParserType []fiber.ParserType{ + Customtype interface{}, + Converter func(string) reflect.Value, + }, + ZeroEmpty bool, + SetAliasTag string, +}) +``` + +```go title="Example" + +type CustomTime time.Time + +// String() returns the time in string +func (ct *CustomTime) String() string { + t := time.Time(*ct).String() + return t +} + +// Register the converter for CustomTime type format as 2006-01-02 +var timeConverter = func(value string) reflect.Value { + fmt.Println("timeConverter", value) + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} +} + +customTime := fiber.ParserType{ + Customtype: CustomTime{}, + Converter: timeConverter, +} + +// Add setting to the Decoder +fiber.SetParserDecoder(fiber.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []fiber.ParserType{customTime}, + ZeroEmpty: true, +}) + +// Example to use CustomType, you pause custom time format not in RFC3339 +type Demo struct { + Date CustomTime `form:"date" query:"date"` + Title string `form:"title" query:"title"` + Body string `form:"body" query:"body"` +} + +app.Post("/body", func(c *fiber.Ctx) error { + var d Demo + c.BodyParser(&d) + fmt.Println("d.Date", d.Date.String()) + return c.JSON(d) +}) + +app.Get("/query", func(c *fiber.Ctx) error { + var d Demo + c.QueryParser(&d) + fmt.Println("d.Date", d.Date.String()) + return c.JSON(d) +}) + +// curl -X POST -F title=title -F body=body -F date=2021-10-20 http://localhost:3000/body + +// curl -X GET "http://localhost:3000/query?title=title&body=body&date=2021-10-20" + +``` + + +## SetUserContext + +Sets the user specified implementation for context interface. + +```go title="Signature" +func (c *Ctx) SetUserContext(ctx context.Context) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + ctx := context.Background() + c.SetUserContext(ctx) + // Here ctx could be any context implementation + + // ... +}) +``` + +## Stale + +[https://expressjs.com/en/4x/api.html\#req.stale](https://expressjs.com/en/4x/api.html#req.stale) + +```go title="Signature" +func (c *Ctx) Stale() bool +``` + +## Status + +Sets the HTTP status for the response. + +:::info +Method is a **chainable**. +::: + +```go title="Signature" +func (c *Ctx) Status(status int) *Ctx +``` + +```go title="Example" +app.Get("/fiber", func(c *fiber.Ctx) error { + c.Status(fiber.StatusOK) + return nil +} + +app.Get("/hello", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusBadRequest).SendString("Bad Request") +} + +app.Get("/world", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusNotFound).SendFile("./public/gopher.png") +}) +``` + +## Subdomains + +Returns a string slice of subdomains in the domain name of the request. + +The application property subdomain offset, which defaults to `2`, is used for determining the beginning of the subdomain segments. + +```go title="Signature" +func (c *Ctx) Subdomains(offset ...int) []string +``` + +```go title="Example" +// Host: "tobi.ferrets.example.com" + +app.Get("/", func(c *fiber.Ctx) error { + c.Subdomains() // ["ferrets", "tobi"] + c.Subdomains(1) // ["tobi"] + + // ... +}) +``` + +## Type + +Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header to the MIME type listed [here](https://github.com/nginx/nginx/blob/master/conf/mime.types) specified by the file **extension**. + +```go title="Signature" +func (c *Ctx) Type(ext string, charset ...string) *Ctx +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Type(".html") // => "text/html" + c.Type("html") // => "text/html" + c.Type("png") // => "image/png" + + c.Type("json", "utf-8") // => "application/json; charset=utf-8" + + // ... +}) +``` + +## UserContext + +UserContext returns a context implementation that was set by user earlier +or returns a non-nil, empty context, if it was not set earlier. + +```go title="Signature" +func (c *Ctx) UserContext() context.Context +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + ctx := c.UserContext() + // ctx is context implementation set by user + + // ... +}) +``` + +## Vary + +Adds the given header field to the [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) response header. This will append the header, if not already listed, otherwise leaves it listed in the current location. + +:::info +Multiple fields are **allowed**. +::: + +```go title="Signature" +func (c *Ctx) Vary(fields ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Vary("Origin") // => Vary: Origin + c.Vary("User-Agent") // => Vary: Origin, User-Agent + + // No duplicates + c.Vary("Origin") // => Vary: Origin, User-Agent + + c.Vary("Accept-Encoding", "Accept") + // => Vary: Origin, User-Agent, Accept-Encoding, Accept + + // ... +}) +``` + +## Write + +Write adopts the Writer interface + +```go title="Signature" +func (c *Ctx) Write(p []byte) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Write([]byte("Hello, World!")) // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## Writef + +Writef adopts the string with variables + +```go title="Signature" +func (c *Ctx) Writef(f string, a ...interface{}) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + world := "World!" + c.Writef("Hello, %s", world) // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## WriteString + +WriteString adopts the string + +```go title="Signature" +func (c *Ctx) WriteString(s string) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.WriteString("Hello, World!") // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## XHR + +A Boolean property, that is `true`, if the request’s [X-Requested-With](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) header field is [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), indicating that the request was issued by a client library \(such as [jQuery](https://api.jquery.com/jQuery.ajax/)\). + +```go title="Signature" +func (c *Ctx) XHR() bool +``` + +```go title="Example" +// X-Requested-With: XMLHttpRequest + +app.Get("/", func(c *fiber.Ctx) error { + c.XHR() // true + + // ... +}) +``` + +## XML + +Converts any **interface** or **string** to XML using the standard `encoding/xml` package. + +:::info +XML also sets the content header to **application/xml**. +::: + +```go title="Signature" +func (c *Ctx) XML(data interface{}) error +``` + +```go title="Example" +type SomeStruct struct { + XMLName xml.Name `xml:"Fiber"` + Name string `xml:"Name"` + Age uint8 `xml:"Age"` +} + +app.Get("/", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.XML(data) + // + // Grame + // 20 + // +}) +``` diff --git a/docs/api/fiber.md b/docs/api/fiber.md new file mode 100644 index 0000000000..fa0ad2412d --- /dev/null +++ b/docs/api/fiber.md @@ -0,0 +1,119 @@ +--- +id: fiber +title: 📦 Fiber +description: Fiber represents the fiber package where you start to create an instance. +sidebar_position: 1 +--- + +## New + +This method creates a new **App** named instance. You can pass optional [config ](#config)when creating a new instance. + +```go title="Signature" +func New(config ...Config) *App +``` + +```go title="Example" +// Default config +app := fiber.New() + +// ... +``` + +## Config + +You can pass an optional Config when creating a new Fiber instance. + +```go title="Example" +// Custom config +app := fiber.New(fiber.Config{ + Prefork: true, + CaseSensitive: true, + StrictRouting: true, + ServerHeader: "Fiber", + AppName: "Test App v1.0.1", +}) + +// ... +``` + +**Config fields** + +| Property | Type | Description | Default | +| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | +| AppName | `string` | This allows to setup app name for the app | `""` | +| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | +| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | +| ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | +| CompressedFileSuffix | `string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `".fiber.gz"` | +| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | +| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | +| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | +| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | +| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | +| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | +| DisableStartupMessage | `bool` | When set to true, it will not print out debug information | `false` | +| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | +| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | +| EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | +| EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | +| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | +| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | +| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | +| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | +| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | +| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | +| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | +| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | +| Prefork | `bool` | Enables use of the[`SO_REUSEPORT`](https://lwn.net/Articles/542629/)socket option. This will spawn multiple Go processes listening on the same port. learn more about [socket sharding](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/). **NOTE: if enabled, the application will need to be ran through a shell because prefork mode sets environment variables. If you're using Docker, make sure the app is ran with `CMD ./app` or `CMD ["sh", "-c", "/app"]`. For more info, see** [**this**](https://github.com/gofiber/fiber/issues/1021#issuecomment-730537971) **issue comment.** | `false` | +| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | +| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | +| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | +| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | +| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | +| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger then the current limit. | `false` | +| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | +| TrustedProxies | `[]string` | Contains the list of trusted proxy IP's. Look at `EnableTrustedProxyCheck` doc.

It can take IP or IP range addresses. If it gets IP range, it iterates all possible addresses. | `[]string*__*` | +| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | +| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | +| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | +| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | +| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | + +## NewError + +NewError creates a new HTTPError instance with an optional message. + +```go title="Signature" +func NewError(code int, message ...string) *Error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return fiber.NewError(782, "Custom error message") +}) +``` + +## IsChild + +IsChild determines if the current process is a result of Prefork. + +```go title="Signature" +func IsChild() bool +``` + +```go title="Example" +// Prefork will spawn child processes +app := fiber.New(fiber.Config{ + Prefork: true, +}) + +if !fiber.IsChild() { + fmt.Println("I'm the parent process") +} else { + fmt.Println("I'm a child process") +} + +// ... +``` diff --git a/docs/api/log.md b/docs/api/log.md new file mode 100644 index 0000000000..9b741b13f7 --- /dev/null +++ b/docs/api/log.md @@ -0,0 +1,155 @@ +--- +id: log +title: 📃 Log +description: Fiber's built-in log package +sidebar_position: 6 +--- + +We can use logs to observe program behavior, diagnose problems, or configure corresponding alarms. +And defining a well structured log can improve search efficiency and facilitate handling of problems. + +Fiber provides a default way to print logs in the standard output. +It also provides several global functions, such as `log.Info`, `log.Errorf`, `log.Warnw`, etc. + +## Log levels + +```go +const ( + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarn + LevelError + LevelFatal + LevelPanic +) +``` + +## Custom log + +Fiber provides the `AllLogger` interface for adapting the various log libraries. + +```go +type CommonLogger interface { + Logger + FormatLogger + WithLogger +} + +type AllLogger interface { + CommonLogger + ControlLogger + WithLogger +} +``` + +## Print log +Note: The method of calling the Fatal level will interrupt the program running after printing the log, please use it with caution. +Directly print logs of different levels, which will be entered into messageKey, the default is msg. + +```go +log.Info("Hello, World!") +log.Debug("Are you OK?") +log.Info("42 is the answer to life, the universe, and everything") +log.Warn("We are under attack!") +log.Error("Houston, we have a problem.") +log.Fatal("So Long, and Thanks for All the Fislog.") +log.Panic("The system is down.") +``` +Format and print logs of different levels, all methods end with f + +```go +log.Debugf("Hello %s", "boy") +log.Infof("%d is the answer to life, the universe, and everything", 233) +log.Warnf("We are under attack %s!", "boss") +log.Errorf("%s, we have a problem.", "Master Shifu") +log.Fatalf("So Long, and Thanks for All the %s.", "banana") +``` + +Print a message with the key and value, or `KEYVALS UNPAIRED` if the key and value are not a pair. + +```go +log.Debugw("", "Hello", "boy") +log.Infow("", "number", 233) +log.Warnw("", "job", "boss") +log.Errorw("", "name", "Master Shifu") +log.Fatalw("", "fruit", "banana") +``` + +## Global log +If you are in a project and just want to use a simple log function that can be printed at any time in the global, we provide a global log. + +```go +import "github.com/gofiber/fiber/v2/log" + +log.Info("info") +log.Warn("warn") +``` + +The above is using the default `log.DefaultLogger` standard output. +You can also find an already implemented adaptation under contrib, or use your own implemented Logger and use `log.SetLogger` to set the global log logger. + +```go +import ( + "log" + fiberlog "github.com/gofiber/fiber/v2/log" +) + +var _ log.AllLogger = (*customLogger)(nil) + +type customLogger struct { + stdlog *log.Logger +} + +// ... +// inject your custom logger +fiberlog.SetLogger(customLogger) +``` + +## Set Level +`log.SetLevel` sets the level of logs below which logs will not be output. +The default logger is LevelTrace. + +Note that this method is not **concurrent-safe**. + +```go +import "github.com/gofiber/fiber/v2/log" + +log.SetLevel(log.LevelInfo) +``` +## Set output + +`log.SetOutput` sets the output destination of the logger. The default logger types the log in the console. + +```go +var logger AllLogger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, +} +``` + +Set the output destination to the file. + +```go +// Output to ./test.log file +f, err := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +if err != nil { + return +} +log.SetOutput(f) +``` +Set the output destination to the console and file. + +```go +// Output to ./test.log file +file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +iw := io.MultiWriter(os.Stdout, file) +log.SetOutput(iw) +``` +## Bind context +Set the context, using the following method will return a `CommonLogger` instance bound to the specified context +```go +commonLogger := log.WithContext(ctx) +commonLogger.Info("info") +``` + diff --git a/docs/api/middleware/_category_.json b/docs/api/middleware/_category_.json new file mode 100644 index 0000000000..133ac5147a --- /dev/null +++ b/docs/api/middleware/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "🧬 Middleware", + "position": 7, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "Middleware is a function chained in the HTTP request cycle with access to the Context which it uses to perform a specific action, for example, logging every request or enabling CORS." + } +} \ No newline at end of file diff --git a/docs/api/middleware/adaptor.md b/docs/api/middleware/adaptor.md new file mode 100644 index 0000000000..64df229ce2 --- /dev/null +++ b/docs/api/middleware/adaptor.md @@ -0,0 +1,169 @@ +--- +id: adaptor +--- + +# Adaptor + +Converter for net/http handlers to/from Fiber request handlers, special thanks to [@arsmn](https://github.com/arsmn)! + +## Signatures +| Name | Signature | Description +| :--- | :--- | :--- +| HTTPHandler | `HTTPHandler(h http.Handler) fiber.Handler` | http.Handler -> fiber.Handler +| HTTPHandlerFunc | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | http.HandlerFunc -> fiber.Handler +| HTTPMiddleware | `HTTPHandlerFunc(mw func(http.Handler) http.Handler) fiber.Handler` | func(http.Handler) http.Handler -> fiber.Handler +| FiberHandler | `FiberHandler(h fiber.Handler) http.Handler` | fiber.Handler -> http.Handler +| FiberHandlerFunc | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | fiber.Handler -> http.HandlerFunc +| FiberApp | `FiberApp(app *fiber.App) http.HandlerFunc` | Fiber app -> http.HandlerFunc +| ConvertRequest | `ConvertRequest(c *fiber.Ctx, forServer bool) (*http.Request, error)` | fiber.Ctx -> http.Request +| CopyContextToFiberContext | `CopyContextToFiberContext(context interface{}, requestContext *fasthttp.RequestCtx)` | context.Context -> fasthttp.RequestCtx + +## Examples + +### net/http to Fiber +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // New fiber app + app := fiber.New() + + // http.Handler -> fiber.Handler + app.Get("/", adaptor.HTTPHandler(handler(greet))) + + // http.HandlerFunc -> fiber.Handler + app.Get("/func", adaptor.HTTPHandlerFunc(greet)) + + // Listen on port 3000 + app.Listen(":3000") +} + +func handler(f http.HandlerFunc) http.Handler { + return http.HandlerFunc(f) +} + +func greet(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello World!") +} +``` + +### net/http middleware to Fiber +```go +package main + +import ( + "log" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // New fiber app + app := fiber.New() + + // http middleware -> fiber.Handler + app.Use(adaptor.HTTPMiddleware(logMiddleware)) + + // Listen on port 3000 + app.Listen(":3000") +} + +func logMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("log middleware") + next.ServeHTTP(w, r) + }) +} +``` + +### Fiber Handler to net/http +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // fiber.Handler -> http.Handler + http.Handle("/", adaptor.FiberHandler(greet)) + + // fiber.Handler -> http.HandlerFunc + http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet)) + + // Listen on port 3000 + http.ListenAndServe(":3000", nil) +} + +func greet(c *fiber.Ctx) error { + return c.SendString("Hello World!") +} +``` + +### Fiber App to net/http +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + app := fiber.New() + + app.Get("/greet", greet) + + // Listen on port 3000 + http.ListenAndServe(":3000", adaptor.FiberApp(app)) +} + +func greet(c *fiber.Ctx) error { + return c.SendString("Hello World!") +} +``` + +### Fiber Context to (net/http).Request +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + app := fiber.New() + + app.Get("/greet", greetWithHTTPReq) + + // Listen on port 3000 + http.ListenAndServe(":3000", adaptor.FiberApp(app)) +} + +func greetWithHTTPReq(c *fiber.Ctx) error { + httpReq, err := adaptor.ConvertRequest(c, false) + if err != nil { + return err + } + + return c.SendString("Request URL: " + httpReq.URL.String()) +} +``` diff --git a/docs/api/middleware/basicauth.md b/docs/api/middleware/basicauth.md new file mode 100644 index 0000000000..0e90eafed0 --- /dev/null +++ b/docs/api/middleware/basicauth.md @@ -0,0 +1,85 @@ +--- +id: basicauth +--- + +# BasicAuth + +Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/basicauth" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Provide a minimal config +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, +})) + +// Or extend your config for customization +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, + Realm: "Forbidden", + Authorizer: func(user, pass string) bool { + if user == "john" && pass == "doe" { + return true + } + if user == "admin" && pass == "123456" { + return true + } + return false + }, + Unauthorized: func(c *fiber.Ctx) error { + return c.SendFile("./unauthorized.html") + }, + ContextUsername: "_user", + ContextPassword: "_pass", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:----------------|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Users | `map[string]string` | Users defines the allowed credentials. | `map[string]string{}` | +| Realm | `string` | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `"Restricted"` | +| Authorizer | `func(string, string) bool` | Authorizer defines a function to check the credentials. It will be called with a username and password and is expected to return true or false to indicate approval. | `nil` | +| Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` | +| ContextUsername | `string` | ContextUsername is the key to store the username in Locals. | `"username"` | +| ContextPassword | `string` | ContextPassword is the key to store the password in Locals. | `"password"` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Users: map[string]string{}, + Realm: "Restricted", + Authorizer: nil, + Unauthorized: nil, + ContextUsername: "username", + ContextPassword: "password", +} +``` diff --git a/docs/api/middleware/cache.md b/docs/api/middleware/cache.md new file mode 100644 index 0000000000..3a87306d3b --- /dev/null +++ b/docs/api/middleware/cache.md @@ -0,0 +1,99 @@ +--- +id: cache +--- + +# Cache + +Cache middleware for [Fiber](https://github.com/gofiber/fiber) designed to intercept responses and cache them. This middleware will cache the `Body`, `Content-Type` and `StatusCode` using the `c.Path()` as unique identifier. Special thanks to [@codemicro](https://github.com/codemicro/fiber-cache) for creating this middleware for Fiber core! + +Request Directives
+`Cache-Control: no-cache` will return the up-to-date response but still caches it. You will always get a `miss` cache status.
+`Cache-Control: no-store` will refrain from caching. You will always get the up-to-date response. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/cache" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(cache.New()) + +// Or extend your config for customization +app.Use(cache.New(cache.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Query("refresh") == "true" + }, + Expiration: 30 * time.Minute, + CacheControl: true, +})) +``` + +Or you can custom key and expire time like this: + +```go +app.Use(cache.New(cache.Config{ + ExpirationGenerator: func(c *fiber.Ctx, cfg *cache.Config) time.Duration { + newCacheTime, _ := strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) + return time.Second * time.Duration(newCacheTime) + }, + KeyGenerator: func(c *fiber.Ctx) string { + return utils.CopyString(c.Path()) + }, +})) + +app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.Add("Cache-Time", "6000") + return c.SendString("hi") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:---------------------|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Expiration | `time.Duration` | Expiration is the time that a cached response will live. | `1 * time.Minute` | +| CacheHeader | `string` | CacheHeader is the header on the response header that indicates the cache status, with the possible return values "hit," "miss," or "unreachable." | `X-Cache` | +| CacheControl | `bool` | CacheControl enables client-side caching if set to true. | `false` | +| KeyGenerator | `func(*fiber.Ctx) string` | Key allows you to generate custom keys. | `func(c *fiber.Ctx) string { return utils.CopyString(c.Path()) }` | +| ExpirationGenerator | `func(*fiber.Ctx, *cache.Config) time.Duration` | ExpirationGenerator allows you to generate custom expiration keys based on the request. | `nil` | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | In-memory store | +| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead. | In-memory store | +| Key (Deprecated) | `func(*fiber.Ctx) string` | Deprecated: Use KeyGenerator instead. | `nil` | +| StoreResponseHeaders | `bool` | StoreResponseHeaders allows you to store additional headers generated by next middlewares & handler. | `false` | +| MaxBytes | `uint` | MaxBytes is the maximum number of bytes of response bodies simultaneously stored in cache. | `0` (No limit) | +| Methods | `[]string` | Methods specifies the HTTP methods to cache. | `[]string{fiber.MethodGet, fiber.MethodHead}` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Expiration: 1 * time.Minute, + CacheHeader: "X-Cache", + CacheControl: false, + KeyGenerator: func(c *fiber.Ctx) string { + return utils.CopyString(c.Path()) + }, + ExpirationGenerator: nil, + StoreResponseHeaders: false, + Storage: nil, + MaxBytes: 0, + Methods: []string{fiber.MethodGet, fiber.MethodHead}, +} +``` diff --git a/docs/api/middleware/compress.md b/docs/api/middleware/compress.md new file mode 100644 index 0000000000..f284f5e92c --- /dev/null +++ b/docs/api/middleware/compress.md @@ -0,0 +1,81 @@ +--- +id: compress +--- + +# Compress + +Compression middleware for [Fiber](https://github.com/gofiber/fiber) that will compress the response using `gzip`, `deflate` and `brotli` compression depending on the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/compress" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(compress.New()) + +// Or extend your config for customization +app.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, // 1 +})) + +// Skip middleware for specific routes +app.Use(compress.New(compress.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Path() == "/dont_compress" + }, + Level: compress.LevelBestSpeed, // 1 +})) +``` + +## Config + +### Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:--------------------------------------------------------------------|:-------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Level | `Level` | Level determines the compression algorithm. | `LevelDefault (0)` | + +Possible values for the "Level" field are: + +- `LevelDisabled (-1)`: Compression is disabled. +- `LevelDefault (0)`: Default compression level. +- `LevelBestSpeed (1)`: Best compression speed. +- `LevelBestCompression (2)`: Best compression. + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Level: LevelDefault, +} +``` + +## Constants + +```go +// Compression levels +const ( + LevelDisabled = -1 + LevelDefault = 0 + LevelBestSpeed = 1 + LevelBestCompression = 2 +) +``` diff --git a/docs/api/middleware/cors.md b/docs/api/middleware/cors.md new file mode 100644 index 0000000000..af9b8c5f2b --- /dev/null +++ b/docs/api/middleware/cors.md @@ -0,0 +1,88 @@ +--- +id: cors +--- + +# CORS + +CORS middleware for [Fiber](https://github.com/gofiber/fiber) that can be used to enable [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(cors.New()) + +// Or extend your config for customization +app.Use(cors.New(cors.Config{ + AllowOrigins: "https://gofiber.io, https://gofiber.net", + AllowHeaders: "Origin, Content-Type, Accept", +})) +``` + +Using the `AllowOriginsFunc` function. In this example any origin will be allowed via CORS. + +For example, if a browser running on `http://localhost:3000` sends a request, this will be accepted and the `access-control-allow-origin` response header will be set to `http://localhost:3000`. + +**Note: Using this feature is discouraged in production and it's best practice to explicitly set CORS origins via `AllowOrigins`.** + +```go +app.Use(cors.New()) + +app.Use(cors.New(cors.Config{ + AllowOriginsFunc: func(origin string) bool { + return os.Getenv("ENVIRONMENT") == "development" + }, +})) +``` + +## Config + +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| AllowOriginsFunc | `func(origin string) bool` | AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' response header to the 'origin' request header when returned true. | `nil` | +| AllowOrigins | `string` | AllowOrigin defines a list of origins that may access the resource. | `"*"` | +| AllowMethods | `string` | AllowMethods defines a list methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` | +| AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` | +| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. | `false` | +| ExposeHeaders | `string` | ExposeHeaders defines a whitelist headers that clients are allowed to access. | `""` | +| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. | `0` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + AllowOriginsFunc: nil, + AllowOrigins: "*", + AllowMethods: strings.Join([]string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, ","), + AllowHeaders: "", + AllowCredentials: false, + ExposeHeaders: "", + MaxAge: 0, +} +``` diff --git a/docs/api/middleware/csrf.md b/docs/api/middleware/csrf.md new file mode 100644 index 0000000000..dbbf6007d9 --- /dev/null +++ b/docs/api/middleware/csrf.md @@ -0,0 +1,111 @@ +--- +id: csrf +--- + +# CSRF + +CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token on requests, other than those defined as "safe" by RFC7231 \(GET, HEAD, OPTIONS, or TRACE\). When the csrf token is invalid, this middleware will return the `fiber.ErrForbidden` error. + +CSRF Tokens are generated on GET requests. You can retrieve the CSRF token with `c.Locals(contextKey)`, where `contextKey` is the string you set in the config (see Custom Config below). + +When no `csrf_` cookie is set, or the token has expired, a new token will be generated and `csrf_` cookie set. + +:::note +This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +::: + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/csrf" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(csrf.New()) + +// Or extend your config for customization +app.Use(csrf.New(csrf.Config{ + KeyLookup: "header:X-Csrf-Token", + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUID, + Extractor: func(c *fiber.Ctx) (string, error) { ... }, +})) +``` + +:::note +KeyLookup will be ignored if Extractor is explicitly set. +::: + +## Config + +### Config + +| Property | Type | Description | Default | +|:------------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to create an Extractor that extracts the token from the request. Possible values: "`header:`", "`query:`", "`param:`", "`form:`", "`cookie:`". Ignored if an Extractor is explicitly set. | "header:X-CSRF-Token" | +| CookieName | `string` | Name of the session cookie. This cookie will store the session key. | "csrf_" | +| CookieDomain | `string` | Domain of the CSRF cookie. | "" | +| CookiePath | `string` | Path of the CSRF cookie. | "" | +| CookieSecure | `bool` | Indicates if the CSRF cookie is secure. | false | +| CookieHTTPOnly | `bool` | Indicates if the CSRF cookie is HTTP-only. | false | +| CookieSameSite | `string` | Value of SameSite cookie. | "Lax" | +| CookieSessionOnly | `bool` | Decides whether the cookie should last for only the browser session. Ignores Expiration if set to true. | false | +| Expiration | `time.Duration` | Expiration is the duration before the CSRF token will expire. | 1 * time.Hour | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | memory.New() | +| ContextKey | `string` | Context key to store the generated CSRF token into the context. If left empty, the token will not be stored in the context. | "" | +| KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID | +| CookieExpires | `time.Duration` (Deprecated) | Deprecated: Please use Expiration. | 0 | +| Cookie | `*fiber.Cookie` (Deprecated) | Deprecated: Please use Cookie* related fields. | nil | +| TokenLookup | `string` (Deprecated) | Deprecated: Please use KeyLookup. | "" | +| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. | DefaultErrorHandler | +| Extractor | `func(*fiber.Ctx) (string, error)` | Extractor returns the CSRF token. If set, this will be used in place of an Extractor based on KeyLookup. | Extractor based on KeyLookup | + +## Default Config + +```go +var ConfigDefault = Config{ + KeyLookup: "header:" + HeaderName, + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUID, + ErrorHandler: defaultErrorHandler, + Extractor: CsrfFromHeader(HeaderName), +} +``` + +## Constants + +```go +const ( + HeaderName = "X-Csrf-Token" +) +``` + +### Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +app.Use(csrf.New(csrf.Config{ + Storage: storage, +})) +``` diff --git a/docs/api/middleware/earlydata.md b/docs/api/middleware/earlydata.md new file mode 100644 index 0000000000..a870131302 --- /dev/null +++ b/docs/api/middleware/earlydata.md @@ -0,0 +1,115 @@ +--- +id: earlydata +--- + +# EarlyData + +The Early Data middleware for [Fiber](https://github.com/gofiber/fiber) adds support for TLS 1.3's early data ("0-RTT") feature. +Citing [RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446#section-2-3), when a client and server share a PSK, TLS 1.3 allows clients to send data on the first flight ("early data") to speed up the request, effectively reducing the regular 1-RTT request to a 0-RTT request. + +Make sure to enable fiber's `EnableTrustedProxyCheck` config option before using this middleware in order to not trust bogus HTTP request headers of the client. + +Also be aware that enabling support for early data in your reverse proxy (e.g. nginx, as done with a simple `ssl_early_data on;`) makes requests replayable. Refer to the following documents before continuing: + +- https://datatracker.ietf.org/doc/html/rfc8446#section-8 +- https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/ + +By default, this middleware allows early data requests on safe HTTP request methods only and rejects the request otherwise, i.e. aborts the request before executing your handler. This behavior can be controlled by the `AllowEarlyData` config option. +Safe HTTP methods — `GET`, `HEAD`, `OPTIONS` and `TRACE` — should not modify a state on the server. + +## 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/earlydata" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(earlydata.New()) + +// Or extend your config for customization +app.Use(earlydata.New(earlydata.Config{ + Error: fiber.ErrTooEarly, + // ... +})) +``` + +## Config + +<<<<<<< HEAD:middleware/earlydata/README.md +```go +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c fiber.Ctx) bool + + // IsEarlyData returns whether the request is an early-data request. + // + // Optional. Default: a function which checks if the "Early-Data" request header equals "1". + IsEarlyData func(c fiber.Ctx) bool + + // AllowEarlyData returns whether the early-data request should be allowed or rejected. + // + // Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods. + AllowEarlyData func(c fiber.Ctx) bool + + // Error is returned in case an early-data request is rejected. + // + // Optional. Default: fiber.ErrTooEarly. + Error error +} +``` + +### Default Config + +```go +var ConfigDefault = Config{ + IsEarlyData: func(c fiber.Ctx) bool { + return c.Get("Early-Data") == "1" +======= +| Property | Type | Description | Default | +|:---------------|:------------------------|:-------------------------------------------------------------------------------------|:-------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| IsEarlyData | `func(*fiber.Ctx) bool` | IsEarlyData returns whether the request is an early-data request. | Function checking if "Early-Data" header equals "1" | +| AllowEarlyData | `func(*fiber.Ctx) bool` | AllowEarlyData returns whether the early-data request should be allowed or rejected. | Function rejecting on unsafe and allowing safe methods | +| Error | `error` | Error is returned in case an early-data request is rejected. | `fiber.ErrTooEarly` | + +## Default Config + +```go +var ConfigDefault = Config{ + IsEarlyData: func(c *fiber.Ctx) bool { + return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue +>>>>>>> origin/master:docs/api/middleware/earlydata.md + }, + + AllowEarlyData: func(c fiber.Ctx) bool { + return fiber.IsMethodSafe(c.Method()) + }, + + Error: fiber.ErrTooEarly, +} +``` + +## Constants + +```go +const ( + DefaultHeaderName = "Early-Data" + DefaultHeaderTrueValue = "1" +) +``` diff --git a/middleware/encryptcookie/README.md b/docs/api/middleware/encryptcookie.md similarity index 53% rename from middleware/encryptcookie/README.md rename to docs/api/middleware/encryptcookie.md index 80bdfff8c2..ce3412dc33 100644 --- a/middleware/encryptcookie/README.md +++ b/docs/api/middleware/encryptcookie.md @@ -1,13 +1,10 @@ -# Encrypt Cookie Middleware +--- +id: encryptcookie +--- -Encrypt middleware for [Fiber](https://github.com/gofiber/fiber) which encrypts cookie values. Note: this middleware does not encrypt cookie names. - -## Table of Contents +# Encrypt Cookie -* [Signatures](encryptcookie.md#signatures) -* [Setup](encryptcookie.md#setup) -* [Config](encryptcookie.md#config) -* [Default Config](encryptcookie.md#default-config) +Encrypt middleware for [Fiber](https://github.com/gofiber/fiber) which encrypts cookie values. Note: this middleware does not encrypt cookie names. ## Signatures @@ -33,7 +30,10 @@ import ( After you initiate your Fiber app, you can use the following possibilities: ```go -// Default middleware config +// Provide a minimal config +// `Key` must be a 32 character string. It's used to encrypt the values, so make sure it is random and keep it secret. +// You can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey()` to create a random key for you. +// Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. app.Use(encryptcookie.New(encryptcookie.Config{ Key: "secret-thirty-2-character-string", })) @@ -55,6 +55,7 @@ app.Post("/", func(c fiber.Ctx) error { ## Config +<<<<<<< HEAD:middleware/encryptcookie/README.md ```go type Config struct { // Next defines a function to skip this middleware when returned true. @@ -69,8 +70,8 @@ type Config struct { // Base64 encoded unique key to encode & decode cookies. // - // Required. Key length should be 32 characters. - // You may use `encryptcookie.GenerateKey()` to generate a new key. + // Required. The key should be 32 bytes of random data in base64-encoded form. + // You may run `openssl rand -base64 32` or use `encryptcookie.GenerateKey()` to generate a new key. Key string // Custom function to encrypt cookies. @@ -84,16 +85,26 @@ type Config struct { Decryptor func(encryptedString, key string) (string, error) } ``` +======= +| Property | Type | Description | Default | +|:----------|:----------------------------------------------------|:----------------------------------------------------------------------------------------------------|:-----------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Except | `[]string` | Array of cookie keys that should not be encrypted. | `[]` | +| Key | `string` | Base64 encoded unique key to encode & decode cookies. Required. Key length should be 32 characters. | (No default, required field) | +| Encryptor | `func(decryptedString, key string) (string, error)` | Custom function to encrypt cookies. | `EncryptCookie` | +| Decryptor | `func(encryptedString, key string) (string, error)` | Custom function to decrypt cookies. | `DecryptCookie` | +>>>>>>> origin/master:docs/api/middleware/encryptcookie.md ## Default Config ```go -// `Key` must be a 32 character string. It's used to encrpyt the values, so make sure it is random and keep it secret. -// You can call `encryptcookie.GenerateKey()` to create a random key for you. -// Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. -app.Use(encryptcookie.New(encryptcookie.Config{ - Key: "secret-thirty-2-character-string", -})) +var ConfigDefault = Config{ + Next: nil, + Except: []string{"csrf_"}, + Key: "", + Encryptor: EncryptCookie, + Decryptor: DecryptCookie, +} ``` ## Usage of CSRF and Encryptcookie Middlewares with Custom Cookie Names @@ -104,10 +115,9 @@ app.Use(encryptcookie.New(encryptcookie.Config{ Key: "secret-thirty-2-character-string", Except: []string{"csrf_1"}, // exclude CSRF cookie })) - app.Use(csrf.New(csrf.Config{ KeyLookup: "form:test", CookieName: "csrf_1", CookieHTTPOnly: true, })) -``` \ No newline at end of file +``` diff --git a/docs/api/middleware/envvar.md b/docs/api/middleware/envvar.md new file mode 100644 index 0000000000..1d9f474297 --- /dev/null +++ b/docs/api/middleware/envvar.md @@ -0,0 +1,69 @@ +--- +id: envvar +--- + +# EnvVar + +EnvVar middleware for [Fiber](https://github.com/gofiber/fiber) that can be used to expose environment variables with various options. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/envvar" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use("/expose/envvars", envvar.New()) + +// Or extend your config for customization +app.Use("/expose/envvars", envvar.New( + envvar.Config{ + ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, + ExcludeVars: map[string]string{"excludeKey": ""}, + }), +) +``` + +:::note +You will need to provide a path to use the envvar middleware. +::: + +## Response + +Http response contract: +``` +{ + "vars": { + "someEnvVariable": "someValue", + "anotherEnvVariable": "anotherValue", + } +} + +``` + +## Config + +| Property | Type | Description | Default | +|:------------|:--------------------|:-----------------------------------------------------------------------------|:--------| +| ExportVars | `map[string]string` | ExportVars specifies the environment variables that should be exported. | `nil` | +| ExcludeVars | `map[string]string` | ExcludeVars specifies the environment variables that should not be exported. | `nil` | + +## Default Config + +```go +Config{} +``` diff --git a/docs/api/middleware/etag.md b/docs/api/middleware/etag.md new file mode 100644 index 0000000000..24be273021 --- /dev/null +++ b/docs/api/middleware/etag.md @@ -0,0 +1,62 @@ +--- +id: etag +--- + +# ETag + +ETag middleware for [Fiber](https://github.com/gofiber/fiber) that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/etag" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(etag.New()) + +// Get / receives Etag: "13-1831710635" in response header +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) + +// Or extend your config for customization +app.Use(etag.New(etag.Config{ + Weak: true, +})) + +// Get / receives Etag: "W/"13-1831710635" in response header +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:-------------------------------------------------------------------------------------------------------------------|:--------| +| Weak | `bool` | Weak indicates that a weak validator is used. Weak etags are easy to generate but are less useful for comparisons. | `false` | +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Weak: false, +} +``` diff --git a/docs/api/middleware/expvar.md b/docs/api/middleware/expvar.md new file mode 100644 index 0000000000..900850e364 --- /dev/null +++ b/docs/api/middleware/expvar.md @@ -0,0 +1,72 @@ +--- +id: expvar +--- + +# ExpVar + +Expvar middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime exposed variants in the JSON format. The package is typically only imported for the side effect of registering its HTTP handlers. The handled path is `/debug/vars`. + +## Signatures + +```go +func New() fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + expvarmw "github.com/gofiber/fiber/v2/middleware/expvar" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +var count = expvar.NewInt("count") + +app.Use(expvarmw.New()) +app.Get("/", func(c *fiber.Ctx) error { + count.Add(1) + + return c.SendString(fmt.Sprintf("hello expvar count %d", count.Value())) +}) +``` + +Visit path `/debug/vars` to see all vars and use query `r=key` to filter exposed variables. + +```bash +curl 127.0.0.1:3000 +hello expvar count 1 + +curl 127.0.0.1:3000/debug/vars +{ + "cmdline": ["xxx"], + "count": 1, + "expvarHandlerCalls": 33, + "expvarRegexpErrors": 0, + "memstats": {...} +} + +curl 127.0.0.1:3000/debug/vars?r=c +{ + "cmdline": ["xxx"], + "count": 1 +} +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:--------------------------------------------------------------------|:--------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, +} +``` diff --git a/docs/api/middleware/favicon.md b/docs/api/middleware/favicon.md new file mode 100644 index 0000000000..3fea8b129a --- /dev/null +++ b/docs/api/middleware/favicon.md @@ -0,0 +1,62 @@ +--- +id: favicon +--- + +# Favicon + +Favicon middleware for [Fiber](https://github.com/gofiber/fiber) that ignores favicon requests or caches a provided icon in memory to improve performance by skipping disk access. User agents request favicon.ico frequently and indiscriminately, so you may wish to exclude these requests from your logs by using this middleware before your logger middleware. + +:::note +This middleware is exclusively for serving the default, implicit favicon, which is GET /favicon.ico or [custom favicon URL](#config). +::: + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/favicon" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(favicon.New()) + +// Or extend your config for customization +app.Use(favicon.New(favicon.Config{ + File: "./favicon.ico", + URL: "/favicon.ico", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:-------------|:------------------------|:---------------------------------------------------------------------------------|:---------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| File | `string` | File holds the path to an actual favicon that will be cached. | "" | +| URL | `string` | URL for favicon handler. | "/favicon.ico" | +| FileSystem | `http.FileSystem` | FileSystem is an optional alternate filesystem to search for the favicon in. | `nil` | +| CacheControl | `string` | CacheControl defines how the Cache-Control header in the response should be set. | "public, max-age=31536000" | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + File: "", + URL: fPath, + CacheControl: "public, max-age=31536000", +} +``` diff --git a/docs/api/middleware/filesystem.md b/docs/api/middleware/filesystem.md new file mode 100644 index 0000000000..38e3622db7 --- /dev/null +++ b/docs/api/middleware/filesystem.md @@ -0,0 +1,255 @@ +--- +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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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/v2" + "github.com/gofiber/fiber/v2/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: "", +} +``` diff --git a/docs/api/middleware/helmet.md b/docs/api/middleware/helmet.md new file mode 100644 index 0000000000..0835f31166 --- /dev/null +++ b/docs/api/middleware/helmet.md @@ -0,0 +1,82 @@ +--- +id: helmet +--- + +# Helmet + +Helmet middleware helps secure your apps by setting various HTTP headers. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/helmet" +) + +func main() { + app := fiber.New() + + app.Use(helmet.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```curl +curl -I http://localhost:3000 +``` + +## Config + +| Property | Type | Description | Default | +|:--------------------------|:------------------------|:--------------------------------------------|:-----------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip middleware. | `nil` | +| XSSProtection | `string` | XSSProtection | "0" | +| ContentTypeNosniff | `string` | ContentTypeNosniff | "nosniff" | +| XFrameOptions | `string` | XFrameOptions | "SAMEORIGIN" | +| HSTSMaxAge | `int` | HSTSMaxAge | 0 | +| HSTSExcludeSubdomains | `bool` | HSTSExcludeSubdomains | false | +| ContentSecurityPolicy | `string` | ContentSecurityPolicy | "" | +| CSPReportOnly | `bool` | CSPReportOnly | false | +| HSTSPreloadEnabled | `bool` | HSTSPreloadEnabled | false | +| ReferrerPolicy | `string` | ReferrerPolicy | "ReferrerPolicy" | +| PermissionPolicy | `string` | Permissions-Policy | "" | +| CrossOriginEmbedderPolicy | `string` | Cross-Origin-Embedder-Policy | "require-corp" | +| CrossOriginOpenerPolicy | `string` | Cross-Origin-Opener-Policy | "same-origin" | +| CrossOriginResourcePolicy | `string` | Cross-Origin-Resource-Policy | "same-origin" | +| OriginAgentCluster | `string` | Origin-Agent-Cluster | "?1" | +| XDNSPrefetchControl | `string` | X-DNS-Prefetch-Control | "off" | +| XDownloadOptions | `string` | X-Download-Options | "noopen" | +| XPermittedCrossDomain | `string` | X-Permitted-Cross-Domain-Policies | "none" | + +## Default Config + +```go +var ConfigDefault = Config{ + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + ReferrerPolicy: "no-referrer", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", +} +``` diff --git a/docs/api/middleware/idempotency.md b/docs/api/middleware/idempotency.md new file mode 100644 index 0000000000..bab7c0e450 --- /dev/null +++ b/docs/api/middleware/idempotency.md @@ -0,0 +1,83 @@ +--- +id: idempotency +--- + +# Idempotency + +Idempotency middleware for [Fiber](https://github.com/gofiber/fiber) allows for fault-tolerant APIs where duplicate requests — for example due to networking issues on the client-side — do not erroneously cause the same action performed multiple times on the server-side. + +Refer to https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02 for a better understanding. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/idempotency" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +### Default Config + +```go +app.Use(idempotency.New()) +``` + +### Custom Config + +```go +app.Use(idempotency.New(idempotency.Config{ + Lifetime: 42 * time.Minute, + // ... +})) +``` + +### Config + +| Property | Type | Description | Default | +|:--------------------|:------------------------|:-----------------------------------------------------------------------------------------|:-------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | A function for safe methods | +| Lifetime | `time.Duration` | Lifetime is the maximum lifetime of an idempotency key. | 30 * time.Minute | +| KeyHeader | `string` | KeyHeader is the name of the header that contains the idempotency key. | "X-Idempotency-Key" | +| KeyHeaderValidate | `func(string) error` | KeyHeaderValidate defines a function to validate the syntax of the idempotency header. | A function for UUID validation | +| KeepResponseHeaders | `[]string` | KeepResponseHeaders is a list of headers that should be kept from the original response. | nil (keep all headers) | +| Lock | `Locker` | Lock locks an idempotency key. | An in-memory locker | +| Storage | `fiber.Storage` | Storage stores response data by idempotency key. | An in-memory storage | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: func(c *fiber.Ctx) bool { + // Skip middleware if the request was done using a safe HTTP method + return fiber.IsMethodSafe(c.Method()) + }, + + Lifetime: 30 * time.Minute, + + KeyHeader: "X-Idempotency-Key", + KeyHeaderValidate: func(k string) error { + if l, wl := len(k), 36; l != wl { // UUID length is 36 chars + return fmt.Errorf("%w: invalid length: %d != %d", ErrInvalidIdempotencyKey, l, wl) + } + + return nil + }, + + KeepResponseHeaders: nil, + + Lock: nil, // Set in configDefault so we don't allocate data here. + + Storage: nil, // Set in configDefault so we don't allocate data here. +} +``` diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md new file mode 100644 index 0000000000..ecabe122e7 --- /dev/null +++ b/docs/api/middleware/keyauth.md @@ -0,0 +1,243 @@ +--- +id: keyauth +--- + +# Keyauth + +Key auth middleware provides a key based authentication. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" +) + +var ( + apiKey = "correct horse battery staple" +) + +func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey +} + +func main() { + app := fiber.New() + + // note that the keyauth middleware needs to be defined before the routes are defined! + app.Use(keyauth.New(keyauth.Config{ + KeyLookup: "cookie:access_token", + Validator: validateAPIKey, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```bash +# No api-key specified -> 400 missing +curl http://localhost:3000 +#> missing or malformed API Key + +curl --cookie "access_token=correct horse battery staple" http://localhost:3000 +#> Successfully authenticated! + +curl --cookie "access_token=Clearly A Wrong Key" http://localhost:3000 +#> missing or malformed API Key +``` + +For a more detailed example, see also the [`github.com/gofiber/recipes`](https://github.com/gofiber/recipes) repository and specifically the `fiber-envoy-extauthz` repository and the [`keyauth example`](https://github.com/gofiber/recipes/blob/master/fiber-envoy-extauthz/authz/main.go) code. + + +### Authenticate only certain endpoints + +If you want to authenticate only certain endpoints, you can use the `Config` of keyauth and apply a filter function (eg. `authFilter`) like so + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" + "regexp" + "strings" +) + +var ( + apiKey = "correct horse battery staple" + protectedURLs = []*regexp.Regexp{ + regexp.MustCompile("^/authenticated$"), + regexp.MustCompile("^/auth2$"), + } +) + +func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey +} + +func authFilter(c *fiber.Ctx) bool { + originalURL := strings.ToLower(c.OriginalURL()) + + for _, pattern := range protectedURLs { + if pattern.MatchString(originalURL) { + return false + } + } + return true +} + +func main() { + app := fiber.New() + + app.Use(keyauth.New(keyauth.Config{ + Next: authFilter, + KeyLookup: "cookie:access_token", + Validator: validateAPIKey, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome") + }) + app.Get("/authenticated", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + app.Get("/auth2", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated 2!") + }) + + app.Listen(":3000") +} +``` + +Which results in this + +```bash +# / does not need to be authenticated +curl http://localhost:3000 +#> Welcome + +# /authenticated needs to be authenticated +curl --cookie "access_token=correct horse battery staple" http://localhost:3000/authenticated +#> Successfully authenticated! + +# /auth2 needs to be authenticated too +curl --cookie "access_token=correct horse battery staple" http://localhost:3000/auth2 +#> Successfully authenticated 2! +``` + +### Specifying middleware in the handler + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" +) + +const ( + apiKey = "my-super-secret-key" +) + +func main() { + app := fiber.New() + + authMiddleware := keyauth.New(keyauth.Config{ + Validator: func(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey + }, + }) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome") + }) + + app.Get("/allowed", authMiddleware, func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + + app.Listen(":3000") +} +``` + +Which results in this + +```bash +# / does not need to be authenticated +curl http://localhost:3000 +#> Welcome + +# /allowed needs to be authenticated too +curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000/allowed +#> Successfully authenticated! +``` + +## Config + +| Property | Type | Description | Default | +|:---------------|:-----------------------------------------|:-----------------------------------------------------------------------------------------------------|:------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `nil` | +| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. | `401 Invalid or expired key` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | +| AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | +| Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | +| ContextKey | `string` | Context key to store the bearer token from the token into context. | "token" | + +## Default Config + +```go +var ConfigDefault = Config{ + SuccessHandler: func(c *fiber.Ctx) error { + return c.Next() + }, + ErrorHandler: func(c *fiber.Ctx, err error) error { + if err == ErrMissingOrMalformedAPIKey { + return c.Status(fiber.StatusUnauthorized).SendString(err.Error()) + } + return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") + }, + KeyLookup: "header:" + fiber.HeaderAuthorization, + AuthScheme: "Bearer", + ContextKey: "token", +} +``` diff --git a/docs/api/middleware/limiter.md b/docs/api/middleware/limiter.md new file mode 100644 index 0000000000..8a48cbd14b --- /dev/null +++ b/docs/api/middleware/limiter.md @@ -0,0 +1,125 @@ +--- +id: limiter +--- + +# Limiter + +Limiter middleware for [Fiber](https://github.com/gofiber/fiber) that is used to limit repeat requests to public APIs and/or endpoints such as password reset. It is also useful for API clients, web crawling, or other tasks that need to be throttled. + +:::note +This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +::: + +:::note +This module does not share state with other processes/servers by default. +::: + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(limiter.New()) + +// Or extend your config for customization +app.Use(limiter.New(limiter.Config{ + Next: func(c *fiber.Ctx) bool { + return c.IP() == "127.0.0.1" + }, + Max: 20, + Expiration: 30 * time.Second, + KeyGenerator: func(c *fiber.Ctx) string { + return c.Get("x-forwarded-for") + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendFile("./toofast.html") + }, + Storage: myCustomStorage{}, +})) +``` + +## Sliding window + +Instead of using the standard fixed window algorithm, you can enable the [sliding window](https://en.wikipedia.org/wiki/Sliding_window_protocol) algorithm. + +A example of such configuration is: + +```go +app.Use(limiter.New(limiter.Config{ + Max: 20, + Expiration: 30 * time.Second, + LimiterMiddleware: limiter.SlidingWindow{}, +})) +``` + +This means that every window will take into account the previous window(if there was any). The given formula for the rate is: +``` +weightOfPreviousWindpw = previous window's amount request * (whenNewWindow / Expiration) +rate = weightOfPreviousWindpw + current window's amount request. +``` + +## Config + +| Property | Type | Description | Default | +|:-----------------------|:--------------------------|:--------------------------------------------------------------------------------------------|:-----------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Max | `int` | Max number of recent connections during `Expiration` seconds before sending a 429 response. | 5 | +| KeyGenerator | `func(*fiber.Ctx) string` | KeyGenerator allows you to generate custom keys, by default c.IP() is used. | A function using c.IP() as the default | +| Expiration | `time.Duration` | Expiration is the time on how long to keep records of requests in memory. | 1 * time.Minute | +| LimitReached | `fiber.Handler` | LimitReached is called when a request hits the limit. | A function sending 429 response | +| SkipFailedRequests | `bool` | When set to true, requests with StatusCode >= 400 won't be counted. | false | +| SkipSuccessfulRequests | `bool` | When set to true, requests with StatusCode < 400 won't be counted. | false | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | An in-memory store for this process only | +| LimiterMiddleware | `LimiterHandler` | LimiterMiddleware is the struct that implements a limiter middleware. | A new Fixed Window Rate Limiter | +| Duration (Deprecated) | `time.Duration` | Deprecated: Use Expiration instead | - | +| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead | - | +| Key (Deprecated) | `func(*fiber.Ctx) string` | Deprecated: Use KeyGenerator instead | - | + +:::note +A custom store can be used if it implements the `Storage` interface - more details and an example can be found in `store.go`. +::: + +## Default Config + +```go +var ConfigDefault = Config{ + Max: 5, + Expiration: 1 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, +} +``` + +### Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +app.Use(limiter.New(limiter.Config{ + Storage: storage, +})) +``` diff --git a/docs/api/middleware/logger.md b/docs/api/middleware/logger.md new file mode 100644 index 0000000000..a01e9bd54c --- /dev/null +++ b/docs/api/middleware/logger.md @@ -0,0 +1,169 @@ +--- +id: logger +--- + +# Logger + +Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP request/response details. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) +``` + +:::tip +The order of registration plays a role. Only all routes that are registered after this one will be logged. +The middleware should therefore be one of the first to be registered. +::: + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(logger.New()) + +// Or extend your config for customization +// Logging remote IP and Port +app.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", +})) + +// Logging Request ID +app.Use(requestid.New()) +app.Use(logger.New(logger.Config{ + // For more options, see the Config section + Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}​\n", +})) + +// Changing TimeZone & TimeFormat +app.Use(logger.New(logger.Config{ + Format: "${pid} ${status} - ${method} ${path}\n", + TimeFormat: "02-Jan-2006", + TimeZone: "America/New_York", +})) + +// Custom File Writer +file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +if err != nil { + log.Fatalf("error opening file: %v", err) +} +defer file.Close() +app.Use(logger.New(logger.Config{ + Output: file, +})) + +// Add Custom Tags +app.Use(logger.New(logger.Config{ + CustomTags: map[string]logger.LogFunc{ + "custom_tag": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) { + return output.WriteString("it is a custom tag") + }, + }, +})) + +// Callback after log is written +app.Use(logger.New(logger.Config{ + TimeFormat: time.RFC3339Nano, + TimeZone: "Asia/Shanghai", + Done: func(c *fiber.Ctx, logString []byte) { + if c.Response().StatusCode() != fiber.StatusOK { + reporter.SendToSlack(logString) + } + }, +})) + +// Disable colors when outputting to default format +app.Use(logger.New(logger.Config{ + DisableColors: true, +})) +``` + +## Config + +### Config + +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | +| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | +| Format | `string` | Format defines the logging tags. | `[${time}] ${status} - ${latency} ${method} ${path}\n` | +| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | +| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | +| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | +| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | +| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | + +## Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Done: nil, + Format: "[${time}] ${status} - ${latency} ${method} ${path}\n", + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Output: os.Stdout, + DisableColors: false, +} +``` + +## Constants +```go +// Logger variables +const ( + TagPid = "pid" + TagTime = "time" + TagReferer = "referer" + TagProtocol = "protocol" + TagPort = "port" + TagIP = "ip" + TagIPs = "ips" + TagHost = "host" + TagMethod = "method" + TagPath = "path" + TagURL = "url" + TagUA = "ua" + TagLatency = "latency" + TagStatus = "status" // response status + TagResBody = "resBody" // response body + TagReqHeaders = "reqHeaders" + TagQueryStringParams = "queryParams" // request query parameters + TagBody = "body" // request body + TagBytesSent = "bytesSent" + TagBytesReceived = "bytesReceived" + TagRoute = "route" + TagError = "error" + // DEPRECATED: Use TagReqHeader instead + TagHeader = "header:" // request header + TagReqHeader = "reqHeader:" // request header + TagRespHeader = "respHeader:" // response header + TagQuery = "query:" // request query + TagForm = "form:" // request form + TagCookie = "cookie:" // request cookie + TagLocals = "locals:" + // colors + TagBlack = "black" + TagRed = "red" + TagGreen = "green" + TagYellow = "yellow" + TagBlue = "blue" + TagMagenta = "magenta" + TagCyan = "cyan" + TagWhite = "white" + TagReset = "reset" +) +``` diff --git a/docs/api/middleware/monitor.md b/docs/api/middleware/monitor.md new file mode 100644 index 0000000000..cbac367ce4 --- /dev/null +++ b/docs/api/middleware/monitor.md @@ -0,0 +1,81 @@ +--- +id: monitor +--- + +# Monitor + +Monitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor) + +:::caution + +Monitor is still in beta, API might change in the future! + +::: + +![](https://i.imgur.com/nHAtBpJ.gif) + +### Signatures +```go +func New() fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/monitor" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Initialize default config (Assign the middleware to /metrics) +app.Get("/metrics", monitor.New()) + +// Or extend your config for customization +// Assign the middleware to /metrics +// and change the Title to `MyService Metrics Page` +app.Get("/metrics", monitor.New(monitor.Config{Title: "MyService Metrics Page"})) +``` +You can also access the API endpoint with +`curl -X GET -H "Accept: application/json" http://localhost:3000/metrics` which returns: +```json +{"pid":{ "cpu":0.4568381746582226, "ram":20516864, "conns":3 }, + "os": { "cpu":8.759124087593099, "ram":3997155328, "conns":44, + "total_ram":8245489664, "load_avg":0.51 }} +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:--------------------------------------------------------------------|:----------------------------------------------------------------------------| +| Title | `string` | Metrics page title | "Fiber Monitor" | +| Refresh | `time.Duration` | Refresh period | 3 seconds | +| APIOnly | `bool` | Whether the service should expose only the monitoring API | false | +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| CustomHead | `string` | Custom HTML Code to Head Section(Before End) | empty | +| FontURL | `string` | FontURL for specify font resource path or URL | "https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap" | +| ChartJsURL | `string` | ChartJsURL for specify ChartJS library path or URL | "https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js" | + +## Default Config + +```go +var ConfigDefault = Config{ + Title: defaultTitle, + Refresh: defaultRefresh, + FontURL: defaultFontURL, + ChartJsURL: defaultChartJSURL, + CustomHead: defaultCustomHead, + APIOnly: false, + Next: nil, + index: newIndex(viewBag{ + defaultTitle, + defaultRefresh, + defaultFontURL, + defaultChartJSURL, + defaultCustomHead, + }), +} +``` diff --git a/docs/api/middleware/pprof.md b/docs/api/middleware/pprof.md new file mode 100644 index 0000000000..c4808f2c1d --- /dev/null +++ b/docs/api/middleware/pprof.md @@ -0,0 +1,53 @@ +--- +id: pprof +--- + +# Pprof + +Pprof middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/. + +## Signatures + +```go +func New() fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/pprof" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(pprof.New()) + +// Or extend your config for customization + +// For example, in systems where you have multiple ingress endpoints, it is common to add a URL prefix, like so: +app.Use(pprof.New(pprof.Config{Prefix: "/endpoint-prefix"})) + +// This prefix will be added to the default path of "/debug/pprof/", for a resulting URL of: "/endpoint-prefix/debug/pprof/". +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Prefix | `string` | Prefix defines a URL prefix added before "/debug/pprof". Note that it should start with (but not end with) a slash. Example: "/federated-fiber" | "" | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, +} +``` diff --git a/docs/api/middleware/proxy.md b/docs/api/middleware/proxy.md new file mode 100644 index 0000000000..e36654fe48 --- /dev/null +++ b/docs/api/middleware/proxy.md @@ -0,0 +1,165 @@ +--- +id: proxy +--- + +# Proxy + +Proxy middleware for [Fiber](https://github.com/gofiber/fiber) that allows you to proxy requests to multiple servers. + +## Signatures + +```go +// Balancer create a load balancer among multiple upstrem servers. +func Balancer(config Config) fiber.Handler +// Forward performs the given http request and fills the given http response. +func Forward(addr string, clients ...*fasthttp.Client) fiber.Handler +// Do performs the given http request and fills the given http response. +func Do(c *fiber.Ctx, addr string, clients ...*fasthttp.Client) error +// DoRedirects performs the given http request and fills the given http response while following up to maxRedirectsCount redirects. +func DoRedirects(c *fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error +// DoDeadline performs the given request and waits for response until the given deadline. +func DoDeadline(c *fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error +// DoTimeout performs the given request and waits for response during the given timeout duration. +func DoTimeout(c *fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error +// DomainForward the given http request based on the given domain and fills the given http response +func DomainForward(hostname string, addr string, clients ...*fasthttp.Client) fiber.Handler +// BalancerForward performs the given http request based round robin balancer and fills the given http response +func BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/proxy" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// if target https site uses a self-signed certificate, you should +// call WithTlsConfig before Do and Forward +proxy.WithTlsConfig(&tls.Config{ + InsecureSkipVerify: true, +}) +// if you need to use global self-custom client, you should use proxy.WithClient. +proxy.WithClient(&fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, +}) + +// Forward to url +app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif")) + +// If you want to forward with a specific domain. You have to use proxy.DomainForward. +app.Get("/payments", proxy.DomainForward("docs.gofiber.io", "http://localhost:8000")) + +// Forward to url with local custom client +app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif", &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, +})) + +// Make request within handler +app.Get("/:id", func(c *fiber.Ctx) error { + url := "https://i.imgur.com/"+c.Params("id")+".gif" + if err := proxy.Do(c, url); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests while following redirects +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoRedirects(c, "http://google.com", 3); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests and wait up to 5 seconds before timing out +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoTimeout(c, "http://localhost:3000", time.Second * 5); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests, timeout a minute from now +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoDeadline(c, "http://localhost", time.Now().Add(time.Minute)); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Minimal round robin balancer +app.Use(proxy.Balancer(proxy.Config{ + Servers: []string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + }, +})) + +// Or extend your balancer for customization +app.Use(proxy.Balancer(proxy.Config{ + Servers: []string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + }, + ModifyRequest: func(c *fiber.Ctx) error { + c.Request().Header.Add("X-Real-IP", c.IP()) + return nil + }, + ModifyResponse: func(c *fiber.Ctx) error { + c.Response().Header.Del(fiber.HeaderServer) + return nil + }, +})) + +// Or this way if the balancer is using https and the destination server is only using http. +app.Use(proxy.BalancerForward([]string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:----------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Servers | `[]string` | Servers defines a list of `://` HTTP servers, which are used in a round-robin manner. i.e.: "https://foobar.com, http://www.foobar.com" | (Required) | +| ModifyRequest | `fiber.Handler` | ModifyRequest allows you to alter the request. | `nil` | +| ModifyResponse | `fiber.Handler` | ModifyResponse allows you to alter the response. | `nil` | +| Timeout | `time.Duration` | Timeout is the request timeout used when calling the proxy client. | 1 second | +| ReadBufferSize | `int` | Per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers (for example, BIG cookies). | (Not specified) | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | (Not specified) | +| TlsConfig | `*tls.Config` (or `*fasthttp.TLSConfig` in v3) | TLS config for the HTTP client. | `nil` | +| Client | `*fasthttp.LBClient` | Client is a custom client when client config is complex. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + ModifyRequest: nil, + ModifyResponse: nil, + Timeout: fasthttp.DefaultLBClientTimeout, +} +``` diff --git a/docs/api/middleware/recover.md b/docs/api/middleware/recover.md new file mode 100644 index 0000000000..81f67fddbc --- /dev/null +++ b/docs/api/middleware/recover.md @@ -0,0 +1,54 @@ +--- +id: recover +--- + +# Recover + +Recover middleware for [Fiber](https://github.com/gofiber/fiber) that recovers from panics anywhere in the stack chain and handles the control to the centralized [ErrorHandler](https://docs.gofiber.io/guide/error-handling). + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(recover.New()) + +// This panic will be caught by the middleware +app.Get("/", func(c *fiber.Ctx) error { + panic("I'm an error") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:------------------|:--------------------------------|:--------------------------------------------------------------------|:-------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| EnableStackTrace | `bool` | EnableStackTrace enables handling stack trace. | `false` | +| StackTraceHandler | `func(*fiber.Ctx, interface{})` | StackTraceHandler defines a function to handle stack trace. | defaultStackTraceHandler | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + EnableStackTrace: false, + StackTraceHandler: defaultStackTraceHandler, +} +``` diff --git a/docs/api/middleware/redirect.md b/docs/api/middleware/redirect.md new file mode 100644 index 0000000000..762aa0b5d3 --- /dev/null +++ b/docs/api/middleware/redirect.md @@ -0,0 +1,68 @@ +--- +id: redirect +--- + +# Redirect + +Redirection middleware for Fiber. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/redirect" +) + +func main() { + app := fiber.New() + + app.Use(redirect.New(redirect.Config{ + Rules: map[string]string{ + "/old": "/new", + "/old/*": "/new/$1", + }, + StatusCode: 301, + })) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Get("/new/*", func(c *fiber.Ctx) error { + return c.SendString("Wildcard: " + c.Params("*")) + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```curl +curl http://localhost:3000/old +curl http://localhost:3000/old/hello +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:---------------------------------------------------------------------------------------------------------------------------|:-----------------------| +| Next | `func(*fiber.Ctx) bool` | Filter defines a function to skip middleware. | `nil` | +| Rules | `map[string]string` | Rules defines the URL path rewrite rules. The values captured in asterisk can be retrieved by index e.g. $1, $2 and so on. | Required | +| StatusCode | `int` | The status code when redirecting. This is ignored if Redirect is disabled. | 302 Temporary Redirect | + +## Default Config + +```go +var ConfigDefault = Config{ + StatusCode: fiber.StatusFound, +} +``` diff --git a/docs/api/middleware/requestid.md b/docs/api/middleware/requestid.md new file mode 100644 index 0000000000..200ebf4bdd --- /dev/null +++ b/docs/api/middleware/requestid.md @@ -0,0 +1,62 @@ +--- +id: requestid +--- + +# RequestID + +RequestID middleware for [Fiber](https://github.com/gofiber/fiber) that adds an indentifier to the response. + +## 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/v2" + "github.com/gofiber/fiber/v2/middleware/requestid" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(requestid.New()) + +// Or extend your config for customization +app.Use(requestid.New(requestid.Config{ + Header: "X-Custom-Header", + Generator: func() string { + return "static-id" + }, +})) +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:--------------------------------------------------------------------------------------------------|:---------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Header | `string` | Header is the header key where to get/set the unique request ID. | "X-Request-ID" | +| Generator | `func() string` | Generator defines a function to generate the unique identifier. | utils.UUID | +| ContextKey | `interface{}` | ContextKey defines the key used when storing the request ID in the locals for a specific request. | "requestid" | + +## Default Config +The default config uses a fast UUID generator which will expose the number of +requests made to the server. To conceal this value for better privacy, use the +`utils.UUIDv4` generator. + +```go +var ConfigDefault = Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: utils.UUID, + ContextKey: "requestid", +} +``` diff --git a/docs/api/middleware/rewrite.md b/docs/api/middleware/rewrite.md new file mode 100644 index 0000000000..fd59595841 --- /dev/null +++ b/docs/api/middleware/rewrite.md @@ -0,0 +1,58 @@ +--- +id: rewrite +--- + +# Rewrite + +Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:-----------------------------------------------------------------------------------------------------|:-----------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip middleware. | `nil` | +| Rules | `map[string]string` | Rules defines the URL path rewrite rules. The values captured in asterisk can be retrieved by index. | (Required) | + +### Examples +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/rewrite" +) + +func main() { + app := fiber.New() + + app.Use(rewrite.New(rewrite.Config{ + Rules: map[string]string{ + "/old": "/new", + "/old/*": "/new/$1", + }, + })) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Get("/new/*", func(c *fiber.Ctx) error { + return c.SendString("Wildcard: " + c.Params("*")) + }) + + app.Listen(":3000") +} + +``` + +**Test:** + +```curl +curl http://localhost:3000/old +curl http://localhost:3000/old/hello +``` diff --git a/docs/api/middleware/session.md b/docs/api/middleware/session.md new file mode 100644 index 0000000000..65d23681e7 --- /dev/null +++ b/docs/api/middleware/session.md @@ -0,0 +1,137 @@ +--- +id: session +--- + +# Session + +Session middleware for [Fiber](https://github.com/gofiber/fiber). + +:::note +This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +::: + +## Signatures + +```go +func New(config ...Config) *Store +func (s *Store) RegisterType(i interface{}) +func (s *Store) Get(c *fiber.Ctx) (*Session, error) +func (s *Store) Reset() error + +func (s *Session) Get(key string) interface{} +func (s *Session) Set(key string, val interface{}) +func (s *Session) Delete(key string) +func (s *Session) Destroy() error +func (s *Session) Regenerate() error +func (s *Session) Save() error +func (s *Session) Fresh() bool +func (s *Session) ID() string +func (s *Session) Keys() []string +``` + +:::caution +Storing `interface{}` values are limited to built-ins Go types. +::: + +## Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +// This stores all of your app's sessions +store := session.New() + +app.Get("/", func(c *fiber.Ctx) error { + // Get session from storage + sess, err := store.Get(c) + if err != nil { + panic(err) + } + + // Get value + name := sess.Get("name") + + // Set key/value + sess.Set("name", "john") + + // Get all Keys + keys := sess.Keys() + + // Delete key + sess.Delete("name") + + // Destroy session + if err := sess.Destroy(); err != nil { + panic(err) + } + + // Sets a specific expiration for this session + sess.SetExpiry(time.Second * 2) + + // Save session + if err := sess.Save(); err != nil { + panic(err) + } + + return c.SendString(fmt.Sprintf("Welcome %v", name)) +}) +``` + +## Config + +| Property | Type | Description | Default | +|:------------------------|:----------------|:------------------------------------------------------------------------------------------------------------|:----------------------| +| Expiration | `time.Duration` | Allowed session duration. | `24 * time.Hour` | +| Storage | `fiber.Storage` | Storage interface to store the session data. | `memory.New()` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract session id from the request. | `"cookie:session_id"` | +| CookieDomain | `string` | Domain of the cookie. | `""` | +| CookiePath | `string` | Path of the cookie. | `""` | +| CookieSecure | `bool` | Indicates if cookie is secure. | `false` | +| CookieHTTPOnly | `bool` | Indicates if cookie is HTTP only. | `false` | +| CookieSameSite | `string` | Value of SameSite cookie. | `"Lax"` | +| CookieSessionOnly | `bool` | Decides whether cookie should last for only the browser session. Ignores Expiration if set to true. | `false` | +| KeyGenerator | `func() string` | KeyGenerator generates the session key. | `utils.UUIDv4` | +| CookieName (Deprecated) | `string` | Deprecated: Please use KeyLookup. The session name. | `""` | + +## Default Config + +```go +var ConfigDefault = Config{ + Expiration: 24 * time.Hour, + KeyLookup: "cookie:session_id", + KeyGenerator: utils.UUIDv4, + source: "cookie", + sessionName: "session_id", +} +``` + +## Constants + +```go +const ( + SourceCookie Source = "cookie" + SourceHeader Source = "header" + SourceURLQuery Source = "query" +) +``` + +### Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +store := session.New(session.Config{ + Storage: storage, +}) +``` + +To use the store, see the [Examples](#examples). diff --git a/docs/api/middleware/skip.md b/docs/api/middleware/skip.md new file mode 100644 index 0000000000..0923bd0ee2 --- /dev/null +++ b/docs/api/middleware/skip.md @@ -0,0 +1,47 @@ +--- +id: skip +--- + +# Skip + +Skip middleware for [Fiber](https://github.com/gofiber/fiber) that skips a wrapped handler if a predicate is true. + +## Signatures +```go +func New(handler fiber.Handler, exclude func(c *fiber.Ctx) bool) fiber.Handler +``` + +## Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/skip" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +func main() { + app := fiber.New() + + app.Use(skip.New(BasicHandler, func(ctx *fiber.Ctx) bool { + return ctx.Method() == fiber.MethodGet + })) + + app.Get("/", func(ctx *fiber.Ctx) error { + return ctx.SendString("It was a GET request!") + }) + + log.Fatal(app.Listen(":3000")) +} + +func BasicHandler(ctx *fiber.Ctx) error { + return ctx.SendString("It was not a GET request!") +} +``` + +:::tip +app.Use will handle requests from any route, and any method. In the example above, it will only skip if the method is GET. +::: diff --git a/middleware/timeout/README.md b/docs/api/middleware/timeout.md similarity index 51% rename from middleware/timeout/README.md rename to docs/api/middleware/timeout.md index 14de7f956b..ece308464e 100644 --- a/middleware/timeout/README.md +++ b/docs/api/middleware/timeout.md @@ -1,22 +1,39 @@ +--- +id: timeout +--- + # Timeout -Timeout middleware for Fiber. As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. +There exist two distinct implementations of timeout middleware [Fiber](https://github.com/gofiber/fiber). + +**New** + +Wraps a `fiber.Handler` with a timeout. If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). + +:::caution +This has been deprecated since it raises race conditions. +::: + +**NewWithContext** + +As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. + If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. -It has no race conditions, ready to use on production. -### Table of Contents -- [Signatures](#signatures) -- [Examples](#examples) +It does not cancel long running executions. Underlying executions must handle timeout by using `context.Context` parameter. +## Signatures -### Signatures ```go func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler +func NewWithContext(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler ``` -### Examples +## Examples + Import the middleware package that is part of the Fiber web framework + ```go import ( "github.com/gofiber/fiber/v3" @@ -24,11 +41,17 @@ import ( ) ``` -Sample timeout middleware usage +After you initiate your Fiber app, you can use the following possibilities: + ```go func main() { app := fiber.New() +<<<<<<< HEAD:middleware/timeout/README.md h := func(c fiber.Ctx) error { +======= + + h := func(c *fiber.Ctx) error { +>>>>>>> origin/master:docs/api/middleware/timeout.md sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { return fmt.Errorf("%w: execution error", err) @@ -37,11 +60,12 @@ func main() { } app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second)) - _ = app.Listen(":3000") + log.Fatal(app.Listen(":3000")) } func sleepWithContext(ctx context.Context, d time.Duration) error { timer := time.NewTimer(d) + select { case <-ctx.Done(): if !timer.Stop() { @@ -55,17 +79,19 @@ func sleepWithContext(ctx context.Context, d time.Duration) error { ``` Test http 200 with curl: + ```bash curl --location -I --request GET 'http://localhost:3000/foo/1000' ``` Test http 408 with curl: + ```bash curl --location -I --request GET 'http://localhost:3000/foo/3000' ``` +Use with custom error: -When using with custom error: ```go var ErrFooTimeOut = errors.New("foo context canceled") @@ -79,8 +105,8 @@ func main() { return nil } - app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second, ErrFooTimeOut)) - _ = app.Listen(":3000") + app.Get("/foo/:sleepTime", timeout.NewWithContext(h, 2*time.Second, ErrFooTimeOut)) + log.Fatal(app.Listen(":3000")) } func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error { @@ -96,3 +122,29 @@ func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error return nil } ``` + +Sample usage with a DB call: + +```go +func main() { + app := fiber.New() + db, _ := gorm.Open(postgres.Open("postgres://localhost/foodb"), &gorm.Config{}) + + handler := func(ctx *fiber.Ctx) error { + tran := db.WithContext(ctx.UserContext()).Begin() + + if tran = tran.Exec("SELECT pg_sleep(50)"); tran.Error != nil { + return tran.Error + } + + if tran = tran.Commit(); tran.Error != nil { + return tran.Error + } + + return nil + } + + app.Get("/foo", timeout.NewWithContext(handler, 10*time.Second)) + log.Fatal(app.Listen(":3000")) +} +``` diff --git a/docs/extra/_category_.json b/docs/extra/_category_.json new file mode 100644 index 0000000000..f17f137ab3 --- /dev/null +++ b/docs/extra/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Extra", + "position": 4, + "link": { + "type": "generated-index", + "description": "Extra contents for Fiber." + } +} \ No newline at end of file diff --git a/docs/extra/benchmarks.md b/docs/extra/benchmarks.md new file mode 100644 index 0000000000..3c2a82037d --- /dev/null +++ b/docs/extra/benchmarks.md @@ -0,0 +1,112 @@ +--- +id: benchmarks +title: 📊 Benchmarks +description: >- + These benchmarks aim to compare the performance of Fiber and other web + frameworks. +sidebar_position: 2 +--- + +## TechEmpower + +[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite) provides a performance comparison of many web application frameworks executing fundamental tasks such as JSON serialization, database access, and server-side template composition. + +Each framework is operating in a realistic production configuration. Results are captured on cloud instances and on physical hardware. The test implementations are largely community-contributed and all source is available at the [GitHub repository](https://github.com/TechEmpower/FrameworkBenchmarks). + +* Fiber `v1.10.0` +* 28 HT Cores Intel\(R\) Xeon\(R\) Gold 5120 CPU @ 2.20GHz +* 32GB RAM +* Ubuntu 18.04.3 4.15.0-88-generic +* Dedicated Cisco 10-Gbit Ethernet switch. + +### Plaintext + +The Plaintext test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of high-performance platforms in particular. Requests will be sent using HTTP pipelining. The response payload is still small, meaning good performance is still necessary in order to saturate the gigabit Ethernet of the test environment. + +See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#single-database-query) + +**Fiber** - **6,162,556** responses per second with an average latency of **2.0** ms. +**Express** - **367,069** responses per second with an average latency of **354.1** ms. + +![](/img/plaintext.png) + +![Fiber vs Express](/img/plaintext_express.png) + +### Data Updates + +**Fiber** handled **11,846** responses per second with an average latency of **42.8** ms. +**Express** handled **2,066** responses per second with an average latency of **390.44** ms. + +![](/img/data_updates.png) + +![Fiber vs Express](/img/data_updates_express.png) + +### Multiple Queries + +**Fiber** handled **19,664** responses per second with an average latency of **25.7** ms. +**Express** handled **4,302** responses per second with an average latency of **117.2** ms. + +![](/img/multiple_queries.png) + +![Fiber vs Express](/img/multiple_queries_express.png) + +### Single Query + +**Fiber** handled **368,647** responses per second with an average latency of **0.7** ms. +**Express** handled **57,880** responses per second with an average latency of **4.4** ms. + +![](/img/single_query.png) + +![Fiber vs Express](/img/single_query_express.png) + +### JSON Serialization + +**Fiber** handled **1,146,667** responses per second with an average latency of **0.4** ms. +**Express** handled **244,847** responses per second with an average latency of **1.1** ms. + +![](/img/json.png) + +![Fiber vs Express](/img/json_express.png) + +## Go web framework benchmark + +🔗 [https://github.com/smallnest/go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark) + +* **CPU** Intel\(R\) Xeon\(R\) Gold 6140 CPU @ 2.30GHz +* **MEM** 4GB +* **GO** go1.13.6 linux/amd64 +* **OS** Linux + +The first test case is to mock **0 ms**, **10 ms**, **100 ms**, **500 ms** processing time in handlers. + +![](/img/benchmark.png) + +The concurrency clients are **5000**. + +![](/img/benchmark_latency.png) + +Latency is the time of real processing time by web servers. _The smaller is the better._ + +![](/img/benchmark_alloc.png) + +Allocs is the heap allocations by web servers when test is running. The unit is MB. _The smaller is the better._ + +If we enable **http pipelining**, test result as below: + +![](/img/benchmark-pipeline.png) + +Concurrency test in **30 ms** processing time, the test result for **100**, **1000**, **5000** clients is: + +![](/img/concurrency.png) + +![](/img/concurrency_latency.png) + +![](/img/concurrency_alloc.png) + +If we enable **http pipelining**, test result as below: + +![](/img/concurrency-pipeline.png) + +Dependency graph for `v1.9.0` + +![](/img/graph.svg) diff --git a/docs/extra/faq.md b/docs/extra/faq.md new file mode 100644 index 0000000000..54dbca5d15 --- /dev/null +++ b/docs/extra/faq.md @@ -0,0 +1,169 @@ +--- +id: faq +title: 🤔 FAQ +description: >- + List of frequently asked questions. Feel free to open an issue to add your + question to this page. +sidebar_position: 1 +--- + +## How should I structure my application? + +There is no definitive answer to this question. The answer depends on the scale of your application and the team that is involved. To be as flexible as possible, Fiber makes no assumptions in terms of structure. + +Routes and other application-specific logic can live in as many files as you wish, in any directory structure you prefer. View the following examples for inspiration: + +* [gofiber/boilerplate](https://github.com/gofiber/boilerplate) +* [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) +* [Youtube - Building a REST API using Gorm and Fiber](https://www.youtube.com/watch?v=Iq2qT0fRhAA) +* [embedmode/fiberseed](https://github.com/embedmode/fiberseed) + +## How do I handle custom 404 responses? + +If you're using v2.32.0 or later, all you need to do is to implement a custom error handler. See below, or see a more detailed explanation at [Error Handling](../guide/error-handling.md#custom-error-handler). + +If you're using v2.31.0 or earlier, the error handler will not capture 404 errors. Instead, you need to add a middleware function at the very bottom of the stack \(below all other functions\) to handle a 404 response: + +```go title="Example" +app.Use(func(c *fiber.Ctx) error { + return c.Status(fiber.StatusNotFound).SendString("Sorry can't find that!") +}) +``` + +## How can i use live reload ? + +[Air](https://github.com/cosmtrek/air) is a handy tool that automatically restarts your Go applications whenever the source code changes, making your development process faster and more efficient. + +To use Air in a Fiber project, follow these steps: + +1. Install Air by downloading the appropriate binary for your operating system from the GitHub release page or by building the tool directly from source. +2. Create a configuration file for Air in your project directory. This file can be named, for example, .air.toml or air.conf. Here's a sample configuration file that works with Fiber: +```toml +# .air.toml +root = "." +tmp_dir = "tmp" +[build] + cmd = "go build -o ./tmp/main ." + bin = "./tmp/main" + delay = 1000 # ms + exclude_dir = ["assets", "tmp", "vendor"] + include_ext = ["go", "tpl", "tmpl", "html"] + exclude_regex = ["_test\\.go"] +``` +3. Start your Fiber application using Air by running the following command in the terminal: +```sh +air +``` + +As you make changes to your source code, Air will detect them and automatically restart the application. + +A complete example demonstrating the use of Air with Fiber can be found in the [Fiber Recipes repository](https://github.com/gofiber/recipes/tree/master/air). This example shows how to configure and use Air in a Fiber project to create an efficient development environment. + + +## How do I set up an error handler? + +To override the default error handler, you can override the default when providing a [Config](../api/fiber.md#config) when initiating a new [Fiber instance](../api/fiber.md#new). + +```go title="Example" +app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + }, +}) +``` + +We have a dedicated page explaining how error handling works in Fiber, see [Error Handling](../guide/error-handling.md). + +## Which template engines does Fiber support? + +Fiber currently supports 9 template engines in our [gofiber/template](https://docs.gofiber.io/template/) middleware: + +* [ace](https://docs.gofiber.io/template/ace/) +* [amber](https://docs.gofiber.io/template/amber/) +* [django](https://docs.gofiber.io/template/django/) +* [handlebars](https://docs.gofiber.io/template/handlebars) +* [html](https://docs.gofiber.io/template/html) +* [jet](https://docs.gofiber.io/template/jet) +* [mustache](https://docs.gofiber.io/template/mustache) +* [pug](https://docs.gofiber.io/template/pug) +* [slim](https://docs.gofiber.io/template/pug) + +To learn more about using Templates in Fiber, see [Templates](../guide/templates.md). + +## Does Fiber have a community chat? + +Yes, we have our own [Discord ](https://gofiber.io/discord)server, where we hang out. We have different rooms for every subject. +If you have questions or just want to have a chat, feel free to join us via this **>** [**invite link**](https://gofiber.io/discord) **<**. + +![](/img/support-discord.png) + +## Does fiber support sub domain routing ? + +Yes we do, here are some examples: +This example works v2 +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +type Host struct { + Fiber *fiber.App +} + +func main() { + // Hosts + hosts := map[string]*Host{} + //----- + // API + //----- + api := fiber.New() + api.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + hosts["api.localhost:3000"] = &Host{api} + api.Get("/", func(c *fiber.Ctx) error { + return c.SendString("API") + }) + //------ + // Blog + //------ + blog := fiber.New() + blog.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + hosts["blog.localhost:3000"] = &Host{blog} + blog.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Blog") + }) + //--------- + // Website + //--------- + site := fiber.New() + site.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + + hosts["localhost:3000"] = &Host{site} + site.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Website") + }) + // Server + app := fiber.New() + app.Use(func(c *fiber.Ctx) error { + host := hosts[c.Hostname()] + if host == nil { + return c.SendStatus(fiber.StatusNotFound) + } else { + host.Fiber.Handler()(c.Context()) + return nil + } + }) + log.Fatal(app.Listen(":3000")) +} +``` +If more information is needed, please refer to this issue [#750](https://github.com/gofiber/fiber/issues/750) diff --git a/docs/guide/_category_.json b/docs/guide/_category_.json new file mode 100644 index 0000000000..b0e157aa76 --- /dev/null +++ b/docs/guide/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Guide", + "position": 3, + "link": { + "type": "generated-index", + "description": "Guides for Fiber." + } +} diff --git a/docs/guide/error-handling.md b/docs/guide/error-handling.md new file mode 100644 index 0000000000..7d3aa361a9 --- /dev/null +++ b/docs/guide/error-handling.md @@ -0,0 +1,128 @@ +--- +id: error-handling +title: 🐛 Error Handling +description: >- + Fiber supports centralized error handling by returning an error to the handler + which allows you to log errors to external services or send a customized HTTP + response to the client. +sidebar_position: 4 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Catching Errors + +It’s essential to ensure that Fiber catches all errors that occur while running route handlers and middleware. You must return them to the handler function, where Fiber will catch and process them. + + + + +```go +app.Get("/", func(c *fiber.Ctx) error { + // Pass error to Fiber + return c.SendFile("file-does-not-exist") +}) +``` + + + +Fiber does not handle [panics](https://go.dev/blog/defer-panic-and-recover) by default. To recover from a panic thrown by any handler in the stack, you need to include the `Recover` middleware below: + +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("This panic is caught by fiber") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +You could use Fiber's custom error struct to pass an additional `status code` using `fiber.NewError()`. It's optional to pass a message; if this is left empty, it will default to the status code message \(`404` equals `Not Found`\). + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // 503 Service Unavailable + return fiber.ErrServiceUnavailable + + // 503 On vacation! + return fiber.NewError(fiber.StatusServiceUnavailable, "On vacation!") +}) +``` + +## Default Error Handler + +Fiber provides an error handler by default. For a standard error, the response is sent as **500 Internal Server Error**. If the error is of type [fiber.Error](https://godoc.org/github.com/gofiber/fiber#Error), the response is sent with the provided status code and message. + +```go title="Example" +// Default error handler +var DefaultErrorHandler = func(c *fiber.Ctx, err error) error { + // Status code defaults to 500 + code := fiber.StatusInternalServerError + + // Retrieve the custom status code if it's a *fiber.Error + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + // Set Content-Type: text/plain; charset=utf-8 + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + // Return status code with error message + return c.Status(code).SendString(err.Error()) +} +``` + +## Custom Error Handler + +A custom error handler can be set using a [Config ](../api/fiber.md#config)when initializing a [Fiber instance](../api/fiber.md#new). + +In most cases, the default error handler should be sufficient. However, a custom error handler can come in handy if you want to capture different types of errors and take action accordingly e.g., send a notification email or log an error to the centralized system. You can also send customized responses to the client e.g., error page or just a JSON response. + +The following example shows how to display error pages for different types of errors. + +```go title="Example" +// Create a new fiber instance with custom config +app := fiber.New(fiber.Config{ + // Override default error handler + ErrorHandler: func(ctx *fiber.Ctx, err error) error { + // Status code defaults to 500 + code := fiber.StatusInternalServerError + + // Retrieve the custom status code if it's a *fiber.Error + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + // Send custom error page + err = ctx.Status(code).SendFile(fmt.Sprintf("./%d.html", code)) + if err != nil { + // In case the SendFile fails + return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") + } + + // Return from handler + return nil + }, +}) + +// ... +``` + +> Special thanks to the [Echo](https://echo.labstack.com/) & [Express](https://expressjs.com/) framework for inspiration regarding error handling. diff --git a/docs/guide/faster-fiber.md b/docs/guide/faster-fiber.md new file mode 100644 index 0000000000..b0b7bcb3e5 --- /dev/null +++ b/docs/guide/faster-fiber.md @@ -0,0 +1,36 @@ +--- +id: faster-fiber +title: ⚡ Make Fiber Faster +sidebar_position: 7 +--- + +## Custom JSON Encoder/Decoder +Since Fiber v2.32.0, we use **encoding/json** as default json library due to stability and producibility. However, the standard library is a bit slow compared to 3rd party libraries. If you're not happy with the performance of **encoding/json**, we recommend you to use these libraries: +- [goccy/go-json](https://github.com/goccy/go-json) +- [bytedance/sonic](https://github.com/bytedance/sonic) +- [segmentio/encoding](https://github.com/segmentio/encoding) +- [mailru/easyjson](https://github.com/mailru/easyjson) +- [minio/simdjson-go](https://github.com/minio/simdjson-go) +- [wI2L/jettison](https://github.com/wI2L/jettison) + +```go title="Example" +package main + +import "github.com/gofiber/fiber/v2" +import "github.com/goccy/go-json" + +func main() { + app := fiber.New(fiber.Config{ + JSONEncoder: json.Marshal, + JSONDecoder: json.Unmarshal, + }) + + # ... +} +``` + +### References +- [Set custom JSON encoder for client](../api/client.md#jsonencoder) +- [Set custom JSON decoder for client](../api/client.md#jsondecoder) +- [Set custom JSON encoder for application](../api/fiber.md#config) +- [Set custom JSON decoder for application](../api/fiber.md#config) \ No newline at end of file diff --git a/docs/guide/grouping.md b/docs/guide/grouping.md new file mode 100644 index 0000000000..429e170229 --- /dev/null +++ b/docs/guide/grouping.md @@ -0,0 +1,79 @@ +--- +id: grouping +title: 🎭 Grouping +sidebar_position: 2 +--- + +:::info +In general, the Group functionality in Fiber behaves similarly to ExpressJS. Groups are declared virtually and all routes declared within the group are flattened into a single list with a prefix, which is then checked by the framework in the order it was declared. This means that the behavior of Group in Fiber is identical to that of ExpressJS. +::: + +## Paths + +Like **Routing**, groups can also have paths that belong to a cluster. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api", middleware) // /api + + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +A **Group** of paths can have an optional handler. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api") // /api + + v1 := api.Group("/v1") // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2") // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +:::caution +Running **/api**, **/v1** or **/v2** will result in **404** error, make sure you have the errors set. +::: + +## Group Handlers + +Group handlers can also be used as a routing path but they must have **Next** added to them so that the flow can continue. + +```go +func main() { + app := fiber.New() + + handler := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + api := app.Group("/api") // /api + + v1 := api.Group("/v1", func(c *fiber.Ctx) error { // middleware for /api/v1 + c.Set("Version", "v1") + return c.Next() + }) + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + log.Fatal(app.Listen(":3000")) +} +``` diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md new file mode 100644 index 0000000000..0b7db96b33 --- /dev/null +++ b/docs/guide/hooks.md @@ -0,0 +1,218 @@ +--- +id: hooks +title: 🪝 Hooks +sidebar_position: 6 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +With Fiber v2.30.0, you can execute custom user functions when to run some methods. Here is a list of this hooks: +- [OnRoute](#onroute) +- [OnName](#onname) +- [OnGroup](#ongroup) +- [OnGroupName](#ongroupname) +- [OnListen](#onlisten) +- [OnFork](#onfork) +- [OnShutdown](#onshutdown) +- [OnMount](#onmount) + +## Constants +```go +// Handlers define a function to create hooks for Fiber. +type OnRouteHandler = func(Route) error +type OnNameHandler = OnRouteHandler +type OnGroupHandler = func(Group) error +type OnGroupNameHandler = OnGroupHandler +type OnListenHandler = func(ListenData) error +type OnForkHandler = func(int) error +type OnShutdownHandler = func() error +type OnMountHandler = func(*App) error +``` + +## OnRoute + +OnRoute is a hook to execute user functions on each route registeration. Also you can get route properties by **route** parameter. + +```go title="Signature" +func (app *App) OnRoute(handler ...OnRouteHandler) +``` + +## OnName + +OnName is a hook to execute user functions on each route naming. Also you can get route properties by **route** parameter. + +:::caution +OnName only works with naming routes, not groups. +::: + +```go title="Signature" +func (app *App) OnName(handler ...OnNameHandler) +``` + + + + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("index") + + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Name: " + r.Name + ", ") + + return nil + }) + + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Method: " + r.Method + "\n") + + return nil + }) + + app.Get("/add/user", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("addUser") + + app.Delete("/destroy/user", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("destroyUser") + + app.Listen(":5000") +} + +// Results: +// Name: addUser, Method: GET +// Name: destroyUser, Method: DELETE +``` + + + +## OnGroup + +OnGroup is a hook to execute user functions on each group registeration. Also you can get group properties by **group** parameter. + +```go title="Signature" +func (app *App) OnGroup(handler ...OnGroupHandler) +``` + +## OnGroupName + +OnGroupName is a hook to execute user functions on each group naming. Also you can get group properties by **group** parameter. + +:::caution +OnGroupName only works with naming groups, not routes. +::: + +```go title="Signature" +func (app *App) OnGroupName(handler ...OnGroupNameHandler) +``` + +## OnListen + +OnListen is a hook to execute user functions on Listen, ListenTLS, Listener. + +```go title="Signature" +func (app *App) OnListen(handler ...OnListenHandler) +``` + + + + +```go +app := fiber.New(fiber.Config{ + DisableStartupMessage: true, +}) + +app.Hooks().OnListen(func(listenData fiber.ListenData) error { + if fiber.IsChild() { + return nil + } + scheme := "http" + if data.TLS { + scheme = "https" + } + log.Println(scheme + "://" + listenData.Host + ":" + listenData.Port) + return nil +}) + +app.Listen(":5000") +``` + + + + +## OnFork + +OnFork is a hook to execute user functions on Fork. + +```go title="Signature" +func (app *App) OnFork(handler ...OnForkHandler) +``` + +## OnShutdown + +OnShutdown is a hook to execute user functions after Shutdown. + +```go title="Signature" +func (app *App) OnShutdown(handler ...OnShutdownHandler) +``` + +## OnMount + +OnMount is a hook to execute user function after mounting process. The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter. It works for app and group mounting. + +```go title="Signature" +func (h *Hooks) OnMount(handler ...OnMountHandler) +``` + + + + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +func main() { + app := New() + app.Get("/", testSimpleHandler).Name("x") + + subApp := New() + subApp.Get("/test", testSimpleHandler) + + subApp.Hooks().OnMount(func(parent *fiber.App) error { + fmt.Print("Mount path of parent app: "+parent.MountPath()) + // ... + + return nil + }) + + app.Mount("/sub", subApp) +} + +// Result: +// Mount path of parent app: +``` + + + + + +:::caution +OnName/OnRoute/OnGroup/OnGroupName hooks are mount-sensitive. If you use one of these routes on sub app and you mount it; paths of routes and groups will start with mount prefix. diff --git a/docs/guide/routing.md b/docs/guide/routing.md new file mode 100644 index 0000000000..615d1aa2f2 --- /dev/null +++ b/docs/guide/routing.md @@ -0,0 +1,287 @@ +--- +id: routing +title: 🔌 Routing +description: >- + Routing refers to how an application's endpoints (URIs) respond to client + requests. +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import RoutingHandler from './../partials/routing/handler.md'; + +## Handlers + + + +## Paths + +Route paths, combined with a request method, define the endpoints at which requests can be made. Route paths can be **strings** or **string patterns**. + +**Examples of route paths based on strings** + +```go +// This route path will match requests to the root route, "/": +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("root") +}) + +// This route path will match requests to "/about": +app.Get("/about", func(c *fiber.Ctx) error { + return c.SendString("about") +}) + +// This route path will match requests to "/random.txt": +app.Get("/random.txt", func(c *fiber.Ctx) error { + return c.SendString("random.txt") +}) +``` + +As with the expressJs framework, the order of the route declaration plays a role. +When a request is received, the routes are checked in the order in which they are declared. + +:::info +So please be careful to write routes with variable parameters after the routes that contain fixed parts, so that these variable parts do not match instead and unexpected behavior occurs. +::: + +## Parameters + +Route parameters are dynamic elements in the route, which are **named** or **not named segments**. This segments that are used to capture the values specified at their position in the URL. The obtained values can be retrieved using the [Params](https://fiber.wiki/context#params) function, with the name of the route parameter specified in the path as their respective keys or for unnamed parameters the character\(\*, +\) and the counter of this. + +The characters :, +, and \* are characters that introduce a parameter. + +Greedy parameters are indicated by wildcard\(\*\) or plus\(+\) signs. + +The routing also offers the possibility to use optional parameters, for the named parameters these are marked with a final "?", unlike the plus sign which is not optional, you can use the wildcard character for a parameter range which is optional and greedy. + +**Example of define routes with route parameters** + +```go +// Parameters +app.Get("/user/:name/books/:title", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s\n", c.Params("name")) + fmt.Fprintf(c, "%s\n", c.Params("title")) + return nil +}) +// Plus - greedy - not optional +app.Get("/user/+", func(c *fiber.Ctx) error { + return c.SendString(c.Params("+")) +}) + +// Optional parameter +app.Get("/user/:name?", func(c *fiber.Ctx) error { + return c.SendString(c.Params("name")) +}) + +// Wildcard - greedy - optional +app.Get("/user/*", func(c *fiber.Ctx) error { + return c.SendString(c.Params("*")) +}) + +// This route path will match requests to "/v1/some/resource/name:customVerb", since the parameter character is escaped +app.Get("/v1/some/resource/name\\:customVerb", func(c *fiber.Ctx) error { + return c.SendString("Hello, Community") +}) +``` + +:::info +Since the hyphen \(`-`\) and the dot \(`.`\) are interpreted literally, they can be used along with route parameters for useful purposes. +::: + +:::info +All special parameter characters can also be escaped with `"\\"` and lose their value, so you can use them in the route if you want, like in the custom methods of the [google api design guide](https://cloud.google.com/apis/design/custom_methods). +::: + +```go +// http://localhost:3000/plantae/prunus.persica +app.Get("/plantae/:genus.:species", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s.%s\n", c.Params("genus"), c.Params("species")) + return nil // prunus.persica +}) +``` + +```go +// http://localhost:3000/flights/LAX-SFO +app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s-%s\n", c.Params("from"), c.Params("to")) + return nil // LAX-SFO +}) +``` + +Our intelligent router recognizes that the introductory parameter characters should be part of the request route in this case and can process them as such. + +```go +// http://localhost:3000/shop/product/color:blue/size:xs +app.Get("/shop/product/color::color/size::size", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s:%s\n", c.Params("color"), c.Params("size")) + return nil // blue:xs +}) +``` + +In addition, several parameters in a row and several unnamed parameter characters in the route, such as the wildcard or plus character, are possible, which greatly expands the possibilities of the router for the user. + +```go +// GET /@v1 +// Params: "sign" -> "@", "param" -> "v1" +app.Get("/:sign:param", handler) + +// GET /api-v1 +// Params: "name" -> "v1" +app.Get("/api-:name", handler) + +// GET /customer/v1/cart/proxy +// Params: "*1" -> "customer/", "*2" -> "/cart" +app.Get("/*v1*/proxy", handler) + +// GET /v1/brand/4/shop/blue/xs +// Params: "*1" -> "brand/4", "*2" -> "blue/xs" +app.Get("/v1/*/shop/*", handler) +``` + +We have adapted the routing strongly to the express routing, but currently without the possibility of the regular expressions, because they are quite slow. The possibilities can be tested with version 0.1.7 \(express 4\) in the online [Express route tester](http://forbeslindesay.github.io/express-route-tester/). + +### Constraints +Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values by parameters. The feature was intorduced in `v2.37.0` and inspired by [.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0#route-constraints). + +:::caution +Constraints aren't validation for parameters. If constraint aren't valid for parameter value, Fiber returns **404 handler**. +::: + +| Constraint | Example | Example matches | +| ----------------- | ------------------------------------ | ------------------------------------------------------------------------------------------- | +| int | :id | 123456789, -123456789 | +| bool | :active | true,false | +| guid | :id | CD2C1638-1638-72D5-1638-DEADBEEF1638 | +| float | :weight | 1.234, -1,001.01e8 | +| minLen(value) | :username | Test (must be at least 4 characters) | +| maxLen(value) | :filename | MyFile (must be no more than 8 characters | +| len(length) | :filename | somefile.txt (exactly 12 characters) | +| min(value) | :age | 19 (Integer value must be at least 18) | +| max(value) | :age | 91 (Integer value must be no more than 120) | +| range(min,max) | :age | 91 (Integer value must be at least 18 but no more than 120) | +| alpha | :name | Rick (String must consist of one or more alphabetical characters, a-z and case-insensitive) | +| datetime | :dob | 2005-11-01 | +| regex(expression) | :date | 2022-08-27 (Must match regular expression) | + +**Examples** + + + + +```go +app.Get("/:test", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) + +// curl -X GET http://localhost:3000/12 +// 12 + +// curl -X GET http://localhost:3000/1 +// Cannot GET /1 +``` + + + +You can use `;` for multiple constraints. +```go +app.Get("/:test", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) + +// curl -X GET http://localhost:3000/120000 +// Cannot GET /120000 + +// curl -X GET http://localhost:3000/1 +// Cannot GET /1 + +// curl -X GET http://localhost:3000/250 +// 250 +``` + + + +Fiber precompiles regex query when to register routes. So there're no performance overhead for regex constraint. +```go +app.Get("/:date", func(c *fiber.Ctx) error { + return c.SendString(c.Params("date")) +}) + +// curl -X GET http://localhost:3000/125 +// Cannot GET /125 + +// curl -X GET http://localhost:3000/test +// Cannot GET /test + +// curl -X GET http://localhost:3000/2022-08-27 +// 2022-08-27 +``` + + + + +:::caution +You should use `\\` before routing-specific characters when to use datetime constraint (`*`, `+`, `?`, `:`, `/`, `<`, `>`, `;`, `(`, `)`), to avoid wrong parsing. +::: + +**Optional Parameter Example** + +You can impose constraints on optional parameters as well. + +```go +app.Get("/:test?", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) +// curl -X GET http://localhost:3000/42 +// 42 +// curl -X GET http://localhost:3000/ +// +// curl -X GET http://localhost:3000/7.0 +// Cannot GET /7.0 +``` + +## Middleware + +Functions that are designed to make changes to the request or response are called **middleware functions**. The [Next](../api/ctx.md#next) is a **Fiber** router function, when called, executes the **next** function that **matches** the current route. + +**Example of a middleware function** + +```go +app.Use(func(c *fiber.Ctx) error { + // Set a custom header on all responses: + c.Set("X-Custom-Header", "Hello, World") + + // Go to next middleware: + return c.Next() +}) + +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +`Use` method path is a **mount**, or **prefix** path, and limits middleware to only apply to any paths requested that begin with it. + +## Grouping + +If you have many endpoints, you can organize your routes using `Group`. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api", middleware) // /api + + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +More information about this in our [Grouping Guide](./grouping.md) diff --git a/docs/guide/templates.md b/docs/guide/templates.md new file mode 100644 index 0000000000..cc52c2d9ac --- /dev/null +++ b/docs/guide/templates.md @@ -0,0 +1,106 @@ +--- +id: templates +title: 📝 Templates +description: Fiber supports server-side template engines. +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Template interfaces + +Fiber provides a Views interface to provide your own template engine: + + + + +```go +type Views interface { + Load() error + Render(io.Writer, string, interface{}, ...string) error +} +``` + + + +`Views` interface contains a `Load` and `Render` method, `Load` is executed by Fiber on app initialization to load/parse the templates. + +```go +// Pass engine to Fiber's Views Engine +app := fiber.New(fiber.Config{ + Views: engine, + // Views Layout is the global layout for all template render until override on Render function. + ViewsLayout: "layouts/main" +}) +``` + +The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. It will use global layout if layout is not being defined in `Render` function. +If the Fiber config option `PassLocalsToViews` is enabled, then all locals set using `ctx.Locals(key, value)` will be passed to the template. + +```go +app.Get("/", func(c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "hello": "world", + }); +}) +``` + +## Engines + +Fiber team maintains [templates](https://docs.gofiber.io/template) package that provides wrappers for multiple template engines: + +* [ace](https://docs.gofiber.io/template/ace/) +* [amber](https://docs.gofiber.io/template/amber/) +* [django](https://docs.gofiber.io/template/django/) +* [handlebars](https://docs.gofiber.io/template/handlebars) +* [html](https://docs.gofiber.io/template/html) +* [jet](https://docs.gofiber.io/template/jet) +* [mustache](https://docs.gofiber.io/template/mustache) +* [pug](https://docs.gofiber.io/template/pug) +* [slim](https://docs.gofiber.io/template/slim) + + + + +```go +package main + +import ( + "log" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html/v2" +) + +func main() { + // Initialize standard Go html template engine + engine := html.New("./views", ".html") + // If you want other engine, just replace with following + // Create a new engine with django + // engine := django.New("./views", ".django") + + app := fiber.New(fiber.Config{ + Views: engine, + }) + app.Get("/", func(c *fiber.Ctx) error { + // Render index template + return c.Render("index", fiber.Map{ + "Title": "Hello, World!", + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + + + +```markup + + +

{{.Title}}

+ + +``` +
+
diff --git a/docs/guide/validation.md b/docs/guide/validation.md new file mode 100644 index 0000000000..417298aebb --- /dev/null +++ b/docs/guide/validation.md @@ -0,0 +1,168 @@ +--- +id: validation +title: 🔎 Validation +sidebar_position: 5 +--- + +## Validator package + +Fiber can make _great_ use of the validator package to ensure correct validation of data to store. + +- [Official validator Github page \(Installation, use, examples..\).](https://github.com/go-playground/validator) + +You can find the detailed descriptions of the _validations_ used in the fields contained on the structs below: + +- [Detailed docs](https://pkg.go.dev/github.com/go-playground/validator?tab=doc) + +```go title="Validation Example" +package main + +import ( + "fmt" + "log" + "strings" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +type ( + User struct { + Name string `validate:"required,min=5,max=20"` // Required field, min 5 char long max 20 + Age int `validate:"required,teener"` // Required field, and client needs to implement our 'teener' tag format which we'll see later + } + + ErrorResponse struct { + Error bool + FailedField string + Tag string + Value interface{} + } + + XValidator struct { + validator *validator.Validate + } + + GlobalErrorHandlerResp struct { + Success bool `json:"success"` + Message string `json:"message"` + } +) + +// This is the validator instance +// for more information see: https://github.com/go-playground/validator +var validate = validator.New() + +func (v XValidator) Validate(data interface{}) []ErrorResponse { + validationErrors := []ErrorResponse{} + + errs := validate.Struct(data) + if errs != nil { + for _, err := range errs.(validator.ValidationErrors) { + // In this case data object is actually holding the User struct + var elem ErrorResponse + + elem.FailedField = err.Field() // Export struct field name + elem.Tag = err.Tag() // Export struct tag + elem.Value = err.Value() // Export field value + elem.Error = true + + validationErrors = append(validationErrors, elem) + } + } + + return validationErrors +} + +func main() { + myValidator := &XValidator{ + validator: validate, + } + + app := fiber.New(fiber.Config{ + // Global custom error handler + ErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(GlobalErrorHandlerResp{ + Success: false, + Message: err.Error(), + }) + }, + }) + + // Custom struct validation tag format + myValidator.validator.RegisterValidation("teener", func(fl validator.FieldLevel) bool { + // User.Age needs to fit our needs, 12-18 years old. + return fl.Field().Int() >= 12 && fl.Field().Int() <= 18 + }) + + app.Get("/", func(c *fiber.Ctx) error { + user := &User{ + Name: c.Query("name"), + Age: c.QueryInt("age"), + } + + // Validation + if errs := myValidator.Validate(user); len(errs) > 0 && errs[0].Error { + errMsgs := make([]string, 0) + + for _, err := range errs { + errMsgs = append(errMsgs, fmt.Sprintf( + "[%s]: '%v' | Needs to implement '%s'", + err.FailedField, + err.Value, + err.Tag, + )) + } + + return &fiber.Error{ + Code: fiber.ErrBadRequest.Code, + Message: strings.Join(errMsgs, " and "), + } + } + + // Logic, validated with success + return c.SendString("Hello, World!") + }) + + log.Fatal(app.Listen(":3000")) +} + +/** +OUTPUT + +[1] +Request: + +GET http://127.0.0.1:3000/ + +Response: + +{"success":false,"message":"[Name]: '' | Needs to implement 'required' and [Age]: '0' | Needs to implement 'required'"} + +[2] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age=9 + +Response: +{"success":false,"message":"[Age]: '9' | Needs to implement 'teener'"} + +[3] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age= + +Response: +{"success":false,"message":"[Age]: '0' | Needs to implement 'required'"} + +[4] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age=18 + +Response: +Hello, World! + +**/ + +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000000..456035da6b --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,195 @@ +--- +slug: / +id: welcome +title: 👋 Welcome +sidebar_position: 1 +--- +An online API documentation with examples so you can start building web apps with Fiber right away! + +**Fiber** is an [Express](https://github.com/expressjs/express) inspired **web framework** built on top of [Fasthttp](https://github.com/valyala/fasthttp), the **fastest** HTTP engine for [Go](https://go.dev/doc/). Designed to **ease** things up for **fast** development with **zero memory allocation** and **performance** in mind. + +These docs are for **Fiber v2**, which was released on **September 15th, 2020**. + +### Installation + +First of all, [download](https://go.dev/dl/) and install Go. `1.17` or higher is required. + +Installation is done using the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get github.com/gofiber/fiber/v2 +``` + +### Zero Allocation +Some values returned from \***fiber.Ctx** are **not** immutable by default. + +Because fiber is optimized for **high-performance**, values returned from **fiber.Ctx** are **not** immutable by default and **will** be re-used across requests. As a rule of thumb, you **must** only use context values within the handler, and you **must not** keep any references. As soon as you return from the handler, any values you have obtained from the context will be re-used in future requests and will change below your feet. Here is an example: + +```go +func handler(c *fiber.Ctx) error { + // Variable is only valid within this handler + result := c.Params("foo") + + // ... +} +``` + +If you need to persist such values outside the handler, make copies of their **underlying buffer** using the [copy](https://pkg.go.dev/builtin/#copy) builtin. Here is an example for persisting a string: + +```go +func handler(c *fiber.Ctx) error { + // Variable is only valid within this handler + result := c.Params("foo") + + // Make a copy + buffer := make([]byte, len(result)) + copy(buffer, result) + resultCopy := string(buffer) + // Variable is now valid forever + + // ... +} +``` + +We created a custom `CopyString` function that does the above and is available under [gofiber/utils](https://github.com/gofiber/fiber/tree/master/utils). + +```go +app.Get("/:foo", func(c *fiber.Ctx) error { + // Variable is now immutable + result := utils.CopyString(c.Params("foo")) + + // ... +}) +``` + +Alternatively, you can also use the `Immutable` setting. It will make all values returned from the context immutable, allowing you to persist them anywhere. Of course, this comes at the cost of performance. + +```go +app := fiber.New(fiber.Config{ + Immutable: true, +}) +``` + +For more information, please check [**\#426**](https://github.com/gofiber/fiber/issues/426) and [**\#185**](https://github.com/gofiber/fiber/issues/185). + +### Hello, World! + +Embedded below is essentially the most straightforward **Fiber** app you can create: + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Listen(":3000") +} +``` + +```text +go run server.go +``` + +Browse to `http://localhost:3000` and you should see `Hello, World!` on the page. + +### Basic routing + +Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (`GET`, `PUT`, `POST`, etc.). + +Each route can have **multiple handler functions** that are executed when the route is matched. + +Route definition takes the following structures: + +```go +// Function signature +app.Method(path string, ...func(*fiber.Ctx) error) +``` + +- `app` is an instance of **Fiber** +- `Method` is an [HTTP request method](https://docs.gofiber.io/api/app#route-handlers): `GET`, `PUT`, `POST`, etc. +- `path` is a virtual path on the server +- `func(*fiber.Ctx) error` is a callback function containing the [Context](https://docs.gofiber.io/api/ctx) executed when the route is matched + +**Simple route** + +```go +// Respond with "Hello, World!" on root path, "/" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +**Parameters** + +```go +// GET http://localhost:8080/hello%20world + +app.Get("/:value", func(c *fiber.Ctx) error { + return c.SendString("value: " + c.Params("value")) + // => Get request with value: hello world +}) +``` + +**Optional parameter** + +```go +// GET http://localhost:3000/john + +app.Get("/:name?", func(c *fiber.Ctx) error { + if c.Params("name") != "" { + return c.SendString("Hello " + c.Params("name")) + // => Hello john + } + return c.SendString("Where is john?") +}) +``` + +**Wildcards** + +```go +// GET http://localhost:3000/api/user/john + +app.Get("/api/*", func(c *fiber.Ctx) error { + return c.SendString("API path: " + c.Params("*")) + // => API path: user/john +}) +``` + +### Static files + +To serve static files such as **images**, **CSS**, and **JavaScript** files, replace your function handler with a file or directory string. + +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.Listen(":3000") +``` + +Now, you can load the files that are in the `./public` directory: + +```bash +http://localhost:3000/hello.html +http://localhost:3000/js/jquery.js +http://localhost:3000/css/style.css +``` + +### Note + +For more information on how to build APIs in Go with Fiber, please check out this excellent article +[on building an express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/). diff --git a/docs/partials/routing/handler.md b/docs/partials/routing/handler.md new file mode 100644 index 0000000000..2d198f02f8 --- /dev/null +++ b/docs/partials/routing/handler.md @@ -0,0 +1,69 @@ +--- +id: route-handlers +title: Route Handlers +--- + +Registers a route bound to a specific [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods). + +```go title="Signatures" +// HTTP methods +func (app *App) Get(path string, handlers ...Handler) Router +func (app *App) Head(path string, handlers ...Handler) Router +func (app *App) Post(path string, handlers ...Handler) Router +func (app *App) Put(path string, handlers ...Handler) Router +func (app *App) Delete(path string, handlers ...Handler) Router +func (app *App) Connect(path string, handlers ...Handler) Router +func (app *App) Options(path string, handlers ...Handler) Router +func (app *App) Trace(path string, handlers ...Handler) Router +func (app *App) Patch(path string, handlers ...Handler) Router + +// Add allows you to specifiy a method as value +func (app *App) Add(method, path string, handlers ...Handler) Router + +// All will register the route on all HTTP methods +// Almost the same as app.Use but not bound to prefixes +func (app *App) All(path string, handlers ...Handler) Router +``` + +```go title="Examples" +// Simple GET handler +app.Get("/api/list", func(c *fiber.Ctx) error { + return c.SendString("I'm a GET request!") +}) + +// Simple POST handler +app.Post("/api/register", func(c *fiber.Ctx) error { + return c.SendString("I'm a POST request!") +}) +``` + +**Use** can be used for middleware packages and prefix catchers. These routes will only match the beginning of each path i.e. `/john` will match `/john/doe`, `/johnnnnn` etc + +```go title="Signature" +func (app *App) Use(args ...interface{}) Router +``` + +```go title="Examples" +// Match any request +app.Use(func(c *fiber.Ctx) error { + return c.Next() +}) + +// Match request starting with /api +app.Use("/api", func(c *fiber.Ctx) error { + return c.Next() +}) + +// Match requests starting with /api or /home (multiple-prefix support) +app.Use([]string{"/api", "/home"}, func(c *fiber.Ctx) error { + return c.Next() +}) + +// Attach multiple handlers +app.Use("/api", func(c *fiber.Ctx) error { + c.Set("X-Custom-Header", random.String(32)) + return c.Next() +}, func(c *fiber.Ctx) error { + return c.Next() +}) +``` diff --git a/go.mod b/go.mod index fec575483a..72fcc42b69 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,26 @@ module github.com/gofiber/fiber/v3 -go 1.19 +go 1.20 require ( - github.com/gofiber/fiber/v2 v2.40.1 - github.com/gofiber/utils/v2 v2.0.0-beta.1 + github.com/gofiber/fiber/v2 v2.48.0 + github.com/gofiber/utils/v2 v2.0.0-beta.3 github.com/google/uuid v1.3.0 github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-isatty v0.0.16 - github.com/stretchr/testify v1.8.1 - github.com/tinylib/msgp v1.1.6 + github.com/mattn/go-isatty v0.0.19 + github.com/stretchr/testify v1.8.4 + github.com/tinylib/msgp v1.1.8 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.42.0 + github.com/valyala/fasthttp v1.48.0 ) require ( - github.com/andybalholm/brotli v1.0.4 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/klauspost/compress v1.15.12 // indirect - github.com/philhofer/fwd v1.1.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/philhofer/fwd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.2.0 // indirect + golang.org/x/sys v0.10.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d1f071c00b..4cba3818ad 100644 --- a/go.sum +++ b/go.sum @@ -1,81 +1,69 @@ -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gofiber/fiber/v2 v2.40.1 h1:pc7n9VVpGIqNsvg9IPLQhyFEMJL8gCs1kneH5D1pIl4= -github.com/gofiber/fiber/v2 v2.40.1/go.mod h1:Gko04sLksnHbzLSRBFWPFdzM9Ws9pRxvvIaohJK1dsk= -github.com/gofiber/utils/v2 v2.0.0-beta.1 h1:ACfPdqeclx+BFIja19UjkKx7k3r5tmpILpNgzrfPLKs= -github.com/gofiber/utils/v2 v2.0.0-beta.1/go.mod h1:CG89nDoIkEFIJaw5LdLO9AmBM11odse/LC79KQujm74= +github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0= +github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8= +github.com/gofiber/utils/v2 v2.0.0-beta.3 h1:pfOhUDDVjBJpkWv6C5jaDyYLvpui7zQ97zpyFFsUOKw= +github.com/gofiber/utils/v2 v2.0.0-beta.3/go.mod h1:jsl17+MsKfwJjM3ONCE9Rzji/j8XNbwjhUVTjzgfDCo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= -github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= -github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= -github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.41.0 h1:zeR0Z1my1wDHTRiamBCXVglQdbUwgb9uWG3k1HQz6jY= -github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= -github.com/valyala/fasthttp v1.42.0 h1:LBMyqvJR8DEBgN79oI8dGbkuj5Lm9jbHESxH131TTN8= -github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= +github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= +github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group.go b/group.go index 1470e970d5..2b8001d5f8 100644 --- a/group.go +++ b/group.go @@ -11,17 +11,26 @@ import ( // Group struct type Group struct { - app *App - parentGroup *Group - name string + app *App + parentGroup *Group + name string + anyRouteDefined bool Prefix string } -// Name Assign name to specific route. +// Name Assign name to specific route or group itself. +// +// If this method is used before any route added to group, it'll set group name and OnGroupNameHook will be used. +// Otherwise, it'll set route name and OnName hook will be used. func (grp *Group) Name(name string) Router { - grp.app.mutex.Lock() + if grp.anyRouteDefined { + grp.app.Name(name) + + return grp + } + grp.app.mutex.Lock() if grp.parentGroup != nil { grp.name = grp.parentGroup.name + name } else { @@ -91,6 +100,10 @@ func (grp *Group) Use(args ...any) Router { grp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, nil, handlers...) } + if !grp.anyRouteDefined { + grp.anyRouteDefined = true + } + return grp } @@ -149,12 +162,22 @@ func (grp *Group) Patch(path string, handler Handler, middleware ...Handler) Rou // Add allows you to specify multiple HTTP methods to register a route. func (grp *Group) Add(methods []string, path string, handler Handler, middleware ...Handler) Router { - return grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, handler, middleware...) + grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, handler, middleware...) + if !grp.anyRouteDefined { + grp.anyRouteDefined = true + } + + return grp } // Static will create a file server serving static files func (grp *Group) Static(prefix, root string, config ...Static) Router { - return grp.app.registerStatic(getGroupPath(grp.Prefix, prefix), root, config...) + 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 @@ -170,7 +193,7 @@ func (grp *Group) All(path string, handler Handler, middleware ...Handler) Route func (grp *Group) Group(prefix string, handlers ...Handler) Router { prefix = getGroupPath(grp.Prefix, prefix) if len(handlers) > 0 { - _ = grp.app.register([]string{methodUse}, prefix, grp, nil, handlers...) + grp.app.register([]string{methodUse}, prefix, grp, nil, handlers...) } // Create new group @@ -180,7 +203,6 @@ func (grp *Group) Group(prefix string, handlers ...Handler) Router { } return newGrp - } // Route is used to define routes with a common prefix inside the common function. diff --git a/helpers.go b/helpers.go index ebf976f904..952668f358 100644 --- a/helpers.go +++ b/helpers.go @@ -7,8 +7,8 @@ package fiber import ( "bytes" "crypto/tls" + "fmt" "io" - "log" "net" "os" "path/filepath" @@ -17,13 +17,25 @@ import ( "time" "unsafe" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/utils/v2" + "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) -/* #nosec */ -// getTlsConfig returns a net listener's tls config -func getTlsConfig(ln net.Listener) *tls.Config { +// acceptType is a struct that holds the parsed value of an Accept header +// along with quality, specificity, and order. +// used for sorting accept headers. +type acceptedType struct { + spec string + quality float64 + specificity int + order int +} + +// getTLSConfig returns a net listener's tls config +func getTLSConfig(ln net.Listener) *tls.Config { // Get listener type pointer := reflect.ValueOf(ln) @@ -34,12 +46,16 @@ func getTlsConfig(ln net.Listener) *tls.Config { // Get private field from value if field := val.FieldByName("config"); field.Type() != nil { // Copy value from pointer field (unsafe) - newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) // #nosec G103 + newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) //nolint:gosec // Probably the only way to extract the *tls.Config from a net.Listener. TODO: Verify there really is no easier way without using unsafe. if newval.Type() != nil { // Get element from pointer if elem := newval.Elem(); elem.Type() != nil { // Cast value to *tls.Config - return elem.Interface().(*tls.Config) + c, ok := elem.Interface().(*tls.Config) + if !ok { + panic(fmt.Errorf("failed to type-assert to *tls.Config")) + } + return c } } } @@ -50,19 +66,21 @@ func getTlsConfig(ln net.Listener) *tls.Config { } // readContent opens a named file and read content from it -func readContent(rf io.ReaderFrom, name string) (n int64, err error) { +func readContent(rf io.ReaderFrom, name string) (int64, error) { // Read file f, err := os.Open(filepath.Clean(name)) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to open: %w", err) } - // #nosec G307 defer func() { if err = f.Close(); err != nil { - log.Printf("Error closing file: %s\n", err) + log.Errorf("Error closing file: %s", err) } }() - return rf.ReadFrom(f) + if n, err := rf.ReadFrom(f); err != nil { + return n, fmt.Errorf("failed to read: %w", err) + } + return 0, nil } // quoteString escape special characters in a given string @@ -75,7 +93,9 @@ func (app *App) quoteString(raw string) string { } // Scan stack if other methods match the request -func (app *App) methodExist(c *DefaultCtx) (exist bool) { +func (app *App) methodExist(c *DefaultCtx) bool { + var exists bool + methods := app.config.RequestMethods for i := 0; i < len(methods); i++ { // Skip original method @@ -106,7 +126,7 @@ func (app *App) methodExist(c *DefaultCtx) (exist bool) { // No match, next route if match { // We matched - exist = true + exists = true // Add method to Allow header c.Append(HeaderAllow, methods[i]) // Break stack loop @@ -114,11 +134,12 @@ func (app *App) methodExist(c *DefaultCtx) (exist bool) { } } } - return + return exists } // Scan stack if other methods match the request -func (app *App) methodExistCustom(c CustomCtx) (exist bool) { +func (app *App) methodExistCustom(c CustomCtx) bool { + var exists bool methods := app.config.RequestMethods for i := 0; i < len(methods); i++ { // Skip original method @@ -149,7 +170,7 @@ func (app *App) methodExistCustom(c CustomCtx) (exist bool) { // No match, next route if match { // We matched - exist = true + exists = true // Add method to Allow header c.Append(HeaderAllow, methods[i]) // Break stack loop @@ -157,7 +178,7 @@ func (app *App) methodExistCustom(c CustomCtx) (exist bool) { } } } - return + return exists } // uniqueRouteStack drop all not unique routes from the slice @@ -196,43 +217,174 @@ func getGroupPath(prefix, path string) string { return strings.TrimRight(prefix, "/") + path } -// return valid offer for header negotiation -func getOffer(header string, offers ...string) string { +// acceptsOffer This function determines if an offer matches a given specification. +// It checks if the specification ends with a '*' or if the offer has the prefix of the specification. +// Returns true if the offer matches the specification, false otherwise. +func acceptsOffer(spec, offer string) bool { + if len(spec) >= 1 && spec[len(spec)-1] == '*' { + return true + } else if strings.HasPrefix(spec, offer) { + return true + } + return false +} + +// acceptsOfferType This function determines if an offer type matches a given specification. +// It checks if the specification is equal to */* (i.e., all types are accepted). +// It gets the MIME type of the offer (either from the offer itself or by its file extension). +// It checks if the offer MIME type matches the specification MIME type or if the specification is of the form /* and the offer MIME type has the same MIME type. +// Returns true if the offer type matches the specification, false otherwise. +func acceptsOfferType(spec, offerType string) bool { + // Accept: */* + if spec == "*/*" { + return true + } + + var mimetype string + if strings.IndexByte(offerType, '/') != -1 { + mimetype = offerType // MIME type + } else { + mimetype = utils.GetMIME(offerType) // extension + } + + if spec == mimetype { + // Accept: / + return true + } + + s := strings.IndexByte(mimetype, '/') + // Accept: /* + if strings.HasPrefix(spec, mimetype[:s]) && (spec[s:] == "/*" || mimetype[s:] == "/*") { + return true + } + + return false +} + +// getOffer return valid offer for header negotiation +func getOffer(header string, isAccepted func(spec, offer string) bool, offers ...string) string { if len(offers) == 0 { return "" - } else if header == "" { + } + if header == "" { return offers[0] } - spec, commaPos := "", 0 - for len(header) > 0 && commaPos != -1 { + // Parse header and get accepted types with their quality and specificity + // See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields + spec, commaPos, order := "", 0, 0 + acceptedTypes := make([]acceptedType, 0, 20) + for len(header) > 0 { + order++ + + // Skip spaces + header = strings.TrimLeft(header, " ") + + // Get spec commaPos = strings.IndexByte(header, ',') if commaPos != -1 { spec = strings.TrimSpace(header[:commaPos]) } else { - spec = header + spec = strings.TrimLeft(header, " ") } + + // Get quality + quality := 1.0 if factorSign := strings.IndexByte(spec, ';'); factorSign != -1 { + factor := strings.Trim(spec[factorSign+1:], " ") + if strings.HasPrefix(factor, "q=") { + if q, err := fasthttp.ParseUfloat(utils.UnsafeBytes(factor[2:])); err == nil { + quality = q + } + } spec = spec[:factorSign] } - for _, offer := range offers { - // has star prefix - if len(spec) >= 1 && spec[len(spec)-1] == '*' { - return offer - } else if strings.HasPrefix(spec, offer) { - return offer + // Skip if quality is 0.0 + // See: https://www.rfc-editor.org/rfc/rfc9110#quality.values + if quality == 0.0 { + if commaPos != -1 { + header = header[commaPos+1:] + } else { + break } + continue } + + // Get specificity + specificity := 0 + // check for wildcard this could be a mime */* or a wildcard character * + if spec == "*/*" || spec == "*" { + specificity = 1 + } else if strings.HasSuffix(spec, "/*") { + specificity = 2 + } else if strings.IndexByte(spec, '/') != -1 { + specificity = 3 + } else { + specificity = 4 + } + + // Add to accepted types + acceptedTypes = append(acceptedTypes, acceptedType{spec, quality, specificity, order}) + + // Next if commaPos != -1 { header = header[commaPos+1:] + } else { + break + } + } + + if len(acceptedTypes) > 1 { + // Sort accepted types by quality and specificity, preserving order of equal elements + sortAcceptedTypes(&acceptedTypes) + } + + // Find the first offer that matches the accepted types + for _, acceptedType := range acceptedTypes { + for _, offer := range offers { + if len(offer) == 0 { + continue + } + if isAccepted(acceptedType.spec, offer) { + return offer + } } } return "" } -func matchEtag(s string, etag string) bool { +// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements +// +// Parameters are not supported, they are ignored when sorting by specificity. +// +// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields +func sortAcceptedTypes(at *[]acceptedType) { + if at == nil || len(*at) < 2 { + return + } + acceptedTypes := *at + + for i := 1; i < len(acceptedTypes); i++ { + lo, hi := 0, i-1 + for lo <= hi { + mid := (lo + hi) / 2 + if acceptedTypes[i].quality < acceptedTypes[mid].quality || + (acceptedTypes[i].quality == acceptedTypes[mid].quality && acceptedTypes[i].specificity < acceptedTypes[mid].specificity) || + (acceptedTypes[i].quality == acceptedTypes[mid].quality && acceptedTypes[i].specificity == acceptedTypes[mid].specificity && acceptedTypes[i].order > acceptedTypes[mid].order) { + lo = mid + 1 + } else { + hi = mid - 1 + } + } + for j := i; j > lo; j-- { + acceptedTypes[j-1], acceptedTypes[j] = acceptedTypes[j], acceptedTypes[j-1] + } + } +} + +func matchEtag(s, etag string) bool { if s == etag || s == "W/"+etag || "W/"+s == etag { return true } @@ -266,7 +418,7 @@ func (app *App) isEtagStale(etag string, noneMatchBytes []byte) bool { return !matchEtag(app.getString(noneMatchBytes[start:end]), etag) } -func parseAddr(raw string) (host, port string) { +func parseAddr(raw string) (string, string) { //nolint:revive // Returns (host, port) if i := strings.LastIndex(raw, ":"); i != -1 { return raw[:i], raw[i+1:] } @@ -306,21 +458,21 @@ type testConn struct { w bytes.Buffer } -func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } -func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } -func (c *testConn) Close() error { return nil } +func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } //nolint:wrapcheck // This must not be wrapped +func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } //nolint:wrapcheck // This must not be wrapped +func (*testConn) Close() error { return nil } -func (c *testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } -func (c *testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } -func (c *testConn) SetDeadline(_ time.Time) error { return nil } -func (c *testConn) SetReadDeadline(_ time.Time) error { return nil } -func (c *testConn) SetWriteDeadline(_ time.Time) error { return nil } +func (*testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } +func (*testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } +func (*testConn) SetDeadline(_ time.Time) error { return nil } +func (*testConn) SetReadDeadline(_ time.Time) error { return nil } +func (*testConn) SetWriteDeadline(_ time.Time) error { return nil } -var getStringImmutable = func(b []byte) string { +func getStringImmutable(b []byte) string { return string(b) } -var getBytesImmutable = func(s string) (b []byte) { +func getBytesImmutable(s string) []byte { return []byte(s) } @@ -328,6 +480,7 @@ var getBytesImmutable = func(s string) (b []byte) { func (app *App) methodInt(s string) int { // For better performance if len(app.configured.RequestMethods) == 0 { + // TODO: Use iota instead switch s { case MethodGet: return 0 @@ -384,8 +537,7 @@ func IsMethodIdempotent(m string) bool { } switch m { - case MethodPut, - MethodDelete: + case MethodPut, MethodDelete: return true default: return false @@ -705,7 +857,7 @@ const ( ConstraintBool = "bool" ConstraintFloat = "float" ConstraintAlpha = "alpha" - ConstraintGuid = "guid" + ConstraintGUID = "guid" ConstraintMinLen = "minLen" ConstraintMaxLen = "maxLen" ConstraintLen = "len" @@ -719,3 +871,12 @@ const ( ConstraintDatetime = "datetime" ConstraintRegex = "regex" ) + +func IndexRune(str string, needle int32) bool { + for _, b := range str { + if b == needle { + return true + } + } + return false +} diff --git a/helpers_test.go b/helpers_test.go index bba282b229..37d975d453 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -15,7 +15,130 @@ import ( "github.com/valyala/fasthttp" ) +func Test_Utils_GetOffer(t *testing.T) { + t.Parallel() + require.Equal(t, "", getOffer("hello", acceptsOffer)) + require.Equal(t, "1", getOffer("", acceptsOffer, "1")) + require.Equal(t, "", getOffer("2", acceptsOffer, "1")) + + require.Equal(t, "", getOffer("", acceptsOfferType)) + require.Equal(t, "", getOffer("text/html", acceptsOfferType)) + require.Equal(t, "", getOffer("text/html", acceptsOfferType, "application/json")) + require.Equal(t, "", getOffer("text/html;q=0", acceptsOfferType, "text/html")) + require.Equal(t, "", getOffer("application/json, */*; q=0", acceptsOfferType, "image/png")) + require.Equal(t, "application/xml", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "application/xml", "application/json")) + require.Equal(t, "text/html", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "text/html")) + require.Equal(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json")) + require.Equal(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json")) + + require.Equal(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer)) + require.Equal(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "ascii")) + require.Equal(t, "utf-8", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "utf-8")) + require.Equal(t, "iso-8859-1", getOffer("utf-8;q=0, iso-8859-1;q=0.5", acceptsOffer, "utf-8", "iso-8859-1")) + + require.Equal(t, "deflate", getOffer("gzip, deflate", acceptsOffer, "deflate")) + require.Equal(t, "", getOffer("gzip, deflate;q=0", acceptsOffer, "deflate")) +} + +func Benchmark_Utils_GetOffer(b *testing.B) { + headers := []string{ + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "application/json", + "utf-8, iso-8859-1;q=0.5", + "gzip, deflate", + } + offers := [][]string{ + {"text/html", "application/xml", "application/xml+xhtml"}, + {"application/json"}, + {"utf-8"}, + {"deflate"}, + } + for n := 0; n < b.N; n++ { + for i, header := range headers { + getOffer(header, acceptsOfferType, offers[i]...) + } + } +} + +func Test_Utils_SortAcceptedTypes(t *testing.T) { + t.Parallel() + acceptedTypes := []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + } + sortAcceptedTypes(&acceptedTypes) + require.Equal(t, acceptedTypes, []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + }) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4 +func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { + acceptedTypes := make([]acceptedType, 3) + for n := 0; n < b.N; n++ { + acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} + acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} + acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} + sortAcceptedTypes(&acceptedTypes) + } + require.Equal(b, "text/html", acceptedTypes[0].spec) + require.Equal(b, "text/*", acceptedTypes[1].spec) + require.Equal(b, "*/*", acceptedTypes[2].spec) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4 +func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { + acceptedTypes := make([]acceptedType, 11) + for n := 0; n < b.N; n++ { + acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0} + acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1} + acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} + acceptedTypes[3] = acceptedType{spec: "application/json", quality: 0.999, specificity: 3, order: 3} + acceptedTypes[4] = acceptedType{spec: "application/xml", quality: 1, specificity: 3, order: 4} + acceptedTypes[5] = acceptedType{spec: "application/pdf", quality: 1, specificity: 3, order: 5} + acceptedTypes[6] = acceptedType{spec: "image/png", quality: 1, specificity: 3, order: 6} + acceptedTypes[7] = acceptedType{spec: "image/jpeg", quality: 1, specificity: 3, order: 7} + acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8} + acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9} + acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10} + sortAcceptedTypes(&acceptedTypes) + } + require.Equal(b, acceptedTypes, []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + }) +} + func Test_Utils_UniqueRouteStack(t *testing.T) { + t.Parallel() route1 := &Route{} route2 := &Route{} route3 := &Route{} @@ -40,7 +163,6 @@ func Test_Utils_UniqueRouteStack(t *testing.T) { route2, route3, })) - } func Test_Utils_getGroupPath(t *testing.T) { @@ -92,6 +214,7 @@ func Benchmark_Utils_Unescape(b *testing.B) { } func Test_Utils_Parse_Address(t *testing.T) { + t.Parallel() testCases := []struct { addr, host, port string }{ @@ -107,13 +230,8 @@ func Test_Utils_Parse_Address(t *testing.T) { } } -func Test_Utils_GetOffset(t *testing.T) { - require.Equal(t, "", getOffer("hello")) - require.Equal(t, "1", getOffer("", "1")) - require.Equal(t, "", getOffer("2", "1")) -} - func Test_Utils_TestConn_Deadline(t *testing.T) { + t.Parallel() conn := &testConn{} require.Nil(t, conn.SetDeadline(time.Time{})) require.Nil(t, conn.SetReadDeadline(time.Time{})) @@ -121,6 +239,7 @@ func Test_Utils_TestConn_Deadline(t *testing.T) { } func Test_Utils_IsNoCache(t *testing.T) { + t.Parallel() testCases := []struct { string bool @@ -139,7 +258,6 @@ func Test_Utils_IsNoCache(t *testing.T) { ok := isNoCache(c.string) require.Equal(t, c.bool, ok, fmt.Sprintf("want %t, got isNoCache(%s)=%t", c.bool, c.string, ok)) - } } @@ -192,12 +310,3 @@ func Benchmark_SlashRecognition(b *testing.B) { require.True(b, result) }) } - -func IndexRune(str string, needle int32) bool { - for _, b := range str { - if b == needle { - return true - } - } - return false -} diff --git a/hooks.go b/hooks.go index b2766a8cab..6b0b860c90 100644 --- a/hooks.go +++ b/hooks.go @@ -1,14 +1,20 @@ package fiber +import ( + "github.com/gofiber/fiber/v2/log" +) + // OnRouteHandler Handlers define a function to create hooks for Fiber. -type OnRouteHandler = func(Route) error -type OnNameHandler = OnRouteHandler -type OnGroupHandler = func(Group) error -type OnGroupNameHandler = OnGroupHandler -type OnListenHandler = func() error -type OnShutdownHandler = OnListenHandler -type OnForkHandler = func(int) error -type OnMountHandler = func(*App) error +type ( + OnRouteHandler = func(Route) error + OnNameHandler = OnRouteHandler + OnGroupHandler = func(Group) error + OnGroupNameHandler = OnGroupHandler + OnListenHandler = func(ListenData) error + OnShutdownHandler = func() error + OnForkHandler = func(int) error + OnMountHandler = func(*App) error +) // Hooks is a struct to use it with App. type Hooks struct { @@ -26,6 +32,13 @@ type Hooks struct { onMount []OnMountHandler } +// ListenData is a struct to use it with OnListenHandler +type ListenData struct { + Host string + Port string + TLS bool +} + func newHooks(app *App) *Hooks { return &Hooks{ app: app, @@ -168,9 +181,9 @@ func (h *Hooks) executeOnGroupNameHooks(group Group) error { return nil } -func (h *Hooks) executeOnListenHooks() error { +func (h *Hooks) executeOnListenHooks(listenData ListenData) error { for _, v := range h.onListen { - if err := v(); err != nil { + if err := v(listenData); err != nil { return err } } @@ -180,13 +193,17 @@ func (h *Hooks) executeOnListenHooks() error { func (h *Hooks) executeOnShutdownHooks() { for _, v := range h.onShutdown { - _ = v() + if err := v(); err != nil { + log.Errorf("failed to call shutdown hook: %v", err) + } } } func (h *Hooks) executeOnForkHooks(pid int) { for _, v := range h.onFork { - _ = v(pid) + if err := v(pid); err != nil { + log.Errorf("failed to call fork hook: %v", err) + } } } diff --git a/hooks_test.go b/hooks_test.go index b4cb642014..ff53cc64bb 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -10,13 +10,12 @@ import ( "github.com/valyala/bytebufferpool" ) -var testSimpleHandler = func(c Ctx) error { +func testSimpleHandler(c Ctx) error { return c.SendString("simple") } func Test_Hook_OnRoute(t *testing.T) { t.Parallel() - app := New() app.Hooks().OnRoute(func(r Route) error { @@ -35,7 +34,6 @@ func Test_Hook_OnRoute(t *testing.T) { func Test_Hook_OnRoute_Mount(t *testing.T) { t.Parallel() - app := New() subApp := New() app.Use("/sub", subApp) @@ -58,7 +56,6 @@ func Test_Hook_OnRoute_Mount(t *testing.T) { func Test_Hook_OnName(t *testing.T) { t.Parallel() - app := New() buf := bytebufferpool.Get() @@ -84,7 +81,6 @@ func Test_Hook_OnName(t *testing.T) { func Test_Hook_OnName_Error(t *testing.T) { t.Parallel() - app := New() defer func() { if err := recover(); err != nil { @@ -101,7 +97,6 @@ func Test_Hook_OnName_Error(t *testing.T) { func Test_Hook_OnGroup(t *testing.T) { t.Parallel() - app := New() buf := bytebufferpool.Get() @@ -121,7 +116,6 @@ func Test_Hook_OnGroup(t *testing.T) { func Test_Hook_OnGroup_Mount(t *testing.T) { t.Parallel() - app := New() micro := New() micro.Use("/john", app) @@ -139,12 +133,14 @@ func Test_Hook_OnGroup_Mount(t *testing.T) { func Test_Hook_OnGroupName(t *testing.T) { t.Parallel() - app := New() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) + buf2 := bytebufferpool.Get() + defer bytebufferpool.Put(buf2) + app.Hooks().OnGroupName(func(g Group) error { _, err := buf.WriteString(g.name) require.NoError(t, nil, err) @@ -152,16 +148,23 @@ func Test_Hook_OnGroupName(t *testing.T) { return nil }) + app.Hooks().OnName(func(r Route) error { + _, err := buf2.WriteString(r.Name) + require.NoError(t, err) + + return nil + }) + grp := app.Group("/x").Name("x.") - grp.Get("/test", testSimpleHandler) + grp.Get("/test", testSimpleHandler).Name("test") grp.Get("/test2", testSimpleHandler) require.Equal(t, "x.", buf.String()) + require.Equal(t, "x.test", buf2.String()) } func Test_Hook_OnGroupName_Error(t *testing.T) { t.Parallel() - app := New() defer func() { if err := recover(); err != nil { @@ -179,7 +182,6 @@ func Test_Hook_OnGroupName_Error(t *testing.T) { func Test_Hook_OnShutdown(t *testing.T) { t.Parallel() - app := New() buf := bytebufferpool.Get() @@ -204,7 +206,30 @@ func Test_Hook_OnListen(t *testing.T) { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - app.Hooks().OnListen(func() error { + app.Hooks().OnListen(func(listenData ListenData) error { + _, err := buf.WriteString("ready") + require.NoError(t, err) + + return nil + }) + + go func() { + time.Sleep(1000 * time.Millisecond) + require.Equal(t, nil, app.Shutdown()) + }() + require.Equal(t, nil, app.Listen(":9000")) + + require.Equal(t, "ready", buf.String()) +} + +func Test_Hook_OnListenPrefork(t *testing.T) { + t.Parallel() + app := New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnListen(func(listenData ListenData) error { _, err := buf.WriteString("ready") require.NoError(t, nil, err) @@ -216,17 +241,17 @@ func Test_Hook_OnListen(t *testing.T) { require.Nil(t, app.Shutdown()) }() - require.Nil(t, app.Listen(":9000", ListenConfig{DisableStartupMessage: true})) + require.Nil(t, app.Listen(":9000", ListenConfig{DisableStartupMessage: true, EnablePrefork: true})) require.Equal(t, "ready", buf.String()) } func Test_Hook_OnHook(t *testing.T) { + app := New() + // Reset test var testPreforkMaster = true testOnPrefork = true - app := New() - go func() { time.Sleep(1000 * time.Millisecond) require.Nil(t, app.Shutdown()) @@ -242,7 +267,6 @@ func Test_Hook_OnHook(t *testing.T) { func Test_Hook_OnMount(t *testing.T) { t.Parallel() - app := New() app.Get("/", testSimpleHandler).Name("x") diff --git a/internal/memory/memory.go b/internal/memory/memory.go index fe23a2581e..cf2b3cac11 100644 --- a/internal/memory/memory.go +++ b/internal/memory/memory.go @@ -4,7 +4,6 @@ package memory import ( "sync" - "sync/atomic" "time" "github.com/gofiber/utils/v2" @@ -35,7 +34,7 @@ func (s *Storage) Get(key string) any { s.RLock() v, ok := s.data[key] s.RUnlock() - if !ok || v.e != 0 && v.e <= atomic.LoadUint32(&utils.Timestamp) { + if !ok || v.e != 0 && v.e <= utils.Timestamp() { return nil } return v.v @@ -45,7 +44,7 @@ func (s *Storage) Get(key string) any { func (s *Storage) Set(key string, val any, ttl time.Duration) { var exp uint32 if ttl > 0 { - exp = uint32(ttl.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + exp = uint32(ttl.Seconds()) + utils.Timestamp() } i := item{exp, val} s.Lock() @@ -73,28 +72,25 @@ func (s *Storage) gc(sleep time.Duration) { defer ticker.Stop() var expired []string - for { - select { - case <-ticker.C: - ts := atomic.LoadUint32(&utils.Timestamp) - expired = expired[:0] - s.RLock() - for key, v := range s.data { - if v.e != 0 && v.e <= ts { - expired = append(expired, key) - } + for range ticker.C { + ts := utils.Timestamp() + expired = expired[:0] + s.RLock() + for key, v := range s.data { + if v.e != 0 && v.e <= ts { + expired = append(expired, key) } - s.RUnlock() - s.Lock() - // Double-checked locking. - // We might have replaced the item in the meantime. - for i := range expired { - v := s.data[expired[i]] - if v.e != 0 && v.e <= ts { - delete(s.data, expired[i]) - } + } + s.RUnlock() + s.Lock() + // Double-checked locking. + // We might have replaced the item in the meantime. + for i := range expired { + v := s.data[expired[i]] + if v.e != 0 && v.e <= ts { + delete(s.data, expired[i]) } - s.Unlock() } + s.Unlock() } } diff --git a/internal/memory/memory_test.go b/internal/memory/memory_test.go index dfc1cb28c4..76a751b78d 100644 --- a/internal/memory/memory_test.go +++ b/internal/memory/memory_test.go @@ -11,7 +11,8 @@ import ( // go test -run Test_Memory -v -race func Test_Memory(t *testing.T) { - var store = New() + t.Parallel() + store := New() var ( key = "john" val any = []byte("doe") @@ -75,7 +76,6 @@ func Benchmark_Memory(b *testing.B) { } for _, key := range keys { d.Delete(key) - } } }) diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index 9efd521fb4..279728f58d 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -4,7 +4,6 @@ package memory import ( "sync" - "sync/atomic" "time" "github.com/gofiber/utils/v2" @@ -51,7 +50,7 @@ func (s *Storage) Get(key string) ([]byte, error) { s.mux.RLock() v, ok := s.db[key] s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { + if !ok || v.expiry != 0 && v.expiry <= utils.Timestamp() { return nil, nil } @@ -67,7 +66,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { var expire uint32 if exp != 0 { - expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + expire = uint32(exp.Seconds()) + utils.Timestamp() } e := entry{val, expire} @@ -114,7 +113,7 @@ func (s *Storage) gc() { case <-s.done: return case <-ticker.C: - ts := atomic.LoadUint32(&utils.Timestamp) + ts := utils.Timestamp() expired = expired[:0] s.mux.RLock() for id, v := range s.db { @@ -139,5 +138,7 @@ func (s *Storage) gc() { // Return database client func (s *Storage) Conn() map[string]entry { + s.mux.RLock() + defer s.mux.RUnlock() return s.db } diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go index 00dcf86b4a..ad2ee21ef3 100644 --- a/internal/storage/memory/memory_test.go +++ b/internal/storage/memory/memory_test.go @@ -11,6 +11,7 @@ import ( var testStore = New() func Test_Storage_Memory_Set(t *testing.T) { + t.Parallel() var ( key = "john" val = []byte("doe") @@ -21,6 +22,7 @@ func Test_Storage_Memory_Set(t *testing.T) { } func Test_Storage_Memory_Set_Override(t *testing.T) { + t.Parallel() var ( key = "john" val = []byte("doe") @@ -34,6 +36,7 @@ func Test_Storage_Memory_Set_Override(t *testing.T) { } func Test_Storage_Memory_Get(t *testing.T) { + t.Parallel() var ( key = "john" val = []byte("doe") @@ -48,6 +51,7 @@ func Test_Storage_Memory_Get(t *testing.T) { } func Test_Storage_Memory_Set_Expiration(t *testing.T) { + t.Parallel() var ( key = "john" val = []byte("doe") @@ -61,9 +65,7 @@ func Test_Storage_Memory_Set_Expiration(t *testing.T) { } func Test_Storage_Memory_Get_Expired(t *testing.T) { - var ( - key = "john" - ) + key := "john" result, err := testStore.Get(key) require.NoError(t, err) @@ -71,6 +73,7 @@ func Test_Storage_Memory_Get_Expired(t *testing.T) { } func Test_Storage_Memory_Get_NotExist(t *testing.T) { + t.Parallel() result, err := testStore.Get("notexist") require.NoError(t, err) @@ -78,6 +81,7 @@ func Test_Storage_Memory_Get_NotExist(t *testing.T) { } func Test_Storage_Memory_Delete(t *testing.T) { + t.Parallel() var ( key = "john" val = []byte("doe") @@ -95,9 +99,8 @@ func Test_Storage_Memory_Delete(t *testing.T) { } func Test_Storage_Memory_Reset(t *testing.T) { - var ( - val = []byte("doe") - ) + t.Parallel() + val := []byte("doe") err := testStore.Set("john1", val, 0) require.NoError(t, err) @@ -118,10 +121,12 @@ func Test_Storage_Memory_Reset(t *testing.T) { } func Test_Storage_Memory_Close(t *testing.T) { + t.Parallel() require.NoError(t, testStore.Close()) } func Test_Storage_Memory_Conn(t *testing.T) { + t.Parallel() require.True(t, testStore.Conn() != nil) } diff --git a/listen.go b/listen.go index 1ecaa87318..feea47e73b 100644 --- a/listen.go +++ b/listen.go @@ -9,7 +9,6 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "log" "net" "os" "path/filepath" @@ -20,6 +19,7 @@ import ( "strings" "text/tabwriter" + "github.com/gofiber/fiber/v3/log" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" ) @@ -32,6 +32,10 @@ var figletFiberText = ` / __/ / / /_/ / __/ / /_/ /_/_.___/\___/_/ %s` +const ( + globalIpv4Addr = "0.0.0.0" +) + // ListenConfig is a struct to customize startup of Fiber. // // TODO: Add timeout for graceful shutdown. @@ -63,7 +67,7 @@ type ListenConfig struct { // GracefulContext is a field to shutdown Fiber by given context gracefully. // // Default: nil - GracefulContext context.Context `json:"graceful_context"` + GracefulContext context.Context `json:"graceful_context"` //nolint:containedctx // It's needed to set context inside Listen. // TLSConfigFunc allows customizing tls.Config as you want. // @@ -112,7 +116,7 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { return ListenConfig{ ListenerNetwork: NetworkTCP4, OnShutdownError: func(err error) { - log.Fatalf("shutdown: %v", err) + log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn }, } } @@ -124,7 +128,7 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { if cfg.OnShutdownError == nil { cfg.OnShutdownError = func(err error) { - log.Fatalf("shutdown: %v", err) + log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn } } @@ -141,11 +145,11 @@ func (app *App) Listen(addr string, config ...ListenConfig) error { cfg := listenConfigDefault(config...) // Configure TLS - var tlsConfig *tls.Config = nil + var tlsConfig *tls.Config if cfg.CertFile != "" && cfg.CertKeyFile != "" { cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.CertKeyFile) if err != nil { - return fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %s", cfg.CertFile, cfg.CertKeyFile, err) + return fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w", cfg.CertFile, cfg.CertKeyFile, err) } tlsHandler := &TLSHandler{} @@ -160,7 +164,7 @@ func (app *App) Listen(addr string, config ...ListenConfig) error { if cfg.CertClientFile != "" { clientCACert, err := os.ReadFile(filepath.Clean(cfg.CertClientFile)) if err != nil { - return err + return fmt.Errorf("failed to read file: %w", err) } clientCertPool := x509.NewCertPool() @@ -194,12 +198,15 @@ func (app *App) Listen(addr string, config ...ListenConfig) error { // Configure Listener ln, err := app.createListener(addr, tlsConfig, cfg) if err != nil { - return err + return fmt.Errorf("failed to listen: %w", err) } // prepare the server for the start app.startupProcess() + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, cfg)) + // Print startup message & routes app.printMessages(cfg, ln) @@ -229,6 +236,9 @@ func (app *App) Listener(ln net.Listener, config ...ListenConfig) error { // prepare the server for the start app.startupProcess() + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, cfg)) + // Print startup message & routes app.printMessages(cfg, ln) @@ -241,14 +251,14 @@ func (app *App) Listener(ln net.Listener, config ...ListenConfig) error { // Prefork is not supported for custom listeners if cfg.EnablePrefork { - fmt.Println("[Warning] Prefork isn't supported for custom listeners.") + log.Warn("Prefork isn't supported for custom listeners.") } return app.server.Serve(ln) } // Create listener function. -func (app *App) createListener(addr string, tlsConfig *tls.Config, cfg ListenConfig) (net.Listener, error) { +func (*App) createListener(addr string, tlsConfig *tls.Config, cfg ListenConfig) (net.Listener, error) { var listener net.Listener var err error @@ -262,13 +272,18 @@ func (app *App) createListener(addr string, tlsConfig *tls.Config, cfg ListenCon cfg.ListenerAddrFunc(listener.Addr()) } + // Wrap error comes from tls.Listen/net.Listen + if err != nil { + err = fmt.Errorf("failed to listen: %w", err) + } + return listener, err } func (app *App) printMessages(cfg ListenConfig, ln net.Listener) { // Print startup message if !cfg.DisableStartupMessage { - app.startupMessage(ln.Addr().String(), getTlsConfig(ln) != nil, "", cfg) + app.startupMessage(ln.Addr().String(), getTLSConfig(ln) != nil, "", cfg) } // Print routes @@ -277,8 +292,26 @@ func (app *App) printMessages(cfg ListenConfig, ln net.Listener) { } } +// prepareListenData create an slice of ListenData +func (*App) prepareListenData(addr string, isTLS bool, cfg ListenConfig) ListenData { //revive:disable-line:flag-parameter // Accepting a bool param named isTLS if fine here + host, port := parseAddr(addr) + if host == "" { + if cfg.ListenerNetwork == NetworkTCP6 { + host = "[::1]" + } else { + host = globalIpv4Addr + } + } + + return ListenData{ + Host: host, + Port: port, + TLS: isTLS, + } +} + // startupMessage prepares the startup message with the handler number, port, address and other information -func (app *App) startupMessage(addr string, tls bool, pids string, cfg ListenConfig) { +func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenConfig) { //nolint: revive // Accepting a bool param named isTLS if fine here // ignore child processes if IsChild() { return @@ -292,13 +325,13 @@ func (app *App) startupMessage(addr string, tls bool, pids string, cfg ListenCon if cfg.ListenerNetwork == NetworkTCP6 { host = "[::1]" } else { - host = "0.0.0.0" + host = globalIpv4Addr } } - scheme := "http" - if tls { - scheme = "https" + scheme := schemeHTTP + if isTLS { + scheme = schemeHTTPS } isPrefork := "Disabled" @@ -389,7 +422,7 @@ func (app *App) printRoutesMessage() { var routes []RouteMessage for _, routeStack := range app.stack { for _, route := range routeStack { - var newRoute = RouteMessage{} + var newRoute RouteMessage newRoute.name = route.Name newRoute.method = route.Method newRoute.path = route.Path @@ -411,20 +444,20 @@ func (app *App) printRoutesMessage() { return routes[i].path < routes[j].path }) - _, _ = fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow) - _, _ = fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow) + _, _ = fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) + _, _ = fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) for _, route := range routes { - _, _ = fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers) + _, _ = fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset) } - _ = w.Flush() + _ = w.Flush() //nolint:errcheck // It is fine to ignore the error here } // shutdown goroutine func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) { <-ctx.Done() - if err := app.Shutdown(); err != nil { + if err := app.Shutdown(); err != nil { //nolint:contextcheck // TODO: Implement it cfg.OnShutdownError(err) } diff --git a/listen_test.go b/listen_test.go index de482e4331..8c8cc0dd68 100644 --- a/listen_test.go +++ b/listen_test.go @@ -1,3 +1,4 @@ +//nolint:wrapcheck // We must not wrap errors in tests package fiber import ( @@ -118,7 +119,6 @@ func Test_Listen_TLS(t *testing.T) { CertFile: "./.github/testdata/ssl.pem", CertKeyFile: "./.github/testdata/ssl.key", })) - } // go test -run Test_Listen_TLS_Prefork @@ -146,7 +146,6 @@ func Test_Listen_TLS_Prefork(t *testing.T) { CertFile: "./.github/testdata/ssl.pem", CertKeyFile: "./.github/testdata/ssl.key", })) - } // go test -run Test_Listen_MutualTLS @@ -170,7 +169,6 @@ func Test_Listen_MutualTLS(t *testing.T) { CertKeyFile: "./.github/testdata/ssl.key", CertClientFile: "./.github/testdata/ca-chain.cert.pem", })) - } // go test -run Test_Listen_MutualTLS_Prefork @@ -200,7 +198,6 @@ func Test_Listen_MutualTLS_Prefork(t *testing.T) { CertKeyFile: "./.github/testdata/ssl.key", CertClientFile: "./.github/testdata/ca-chain.cert.pem", })) - } // go test -run Test_Listener @@ -217,13 +214,16 @@ func Test_Listener(t *testing.T) { } func Test_App_Listener_TLS_Listener(t *testing.T) { + t.Parallel() // Create tls certificate cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") if err != nil { require.NoError(t, err) } + //nolint:gosec // We're in a test so using old ciphers is fine config := &tls.Config{Certificates: []tls.Certificate{cer}} + //nolint:gosec // We're in a test so listening on all interfaces is fine ln, err := tls.Listen(NetworkTCP4, ":0", config) require.NoError(t, err) @@ -350,7 +350,6 @@ func Test_Listen_Master_Process_Show_Startup_Message(t *testing.T) { startupMessage(":3000", true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10), cfg) }) colors := Colors{} - fmt.Println(startupMessage) require.True(t, strings.Contains(startupMessage, "https://127.0.0.1:3000")) require.True(t, strings.Contains(startupMessage, "(bound on host 0.0.0.0 and port 3000)")) require.True(t, strings.Contains(startupMessage, "Child PIDs")) @@ -368,7 +367,6 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithAppName(t *testing.T) { startupMessage := captureOutput(func() { app.startupMessage(":3000", true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10), cfg) }) - fmt.Println(startupMessage) require.Equal(t, "Test App v3.0.0", app.Config().AppName) require.True(t, strings.Contains(startupMessage, app.Config().AppName)) } @@ -385,7 +383,6 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithAppNameNonAscii(t *testi startupMessage := captureOutput(func() { app.startupMessage(":3000", false, "", cfg) }) - fmt.Println(startupMessage) require.True(t, strings.Contains(startupMessage, "Serveur de vérification des données")) } @@ -401,7 +398,6 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithDisabledPreforkAndCustom app.startupMessage("server.com:8081", true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 5), cfg) }) colors := Colors{} - fmt.Println(startupMessage) require.True(t, strings.Contains(startupMessage, fmt.Sprintf("%sINFO%s", colors.Green, colors.Reset))) require.True(t, strings.Contains(startupMessage, fmt.Sprintf("%s%s%s", colors.Blue, appName, colors.Reset))) require.True(t, strings.Contains(startupMessage, fmt.Sprintf("%s%s%s", colors.Blue, "https://server.com:8081", colors.Reset))) @@ -415,8 +411,7 @@ func Test_Listen_Print_Route(t *testing.T) { printRoutesMessage := captureOutput(func() { app.printRoutesMessage() }) - fmt.Println(printRoutesMessage) - require.True(t, strings.Contains(printRoutesMessage, "GET")) + require.True(t, strings.Contains(printRoutesMessage, MethodGet)) require.True(t, strings.Contains(printRoutesMessage, "/")) require.True(t, strings.Contains(printRoutesMessage, "emptyHandler")) require.True(t, strings.Contains(printRoutesMessage, "routeName")) @@ -436,7 +431,7 @@ func Test_Listen_Print_Route_With_Group(t *testing.T) { app.printRoutesMessage() }) - require.True(t, strings.Contains(printRoutesMessage, "GET")) + require.True(t, strings.Contains(printRoutesMessage, MethodGet)) require.True(t, strings.Contains(printRoutesMessage, "/")) require.True(t, strings.Contains(printRoutesMessage, "emptyHandler")) require.True(t, strings.Contains(printRoutesMessage, "/v1/test")) diff --git a/log/default.go b/log/default.go new file mode 100644 index 0000000000..e78f1e3daf --- /dev/null +++ b/log/default.go @@ -0,0 +1,205 @@ +package log + +import ( + "context" + "fmt" + "io" + "log" + "os" + "sync" + + "github.com/valyala/bytebufferpool" +) + +var _ AllLogger = (*defaultLogger)(nil) + +type defaultLogger struct { + stdlog *log.Logger + level Level + depth int +} + +// privateLog logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLog(lv Level, fmtArgs []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(fmt.Sprint(fmtArgs...)) //nolint:errcheck // It is fine to ignore the error + + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +// privateLog logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + + if len(fmtArgs) > 0 { + _, _ = fmt.Fprintf(buf, format, fmtArgs...) + } else { + _, _ = fmt.Fprint(buf, fmtArgs...) + } + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +// privateLogw logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + + // Write format privateLog buffer + if format != "" { + _, _ = buf.WriteString(format) //nolint:errcheck // It is fine to ignore the error + } + var once sync.Once + isFirst := true + // Write keys and values privateLog buffer + if len(keysAndValues) > 0 { + if (len(keysAndValues) & 1) == 1 { + keysAndValues = append(keysAndValues, "KEYVALS UNPAIRED") + } + + for i := 0; i < len(keysAndValues); i += 2 { + if format == "" && isFirst { + once.Do(func() { + _, _ = fmt.Fprintf(buf, "%s=%v", keysAndValues[i], keysAndValues[i+1]) + isFirst = false + }) + continue + } + _, _ = fmt.Fprintf(buf, " %s=%v", keysAndValues[i], keysAndValues[i+1]) + } + } + + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +func (l *defaultLogger) Trace(v ...interface{}) { + l.privateLog(LevelTrace, v) +} + +func (l *defaultLogger) Debug(v ...interface{}) { + l.privateLog(LevelDebug, v) +} + +func (l *defaultLogger) Info(v ...interface{}) { + l.privateLog(LevelInfo, v) +} + +func (l *defaultLogger) Warn(v ...interface{}) { + l.privateLog(LevelWarn, v) +} + +func (l *defaultLogger) Error(v ...interface{}) { + l.privateLog(LevelError, v) +} + +func (l *defaultLogger) Fatal(v ...interface{}) { + l.privateLog(LevelFatal, v) +} + +func (l *defaultLogger) Panic(v ...interface{}) { + l.privateLog(LevelPanic, v) +} + +func (l *defaultLogger) Tracef(format string, v ...interface{}) { + l.privateLogf(LevelTrace, format, v) +} + +func (l *defaultLogger) Debugf(format string, v ...interface{}) { + l.privateLogf(LevelDebug, format, v) +} + +func (l *defaultLogger) Infof(format string, v ...interface{}) { + l.privateLogf(LevelInfo, format, v) +} + +func (l *defaultLogger) Warnf(format string, v ...interface{}) { + l.privateLogf(LevelWarn, format, v) +} + +func (l *defaultLogger) Errorf(format string, v ...interface{}) { + l.privateLogf(LevelError, format, v) +} + +func (l *defaultLogger) Fatalf(format string, v ...interface{}) { + l.privateLogf(LevelFatal, format, v) +} + +func (l *defaultLogger) Panicf(format string, v ...interface{}) { + l.privateLogf(LevelPanic, format, v) +} + +func (l *defaultLogger) Tracew(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelTrace, msg, keysAndValues) +} + +func (l *defaultLogger) Debugw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelDebug, msg, keysAndValues) +} + +func (l *defaultLogger) Infow(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelInfo, msg, keysAndValues) +} + +func (l *defaultLogger) Warnw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelWarn, msg, keysAndValues) +} + +func (l *defaultLogger) Errorw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelError, msg, keysAndValues) +} + +func (l *defaultLogger) Fatalw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelFatal, msg, keysAndValues) +} + +func (l *defaultLogger) Panicw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelPanic, msg, keysAndValues) +} + +func (l *defaultLogger) WithContext(_ context.Context) CommonLogger { + return l +} + +func (l *defaultLogger) SetLevel(level Level) { + l.level = level +} + +func (l *defaultLogger) SetOutput(writer io.Writer) { + l.stdlog.SetOutput(writer) +} + +// DefaultLogger returns the default logger. +func DefaultLogger() AllLogger { + return logger +} diff --git a/log/default_test.go b/log/default_test.go new file mode 100644 index 0000000000..7562b0a169 --- /dev/null +++ b/log/default_test.go @@ -0,0 +1,196 @@ +package log + +import ( + "bytes" + "context" + "log" + "os" + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +const work = "work" + +func initDefaultLogger() { + logger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", 0), + depth: 4, + } +} + +type byteSliceWriter struct { + b []byte +} + +func (w *byteSliceWriter) Write(p []byte) (int, error) { + w.b = append(w.b, p...) + return len(p), nil +} + +func Test_DefaultLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + Trace("trace work") + Debug("received work order") + Info("starting work") + Warn("work may fail") + Error("work failed") + Panic("work panic") + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_DefaultFormatLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + Tracef("trace %s", work) + Debugf("received %s order", work) + Infof("starting %s", work) + Warnf("%s may fail", work) + Errorf("%s failed", work) + Panicf("%s panic", work) + + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_CtxLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + ctx := context.Background() + + WithContext(ctx).Tracef("trace %s", work) + WithContext(ctx).Debugf("received %s order", work) + WithContext(ctx).Infof("starting %s", work) + WithContext(ctx).Warnf("%s may fail", work) + WithContext(ctx).Errorf("%s failed", work) + WithContext(ctx).Panicf("%s panic", work) + + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_LogfKeyAndValues(t *testing.T) { + tests := []struct { + name string + level Level + format string + fmtArgs []interface{} + keysAndValues []interface{} + wantOutput string + }{ + { + name: "test logf with debug level and key-values", + level: LevelDebug, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"name", "Bob", "age", 30}, + wantOutput: "[Debug] name=Bob age=30\n", + }, + { + name: "test logf with info level and key-values", + level: LevelInfo, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"status", "ok", "code", 200}, + wantOutput: "[Info] status=ok code=200\n", + }, + { + name: "test logf with warn level and key-values", + level: LevelWarn, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"error", "not found", "id", 123}, + wantOutput: "[Warn] error=not found id=123\n", + }, + { + name: "test logf with format and key-values", + level: LevelWarn, + format: "test", + fmtArgs: nil, + keysAndValues: []interface{}{"error", "not found", "id", 123}, + wantOutput: "[Warn] test error=not found id=123\n", + }, + { + name: "test logf with one key", + level: LevelWarn, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"error"}, + wantOutput: "[Warn] error=KEYVALS UNPAIRED\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: tt.level, + depth: 4, + } + l.privateLogw(tt.level, tt.format, tt.keysAndValues) + utils.AssertEqual(t, tt.wantOutput, buf.String()) + }) + } +} + +func Test_SetLevel(t *testing.T) { + setLogger := &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, + } + + setLogger.SetLevel(LevelTrace) + utils.AssertEqual(t, LevelTrace, setLogger.level) + utils.AssertEqual(t, LevelTrace.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelDebug) + utils.AssertEqual(t, LevelDebug, setLogger.level) + utils.AssertEqual(t, LevelDebug.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelInfo) + utils.AssertEqual(t, LevelInfo, setLogger.level) + utils.AssertEqual(t, LevelInfo.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelWarn) + utils.AssertEqual(t, LevelWarn, setLogger.level) + utils.AssertEqual(t, LevelWarn.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelError) + utils.AssertEqual(t, LevelError, setLogger.level) + utils.AssertEqual(t, LevelError.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelFatal) + utils.AssertEqual(t, LevelFatal, setLogger.level) + utils.AssertEqual(t, LevelFatal.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelPanic) + utils.AssertEqual(t, LevelPanic, setLogger.level) + utils.AssertEqual(t, LevelPanic.toString(), setLogger.level.toString()) + + setLogger.SetLevel(8) + utils.AssertEqual(t, 8, int(setLogger.level)) + utils.AssertEqual(t, "[?8] ", setLogger.level.toString()) +} diff --git a/log/fiberlog.go b/log/fiberlog.go new file mode 100644 index 0000000000..90333eef3d --- /dev/null +++ b/log/fiberlog.go @@ -0,0 +1,141 @@ +package log + +import ( + "context" + "io" +) + +// Fatal calls the default logger's Fatal method and then os.Exit(1). +func Fatal(v ...interface{}) { + logger.Fatal(v...) +} + +// Error calls the default logger's Error method. +func Error(v ...interface{}) { + logger.Error(v...) +} + +// Warn calls the default logger's Warn method. +func Warn(v ...interface{}) { + logger.Warn(v...) +} + +// Info calls the default logger's Info method. +func Info(v ...interface{}) { + logger.Info(v...) +} + +// Debug calls the default logger's Debug method. +func Debug(v ...interface{}) { + logger.Debug(v...) +} + +// Trace calls the default logger's Trace method. +func Trace(v ...interface{}) { + logger.Trace(v...) +} + +// Panic calls the default logger's Panic method. +func Panic(v ...interface{}) { + logger.Panic(v...) +} + +// Fatalf calls the default logger's Fatalf method and then os.Exit(1). +func Fatalf(format string, v ...interface{}) { + logger.Fatalf(format, v...) +} + +// Errorf calls the default logger's Errorf method. +func Errorf(format string, v ...interface{}) { + logger.Errorf(format, v...) +} + +// Warnf calls the default logger's Warnf method. +func Warnf(format string, v ...interface{}) { + logger.Warnf(format, v...) +} + +// Infof calls the default logger's Infof method. +func Infof(format string, v ...interface{}) { + logger.Infof(format, v...) +} + +// Debugf calls the default logger's Debugf method. +func Debugf(format string, v ...interface{}) { + logger.Debugf(format, v...) +} + +// Tracef calls the default logger's Tracef method. +func Tracef(format string, v ...interface{}) { + logger.Tracef(format, v...) +} + +// Panicf calls the default logger's Tracef method. +func Panicf(format string, v ...interface{}) { + logger.Panicf(format, v...) +} + +// Tracew logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Tracew(msg string, keysAndValues ...interface{}) { + logger.Tracew(msg, keysAndValues...) +} + +// Debugw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Debugw(msg string, keysAndValues ...interface{}) { + logger.Debugw(msg, keysAndValues...) +} + +// Infow logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Infow(msg string, keysAndValues ...interface{}) { + logger.Infow(msg, keysAndValues...) +} + +// Warnw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Warnw(msg string, keysAndValues ...interface{}) { + logger.Warnw(msg, keysAndValues...) +} + +// Errorw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Errorw(msg string, keysAndValues ...interface{}) { + logger.Errorw(msg, keysAndValues...) +} + +// Fatalw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Fatalw(msg string, keysAndValues ...interface{}) { + logger.Fatalw(msg, keysAndValues...) +} + +// Panicw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Panicw(msg string, keysAndValues ...interface{}) { + logger.Panicw(msg, keysAndValues...) +} + +func WithContext(ctx context.Context) CommonLogger { + return logger.WithContext(ctx) +} + +// SetLogger sets the default logger and the system logger. +// Note that this method is not concurrent-safe and must not be called +// after the use of DefaultLogger and global functions privateLog this package. +func SetLogger(v AllLogger) { + logger = v +} + +// SetOutput sets the output of default logger and system logger. By default, it is stderr. +func SetOutput(w io.Writer) { + logger.SetOutput(w) +} + +// SetLevel sets the level of logs below which logs will not be output. +// The default logger is LevelTrace. +// Note that this method is not concurrent-safe. +func SetLevel(lv Level) { + logger.SetLevel(lv) +} diff --git a/log/fiberlog_test.go b/log/fiberlog_test.go new file mode 100644 index 0000000000..15b1a2cd9d --- /dev/null +++ b/log/fiberlog_test.go @@ -0,0 +1,24 @@ +package log + +import ( + "log" + "os" + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +func Test_DefaultSystemLogger(t *testing.T) { + defaultL := DefaultLogger() + utils.AssertEqual(t, logger, defaultL) +} + +func Test_SetLogger(t *testing.T) { + setLog := &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 6, + } + + SetLogger(setLog) + utils.AssertEqual(t, logger, setLog) +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000000..31b4cc8af9 --- /dev/null +++ b/log/log.go @@ -0,0 +1,100 @@ +package log + +import ( + "context" + "fmt" + "io" + "log" + "os" +) + +var logger AllLogger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, +} + +// Logger is a logger interface that provides logging function with levels. +type Logger interface { + Trace(v ...interface{}) + Debug(v ...interface{}) + Info(v ...interface{}) + Warn(v ...interface{}) + Error(v ...interface{}) + Fatal(v ...interface{}) + Panic(v ...interface{}) +} + +// FormatLogger is a logger interface that output logs with a format. +type FormatLogger interface { + Tracef(format string, v ...interface{}) + Debugf(format string, v ...interface{}) + Infof(format string, v ...interface{}) + Warnf(format string, v ...interface{}) + Errorf(format string, v ...interface{}) + Fatalf(format string, v ...interface{}) + Panicf(format string, v ...interface{}) +} + +// WithLogger is a logger interface that output logs with a message and key-value pairs. +type WithLogger interface { + Tracew(msg string, keysAndValues ...interface{}) + Debugw(msg string, keysAndValues ...interface{}) + Infow(msg string, keysAndValues ...interface{}) + Warnw(msg string, keysAndValues ...interface{}) + Errorw(msg string, keysAndValues ...interface{}) + Fatalw(msg string, keysAndValues ...interface{}) + Panicw(msg string, keysAndValues ...interface{}) +} + +type CommonLogger interface { + Logger + FormatLogger + WithLogger +} + +// ControlLogger provides methods to config a logger. +type ControlLogger interface { + SetLevel(Level) + SetOutput(io.Writer) +} + +// AllLogger is the combination of Logger, FormatLogger, CtxLogger and ControlLogger. +// Custom extensions can be made through AllLogger +type AllLogger interface { + CommonLogger + ControlLogger + WithContext(ctx context.Context) CommonLogger +} + +// Level defines the priority of a log message. +// When a logger is configured with a level, any log message with a lower +// log level (smaller by integer comparison) will not be output. +type Level int + +// The levels of logs. +const ( + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarn + LevelError + LevelFatal + LevelPanic +) + +var strs = []string{ + "[Trace] ", + "[Debug] ", + "[Info] ", + "[Warn] ", + "[Error] ", + "[Fatal] ", + "[Panic] ", +} + +func (lv Level) toString() string { + if lv >= LevelTrace && lv <= LevelPanic { + return strs[lv] + } + return fmt.Sprintf("[?%d] ", lv) +} diff --git a/middleware/adaptor/adopter.go b/middleware/adaptor/adaptor.go similarity index 60% rename from middleware/adaptor/adopter.go rename to middleware/adaptor/adaptor.go index 2fe2a46536..79f779865a 100644 --- a/middleware/adaptor/adopter.go +++ b/middleware/adaptor/adaptor.go @@ -1,13 +1,11 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - package adaptor import ( "io" "net" "net/http" + "reflect" + "unsafe" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" @@ -29,6 +27,44 @@ func HTTPHandler(h http.Handler) fiber.Handler { } } +// ConvertRequest converts a fiber.Ctx to a http.Request. +// forServer should be set to true when the http.Request is going to be passed to a http.Handler. +func ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error) { + var req http.Request + if err := fasthttpadaptor.ConvertRequest(c.Context(), &req, forServer); err != nil { + return nil, err //nolint:wrapcheck // This must not be wrapped + } + return &req, nil +} + +// CopyContextToFiberContext copies the values of context.Context to a fasthttp.RequestCtx +func CopyContextToFiberContext(context interface{}, requestContext *fasthttp.RequestCtx) { + contextValues := reflect.ValueOf(context).Elem() + contextKeys := reflect.TypeOf(context).Elem() + if contextKeys.Kind() == reflect.Struct { + var lastKey interface{} + for i := 0; i < contextValues.NumField(); i++ { + reflectValue := contextValues.Field(i) + /* #nosec */ + reflectValue = reflect.NewAt(reflectValue.Type(), unsafe.Pointer(reflectValue.UnsafeAddr())).Elem() + + reflectField := contextKeys.Field(i) + + if reflectField.Name == "noCopy" { + break + } else if reflectField.Name == "Context" { + CopyContextToFiberContext(reflectValue.Interface(), requestContext) + } else if reflectField.Name == "key" { + lastKey = reflectValue.Interface() + } else if lastKey != nil && reflectField.Name == "val" { + requestContext.SetUserValue(lastKey, reflectValue.Interface()) + } else { + lastKey = nil + } + } + } +} + // HTTPMiddleware wraps net/http middleware to fiber middleware func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { return func(c fiber.Ctx) error { @@ -44,8 +80,13 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { c.Request().Header.Set(key, v) } } + CopyContextToFiberContext(r.Context(), c.Context()) }) - _ = HTTPHandler(mw(nextHandler))(c) + + if err := HTTPHandler(mw(nextHandler))(c); err != nil { + return err + } + if next { return c.Next() } @@ -80,8 +121,13 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) return } + req.Header.SetContentLength(len(body)) - _, _ = req.BodyWriter().Write(body) + _, err = req.BodyWriter().Write(body) + if err != nil { + http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) + return + } } req.Header.SetMethod(r.Method) req.SetRequestURI(r.RequestURI) @@ -91,7 +137,7 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { req.Header.Set(key, v) } } - if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil && err.(*net.AddrError).Err == "missing port in address" { + if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil && err.(*net.AddrError).Err == "missing port in address" { //nolint:errorlint, forcetypeassert // overlinting r.RemoteAddr = net.JoinHostPort(r.RemoteAddr, "80") } remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) @@ -109,7 +155,7 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { // Execute fiber Ctx err := h[0](ctx) if err != nil { - _ = app.Config().ErrorHandler(ctx, err) + _ = app.Config().ErrorHandler(ctx, err) //nolint:errcheck // not needed } } else { // Execute fasthttp Ctx though app.Handler @@ -121,6 +167,6 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { w.Header().Add(string(k), string(v)) }) w.WriteHeader(fctx.Response.StatusCode()) - _, _ = w.Write(fctx.Response.Body()) + _, _ = w.Write(fctx.Response.Body()) //nolint:errcheck // not needed } } diff --git a/middleware/adaptor/adopter_test.go b/middleware/adaptor/adaptor_test.go similarity index 80% rename from middleware/adaptor/adopter_test.go rename to middleware/adaptor/adaptor_test.go index 8e9531e7b1..044e59fb93 100644 --- a/middleware/adaptor/adopter_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -1,19 +1,19 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - +//nolint:bodyclose, contextcheck, revive // Much easier to just ignore memory leaks in tests package adaptor import ( + "context" "fmt" "io" "net" "net/http" + "net/http/httptest" "net/url" "reflect" "testing" "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) @@ -70,7 +70,6 @@ func Test_HTTPHandler(t *testing.T) { t.Fatalf("unexpected remoteAddr %q. Expecting %q", r.RemoteAddr, expectedRemoteAddr) } body, err := io.ReadAll(r.Body) - r.Body.Close() if err != nil { t.Fatalf("unexpected error when reading request body: %s", err) } @@ -105,7 +104,7 @@ func Test_HTTPHandler(t *testing.T) { req.Header.SetMethod(expectedMethod) req.SetRequestURI(expectedRequestURI) req.Header.SetHost(expectedHost) - req.BodyWriter().Write([]byte(expectedBody)) // nolint:errcheck + req.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck, gosec // not needed for k, v := range expectedHeader { req.Header.Set(k, v) } @@ -117,8 +116,12 @@ func Test_HTTPHandler(t *testing.T) { fctx.Init(&req, remoteAddr, nil) app := fiber.New() ctx := app.NewCtx(&fctx) + defer app.ReleaseCtx(ctx) - fiberH(ctx) + err = fiberH(ctx) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } if callsCount != 1 { t.Fatalf("unexpected callsCount: %d. Expecting 1", callsCount) @@ -140,8 +143,18 @@ func Test_HTTPHandler(t *testing.T) { } } -func Test_HTTPMiddleware(t *testing.T) { +type contextKey string +func (c contextKey) String() string { + return "test-" + string(c) +} + +var ( + TestContextKey = contextKey("TestContextKey") + TestContextSecondKey = contextKey("TestContextSecondKey") +) + +func Test_HTTPMiddleware(t *testing.T) { tests := []struct { name string url string @@ -174,6 +187,10 @@ func Test_HTTPMiddleware(t *testing.T) { w.WriteHeader(http.StatusMethodNotAllowed) return } + r = r.WithContext(context.WithValue(r.Context(), TestContextKey, "okay")) + r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "not_okay")) + r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "okay")) + next.ServeHTTP(w, r) }) } @@ -181,11 +198,30 @@ func Test_HTTPMiddleware(t *testing.T) { app := fiber.New() app.Use(HTTPMiddleware(nethttpMW)) app.Post("/", func(c fiber.Ctx) error { + value := c.Context().Value(TestContextKey) + val, ok := value.(string) + if !ok { + t.Error("unexpected error on type-assertion") + } + if value != nil { + c.Set("context_okay", val) + } + value = c.Context().Value(TestContextSecondKey) + if value != nil { + val, ok := value.(string) + if !ok { + t.Error("unexpected error on type-assertion") + } + c.Set("context_second_okay", val) + } return c.SendStatus(fiber.StatusOK) }) for _, tt := range tests { - req, _ := http.NewRequest(tt.method, tt.url, nil) + req, err := http.NewRequestWithContext(context.Background(), tt.method, tt.url, nil) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } resp, err := app.Test(req) if err != nil { t.Fatalf(`%s: %s`, t.Name(), err) @@ -194,6 +230,21 @@ func Test_HTTPMiddleware(t *testing.T) { t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) } } + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "/", nil) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + resp, err := app.Test(req) + if err != nil { + t.Fatalf(`%s: %s`, t.Name(), err) + } + if resp.Header.Get("context_okay") != "okay" { + t.Fatalf(`%s: Header context_okay: got %v - expected %v`, t.Name(), resp.Header.Get("context_okay"), "okay") + } + if resp.Header.Get("context_second_okay") != "okay" { + t.Fatalf(`%s: Header context_second_okay: got %v - expected %v`, t.Name(), resp.Header.Get("context_second_okay"), "okay") + } } func Test_FiberHandler(t *testing.T) { @@ -213,6 +264,8 @@ func Test_FiberAppDefaultPort(t *testing.T) { } func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.App) { + t.Helper() + expectedMethod := fiber.MethodPost expectedRequestURI := "/foo/bar?baz=123" expectedBody := "body 123 foo bar baz" @@ -245,8 +298,8 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A if contentLength != expectedContentLength { t.Fatalf("unexpected contentLength %d. Expecting %d", contentLength, expectedContentLength) } - if c.Host() != expectedHost { - t.Fatalf("unexpected host %q. Expecting %q", c.Host(), expectedHost) + if c.Hostname() != expectedHost { + t.Fatalf("unexpected host %q. Expecting %q", c.Hostname(), expectedHost) } remoteAddr := c.Context().RemoteAddr().String() if remoteAddr != expectedRemoteAddr { @@ -318,7 +371,7 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A } } -func setFiberContextValueMiddleware(next fiber.Handler, key string, value any) fiber.Handler { +func setFiberContextValueMiddleware(next fiber.Handler, key string, value interface{}) fiber.Handler { return func(c fiber.Ctx) error { c.Locals(key, value) return next(c) @@ -409,3 +462,26 @@ func (w *netHTTPResponseWriter) Write(p []byte) (int, error) { w.body = append(w.body, p...) return len(p), nil } + +func Test_ConvertRequest(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Get("/test", func(c fiber.Ctx) error { + httpReq, err := ConvertRequest(c, false) + if err != nil { + return err + } + + return c.SendString("Request URL: " + httpReq.URL.String()) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test?hello=world&another=test", http.NoBody)) + require.Equal(t, nil, err, "app.Test(req)") + require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "Request URL: /test?hello=world&another=test", string(body)) +} diff --git a/middleware/basicauth/README.md b/middleware/basicauth/README.md deleted file mode 100644 index ecebee9bf8..0000000000 --- a/middleware/basicauth/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Basic Authentication Middleware - -Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials. - -## Table of Contents - -- [Basic Authentication Middleware](#basic-authentication-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config) - -## Signatures - -```go -func New(config Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/basicauth" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Custom Config - -```go -// Provide a minimal config -app.Use(basicauth.New(basicauth.Config{ - Users: map[string]string{ - "john": "doe", - "admin": "123456", - }, -})) - -// Or extend your config for customization -app.Use(basicauth.New(basicauth.Config{ - Users: map[string]string{ - "john": "doe", - "admin": "123456", - }, - Realm: "Forbidden", - Authorizer: func(user, pass string) bool { - if user == "john" && pass == "doe" { - return true - } - if user == "admin" && pass == "123456" { - return true - } - return false - }, - Unauthorized: func(c fiber.Ctx) error { - return c.SendFile("./unauthorized.html") - }, - ContextUsername: "_user", - ContextPassword: "_pass", -})) -``` - -## Config - -```go -// 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 - - // Users defines the allowed credentials - // - // Required. Default: map[string]string{} - Users map[string]string - - // Realm is a string to define realm attribute of BasicAuth. - // the realm identifies the system to authenticate against - // and can be used by clients to save credentials - // - // Optional. Default: "Restricted". - Realm string - - // Authorizer defines a function you can pass - // to check the credentials however you want. - // It will be called with a username and password - // and is expected to return true or false to indicate - // that the credentials were approved or not. - // - // Optional. Default: nil. - Authorizer func(string, string) bool - - // Unauthorized defines the response body for unauthorized responses. - // By default it will return with a 401 Unauthorized and the correct WWW-Auth header - // - // Optional. Default: nil - Unauthorized fiber.Handler - - // ContextUser is the key to store the username in Locals - // - // Optional. Default: "username" - ContextUsername string - - // ContextPass is the key to store the password in Locals - // - // Optional. Default: "password" - ContextPassword string -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - Users: map[string]string{}, - Realm: "Restricted", - Authorizer: nil, - Unauthorized: nil, - ContextUsername: "username", - ContextPassword: "password", -} -``` diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 9e1891f4bb..5a07f77cd0 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -24,7 +24,7 @@ func New(config Config) fiber.Handler { auth := c.Get(fiber.HeaderAuthorization) // Check if the header contains content besides "basic". - if len(auth) <= 6 || strings.ToLower(auth[:5]) != "basic" { + if len(auth) <= 6 || !utils.EqualFold(auth[:6], "basic ") { return cfg.Unauthorized(c) } diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 7656c9eb6e..673765fd2a 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -1,13 +1,12 @@ package basicauth import ( + "encoding/base64" "fmt" "io" "net/http/httptest" "testing" - b64 "encoding/base64" - "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" @@ -16,7 +15,6 @@ import ( // go test -run Test_BasicAuth_Next func Test_BasicAuth_Next(t *testing.T) { t.Parallel() - app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -24,7 +22,7 @@ func Test_BasicAuth_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } @@ -41,8 +39,8 @@ func Test_Middleware_BasicAuth(t *testing.T) { })) app.Get("/testauth", func(c fiber.Ctx) error { - username := c.Locals("username").(string) - password := c.Locals("password").(string) + username := c.Locals("username").(string) //nolint:errcheck, forcetypeassert // not needed + password := c.Locals("password").(string) //nolint:errcheck, forcetypeassert // not needed return c.SendString(username + password) }) @@ -75,9 +73,9 @@ func Test_Middleware_BasicAuth(t *testing.T) { for _, tt := range tests { // Base64 encode credentials for http auth header - creds := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", tt.username, tt.password))) + creds := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", tt.username, tt.password))) - req := httptest.NewRequest("GET", "/testauth", nil) + req := httptest.NewRequest(fiber.MethodGet, "/testauth", nil) req.Header.Add("Authorization", "Basic "+creds) resp, err := app.Test(req) require.NoError(t, err) @@ -109,7 +107,7 @@ func Benchmark_Middleware_BasicAuth(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") fctx.Request.Header.Set(fiber.HeaderAuthorization, "basic am9objpkb2U=") // john:doe @@ -122,3 +120,33 @@ func Benchmark_Middleware_BasicAuth(b *testing.B) { require.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) } + +// go test -v -run=^$ -bench=Benchmark_Middleware_BasicAuth -benchmem -count=4 +func Benchmark_Middleware_BasicAuth_Upper(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Users: map[string]string{ + "john": "doe", + }, + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + fctx.Request.Header.Set(fiber.HeaderAuthorization, "Basic am9objpkb2U=") // john:doe + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + require.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) +} diff --git a/middleware/cache/README.md b/middleware/cache/README.md deleted file mode 100644 index b30b452878..0000000000 --- a/middleware/cache/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Cache Middleware - -Cache middleware for [Fiber](https://github.com/gofiber/fiber) designed to intercept responses and cache them. This middleware will cache the `Body`, `Content-Type` and `StatusCode` using the `c.Path()` (or a string returned by the Key function) as unique identifier. Special thanks to [@codemicro](https://github.com/codemicro/fiber-cache) for creating this middleware for Fiber core! - -Request Directives
-`Cache-Control: no-cache` will return the up-to-date response but still caches it. You will always get a `miss` cache status.
-`Cache-Control: no-store` will refrain from caching. You will always get the up-to-date response. - -## Table of Contents - -- [Cache Middleware](#cache-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Custom Cache Key Or Expiration](#custom-cache-key-or-expiration) - - [Config](#config) - - [Default Config](#default-config-1) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/cache" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -app.Use(cache.New()) -``` - -### Custom Config - -```go -app.Use(cache.New(cache.Config{ - Next: func(c fiber.Ctx) bool { - return c.Query("refresh") == "true" - }, - Expiration: 30 * time.Minute, - CacheControl: true, -})) -``` - -### Custom Cache Key Or Expiration - -```go -app.Use(New(Config{ - ExpirationGenerator: func(c fiber.Ctx, cfg *Config) time.Duration { - newCacheTime, _ := strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) - return time.Second * time.Duration(newCacheTime) - }, - KeyGenerator: func(c fiber.Ctx) string { - return utils.CopyString(c.Path()) - } -})) - -app.Get("/", func(c fiber.Ctx) error { - c.Response().Header.Add("Cache-Time", "6000") - return c.SendString("hi") -}) -``` - -### Config - -```go -// 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 - - // Expiration is the time that an cached response will live - // - // Optional. Default: 1 * time.Minute - Expiration time.Duration - - // CacheHeader header on response header, indicate cache status, with the following possible return value - // - // hit, miss, unreachable - // - // Optional. Default: X-Cache - CacheHeader string - - // CacheControl enables client side caching if set to true - // - // Optional. Default: false - CacheControl bool - - // Key allows you to generate custom keys, by default c.Path() is used - // - // Default: func(c fiber.Ctx) string { - // return utils.CopyString(c.Path()) - // } - KeyGenerator func(fiber.Ctx) string - - // allows you to generate custom Expiration Key By Key, default is Expiration (Optional) - // - // Default: nil - ExpirationGenerator func(fiber.Ctx, *Config) time.Duration - - // Store is used to store the state of the middleware - // - // Default: an in memory store for this process only - Storage fiber.Storage - - // allows you to store additional headers generated by next middlewares & handler - // - // Default: false - StoreResponseHeaders bool - - // Max number of bytes of response bodies simultaneously stored in cache. When limit is reached, - // entries with the nearest expiration are deleted to make room for new. - // 0 means no limit - // - // Default: 0 - MaxBytes uint - - // You can specify HTTP methods to cache. - // The middleware just caches the routes of its methods in this slice. - // - // Default: []string{fiber.MethodGet, fiber.MethodHead} - Methods []string -} -``` - -### Default Config - -```go -// ConfigDefault is the default config -var ConfigDefault = Config{ - Next: nil, - Expiration: 1 * time.Minute, - CacheHeader: "X-Cache", - CacheControl: false, - KeyGenerator: func(c fiber.Ctx) string { - return utils.CopyString(c.Path()) - }, - ExpirationGenerator: nil, - StoreResponseHeaders: false, - Storage: nil, - MaxBytes: 0, - Methods: []string{fiber.MethodGet, fiber.MethodHead}, -} -``` diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 87712c1922..14ec063191 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -43,8 +43,8 @@ var ignoreHeaders = map[string]any{ "Trailers": nil, "Transfer-Encoding": nil, "Upgrade": nil, - "Content-Type": nil, // already stored explicitely by the cache manager - "Content-Encoding": nil, // already stored explicitely by the cache manager + "Content-Type": nil, // already stored explicitly by the cache manager + "Content-Encoding": nil, // already stored explicitly by the cache manager } // New creates a new middleware handler @@ -69,7 +69,7 @@ func New(config ...Config) fiber.Handler { // Create indexed heap for tracking expirations ( see heap.go ) heap := &indexedHeap{} // count stored bytes (sizes of response bodies) - var storedBytes uint = 0 + var storedBytes uint // Update timestamp in the configured interval go func() { @@ -81,10 +81,10 @@ func New(config ...Config) fiber.Handler { // Delete key from both manager and storage deleteKey := func(dkey string) { - manager.delete(dkey) + manager.del(dkey) // External storage saves body data with different key if cfg.Storage != nil { - manager.delete(dkey + "_body") + manager.del(dkey + "_body") } } @@ -205,7 +205,7 @@ func New(config ...Config) fiber.Handler { if cfg.StoreResponseHeaders { e.headers = make(map[string][]byte) c.Response().Header.VisitAll( - func(key []byte, value []byte) { + func(key, value []byte) { // create real copy keyS := string(key) if _, ok := ignoreHeaders[keyS]; !ok { diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 5567f0b998..1da4496f58 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "math" - "net/http" "net/http/httptest" "os" "strconv" @@ -36,10 +35,10 @@ func Test_Cache_CacheControl(t *testing.T) { return c.SendString("Hello, World!") }) - _, err := app.Test(httptest.NewRequest("GET", "/", nil)) + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "public, max-age=10", resp.Header.Get(fiber.HeaderCacheControl)) } @@ -54,7 +53,7 @@ func Test_Cache_Expired(t *testing.T) { return c.SendString(fmt.Sprintf("%d", time.Now().UnixNano())) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -62,7 +61,7 @@ func Test_Cache_Expired(t *testing.T) { // Sleep until the cache is expired time.Sleep(3 * time.Second) - respCached, err := app.Test(httptest.NewRequest("GET", "/", nil)) + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) bodyCached, err := io.ReadAll(respCached.Body) require.NoError(t, err) @@ -72,7 +71,7 @@ func Test_Cache_Expired(t *testing.T) { } // Next response should be also cached - respCachedNextRound, err := app.Test(httptest.NewRequest("GET", "/", nil)) + respCachedNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) bodyCachedNextRound, err := io.ReadAll(respCachedNextRound.Body) require.NoError(t, err) @@ -93,11 +92,11 @@ func Test_Cache(t *testing.T) { return c.SendString(now) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err := app.Test(req) require.NoError(t, err) - cachedReq := httptest.NewRequest("GET", "/", nil) + cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil) cachedResp, err := app.Test(cachedReq) require.NoError(t, err) @@ -121,31 +120,31 @@ func Test_Cache_WithNoCacheRequestDirective(t *testing.T) { }) // Request id = 1 - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err := app.Test(req) - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + require.NoError(t, err) + body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, cacheMiss, resp.Header.Get("X-Cache")) require.Equal(t, []byte("1"), body) // Response cached, entry id = 1 // Request id = 2 without Cache-Control: no-cache - cachedReq := httptest.NewRequest("GET", "/?id=2", nil) + cachedReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) cachedResp, err := app.Test(cachedReq) - defer cachedResp.Body.Close() - cachedBody, _ := io.ReadAll(cachedResp.Body) + require.NoError(t, err) + cachedBody, err := io.ReadAll(cachedResp.Body) require.NoError(t, err) require.Equal(t, cacheHit, cachedResp.Header.Get("X-Cache")) require.Equal(t, []byte("1"), cachedBody) // Response not cached, returns cached response, entry id = 1 // Request id = 2 with Cache-Control: no-cache - noCacheReq := httptest.NewRequest("GET", "/?id=2", nil) + noCacheReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache) noCacheResp, err := app.Test(noCacheReq) - defer noCacheResp.Body.Close() - noCacheBody, _ := io.ReadAll(noCacheResp.Body) + require.NoError(t, err) + noCacheBody, err := io.ReadAll(noCacheResp.Body) require.NoError(t, err) require.Equal(t, cacheMiss, noCacheResp.Header.Get("X-Cache")) require.Equal(t, []byte("2"), noCacheBody) @@ -153,21 +152,21 @@ func Test_Cache_WithNoCacheRequestDirective(t *testing.T) { /* Check Test_Cache_WithETagAndNoCacheRequestDirective */ // Request id = 2 with Cache-Control: no-cache again - noCacheReq1 := httptest.NewRequest("GET", "/?id=2", nil) + noCacheReq1 := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache) noCacheResp1, err := app.Test(noCacheReq1) - defer noCacheResp1.Body.Close() - noCacheBody1, _ := io.ReadAll(noCacheResp1.Body) + require.NoError(t, err) + noCacheBody1, err := io.ReadAll(noCacheResp1.Body) require.NoError(t, err) require.Equal(t, cacheMiss, noCacheResp1.Header.Get("X-Cache")) require.Equal(t, []byte("2"), noCacheBody1) // Response cached, returns updated response, entry = 2 // Request id = 1 without Cache-Control: no-cache - cachedReq1 := httptest.NewRequest("GET", "/", nil) + cachedReq1 := httptest.NewRequest(fiber.MethodGet, "/", nil) cachedResp1, err := app.Test(cachedReq1) - defer cachedResp1.Body.Close() - cachedBody1, _ := io.ReadAll(cachedResp1.Body) + require.NoError(t, err) + cachedBody1, err := io.ReadAll(cachedResp1.Body) require.NoError(t, err) require.Equal(t, cacheHit, cachedResp1.Header.Get("X-Cache")) require.Equal(t, []byte("2"), cachedBody1) @@ -189,7 +188,7 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { }) // Request id = 1 - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, cacheMiss, resp.Header.Get("X-Cache")) @@ -200,7 +199,7 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { etagToken := resp.Header.Get("Etag") // Request id = 2 with ETag but without Cache-Control: no-cache - cachedReq := httptest.NewRequest("GET", "/?id=2", nil) + cachedReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) cachedReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken) cachedResp, err := app.Test(cachedReq) require.NoError(t, err) @@ -209,7 +208,7 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { // Response not cached, returns cached response, entry id = 1, status not modified // Request id = 2 with ETag and Cache-Control: no-cache - noCacheReq := httptest.NewRequest("GET", "/?id=2", nil) + noCacheReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache) noCacheReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken) noCacheResp, err := app.Test(noCacheReq) @@ -222,7 +221,7 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { etagToken = noCacheResp.Header.Get("Etag") // Request id = 2 with ETag and Cache-Control: no-cache again - noCacheReq1 := httptest.NewRequest("GET", "/?id=2", nil) + noCacheReq1 := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache) noCacheReq1.Header.Set(fiber.HeaderIfNoneMatch, etagToken) noCacheResp1, err := app.Test(noCacheReq1) @@ -232,7 +231,7 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { // Response cached, returns updated response, entry id = 2, status not modified // Request id = 1 without ETag and Cache-Control: no-cache - cachedReq1 := httptest.NewRequest("GET", "/", nil) + cachedReq1 := httptest.NewRequest(fiber.MethodGet, "/", nil) cachedResp1, err := app.Test(cachedReq1) require.NoError(t, err) require.Equal(t, cacheHit, cachedResp1.Header.Get("X-Cache")) @@ -252,11 +251,11 @@ func Test_Cache_WithNoStoreRequestDirective(t *testing.T) { }) // Request id = 2 - noStoreReq := httptest.NewRequest("GET", "/?id=2", nil) + noStoreReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) noStoreReq.Header.Set(fiber.HeaderCacheControl, noStore) noStoreResp, err := app.Test(noStoreReq) - defer noStoreResp.Body.Close() - noStoreBody, _ := io.ReadAll(noStoreResp.Body) + require.NoError(t, err) + noStoreBody, err := io.ReadAll(noStoreResp.Body) require.NoError(t, err) require.Equal(t, []byte("2"), noStoreBody) // Response not cached, returns updated response @@ -279,11 +278,11 @@ func Test_Cache_WithSeveralRequests(t *testing.T) { for runs := 0; runs < 10; runs++ { for i := 0; i < 10; i++ { func(id int) { - rsp, err := app.Test(httptest.NewRequest(http.MethodGet, fmt.Sprintf("/%d", id), nil)) + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf("/%d", id), nil)) require.NoError(t, err) - defer func(Body io.ReadCloser) { - err := Body.Close() + defer func(body io.ReadCloser) { + err := body.Close() require.NoError(t, err) }(rsp.Body) @@ -312,11 +311,11 @@ func Test_Cache_Invalid_Expiration(t *testing.T) { return c.SendString(now) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err := app.Test(req) require.NoError(t, err) - cachedReq := httptest.NewRequest("GET", "/", nil) + cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil) cachedResp, err := app.Test(cachedReq) require.NoError(t, err) @@ -343,25 +342,25 @@ func Test_Cache_Get(t *testing.T) { return c.SendString(c.Query("cache")) }) - resp, err := app.Test(httptest.NewRequest("POST", "/?cache=123", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "123", string(body)) - resp, err = app.Test(httptest.NewRequest("POST", "/?cache=12345", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "12345", string(body)) - resp, err = app.Test(httptest.NewRequest("GET", "/get?cache=123", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "123", string(body)) - resp, err = app.Test(httptest.NewRequest("GET", "/get?cache=12345", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) @@ -385,25 +384,25 @@ func Test_Cache_Post(t *testing.T) { return c.SendString(c.Query("cache")) }) - resp, err := app.Test(httptest.NewRequest("POST", "/?cache=123", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "123", string(body)) - resp, err = app.Test(httptest.NewRequest("POST", "/?cache=12345", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "123", string(body)) - resp, err = app.Test(httptest.NewRequest("GET", "/get?cache=123", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, "123", string(body)) - resp, err = app.Test(httptest.NewRequest("GET", "/get?cache=12345", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil)) require.NoError(t, err) body, err = io.ReadAll(resp.Body) require.NoError(t, err) @@ -421,14 +420,14 @@ func Test_Cache_NothingToCache(t *testing.T) { return c.SendString(time.Now().String()) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) time.Sleep(500 * time.Millisecond) - respCached, err := app.Test(httptest.NewRequest("GET", "/", nil)) + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) bodyCached, err := io.ReadAll(respCached.Body) require.NoError(t, err) @@ -458,22 +457,22 @@ func Test_Cache_CustomNext(t *testing.T) { return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) - respCached, err := app.Test(httptest.NewRequest("GET", "/", nil)) + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) bodyCached, err := io.ReadAll(respCached.Body) require.NoError(t, err) require.True(t, bytes.Equal(body, bodyCached)) require.True(t, respCached.Header.Get(fiber.HeaderCacheControl) != "") - _, err = app.Test(httptest.NewRequest("GET", "/error", nil)) + _, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) require.NoError(t, err) - errRespCached, err := app.Test(httptest.NewRequest("GET", "/error", nil)) + errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) require.NoError(t, err) require.True(t, errRespCached.Header.Get(fiber.HeaderCacheControl) == "") } @@ -492,7 +491,7 @@ func Test_CustomKey(t *testing.T) { return c.SendString("hi") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) _, err := app.Test(req) require.NoError(t, err) require.True(t, called) @@ -506,7 +505,9 @@ func Test_CustomExpiration(t *testing.T) { var newCacheTime int app.Use(New(Config{ExpirationGenerator: func(c fiber.Ctx, cfg *Config) time.Duration { called = true - newCacheTime, _ = strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) + var err error + newCacheTime, err = strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) + require.NoError(t, err) return time.Second * time.Duration(newCacheTime) }})) @@ -516,7 +517,7 @@ func Test_CustomExpiration(t *testing.T) { return c.SendString(now) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.True(t, called) require.Equal(t, 1, newCacheTime) @@ -524,7 +525,7 @@ func Test_CustomExpiration(t *testing.T) { // Sleep until the cache is expired time.Sleep(1 * time.Second) - cachedResp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + cachedResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) body, err := io.ReadAll(resp.Body) @@ -537,7 +538,7 @@ func Test_CustomExpiration(t *testing.T) { } // Next response should be cached - cachedRespNextRound, err := app.Test(httptest.NewRequest("GET", "/", nil)) + cachedRespNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) cachedBodyNextRound, err := io.ReadAll(cachedRespNextRound.Body) require.NoError(t, err) @@ -560,12 +561,12 @@ func Test_AdditionalE2EResponseHeaders(t *testing.T) { return c.SendString("hi") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, "foobar", resp.Header.Get("X-Foobar")) - req = httptest.NewRequest("GET", "/", nil) + req = httptest.NewRequest(fiber.MethodGet, "/", nil) resp, err = app.Test(req) require.NoError(t, err) require.Equal(t, "foobar", resp.Header.Get("X-Foobar")) @@ -595,19 +596,19 @@ func Test_CacheHeader(t *testing.T) { return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, cacheMiss, resp.Header.Get("X-Cache")) - resp, err = app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, cacheHit, resp.Header.Get("X-Cache")) - resp, err = app.Test(httptest.NewRequest("POST", "/?cache=12345", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) require.NoError(t, err) require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache")) - errRespCached, err := app.Test(httptest.NewRequest("GET", "/error", nil)) + errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) require.NoError(t, err) require.Equal(t, cacheUnreachable, errRespCached.Header.Get("X-Cache")) } @@ -624,12 +625,12 @@ func Test_Cache_WithHead(t *testing.T) { } app.Route("/").Get(handler).Head(handler) - req := httptest.NewRequest("HEAD", "/", nil) + req := httptest.NewRequest(fiber.MethodHead, "/", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, cacheMiss, resp.Header.Get("X-Cache")) - cachedReq := httptest.NewRequest("HEAD", "/", nil) + cachedReq := httptest.NewRequest(fiber.MethodHead, "/", nil) cachedResp, err := app.Test(cachedReq) require.NoError(t, err) require.Equal(t, cacheHit, cachedResp.Header.Get("X-Cache")) @@ -653,28 +654,28 @@ func Test_Cache_WithHeadThenGet(t *testing.T) { } app.Route("/").Get(handler).Head(handler) - headResp, err := app.Test(httptest.NewRequest("HEAD", "/?cache=123", nil)) + headResp, err := app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil)) require.NoError(t, err) headBody, err := io.ReadAll(headResp.Body) require.NoError(t, err) require.Equal(t, "", string(headBody)) require.Equal(t, cacheMiss, headResp.Header.Get("X-Cache")) - headResp, err = app.Test(httptest.NewRequest("HEAD", "/?cache=123", nil)) + headResp, err = app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil)) require.NoError(t, err) headBody, err = io.ReadAll(headResp.Body) require.NoError(t, err) require.Equal(t, "", string(headBody)) require.Equal(t, cacheHit, headResp.Header.Get("X-Cache")) - getResp, err := app.Test(httptest.NewRequest("GET", "/?cache=123", nil)) + getResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil)) require.NoError(t, err) getBody, err := io.ReadAll(getResp.Body) require.NoError(t, err) require.Equal(t, "123", string(getBody)) require.Equal(t, cacheMiss, getResp.Header.Get("X-Cache")) - getResp, err = app.Test(httptest.NewRequest("GET", "/?cache=123", nil)) + getResp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil)) require.NoError(t, err) getBody, err = io.ReadAll(getResp.Body) require.NoError(t, err) @@ -695,7 +696,7 @@ func Test_CustomCacheHeader(t *testing.T) { return c.SendString("Hello, World!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, cacheMiss, resp.Header.Get("Cache-Status")) } @@ -706,7 +707,7 @@ func Test_CustomCacheHeader(t *testing.T) { func stableAscendingExpiration() func(c1 fiber.Ctx, c2 *Config) time.Duration { i := 0 return func(c1 fiber.Ctx, c2 *Config) time.Duration { - i += 1 + i++ return time.Hour * time.Duration(i) } } @@ -742,7 +743,7 @@ func Test_Cache_MaxBytesOrder(t *testing.T) { } for idx, tcase := range cases { - rsp, err := app.Test(httptest.NewRequest("GET", tcase[0], nil)) + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil)) require.NoError(t, err) require.Equal(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx)) } @@ -760,7 +761,8 @@ func Test_Cache_MaxBytesSizes(t *testing.T) { app.Get("/*", func(c fiber.Ctx) error { path := c.Context().URI().LastPathSegment() - size, _ := strconv.Atoi(string(path)) + size, err := strconv.Atoi(string(path)) + require.NoError(t, err) return c.Send(make([]byte, size)) }) @@ -776,7 +778,7 @@ func Test_Cache_MaxBytesSizes(t *testing.T) { } for idx, tcase := range cases { - rsp, err := app.Test(httptest.NewRequest("GET", tcase[0], nil)) + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil)) require.NoError(t, err) require.Equal(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx)) } @@ -789,14 +791,14 @@ func Benchmark_Cache(b *testing.B) { app.Use(New()) app.Get("/demo", func(c fiber.Ctx) error { - data, _ := os.ReadFile("../../.github/README.md") + data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark return c.Status(fiber.StatusTeapot).Send(data) }) h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/demo") b.ReportAllocs() @@ -819,14 +821,14 @@ func Benchmark_Cache_Storage(b *testing.B) { })) app.Get("/demo", func(c fiber.Ctx) error { - data, _ := os.ReadFile("../../.github/README.md") + data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark return c.Status(fiber.StatusTeapot).Send(data) }) h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/demo") b.ReportAllocs() @@ -854,7 +856,7 @@ func Benchmark_Cache_AdditionalHeaders(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/demo") b.ReportAllocs() @@ -886,7 +888,7 @@ func Benchmark_Cache_MaxSize(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) b.ReportAllocs() b.ResetTimer() diff --git a/middleware/cache/heap.go b/middleware/cache/heap.go index bcc279c2c5..fa97871595 100644 --- a/middleware/cache/heap.go +++ b/middleware/cache/heap.go @@ -41,7 +41,7 @@ func (h indexedHeap) Swap(i, j int) { } func (h *indexedHeap) Push(x any) { - h.pushInternal(x.(heapEntry)) + h.pushInternal(x.(heapEntry)) //nolint:forcetypeassert // Forced type assertion required to implement the heap.Interface interface } func (h *indexedHeap) Pop() any { @@ -65,7 +65,7 @@ func (h *indexedHeap) put(key string, exp uint64, bytes uint) int { idx = h.entries[:n+1][n].idx } else { idx = h.maxidx - h.maxidx += 1 + h.maxidx++ h.indices = append(h.indices, idx) } // Push manually to avoid allocation @@ -77,7 +77,7 @@ func (h *indexedHeap) put(key string, exp uint64, bytes uint) int { } func (h *indexedHeap) removeInternal(realIdx int) (string, uint) { - x := heap.Remove(h, realIdx).(heapEntry) + x := heap.Remove(h, realIdx).(heapEntry) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface return x.key, x.bytes } diff --git a/middleware/cache/manager.go b/middleware/cache/manager.go index 617e0d8991..c6ae542805 100644 --- a/middleware/cache/manager.go +++ b/middleware/cache/manager.go @@ -8,7 +8,8 @@ import ( "github.com/gofiber/fiber/v3/internal/memory" ) -//go:generate msgp -o=manager_msgp.go -io=false -unexported +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported type item struct { body []byte ctype []byte @@ -48,7 +49,7 @@ func newManager(storage fiber.Storage) *manager { // acquire returns an *entry from the sync.Pool func (m *manager) acquire() *item { - return m.pool.Get().(*item) + return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool } // release and reset *entry to sync.Pool @@ -66,38 +67,47 @@ func (m *manager) release(e *item) { } // get data from storage or memory -func (m *manager) get(key string) (it *item) { +func (m *manager) get(key string) *item { + var it *item if m.storage != nil { it = m.acquire() - if raw, _ := m.storage.Get(key); raw != nil { + raw, err := m.storage.Get(key) + if err != nil { + return it + } + if raw != nil { if _, err := it.UnmarshalMsg(raw); err != nil { - return + return it } } - return + return it } - if it, _ = m.memory.Get(key).(*item); it == nil { + if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool it = m.acquire() + return it } - return + return it } // get raw data from storage or memory -func (m *manager) getRaw(key string) (raw []byte) { +func (m *manager) getRaw(key string) []byte { + var raw []byte if m.storage != nil { - raw, _ = m.storage.Get(key) + raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Handle error here } else { - raw, _ = m.memory.Get(key).([]byte) + raw, _ = m.memory.Get(key).([]byte) //nolint:errcheck // TODO: Handle error here } - return + return raw } // set data to storage or memory func (m *manager) set(key string, it *item, exp time.Duration) { if m.storage != nil { if raw, err := it.MarshalMsg(nil); err == nil { - _ = m.storage.Set(key, raw, exp) + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here } + // we can release data because it's serialized to database + m.release(it) } else { m.memory.Set(key, it, exp) } @@ -106,16 +116,16 @@ func (m *manager) set(key string, it *item, exp time.Duration) { // set data to storage or memory func (m *manager) setRaw(key string, raw []byte, exp time.Duration) { if m.storage != nil { - _ = m.storage.Set(key, raw, exp) + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here } else { m.memory.Set(key, raw, exp) } } // delete data from storage or memory -func (m *manager) delete(key string) { +func (m *manager) del(key string) { if m.storage != nil { - _ = m.storage.Delete(key) + _ = m.storage.Delete(key) //nolint:errcheck // TODO: Handle error here } else { m.memory.Delete(key) } diff --git a/middleware/compress/README.md b/middleware/compress/README.md deleted file mode 100644 index c5b89046eb..0000000000 --- a/middleware/compress/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Compress Middleware - -Compression middleware for [Fiber](https://github.com/gofiber/fiber) that will compress the response using `gzip`, `deflate` and `brotli` compression depending on the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. - -- [Compress Middleware](#compress-middleware) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config-1) - - [Constants](#constants) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/compress" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -app.Use(compress.New()) -``` - -### Custom Config - -```go -// Provide a custom compression level -app.Use(compress.New(compress.Config{ - Level: compress.LevelBestSpeed, // 1 -})) - -// Skip middleware for specific routes -app.Use(compress.New(compress.Config{ - Next: func(c fiber.Ctx) bool { - return c.Path() == "/dont_compress" - }, - Level: compress.LevelBestSpeed, // 1 -})) -``` - -## Config - -```go -// 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 - - // CompressLevel determines the compression algoritm - // - // Optional. Default: LevelDefault - // LevelDisabled: -1 - // LevelDefault: 0 - // LevelBestSpeed: 1 - // LevelBestCompression: 2 - Level int -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - Level: LevelDefault, -} -``` - -## Constants - -```go -// Compression levels -const ( - LevelDisabled = -1 - LevelDefault = 0 - LevelBestSpeed = 1 - LevelBestCompression = 2 -) -``` diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go index ced70df3c3..0960cfef39 100644 --- a/middleware/compress/compress_test.go +++ b/middleware/compress/compress_test.go @@ -7,7 +7,6 @@ import ( "net/http/httptest" "os" "testing" - "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" @@ -25,6 +24,7 @@ func init() { // go test -run Test_Compress_Gzip func Test_Compress_Gzip(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -34,7 +34,7 @@ func Test_Compress_Gzip(t *testing.T) { return c.Send(filedata) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "gzip") resp, err := app.Test(req) @@ -50,9 +50,11 @@ func Test_Compress_Gzip(t *testing.T) { // go test -run Test_Compress_Different_Level func Test_Compress_Different_Level(t *testing.T) { + t.Parallel() levels := []Level{LevelBestSpeed, LevelBestCompression} for _, level := range levels { t.Run(fmt.Sprintf("level %d", level), func(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Level: level})) @@ -62,7 +64,7 @@ func Test_Compress_Different_Level(t *testing.T) { return c.Send(filedata) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "gzip") resp, err := app.Test(req) @@ -79,6 +81,7 @@ func Test_Compress_Different_Level(t *testing.T) { } func Test_Compress_Deflate(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -87,7 +90,7 @@ func Test_Compress_Deflate(t *testing.T) { return c.Send(filedata) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "deflate") resp, err := app.Test(req) @@ -102,6 +105,7 @@ func Test_Compress_Deflate(t *testing.T) { } func Test_Compress_Brotli(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -110,10 +114,10 @@ func Test_Compress_Brotli(t *testing.T) { return c.Send(filedata) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "br") - resp, err := app.Test(req, 10*time.Second) + resp, err := app.Test(req, 10000) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, "br", resp.Header.Get(fiber.HeaderContentEncoding)) @@ -125,6 +129,7 @@ func Test_Compress_Brotli(t *testing.T) { } func Test_Compress_Disabled(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Level: LevelDisabled})) @@ -133,7 +138,7 @@ func Test_Compress_Disabled(t *testing.T) { return c.Send(filedata) }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "br") resp, err := app.Test(req) @@ -148,6 +153,7 @@ func Test_Compress_Disabled(t *testing.T) { } func Test_Compress_Next_Error(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -156,7 +162,7 @@ func Test_Compress_Next_Error(t *testing.T) { return errors.New("next error") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "gzip") resp, err := app.Test(req) @@ -171,6 +177,7 @@ func Test_Compress_Next_Error(t *testing.T) { // go test -run Test_Compress_Next func Test_Compress_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -178,7 +185,7 @@ func Test_Compress_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } diff --git a/middleware/cors/README.md b/middleware/cors/README.md deleted file mode 100644 index 428984f8e8..0000000000 --- a/middleware/cors/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Cross-Origin Resource Sharing (CORS) Middleware - -CORS middleware for [Fiber](https://github.com/gofiber/fiber) that that can be used to enable [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options. - -## Table of Contents - -- [Cross-Origin Resource Sharing (CORS) Middleware](#cross-origin-resource-sharing-cors-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config-1) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/cors" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -app.Use(cors.New()) -``` - -### Custom Config - -```go -app.Use(cors.New(cors.Config{ - AllowOrigins: "https://gofiber.io, https://gofiber.net", - AllowHeaders: "Origin, Content-Type, Accept", -})) -``` - -## Config - -```go -// 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 - - // AllowOrigin defines a list of origins that may access the resource. - // - // Optional. Default value "*" - AllowOrigins string - - // AllowMethods defines a list methods allowed when accessing the resource. - // This is used in response to a preflight request. - // - // Optional. Default value "GET,POST,HEAD,PUT,DELETE,PATCH" - AllowMethods string - - // AllowHeaders defines a list of request headers that can be used when - // making the actual request. This is in response to a preflight request. - // - // Optional. Default value "". - AllowHeaders string - - // AllowCredentials indicates whether or not the response to the request - // can be exposed when the credentials flag is true. When used as part of - // a response to a preflight request, this indicates whether or not the - // actual request can be made using credentials. - // - // Optional. Default value false. - AllowCredentials bool - - // ExposeHeaders defines a whitelist headers that clients are allowed to - // access. - // - // Optional. Default value "". - ExposeHeaders string - - // MaxAge indicates how long (in seconds) the results of a preflight request - // can be cached. - // - // Optional. Default value 0. - MaxAge int -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - AllowOrigins: "*", - AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH", - AllowHeaders: "", - AllowCredentials: false, - ExposeHeaders: "", - MaxAge: 0, -} -``` diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index 0356f38d0c..de66849b8c 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -1,11 +1,11 @@ package cors import ( - "net/http" "strconv" "strings" "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" ) // Config defines the config for middleware. @@ -15,6 +15,12 @@ type Config struct { // Optional. Default: nil Next func(c fiber.Ctx) bool + // AllowOriginsFunc defines a function that will set the 'access-control-allow-origin' + // response header to the 'origin' request header when returned true. + // + // Optional. Default: nil + AllowOriginsFunc func(origin string) bool + // AllowOrigin defines a list of origins that may access the resource. // // Optional. Default value "*" @@ -55,8 +61,9 @@ type Config struct { // ConfigDefault is the default config var ConfigDefault = Config{ - Next: nil, - AllowOrigins: "*", + Next: nil, + AllowOriginsFunc: nil, + AllowOrigins: "*", AllowMethods: strings.Join([]string{ fiber.MethodGet, fiber.MethodPost, @@ -89,6 +96,11 @@ func New(config ...Config) fiber.Handler { } } + // Warning logs if both AllowOrigins and AllowOriginsFunc are set + if cfg.AllowOrigins != ConfigDefault.AllowOrigins && cfg.AllowOriginsFunc != nil { + log.Warn("[CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.") + } + // Convert string to slice allowOrigins := strings.Split(strings.ReplaceAll(cfg.AllowOrigins, " ", ""), ",") @@ -113,11 +125,11 @@ func New(config ...Config) fiber.Handler { // Check allowed origins for _, o := range allowOrigins { - if o == "*" && cfg.AllowCredentials { - allowOrigin = origin + if o == "*" { + allowOrigin = "*" break } - if o == "*" || o == origin { + if o == origin { allowOrigin = o break } @@ -127,8 +139,17 @@ func New(config ...Config) fiber.Handler { } } + // Run AllowOriginsFunc if the logic for + // handling the value in 'AllowOrigins' does + // not result in allowOrigin being set. + if (allowOrigin == "" || allowOrigin == ConfigDefault.AllowOrigins) && cfg.AllowOriginsFunc != nil { + if cfg.AllowOriginsFunc(origin) { + allowOrigin = origin + } + } + // Simple request - if c.Method() != http.MethodOptions { + if c.Method() != fiber.MethodOptions { c.Vary(fiber.HeaderOrigin) c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index 84e7bd9b73..4173d8addb 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -2,6 +2,7 @@ package cors import ( "net/http/httptest" + "strings" "testing" "github.com/gofiber/fiber/v3" @@ -10,6 +11,7 @@ import ( ) func Test_CORS_Defaults(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -17,6 +19,7 @@ func Test_CORS_Defaults(t *testing.T) { } func Test_CORS_Empty_Config(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{})) @@ -49,6 +52,7 @@ func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { // go test -run -v Test_CORS_Wildcard func Test_CORS_Wildcard(t *testing.T) { + t.Parallel() // New fiber instance app := fiber.New() // OPTIONS (preflight) response headers when AllowOrigins is * @@ -72,7 +76,7 @@ func Test_CORS_Wildcard(t *testing.T) { handler(ctx) // Check result - require.Equal(t, "localhost", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + require.Equal(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) require.Equal(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) require.Equal(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) require.Equal(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) @@ -88,6 +92,7 @@ func Test_CORS_Wildcard(t *testing.T) { // go test -run -v Test_CORS_Subdomain func Test_CORS_Subdomain(t *testing.T) { + t.Parallel() // New fiber instance app := fiber.New() // OPTIONS (preflight) response headers when AllowOrigins is set to a subdomain @@ -122,6 +127,7 @@ func Test_CORS_Subdomain(t *testing.T) { } func Test_CORS_AllowOriginScheme(t *testing.T) { + t.Parallel() tests := []struct { reqOrigin, pattern string shouldAllowOrigin bool @@ -224,6 +230,7 @@ func Test_CORS_AllowOriginScheme(t *testing.T) { // go test -run Test_CORS_Next func Test_CORS_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -231,7 +238,98 @@ func Test_CORS_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } + +func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOrigins: "http://example-1.com", + AllowOriginsFunc: func(origin string) bool { + return strings.Contains(origin, "example-2") + }, + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be "" because http://google.com does not satisfy http://example-1.com or 'strings.Contains(origin, "example-2")' + require.Equal(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-1.com") + + handler(ctx) + + require.Equal(t, "http://example-1.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") + + handler(ctx) + + require.Equal(t, "http://example-2.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} + +func Test_CORS_AllowOriginsFunc(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOriginsFunc: func(origin string) bool { + return strings.Contains(origin, "example-2") + }, + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be "*" because http://google.com does not satisfy 'strings.Contains(origin, "example-2")' + // and AllowOrigins has not been set so the default "*" is used + require.Equal(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") + + handler(ctx) + + // Allow-Origin header should be "http://example-2.com" + require.Equal(t, "http://example-2.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go index fee658ef93..8b6114bdab 100644 --- a/middleware/cors/utils.go +++ b/middleware/cors/utils.go @@ -1,6 +1,8 @@ package cors -import "strings" +import ( + "strings" +) func matchScheme(domain, pattern string) bool { didx := strings.Index(domain, ":") @@ -20,18 +22,20 @@ func matchSubdomain(domain, pattern string) bool { } domAuth := domain[didx+3:] // to avoid long loop by invalid long domain - if len(domAuth) > 253 { + const maxDomainLen = 253 + if len(domAuth) > maxDomainLen { return false } patAuth := pattern[pidx+3:] domComp := strings.Split(domAuth, ".") patComp := strings.Split(patAuth, ".") - for i := len(domComp)/2 - 1; i >= 0; i-- { + const divHalf = 2 + for i := len(domComp)/divHalf - 1; i >= 0; i-- { opp := len(domComp) - 1 - i domComp[i], domComp[opp] = domComp[opp], domComp[i] } - for i := len(patComp)/2 - 1; i >= 0; i-- { + for i := len(patComp)/divHalf - 1; i >= 0; i-- { opp := len(patComp) - 1 - i patComp[i], patComp[opp] = patComp[opp], patComp[i] } diff --git a/middleware/csrf/README.md b/middleware/csrf/README.md deleted file mode 100644 index 347b4d032e..0000000000 --- a/middleware/csrf/README.md +++ /dev/null @@ -1,161 +0,0 @@ -# CSRF Middleware - -CSRF middleware for [Fiber](https://github.com/gofiber/fiber) that provides [Cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection by passing a csrf token via cookies. This cookie value will be used to compare against the client csrf token in POST requests. When the csrf token is invalid, this middleware will delete the `csrf_` cookie and return the `fiber.ErrForbidden` error. -CSRF Tokens are generated on GET requests. You can retrieve the CSRF token with `c.Locals(contextKey)`, where `contextKey` is the string you set in the config (see Custom Config below). - -_NOTE: This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases._ - -## Table of Contents - -- [CSRF Middleware](#csrf-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Custom Storage/Database](#custom-storagedatabase) - - [Config](#config) - - [Default Config](#default-config-1) - -## 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/crsf" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: - -```go -app.Use(csrf.New()) // Default config -``` - -### Custom Config - -```go -app.Use(csrf.New(csrf.Config{ - KeyLookup: "header:X-Csrf-Token", - CookieName: "csrf_", - CookieSameSite: "Lax", - Expiration: 1 * time.Hour, - KeyGenerator: utils.UUID, - Extractor: func(c fiber.Ctx) (string, error) { ... }, -})) -``` - -Note: KeyLookup will be ignored if Extractor is explicitly set. - -### Custom Storage/Database - -You can use any storage from our [storage](https://github.com/gofiber/storage/) package. - -```go -storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 -app.Use(csrf.New(csrf.Config{ - Storage: storage, -})) -``` - -### Config - -```go -// 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 - - // KeyLookup is a string in the form of ":" that is used - // to create an Extractor that extracts the token from the request. - // Possible values: - // - "header:" - // - "query:" - // - "param:" - // - "form:" - // - "cookie:" - // - // Ignored if an Extractor is explicitly set. - // - // Optional. Default: "header:X-CSRF-Token" - KeyLookup string - - // Name of the session cookie. This cookie will store session key. - // Optional. Default value "csrf_". - CookieName string - - // Domain of the CSRF cookie. - // Optional. Default value "". - CookieDomain string - - // Path of the CSRF cookie. - // Optional. Default value "". - CookiePath string - - // Indicates if CSRF cookie is secure. - // Optional. Default value false. - CookieSecure bool - - // Indicates if CSRF cookie is HTTP only. - // Optional. Default value false. - CookieHTTPOnly bool - - // Indicates if CSRF cookie is requested by SameSite. - // Optional. Default value "Lax". - CookieSameSite string - - // Decides whether cookie should last for only the browser sesison. - // Ignores Expiration if set to true - CookieSessionOnly bool - - // Expiration is the duration before csrf token will expire - // - // Optional. Default: 1 * time.Hour - Expiration time.Duration - - // Store is used to store the state of the middleware - // - // Optional. Default: memory.New() - Storage fiber.Storage - - // Context key to store generated CSRF token into context. - // If left empty, token will not be stored in context. - // - // Optional. Default: "" - ContextKey string - - // KeyGenerator creates a new CSRF token - // - // Optional. Default: utils.UUID - KeyGenerator func() string - - // Extractor returns the csrf token - // - // If set this will be used in place of an Extractor based on KeyLookup. - // - // Optional. Default will create an Extractor based on KeyLookup. - Extractor func(c fiber.Ctx) (string, error) -} -``` - -### Default Config - -```go -var ConfigDefault = Config{ - KeyLookup: "header:X-Csrf-Token", - CookieName: "csrf_", - CookieSameSite: "Lax", - Expiration: 1 * time.Hour, - KeyGenerator: utils.UUID, -} -``` diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index 8c01c6c46f..c512082cbf 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -106,7 +106,7 @@ var ConfigDefault = Config{ } // default ErrorHandler that process return error from fiber.Handler -var defaultErrorHandler = func(c fiber.Ctx, err error) error { +func defaultErrorHandler(_ fiber.Ctx, _ error) error { return fiber.ErrForbidden } @@ -143,7 +143,8 @@ func configDefault(config ...Config) Config { // Generate the correct extractor to get the token from the correct location selectors := strings.Split(cfg.KeyLookup, ":") - if len(selectors) != 2 { + const numParts = 2 + if len(selectors) != numParts { panic("[CSRF] KeyLookup must in the form of :") } diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 29512daba9..bd6c32cac8 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -7,9 +7,7 @@ import ( "github.com/gofiber/fiber/v3" ) -var ( - errTokenNotFound = errors.New("csrf token not found") -) +var errTokenNotFound = errors.New("csrf token not found") // New creates a new middleware handler func New(config ...Config) fiber.Handler { @@ -22,7 +20,7 @@ func New(config ...Config) fiber.Handler { dummyValue := []byte{'+'} // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() @@ -39,7 +37,7 @@ func New(config ...Config) fiber.Handler { // Assume that anything not defined as 'safe' by RFC7231 needs protection // Extract token from client request i.e. header, query, param, form or cookie - token, err = cfg.Extractor(c) + token, err := cfg.Extractor(c) if err != nil { return cfg.ErrorHandler(c, err) } diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 45049da8df..24c3b97dea 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -12,6 +12,7 @@ import ( ) func Test_CSRF(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -23,7 +24,7 @@ func Test_CSRF(t *testing.T) { h := app.Handler() ctx := &fasthttp.RequestCtx{} - methods := [4]string{"GET", "HEAD", "OPTIONS", "TRACE"} + methods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace} for _, method := range methods { // Generate CSRF token @@ -33,14 +34,14 @@ func Test_CSRF(t *testing.T) { // Without CSRF cookie ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) // Empty/invalid CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(HeaderName, "johndoe") h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) @@ -55,7 +56,7 @@ func Test_CSRF(t *testing.T) { ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(HeaderName, token) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) @@ -64,6 +65,7 @@ func Test_CSRF(t *testing.T) { // go test -run Test_CSRF_Next func Test_CSRF_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -71,12 +73,13 @@ func Test_CSRF_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_CSRF_Invalid_KeyLookup(t *testing.T) { + t.Parallel() defer func() { require.Equal(t, "[CSRF] KeyLookup must in the form of :", recover()) }() @@ -90,11 +93,12 @@ func Test_CSRF_Invalid_KeyLookup(t *testing.T) { h := app.Handler() ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) } func Test_CSRF_From_Form(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{KeyLookup: "form:_csrf"})) @@ -107,7 +111,7 @@ func Test_CSRF_From_Form(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Invalid CSRF token - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm) h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) @@ -115,12 +119,12 @@ func Test_CSRF_From_Form(t *testing.T) { // Generate CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) token = strings.Split(strings.Split(token, ";")[0], "=")[1] - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm) ctx.Request.SetBodyString("_csrf=" + token) h(ctx) @@ -128,6 +132,7 @@ func Test_CSRF_From_Form(t *testing.T) { } func Test_CSRF_From_Query(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{KeyLookup: "query:_csrf"})) @@ -140,7 +145,7 @@ func Test_CSRF_From_Query(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Invalid CSRF token - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.SetRequestURI("/?_csrf=" + utils.UUID()) h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) @@ -148,7 +153,7 @@ func Test_CSRF_From_Query(t *testing.T) { // Generate CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.SetRequestURI("/") h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) @@ -157,13 +162,14 @@ func Test_CSRF_From_Query(t *testing.T) { ctx.Request.Reset() ctx.Response.Reset() ctx.Request.SetRequestURI("/?_csrf=" + token) - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) require.Equal(t, "OK", string(ctx.Response.Body())) } func Test_CSRF_From_Param(t *testing.T) { + t.Parallel() app := fiber.New() csrfGroup := app.Group("/:csrf", New(Config{KeyLookup: "param:csrf"})) @@ -176,7 +182,7 @@ func Test_CSRF_From_Param(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Invalid CSRF token - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.SetRequestURI("/" + utils.UUID()) h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) @@ -184,7 +190,7 @@ func Test_CSRF_From_Param(t *testing.T) { // Generate CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.SetRequestURI("/" + utils.UUID()) h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) @@ -193,13 +199,14 @@ func Test_CSRF_From_Param(t *testing.T) { ctx.Request.Reset() ctx.Response.Reset() ctx.Request.SetRequestURI("/" + token) - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) require.Equal(t, "OK", string(ctx.Response.Body())) } func Test_CSRF_From_Cookie(t *testing.T) { + t.Parallel() app := fiber.New() csrfGroup := app.Group("/", New(Config{KeyLookup: "cookie:csrf"})) @@ -212,7 +219,7 @@ func Test_CSRF_From_Cookie(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Invalid CSRF token - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.SetRequestURI("/") ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+utils.UUID()+";") h(ctx) @@ -221,7 +228,7 @@ func Test_CSRF_From_Cookie(t *testing.T) { // Generate CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.SetRequestURI("/") h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) @@ -229,7 +236,7 @@ func Test_CSRF_From_Cookie(t *testing.T) { ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+token+";") ctx.Request.SetRequestURI("/") h(ctx) @@ -238,6 +245,7 @@ func Test_CSRF_From_Cookie(t *testing.T) { } func Test_CSRF_From_Custom(t *testing.T) { + t.Parallel() app := fiber.New() extractor := func(c fiber.Ctx) (string, error) { @@ -261,7 +269,7 @@ func Test_CSRF_From_Custom(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Invalid CSRF token - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain) h(ctx) require.Equal(t, 403, ctx.Response.StatusCode()) @@ -269,12 +277,12 @@ func Test_CSRF_From_Custom(t *testing.T) { // Generate CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) token = strings.Split(strings.Split(token, ";")[0], "=")[1] - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain) ctx.Request.SetBodyString("_csrf=" + token) h(ctx) @@ -282,6 +290,7 @@ func Test_CSRF_From_Custom(t *testing.T) { } func Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) { + t.Parallel() app := fiber.New() errHandler := func(ctx fiber.Ctx, err error) error { @@ -299,13 +308,13 @@ func Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Generate CSRF token - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) // invalid CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(HeaderName, "johndoe") h(ctx) require.Equal(t, 419, ctx.Response.StatusCode()) @@ -313,6 +322,7 @@ func Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) { } func Test_CSRF_ErrorHandler_EmptyToken(t *testing.T) { + t.Parallel() app := fiber.New() errHandler := func(ctx fiber.Ctx, err error) error { @@ -330,68 +340,69 @@ func Test_CSRF_ErrorHandler_EmptyToken(t *testing.T) { ctx := &fasthttp.RequestCtx{} // Generate CSRF token - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) // empty CSRF token ctx.Request.Reset() ctx.Response.Reset() - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 419, ctx.Response.StatusCode()) require.Equal(t, "empty CSRF token", string(ctx.Response.Body())) } // TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase -//func Test_CSRF_UnsafeHeaderValue(t *testing.T) { -// app := fiber.New() -// -// app.Use(New()) -// app.Get("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Get("/test", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Post("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// -// resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) -// utils.AssertEqual(t, nil, err) -// utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) -// -// var token string -// for _, c := range resp.Cookies() { -// if c.Name != ConfigDefault.CookieName { -// continue -// } -// token = c.Value -// break -// } -// -// fmt.Println("token", token) -// -// getReq := httptest.NewRequest(http.MethodGet, "/", nil) -// getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) -// -// getReq = httptest.NewRequest(http.MethodGet, "/test", nil) -// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// getReq.Header.Set(fiber.HeaderCacheControl, "no") -// getReq.Header.Set(HeaderName, token) -// -// resp, err = app.Test(getReq) -// -// getReq.Header.Set(fiber.HeaderAccept, "*/*") -// getReq.Header.Del(HeaderName) -// resp, err = app.Test(getReq) -// -// postReq := httptest.NewRequest(http.MethodPost, "/", nil) -// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// postReq.Header.Set(HeaderName, token) -// resp, err = app.Test(postReq) -//} +// func Test_CSRF_UnsafeHeaderValue(t *testing.T) { +// t.Parallel() +// app := fiber.New() + +// app.Use(New()) +// app.Get("/", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) +// app.Get("/test", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) +// app.Post("/", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) + +// resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) +// utils.AssertEqual(t, nil, err) +// utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + +// var token string +// for _, c := range resp.Cookies() { +// if c.Name != ConfigDefault.CookieName { +// continue +// } +// token = c.Value +// break +// } + +// fmt.Println("token", token) + +// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) +// getReq.Header.Set(HeaderName, token) +// resp, err = app.Test(getReq) + +// getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) +// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") +// getReq.Header.Set(fiber.HeaderCacheControl, "no") +// getReq.Header.Set(HeaderName, token) + +// resp, err = app.Test(getReq) + +// getReq.Header.Set(fiber.HeaderAccept, "*/*") +// getReq.Header.Del(HeaderName) +// resp, err = app.Test(getReq) + +// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) +// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") +// postReq.Header.Set(HeaderName, token) +// resp, err = app.Test(postReq) +// } // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4 func Benchmark_Middleware_CSRF_Check(b *testing.B) { @@ -407,12 +418,12 @@ func Benchmark_Middleware_CSRF_Check(b *testing.B) { ctx := &fasthttp.RequestCtx{} // Generate CSRF token - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) token = strings.Split(strings.Split(token, ";")[0], "=")[1] - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) ctx.Request.Header.Set(HeaderName, token) b.ReportAllocs() @@ -439,7 +450,7 @@ func Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) { ctx := &fasthttp.RequestCtx{} // Generate CSRF token - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) b.ReportAllocs() b.ResetTimer() diff --git a/middleware/csrf/manager.go b/middleware/csrf/manager.go index a0f5b6aff8..e16e8d19fb 100644 --- a/middleware/csrf/manager.go +++ b/middleware/csrf/manager.go @@ -9,7 +9,8 @@ import ( "github.com/gofiber/utils/v2" ) -//go:generate msgp -o=manager_msgp.go -io=false -unexported +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported type item struct{} //msgp:ignore manager @@ -38,74 +39,23 @@ func newManager(storage fiber.Storage) *manager { return manager } -// acquire returns an *entry from the sync.Pool -func (m *manager) acquire() *item { - return m.pool.Get().(*item) -} - -// release and reset *entry to sync.Pool -func (m *manager) release(e *item) { - // don't release item if we using memory storage - if m.storage != nil { - return - } - m.pool.Put(e) -} - -// get data from storage or memory -func (m *manager) get(key string) (it *item) { - if m.storage != nil { - it = m.acquire() - if raw, _ := m.storage.Get(key); raw != nil { - if _, err := it.UnmarshalMsg(raw); err != nil { - return - } - } - return - } - if it, _ = m.memory.Get(key).(*item); it == nil { - it = m.acquire() - } - return -} - // get raw data from storage or memory -func (m *manager) getRaw(key string) (raw []byte) { +func (m *manager) getRaw(key string) []byte { + var raw []byte if m.storage != nil { - raw, _ = m.storage.Get(key) + raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Do not ignore error } else { - raw, _ = m.memory.Get(key).([]byte) - } - return -} - -// set data to storage or memory -func (m *manager) set(key string, it *item, exp time.Duration) { - if m.storage != nil { - if raw, err := it.MarshalMsg(nil); err == nil { - _ = m.storage.Set(key, raw, exp) - } - } else { - // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here - m.memory.Set(utils.CopyString(key), it, exp) + raw, _ = m.memory.Get(key).([]byte) //nolint:errcheck // TODO: Do not ignore error } + return raw } // set data to storage or memory func (m *manager) setRaw(key string, raw []byte, exp time.Duration) { if m.storage != nil { - _ = m.storage.Set(key, raw, exp) + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Do not ignore error } else { // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here m.memory.Set(utils.CopyString(key), raw, exp) } } - -// delete data from storage or memory -func (m *manager) delete(key string) { - if m.storage != nil { - _ = m.storage.Delete(key) - } else { - m.memory.Delete(key) - } -} diff --git a/middleware/earlydata/config.go b/middleware/earlydata/config.go new file mode 100644 index 0000000000..ced705dd57 --- /dev/null +++ b/middleware/earlydata/config.go @@ -0,0 +1,73 @@ +package earlydata + +import ( + "github.com/gofiber/fiber/v3" +) + +const ( + DefaultHeaderName = "Early-Data" + DefaultHeaderTrueValue = "1" +) + +// 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 + + // IsEarlyData returns whether the request is an early-data request. + // + // Optional. Default: a function which checks if the "Early-Data" request header equals "1". + IsEarlyData func(c fiber.Ctx) bool + + // AllowEarlyData returns whether the early-data request should be allowed or rejected. + // + // Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods. + AllowEarlyData func(c fiber.Ctx) bool + + // Error is returned in case an early-data request is rejected. + // + // Optional. Default: fiber.ErrTooEarly. + Error error +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + IsEarlyData: func(c fiber.Ctx) bool { + return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue + }, + + AllowEarlyData: func(c fiber.Ctx) bool { + return fiber.IsMethodSafe(c.Method()) + }, + + Error: fiber.ErrTooEarly, +} + +// 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.IsEarlyData == nil { + cfg.IsEarlyData = ConfigDefault.IsEarlyData + } + + if cfg.AllowEarlyData == nil { + cfg.AllowEarlyData = ConfigDefault.AllowEarlyData + } + + if cfg.Error == nil { + cfg.Error = ConfigDefault.Error + } + + return cfg +} diff --git a/middleware/earlydata/earlydata.go b/middleware/earlydata/earlydata.go new file mode 100644 index 0000000000..2b53341f9c --- /dev/null +++ b/middleware/earlydata/earlydata.go @@ -0,0 +1,47 @@ +package earlydata + +import ( + "github.com/gofiber/fiber/v3" +) + +const ( + localsKeyAllowed = "earlydata_allowed" +) + +func IsEarly(c fiber.Ctx) bool { + return c.Locals(localsKeyAllowed) != nil +} + +// New creates a new middleware handler +// https://datatracker.ietf.org/doc/html/rfc8470#section-5.1 +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // 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() + } + + // Abort if we can't trust the early-data header + if !c.IsProxyTrusted() { + return cfg.Error + } + + // Continue stack if request is not an early-data request + if !cfg.IsEarlyData(c) { + return c.Next() + } + + // Continue stack if we allow early-data for this request + if cfg.AllowEarlyData(c) { + _ = c.Locals(localsKeyAllowed, true) + return c.Next() + } + + // Else return our error + return cfg.Error + } +} diff --git a/middleware/earlydata/earlydata_test.go b/middleware/earlydata/earlydata_test.go new file mode 100644 index 0000000000..7e3c6d3940 --- /dev/null +++ b/middleware/earlydata/earlydata_test.go @@ -0,0 +1,193 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package earlydata_test + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/earlydata" + "github.com/stretchr/testify/require" +) + +const ( + headerName = "Early-Data" + headerValOn = "1" + headerValOff = "0" +) + +func appWithConfig(t *testing.T, c *fiber.Config) *fiber.App { + t.Helper() + t.Parallel() + + var app *fiber.App + if c == nil { + app = fiber.New() + } else { + app = fiber.New(*c) + } + + app.Use(earlydata.New()) + + // Middleware to test IsEarly func + const localsKeyTestValid = "earlydata_testvalid" + app.Use(func(c fiber.Ctx) error { + isEarly := earlydata.IsEarly(c) + + switch h := c.Get(headerName); h { + case "", headerValOff: + if isEarly { + return errors.New("is early-data even though it's not") + } + + case headerValOn: + switch { + case fiber.IsMethodSafe(c.Method()): + if !isEarly { + return errors.New("should be early-data on safe HTTP methods") + } + default: + if isEarly { + return errors.New("early-data unsuported on unsafe HTTP methods") + } + } + + default: + return fmt.Errorf("header has unsupported value: %s", h) + } + + _ = c.Locals(localsKeyTestValid, true) + + return c.Next() + }) + + app.Add([]string{ + fiber.MethodGet, + fiber.MethodPost, + }, "/", func(c fiber.Ctx) error { + valid, ok := c.Locals(localsKeyTestValid).(bool) + if !ok { + panic(fmt.Errorf("failed to type-assert to bool")) + } + if !valid { + return errors.New("handler called even though validation failed") + } + + return nil + }) + + return app +} + +// go test -run Test_EarlyData +func Test_EarlyData(t *testing.T) { + t.Parallel() + + trustedRun := func(t *testing.T, app *fiber.App) { + t.Helper() + + { + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + } + + { + req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + } + } + + untrustedRun := func(t *testing.T, app *fiber.App) { + t.Helper() + + { + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + } + + { + req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusTooEarly, resp.StatusCode) + } + } + + t.Run("empty config", func(t *testing.T) { + app := appWithConfig(t, nil) + trustedRun(t, app) + }) + t.Run("default config", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{}) + trustedRun(t, app) + }) + + t.Run("config with EnableTrustedProxyCheck", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{ + EnableTrustedProxyCheck: true, + }) + untrustedRun(t, app) + }) + t.Run("config with EnableTrustedProxyCheck and trusted TrustedProxies", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{ + "0.0.0.0", + }, + }) + trustedRun(t, app) + }) +} diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go index 79067abb61..a68877308d 100644 --- a/middleware/encryptcookie/config.go +++ b/middleware/encryptcookie/config.go @@ -1,6 +1,8 @@ package encryptcookie -import "github.com/gofiber/fiber/v3" +import ( + "github.com/gofiber/fiber/v3" +) // Config defines the config for middleware. type Config struct { diff --git a/middleware/encryptcookie/encryptcookie.go b/middleware/encryptcookie/encryptcookie.go index 0bbc892a10..a8ce216be2 100644 --- a/middleware/encryptcookie/encryptcookie.go +++ b/middleware/encryptcookie/encryptcookie.go @@ -41,12 +41,12 @@ func New(config ...Config) fiber.Handler { cookieValue.SetKeyBytes(key) if c.Response().Header.Cookie(&cookieValue) { encryptedValue, err := cfg.Encryptor(string(cookieValue.Value()), cfg.Key) - if err == nil { - cookieValue.SetValue(encryptedValue) - c.Response().Header.SetCookie(&cookieValue) - } else { + if err != nil { panic(err) } + + cookieValue.SetValue(encryptedValue) + c.Response().Header.SetCookie(&cookieValue) } } }) diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index c4d400e94c..ee7ca9ae60 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -13,6 +13,7 @@ import ( var testKey = GenerateKey() func Test_Middleware_Encrypt_Cookie(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -34,14 +35,14 @@ func Test_Middleware_Encrypt_Cookie(t *testing.T) { // Test empty cookie ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) require.Equal(t, "value=", string(ctx.Response.Body())) // Test invalid cookie ctx = &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.Header.SetCookie("test", "Invalid") h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) @@ -53,18 +54,19 @@ func Test_Middleware_Encrypt_Cookie(t *testing.T) { // Test valid cookie ctx = &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) encryptedCookie := fasthttp.Cookie{} encryptedCookie.SetKey("test") require.True(t, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") - decryptedCookieValue, _ := DecryptCookie(string(encryptedCookie.Value()), testKey) + decryptedCookieValue, err := DecryptCookie(string(encryptedCookie.Value()), testKey) + require.NoError(t, err) require.Equal(t, "SomeThing", decryptedCookieValue) ctx = &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) @@ -72,6 +74,7 @@ func Test_Middleware_Encrypt_Cookie(t *testing.T) { } func Test_Encrypt_Cookie_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -89,12 +92,13 @@ func Test_Encrypt_Cookie_Next(t *testing.T) { return nil }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "SomeThing", resp.Cookies()[0].Value) } func Test_Encrypt_Cookie_Except(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -120,7 +124,7 @@ func Test_Encrypt_Cookie_Except(t *testing.T) { h := app.Handler() ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) @@ -132,11 +136,13 @@ func Test_Encrypt_Cookie_Except(t *testing.T) { encryptedCookie := fasthttp.Cookie{} encryptedCookie.SetKey("test2") require.True(t, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") - decryptedCookieValue, _ := DecryptCookie(string(encryptedCookie.Value()), testKey) + decryptedCookieValue, err := DecryptCookie(string(encryptedCookie.Value()), testKey) + require.NoError(t, err) require.Equal(t, "SomeThing", decryptedCookieValue) } func Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -165,18 +171,19 @@ func Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) { h := app.Handler() ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("POST") + ctx.Request.Header.SetMethod(fiber.MethodPost) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) encryptedCookie := fasthttp.Cookie{} encryptedCookie.SetKey("test") require.True(t, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") - decodedBytes, _ := base64.StdEncoding.DecodeString(string(encryptedCookie.Value())) + decodedBytes, err := base64.StdEncoding.DecodeString(string(encryptedCookie.Value())) + require.NoError(t, err) require.Equal(t, "SomeThing", string(decodedBytes)) ctx = &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") + ctx.Request.Header.SetMethod(fiber.MethodGet) ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) h(ctx) require.Equal(t, 200, ctx.Response.StatusCode()) diff --git a/middleware/encryptcookie/utils.go b/middleware/encryptcookie/utils.go index 542d160c17..c35064d954 100644 --- a/middleware/encryptcookie/utils.go +++ b/middleware/encryptcookie/utils.go @@ -6,47 +6,56 @@ import ( "crypto/rand" "encoding/base64" "errors" + "fmt" "io" ) // EncryptCookie Encrypts a cookie value with specific encryption key func EncryptCookie(value, key string) (string, error) { - keyDecoded, _ := base64.StdEncoding.DecodeString(key) - plaintext := []byte(value) + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", fmt.Errorf("failed to base64-decode key: %w", err) + } block, err := aes.NewCipher(keyDecoded) if err != nil { - return "", err + return "", fmt.Errorf("failed to create AES cipher: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { - return "", err + return "", fmt.Errorf("failed to create GCM mode: %w", err) } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return "", err + return "", fmt.Errorf("failed to read: %w", err) } - ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // DecryptCookie Decrypts a cookie value with specific encryption key func DecryptCookie(value, key string) (string, error) { - keyDecoded, _ := base64.StdEncoding.DecodeString(key) - enc, _ := base64.StdEncoding.DecodeString(value) + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", fmt.Errorf("failed to base64-decode key: %w", err) + } + enc, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", fmt.Errorf("failed to base64-decode value: %w", err) + } block, err := aes.NewCipher(keyDecoded) if err != nil { - return "", err + return "", fmt.Errorf("failed to create AES cipher: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { - return "", err + return "", fmt.Errorf("failed to create GCM mode: %w", err) } nonceSize := gcm.NonceSize() @@ -59,7 +68,7 @@ func DecryptCookie(value, key string) (string, error) { plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { - return "", err + return "", fmt.Errorf("failed to decrypt ciphertext: %w", err) } return string(plaintext), nil @@ -67,7 +76,8 @@ func DecryptCookie(value, key string) (string, error) { // GenerateKey Generates an encryption key func GenerateKey() string { - ret := make([]byte, 32) + const keyLen = 32 + ret := make([]byte, keyLen) if _, err := rand.Read(ret); err != nil { panic(err) diff --git a/middleware/envvar/README.md b/middleware/envvar/README.md deleted file mode 100644 index a16923d048..0000000000 --- a/middleware/envvar/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# Exposing Environment Variables Middleware - -EnvVar middleware for [Fiber](https://github.com/gofiber/fiber) that can be used to expose environment variables with various options. - -## Table of Contents - -- [Environment Variables (EnvVar) Middleware](#environment-variables-envvar-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Response](#response) - - [Config](#config) - - [Default Config](#default-config-1) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/envvar" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -**Note**: You need to provide a path to use envvar middleware. - -### Default Config - -```go -app.Use("/expose/envvars", envvar.New()) -``` - -### Custom Config - -```go -app.Use("/expose/envvars", envvar.New( - envvar.Config{ - ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, - ExcludeVars: map[string]string{"excludeKey": ""}, - }), -) -``` - -### Response - -Http response contract: -``` -{ - "vars": { - "someEnvVariable": "someValue", - "anotherEnvVariable": "anotherValue" - } -} -``` - -## Config - -```go -// Config defines the config for middleware. -type Config struct { - // ExportVars specifies the environment variables that should export - ExportVars map[string]string - // ExcludeVars specifies the environment variables that should not export - ExcludeVars map[string]string -} -``` - -## Default Config - -```go -Config{} -``` diff --git a/middleware/envvar/envvar.go b/middleware/envvar/envvar.go index 47cd93f1c1..4b9aaf802a 100644 --- a/middleware/envvar/envvar.go +++ b/middleware/envvar/envvar.go @@ -23,10 +23,8 @@ func (envVar *EnvVar) set(key, val string) { envVar.Vars[key] = val } -var defaultConfig = Config{} - func New(config ...Config) fiber.Handler { - var cfg = defaultConfig + var cfg Config if len(config) > 0 { cfg = config[0] } @@ -57,8 +55,9 @@ func newEnvVar(cfg Config) *EnvVar { } } } else { + const numElems = 2 for _, envVal := range os.Environ() { - keyVal := strings.SplitN(envVal, "=", 2) + keyVal := strings.SplitN(envVal, "=", numElems) if _, exists := cfg.ExcludeVars[keyVal[0]]; !exists { vars.set(keyVal[0], keyVal[1]) } diff --git a/middleware/envvar/envvar_test.go b/middleware/envvar/envvar_test.go index 527431d2e0..4f6462c942 100644 --- a/middleware/envvar/envvar_test.go +++ b/middleware/envvar/envvar_test.go @@ -1,6 +1,8 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package envvar import ( + "context" "encoding/json" "io" "net/http" @@ -17,7 +19,8 @@ func TestEnvVarStructWithExportVarsExcludeVars(t *testing.T) { vars := newEnvVar(Config{ ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, - ExcludeVars: map[string]string{"excludeKey": ""}}) + ExcludeVars: map[string]string{"excludeKey": ""}, + }) require.Equal(t, vars.Vars["testKey"], "testEnvValue") require.Equal(t, vars.Vars["testDefaultKey"], "testDefaultVal") @@ -28,23 +31,26 @@ func TestEnvVarStructWithExportVarsExcludeVars(t *testing.T) { func TestEnvVarHandler(t *testing.T) { t.Setenv("testKey", "testVal") - expectedEnvVarResponse, _ := json.Marshal( + expectedEnvVarResponse, err := json.Marshal( struct { Vars map[string]string `json:"vars"` }{ map[string]string{"testKey": "testVal"}, }) + require.NoError(t, err) app := fiber.New() app.Use("/envvars", New(Config{ - ExportVars: map[string]string{"testKey": ""}})) + ExportVars: map[string]string{"testKey": ""}, + })) - req, _ := http.NewRequest("GET", "http://localhost/envvars", nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + require.NoError(t, err) resp, err := app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) respBody, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, expectedEnvVarResponse, respBody) } @@ -52,19 +58,21 @@ func TestEnvVarHandler(t *testing.T) { func TestEnvVarHandlerNotMatched(t *testing.T) { app := fiber.New() app.Use("/envvars", New(Config{ - ExportVars: map[string]string{"testKey": ""}})) + ExportVars: map[string]string{"testKey": ""}, + })) app.Get("/another-path", func(ctx fiber.Ctx) error { require.NoError(t, ctx.SendString("OK")) return nil }) - req, _ := http.NewRequest("GET", "http://localhost/another-path", nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/another-path", nil) + require.NoError(t, err) resp, err := app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) respBody, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, []byte("OK"), respBody) } @@ -75,12 +83,13 @@ func TestEnvVarHandlerDefaultConfig(t *testing.T) { app := fiber.New() app.Use("/envvars", New()) - req, _ := http.NewRequest("GET", "http://localhost/envvars", nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + require.NoError(t, err) resp, err := app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) respBody, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) var envVars EnvVar require.Equal(t, nil, json.Unmarshal(respBody, &envVars)) @@ -92,9 +101,10 @@ func TestEnvVarHandlerMethod(t *testing.T) { app := fiber.New() app.Use("/envvars", New()) - req, _ := http.NewRequest("POST", "http://localhost/envvars", nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "http://localhost/envvars", nil) + require.NoError(t, err) resp, err := app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode) } @@ -107,24 +117,26 @@ func TestEnvVarHandlerSpecialValue(t *testing.T) { app.Use("/envvars", New()) app.Use("/envvars/export", New(Config{ExportVars: map[string]string{testEnvKey: ""}})) - req, _ := http.NewRequest("GET", "http://localhost/envvars", nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + require.NoError(t, err) resp, err := app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) respBody, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) var envVars EnvVar require.Equal(t, nil, json.Unmarshal(respBody, &envVars)) val := envVars.Vars[testEnvKey] require.Equal(t, fakeBase64, val) - req, _ = http.NewRequest("GET", "http://localhost/envvars/export", nil) + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars/export", nil) + require.NoError(t, err) resp, err = app.Test(req) - require.Equal(t, nil, err) + require.NoError(t, err) respBody, err = io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) var envVarsExport EnvVar require.Equal(t, nil, json.Unmarshal(respBody, &envVarsExport)) diff --git a/middleware/etag/README.md b/middleware/etag/README.md deleted file mode 100644 index 7026a2a355..0000000000 --- a/middleware/etag/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# ETag Middleware - -ETag middleware for [Fiber](https://github.com/gofiber/fiber) that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. - -## Table of Contents - -- [ETag Middleware](#etag-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config-2) - -## 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/etag" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: - -### Default Config - -```go -app.Use(etag.New()) - -// Get / receives Etag: "13-1831710635" in response header -app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") -}) -``` - -### Custom Config - -```go -app.Use(etag.New(etag.Config{ - Weak: true, -})) - -// Get / receives Etag: "W/"13-1831710635" in response header -app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") -}) -``` - -## Config - -```go -// 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 - - // Weak indicates that a weak validator is used. Weak etags are easy - // to generate, but are far less useful for comparisons. Strong - // validators are ideal for comparisons but can be very difficult - // to generate efficiently. Weak ETag values of two representations - // of the same resources might be semantically equivalent, but not - // byte-for-byte identical. This means weak etags prevent caching - // when byte range requests are used, but strong etags mean range - // requests can still be cached. - Weak bool -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - Weak: false, -} -``` diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go index fe94fc9337..881e18c5e7 100644 --- a/middleware/etag/etag.go +++ b/middleware/etag/etag.go @@ -8,42 +8,43 @@ import ( "github.com/valyala/bytebufferpool" ) -var ( - normalizedHeaderETag = []byte("Etag") - weakPrefix = []byte("W/") -) - // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) - crc32q := crc32.MakeTable(0xD5828281) + var ( + normalizedHeaderETag = []byte("Etag") + weakPrefix = []byte("W/") + ) + + const crcPol = 0xD5828281 + crc32q := crc32.MakeTable(crcPol) // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } // Return err if next handler returns one - if err = c.Next(); err != nil { - return + if err := c.Next(); err != nil { + return err } // Don't generate ETags for invalid responses if c.Response().StatusCode() != fiber.StatusOK { - return + return nil } body := c.Response().Body() // Skips ETag if no response body is present if len(body) == 0 { - return + return nil } // Skip ETag if header is already present if c.Response().Header.PeekBytes(normalizedHeaderETag) != nil { - return + return nil } // Generate ETag for response @@ -52,14 +53,14 @@ func New(config ...Config) fiber.Handler { // Enable weak tag if cfg.Weak { - _, _ = bb.Write(weakPrefix) + _, _ = bb.Write(weakPrefix) //nolint:errcheck // This will never fail } - _ = bb.WriteByte('"') + _ = bb.WriteByte('"') //nolint:errcheck // This will never fail bb.B = appendUint(bb.Bytes(), uint32(len(body))) - _ = bb.WriteByte('-') + _ = bb.WriteByte('-') //nolint:errcheck // This will never fail bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q)) - _ = bb.WriteByte('"') + _ = bb.WriteByte('"') //nolint:errcheck // This will never fail etag := bb.Bytes() @@ -78,7 +79,7 @@ func New(config ...Config) fiber.Handler { // W/1 != W/2 || W/1 != 2 c.Response().Header.SetCanonical(normalizedHeaderETag, etag) - return + return nil } if bytes.Contains(clientEtag, etag) { @@ -90,7 +91,7 @@ func New(config ...Config) fiber.Handler { // 1 != 2 c.Response().Header.SetCanonical(normalizedHeaderETag, etag) - return + return nil } } diff --git a/middleware/etag/etag_test.go b/middleware/etag/etag_test.go index 4768eb5793..19394534e1 100644 --- a/middleware/etag/etag_test.go +++ b/middleware/etag/etag_test.go @@ -13,6 +13,7 @@ import ( // go test -run Test_ETag_Next func Test_ETag_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -20,13 +21,14 @@ func Test_ETag_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } // go test -run Test_ETag_SkipError func Test_ETag_SkipError(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -35,13 +37,14 @@ func Test_ETag_SkipError(t *testing.T) { return fiber.ErrForbidden }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusForbidden, resp.StatusCode) } // go test -run Test_ETag_NotStatusOK func Test_ETag_NotStatusOK(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -50,13 +53,14 @@ func Test_ETag_NotStatusOK(t *testing.T) { return c.SendStatus(fiber.StatusCreated) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusCreated, resp.StatusCode) } // go test -run Test_ETag_NoBody func Test_ETag_NoBody(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -65,25 +69,29 @@ func Test_ETag_NoBody(t *testing.T) { return nil }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) } // go test -run Test_ETag_NewEtag func Test_ETag_NewEtag(t *testing.T) { + t.Parallel() t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() testETagNewEtag(t, false, false) }) t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() testETagNewEtag(t, true, false) }) t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() testETagNewEtag(t, true, true) }) } -func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { +func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine t.Helper() app := fiber.New() @@ -94,7 +102,7 @@ func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { return c.SendString("Hello, World!") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) if headerIfNoneMatch { etag := `"non-match"` if matched { @@ -122,18 +130,22 @@ func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { // go test -run Test_ETag_WeakEtag func Test_ETag_WeakEtag(t *testing.T) { + t.Parallel() t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() testETagWeakEtag(t, false, false) }) t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() testETagWeakEtag(t, true, false) }) t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() testETagWeakEtag(t, true, true) }) } -func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { +func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine t.Helper() app := fiber.New() @@ -144,7 +156,7 @@ func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { return c.SendString("Hello, World!") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) if headerIfNoneMatch { etag := `W/"non-match"` if matched { @@ -172,18 +184,22 @@ func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { // go test -run Test_ETag_CustomEtag func Test_ETag_CustomEtag(t *testing.T) { + t.Parallel() t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() testETagCustomEtag(t, false, false) }) t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() testETagCustomEtag(t, true, false) }) t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() testETagCustomEtag(t, true, true) }) } -func testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { +func testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine t.Helper() app := fiber.New() @@ -198,7 +214,7 @@ func testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { return c.SendString("Hello, World!") }) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) if headerIfNoneMatch { etag := `"non-match"` if matched { @@ -226,6 +242,7 @@ func testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { // go test -run Test_ETag_CustomEtagPut func Test_ETag_CustomEtagPut(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -238,7 +255,7 @@ func Test_ETag_CustomEtagPut(t *testing.T) { return c.SendString("Hello, World!") }) - req := httptest.NewRequest("PUT", "/", nil) + req := httptest.NewRequest(fiber.MethodPut, "/", nil) req.Header.Set(fiber.HeaderIfMatch, `"non-match"`) resp, err := app.Test(req) require.NoError(t, err) @@ -258,7 +275,7 @@ func Benchmark_Etag(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") b.ReportAllocs() diff --git a/middleware/expvar/README.md b/middleware/expvar/README.md deleted file mode 100644 index b6f0a51b85..0000000000 --- a/middleware/expvar/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Expvar Middleware - -Expvar middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime exposed variants in the JSON format. The package is typically only imported for the side effect of registering its HTTP handlers. The handled path is `/debug/vars`. - -- [Expvar Middleware](#expvar-middleware) - - [Signatures](#signatures) - - [Example](#example) - -## Signatures - -```go -func New() fiber.Handler -``` - -## Example - -Import the expvar package that is part of the Fiber web framework - -```go -package main - -import ( - "expvar" - "fmt" - - "github.com/gofiber/fiber/v3" - expvarmw "github.com/gofiber/fiber/v3/middleware/expvar" -) - -var count = expvar.NewInt("count") - -func main() { - app := fiber.New() - app.Use(expvarmw.New()) - app.Get("/", func(c fiber.Ctx) error { - count.Add(1) - - return c.SendString(fmt.Sprintf("hello expvar count %d", count.Value())) - }) - - fmt.Println(app.Listen(":3000")) -} -``` - -Visit path `/debug/vars` to see all vars and use query `r=key` to filter exposed variables. - -```bash -curl 127.0.0.1:3000 -hello expvar count 1 - -curl 127.0.0.1:3000/debug/vars -{ - "cmdline": ["xxx"], - "count": 1, - "expvarHandlerCalls": 33, - "expvarRegexpErrors": 0, - "memstats": {...} -} - -curl 127.0.0.1:3000/debug/vars?r=c -{ - "cmdline": ["xxx"], - "count": 1 -} -``` - -## Config - -```go -// 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 -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, -} -``` \ No newline at end of file diff --git a/middleware/expvar/config.go b/middleware/expvar/config.go index 28ac231af2..1def8f2ca8 100644 --- a/middleware/expvar/config.go +++ b/middleware/expvar/config.go @@ -1,6 +1,8 @@ package expvar -import "github.com/gofiber/fiber/v3" +import ( + "github.com/gofiber/fiber/v3" +) // Config defines the config for middleware. type Config struct { diff --git a/middleware/expvar/expvar_test.go b/middleware/expvar/expvar_test.go index 98e1a48560..a55198255f 100644 --- a/middleware/expvar/expvar_test.go +++ b/middleware/expvar/expvar_test.go @@ -11,6 +11,7 @@ import ( ) func Test_Non_Expvar_Path(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -29,6 +30,7 @@ func Test_Non_Expvar_Path(t *testing.T) { } func Test_Expvar_Index(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -49,6 +51,7 @@ func Test_Expvar_Index(t *testing.T) { } func Test_Expvar_Filter(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -69,6 +72,7 @@ func Test_Expvar_Filter(t *testing.T) { } func Test_Expvar_Other_Path(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -85,7 +89,6 @@ func Test_Expvar_Other_Path(t *testing.T) { // go test -run Test_Expvar_Next func Test_Expvar_Next(t *testing.T) { t.Parallel() - app := fiber.New() app.Use(New(Config{ diff --git a/middleware/favicon/README.md b/middleware/favicon/README.md deleted file mode 100644 index 8b74ba2b3b..0000000000 --- a/middleware/favicon/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Favicon Middleware - -Favicon middleware for [Fiber](https://github.com/gofiber/fiber) that ignores favicon requests or caches a provided icon in memory to improve performance by skipping disk access. User agents request favicon.ico frequently and indiscriminately, so you may wish to exclude these requests from your logs by using this middleware before your logger middleware. - -**Note** This middleware is exclusively for serving the default, implicit favicon, which is GET /favicon.ico. - -## Table of Contents -- [Favicon Middleware](#favicon-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config-1) -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/favicon" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -app.Use(favicon.New()) -``` - -### Custom Config -```go -app.Use(favicon.New(favicon.Config{ - File: "./favicon.ico", -})) -``` - -### Config - -```go -// 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 - - // File holds the path to an actual favicon that will be cached - // - // Optional. Default: "" - File string -} -``` - -### Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - File: "" -} -``` diff --git a/middleware/favicon/favicon.go b/middleware/favicon/favicon.go index ce5d042115..1e94ef983a 100644 --- a/middleware/favicon/favicon.go +++ b/middleware/favicon/favicon.go @@ -21,6 +21,11 @@ type Config struct { // Optional. Default: "" File string `json:"file"` + // URL for favicon handler + // + // Optional. Default: "/favicon.ico" + URL string `json:"url"` + // FileSystem is an optional alternate filesystem to search for the favicon in. // An example of this could be an embedded or network filesystem // @@ -37,6 +42,7 @@ type Config struct { var ConfigDefault = Config{ Next: nil, File: "", + URL: fPath, CacheControl: "public, max-age=31536000", } @@ -60,6 +66,9 @@ func New(config ...Config) fiber.Handler { if cfg.Next == nil { cfg.Next = ConfigDefault.Next } + if cfg.URL == "" { + cfg.URL = ConfigDefault.URL + } if cfg.File == "" { cfg.File = ConfigDefault.File } @@ -99,7 +108,7 @@ func New(config ...Config) fiber.Handler { } // Only respond to favicon requests - if c.Path() != fPath { + if c.Path() != cfg.URL { return c.Next() } diff --git a/middleware/favicon/favicon_test.go b/middleware/favicon/favicon_test.go index 3db323cd7e..a39330b074 100644 --- a/middleware/favicon/favicon_test.go +++ b/middleware/favicon/favicon_test.go @@ -13,6 +13,7 @@ import ( // go test -run Test_Middleware_Favicon func Test_Middleware_Favicon(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -22,19 +23,19 @@ func Test_Middleware_Favicon(t *testing.T) { }) // Skip Favicon middleware - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") - resp, err = app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusNoContent, resp.StatusCode, "Status code") - resp, err = app.Test(httptest.NewRequest("OPTIONS", "/favicon.ico", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodOptions, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") - resp, err = app.Test(httptest.NewRequest("PUT", "/favicon.ico", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodPut, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode, "Status code") require.Equal(t, "GET, HEAD, OPTIONS", resp.Header.Get(fiber.HeaderAllow)) @@ -42,6 +43,7 @@ func Test_Middleware_Favicon(t *testing.T) { // go test -run Test_Middleware_Favicon_Not_Found func Test_Middleware_Favicon_Not_Found(t *testing.T) { + t.Parallel() defer func() { if err := recover(); err == nil { t.Fatal("should cache panic") @@ -55,6 +57,7 @@ func Test_Middleware_Favicon_Not_Found(t *testing.T) { // go test -run Test_Middleware_Favicon_Found func Test_Middleware_Favicon_Found(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -65,7 +68,7 @@ func Test_Middleware_Favicon_Found(t *testing.T) { return nil }) - resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") @@ -75,6 +78,7 @@ func Test_Middleware_Favicon_Found(t *testing.T) { // go test -run Test_Middleware_Favicon_FileSystem func Test_Middleware_Favicon_FileSystem(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -82,7 +86,7 @@ func Test_Middleware_Favicon_FileSystem(t *testing.T) { FileSystem: os.DirFS("../../.github/testdata"), })) - resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") require.Equal(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) @@ -91,6 +95,7 @@ func Test_Middleware_Favicon_FileSystem(t *testing.T) { // go test -run Test_Middleware_Favicon_CacheControl func Test_Middleware_Favicon_CacheControl(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -98,7 +103,7 @@ func Test_Middleware_Favicon_CacheControl(t *testing.T) { File: "../../.github/testdata/favicon.ico", })) - resp, err := app.Test(httptest.NewRequest("GET", "/favicon.ico", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") require.Equal(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) @@ -126,6 +131,7 @@ func Benchmark_Middleware_Favicon(b *testing.B) { // go test -run Test_Favicon_Next func Test_Favicon_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -133,7 +139,27 @@ func Test_Favicon_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } + +// go test -run Test_Custom_Favicon_URL +func Test_Custom_Favicon_URL(t *testing.T) { + app := fiber.New() + const customURL = "/favicon.svg" + app.Use(New(Config{ + File: "../../.github/testdata/favicon.ico", + URL: customURL, + })) + + app.Get("/", func(c fiber.Ctx) error { + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, customURL, nil)) + + require.NoError(t, err, "app.Test(req)") + require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") + require.Equal(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) +} diff --git a/middleware/filesystem/README.md b/middleware/filesystem/README.md deleted file mode 100644 index e7f3cbbb5e..0000000000 --- a/middleware/filesystem/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# Filesystem Middleware - -Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables you to serve files from a directory. - -⚠️ **`:params` & `:optionals?` within the prefix path are not supported!** - -## Table of Contents - -- [Filesystem Middleware](#filesystem-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Config](#config) - - [embed](#embed) - - [Config](#config-1) - - [Default Config](#default-config) - -## Signatures - -```go -func New(config Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/filesystem" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Config - -```go -// Provide a minimal config -app.Use(filesystem.New(filesystem.Config{ - Root: os.DirFS("./assets"), -})) - -// Or extend your config for customization -app.Use(filesystem.New(filesystem.Config{ - Root: os.DirFS("./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: 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: embedDirStatic, - Browse: true, - })) - - log.Fatal(app.Listen(":3000")) -} -``` - -## Config - -```go -// 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"` - - // 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"` -} -``` - -### Default Config - -```go -var ConfigDefault = Config{ - Next: nil, - Root: nil, - PathPrefix: ".", - Browse: false, - Index: "/index.html", - MaxAge: 0, -} -``` diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go index c8f08b636f..055cb9ae64 100644 --- a/middleware/filesystem/filesystem.go +++ b/middleware/filesystem/filesystem.go @@ -1,6 +1,8 @@ package filesystem import ( + "errors" + "fmt" "io/fs" "net/http" "os" @@ -56,19 +58,30 @@ type Config struct { // // 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, + Next: nil, + Root: nil, + PathPrefix: ".", + Browse: false, + Index: "/index.html", + MaxAge: 0, + ContentTypeCharset: "", } -// New creates a new middleware handler +// 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 @@ -110,7 +123,7 @@ func New(config ...Config) fiber.Handler { cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge) // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() @@ -149,21 +162,22 @@ func New(config ...Config) fiber.Handler { path = strings.TrimRight(path, "/") } - file, err = openFile(cfg.Root, path) + file, err := openFile(cfg.Root, path) - if err != nil && os.IsNotExist(err) && cfg.NotFoundFile != "" { + if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" { file, err = openFile(cfg.Root, cfg.NotFoundFile) } if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return c.Status(fiber.StatusNotFound).Next() } - return + return fmt.Errorf("failed to open: %w", err) } - if stat, err = file.Stat(); err != nil { - return + stat, err = file.Stat() + if err != nil { + return fmt.Errorf("failed to stat: %w", err) } // Serve index if path is directory @@ -194,7 +208,11 @@ func New(config ...Config) fiber.Handler { contentLength := int(stat.Size()) // Set Content Type header - c.Type(getFileExtension(stat.Name())) + if cfg.ContentTypeCharset == "" { + c.Type(getFileExtension(stat.Name())) + } else { + c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset) + } // Set Last Modified header if !modTime.IsZero() { @@ -219,7 +237,7 @@ func New(config ...Config) fiber.Handler { c.Response().SkipBody = true c.Response().Header.SetContentLength(contentLength) if err := file.Close(); err != nil { - return err + return fmt.Errorf("failed to close: %w", err) } return nil } @@ -229,7 +247,7 @@ func New(config ...Config) fiber.Handler { } // SendFile ... -func SendFile(c fiber.Ctx, filesystem fs.FS, path string) (err error) { +func SendFile(c fiber.Ctx, filesystem fs.FS, path string) error { var ( file fs.File stat os.FileInfo @@ -237,16 +255,17 @@ func SendFile(c fiber.Ctx, filesystem fs.FS, path string) (err error) { path = filepath.Join(".", filepath.Clean("/"+path)) - file, err = openFile(filesystem, path) + file, err := openFile(filesystem, path) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return fiber.ErrNotFound } - return err + return fmt.Errorf("failed to open: %w", err) } - if stat, err = file.Stat(); err != nil { - return err + stat, err = file.Stat() + if err != nil { + return fmt.Errorf("failed to stat: %w", err) } // Serve index if path is directory @@ -289,7 +308,7 @@ func SendFile(c fiber.Ctx, filesystem fs.FS, path string) (err error) { c.Response().SkipBody = true c.Response().Header.SetContentLength(contentLength) if err := file.Close(); err != nil { - return err + return fmt.Errorf("failed to close: %w", err) } return nil } diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go index dd2e8c0fbd..b7c24b8fcc 100644 --- a/middleware/filesystem/filesystem_test.go +++ b/middleware/filesystem/filesystem_test.go @@ -1,6 +1,8 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package filesystem import ( + "context" "net/http" "net/http/httptest" "os" @@ -12,6 +14,7 @@ import ( // go test -run Test_FileSystem func Test_FileSystem(t *testing.T) { + t.Parallel() app := fiber.New() app.Use("/test", New(Config{ @@ -118,7 +121,7 @@ func Test_FileSystem(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - resp, err := app.Test(httptest.NewRequest("GET", tt.url, nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tt.url, nil)) require.NoError(t, err) require.Equal(t, tt.statusCode, resp.StatusCode) @@ -132,6 +135,7 @@ func Test_FileSystem(t *testing.T) { // 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"), @@ -140,7 +144,7 @@ func Test_FileSystem_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } @@ -153,7 +157,7 @@ func Test_FileSystem_Download(t *testing.T) { Download: true, })) - resp, err := app.Test(httptest.NewRequest("GET", "/img/fiber.png", nil)) + 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.False(t, resp.Header.Get(fiber.HeaderContentLength) == "") @@ -162,6 +166,7 @@ func Test_FileSystem_Download(t *testing.T) { } func Test_FileSystem_NonGetAndHead(t *testing.T) { + t.Parallel() app := fiber.New() app.Use("/test", New(Config{ @@ -174,50 +179,73 @@ func Test_FileSystem_NonGetAndHead(t *testing.T) { } func Test_FileSystem_Head(t *testing.T) { + t.Parallel() app := fiber.New() app.Use("/test", New(Config{ Root: os.DirFS("../../.github/testdata/fs"), })) - req, _ := http.NewRequest(fiber.MethodHead, "/test", nil) + 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()) - _, _ = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + _, 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, _ := http.NewRequest(fiber.MethodHead, "/index", nil) + 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, _ := http.NewRequest(fiber.MethodHead, "/template", nil) + 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 index 3561870a0a..3cf7fddc1f 100644 --- a/middleware/filesystem/utils.go +++ b/middleware/filesystem/utils.go @@ -12,19 +12,22 @@ import ( "github.com/gofiber/fiber/v3" ) -func getFileExtension(path string) string { - n := strings.LastIndexByte(path, '.') +func getFileExtension(p string) string { + n := strings.LastIndexByte(p, '.') if n < 0 { return "" } - return path[n:] + return p[n:] } func dirList(c fiber.Ctx, f fs.File) error { - ff := f.(fs.ReadDirFile) + ff, ok := f.(fs.ReadDirFile) + if !ok { + return fmt.Errorf("failed to type-assert to fs.ReadDirFile") + } fileinfos, err := ff.ReadDir(-1) if err != nil { - return err + return fmt.Errorf("failed to read dir: %w", err) } fm := make(map[string]fs.FileInfo, len(fileinfos)) @@ -33,7 +36,7 @@ func dirList(c fiber.Ctx, f fs.File) error { name := fi.Name() info, err := fi.Info() if err != nil { - return err + return fmt.Errorf("failed to get file info: %w", err) } fm[name] = info @@ -41,13 +44,13 @@ func dirList(c fiber.Ctx, f fs.File) error { } basePathEscaped := html.EscapeString(c.Path()) - fmt.Fprintf(c, "%s", basePathEscaped) - fmt.Fprintf(c, "

%s

", basePathEscaped) - fmt.Fprint(c, "
    ") + _, _ = 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) + _, _ = fmt.Fprintf(c, `
    • ..
    • `, parentPathEscaped) } sort.Strings(filenames) @@ -60,18 +63,23 @@ func dirList(c fiber.Ctx, f fs.File) error { auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) className = "file" } - fmt.Fprintf(c, `
    • %s, %s, last modified %s
    • `, + _, _ = fmt.Fprintf(c, `
    • %s, %s, last modified %s
    • `, pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime()) } - fmt.Fprint(c, "
    ") + _, _ = fmt.Fprint(c, "
") c.Type("html") return nil } -func openFile(fs fs.FS, name string) (fs.File, error) { +func openFile(filesystem fs.FS, name string) (fs.File, error) { name = filepath.ToSlash(name) - return fs.Open(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/helmet/config.go b/middleware/helmet/config.go new file mode 100644 index 0000000000..dbbcd37759 --- /dev/null +++ b/middleware/helmet/config.go @@ -0,0 +1,154 @@ +package helmet + +import ( + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(fiber.Ctx) bool + + // XSSProtection + // Optional. Default value "0". + XSSProtection string + + // ContentTypeNosniff + // Optional. Default value "nosniff". + ContentTypeNosniff string + + // XFrameOptions + // Optional. Default value "SAMEORIGIN". + // Possible values: "SAMEORIGIN", "DENY", "ALLOW-FROM uri" + XFrameOptions string + + // HSTSMaxAge + // Optional. Default value 0. + HSTSMaxAge int + + // HSTSExcludeSubdomains + // Optional. Default value false. + HSTSExcludeSubdomains bool + + // ContentSecurityPolicy + // Optional. Default value "". + ContentSecurityPolicy string + + // CSPReportOnly + // Optional. Default value false. + CSPReportOnly bool + + // HSTSPreloadEnabled + // Optional. Default value false. + HSTSPreloadEnabled bool + + // ReferrerPolicy + // Optional. Default value "ReferrerPolicy". + ReferrerPolicy string + + // Permissions-Policy + // Optional. Default value "". + PermissionPolicy string + + // Cross-Origin-Embedder-Policy + // Optional. Default value "require-corp". + CrossOriginEmbedderPolicy string + + // Cross-Origin-Opener-Policy + // Optional. Default value "same-origin". + CrossOriginOpenerPolicy string + + // Cross-Origin-Resource-Policy + // Optional. Default value "same-origin". + CrossOriginResourcePolicy string + + // Origin-Agent-Cluster + // Optional. Default value "?1". + OriginAgentCluster string + + // X-DNS-Prefetch-Control + // Optional. Default value "off". + XDNSPrefetchControl string + + // X-Download-Options + // Optional. Default value "noopen". + XDownloadOptions string + + // X-Permitted-Cross-Domain-Policies + // Optional. Default value "none". + XPermittedCrossDomain string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + ReferrerPolicy: "no-referrer", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", +} + +// 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.XSSProtection == "" { + cfg.XSSProtection = ConfigDefault.XSSProtection + } + + if cfg.ContentTypeNosniff == "" { + cfg.ContentTypeNosniff = ConfigDefault.ContentTypeNosniff + } + + if cfg.XFrameOptions == "" { + cfg.XFrameOptions = ConfigDefault.XFrameOptions + } + + if cfg.ReferrerPolicy == "" { + cfg.ReferrerPolicy = ConfigDefault.ReferrerPolicy + } + + if cfg.CrossOriginEmbedderPolicy == "" { + cfg.CrossOriginEmbedderPolicy = ConfigDefault.CrossOriginEmbedderPolicy + } + + if cfg.CrossOriginOpenerPolicy == "" { + cfg.CrossOriginOpenerPolicy = ConfigDefault.CrossOriginOpenerPolicy + } + + if cfg.CrossOriginResourcePolicy == "" { + cfg.CrossOriginResourcePolicy = ConfigDefault.CrossOriginResourcePolicy + } + + if cfg.OriginAgentCluster == "" { + cfg.OriginAgentCluster = ConfigDefault.OriginAgentCluster + } + + if cfg.XDNSPrefetchControl == "" { + cfg.XDNSPrefetchControl = ConfigDefault.XDNSPrefetchControl + } + + if cfg.XDownloadOptions == "" { + cfg.XDownloadOptions = ConfigDefault.XDownloadOptions + } + + if cfg.XPermittedCrossDomain == "" { + cfg.XPermittedCrossDomain = ConfigDefault.XPermittedCrossDomain + } + + return cfg +} diff --git a/middleware/helmet/helmet.go b/middleware/helmet/helmet.go index f966a73131..6548d9bee8 100644 --- a/middleware/helmet/helmet.go +++ b/middleware/helmet/helmet.go @@ -1,7 +1,3 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://docs.gofiber.io/ -// 📝 Github Repository: https://github.com/gofiber/fiber - package helmet import ( @@ -10,87 +6,76 @@ import ( "github.com/gofiber/fiber/v3" ) -// Config ... -type Config struct { - // Filter defines a function to skip middleware. - // Optional. Default: nil - Filter func(fiber.Ctx) bool - // XSSProtection - // Optional. Default value "1; mode=block". - XSSProtection string - // ContentTypeNosniff - // Optional. Default value "nosniff". - ContentTypeNosniff string - // XFrameOptions - // Optional. Default value "SAMEORIGIN". - // Possible values: "SAMEORIGIN", "DENY", "ALLOW-FROM uri" - XFrameOptions string - // HSTSMaxAge - // Optional. Default value 0. - HSTSMaxAge int - // HSTSExcludeSubdomains - // Optional. Default value false. - HSTSExcludeSubdomains bool - // ContentSecurityPolicy - // Optional. Default value "". - ContentSecurityPolicy string - // CSPReportOnly - // Optional. Default value false. - CSPReportOnly bool - // HSTSPreloadEnabled - // Optional. Default value false. - HSTSPreloadEnabled bool - // ReferrerPolicy - // Optional. Default value "". - ReferrerPolicy string - - // Permissions-Policy - // Optional. Default value "". - PermissionPolicy string -} - -// New ... +// New creates a new middleware handler func New(config ...Config) fiber.Handler { // Init config - var cfg Config - if len(config) > 0 { - cfg = config[0] - } - // Set config default values - if cfg.XSSProtection == "" { - cfg.XSSProtection = "1; mode=block" - } - if cfg.ContentTypeNosniff == "" { - cfg.ContentTypeNosniff = "nosniff" - } - if cfg.XFrameOptions == "" { - cfg.XFrameOptions = "SAMEORIGIN" - } + cfg := configDefault(config...) + // Return middleware handler return func(c fiber.Ctx) error { - // Filter request to skip middleware - if cfg.Filter != nil && cfg.Filter(c) { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { return c.Next() } + + // Set headers if cfg.XSSProtection != "" { c.Set(fiber.HeaderXXSSProtection, cfg.XSSProtection) } + if cfg.ContentTypeNosniff != "" { c.Set(fiber.HeaderXContentTypeOptions, cfg.ContentTypeNosniff) } + if cfg.XFrameOptions != "" { c.Set(fiber.HeaderXFrameOptions, cfg.XFrameOptions) } - if (c.Secure() || (c.Get(fiber.HeaderXForwardedProto) == "https")) && cfg.HSTSMaxAge != 0 { + + if cfg.CrossOriginEmbedderPolicy != "" { + c.Set("Cross-Origin-Embedder-Policy", cfg.CrossOriginEmbedderPolicy) + } + + if cfg.CrossOriginOpenerPolicy != "" { + c.Set("Cross-Origin-Opener-Policy", cfg.CrossOriginOpenerPolicy) + } + + if cfg.CrossOriginResourcePolicy != "" { + c.Set("Cross-Origin-Resource-Policy", cfg.CrossOriginResourcePolicy) + } + + if cfg.OriginAgentCluster != "" { + c.Set("Origin-Agent-Cluster", cfg.OriginAgentCluster) + } + + if cfg.ReferrerPolicy != "" { + c.Set("Referrer-Policy", cfg.ReferrerPolicy) + } + + if cfg.XDNSPrefetchControl != "" { + c.Set("X-DNS-Prefetch-Control", cfg.XDNSPrefetchControl) + } + + if cfg.XDownloadOptions != "" { + c.Set("X-Download-Options", cfg.XDownloadOptions) + } + + if cfg.XPermittedCrossDomain != "" { + c.Set("X-Permitted-Cross-Domain-Policies", cfg.XPermittedCrossDomain) + } + + // Handle HSTS headers + if c.Protocol() == "https" && cfg.HSTSMaxAge != 0 { subdomains := "" if !cfg.HSTSExcludeSubdomains { - subdomains = "; includeSubdomains" + subdomains = "; includeSubDomains" } if cfg.HSTSPreloadEnabled { subdomains = fmt.Sprintf("%s; preload", subdomains) } c.Set(fiber.HeaderStrictTransportSecurity, fmt.Sprintf("max-age=%d%s", cfg.HSTSMaxAge, subdomains)) } + + // Handle Content-Security-Policy headers if cfg.ContentSecurityPolicy != "" { if cfg.CSPReportOnly { c.Set(fiber.HeaderContentSecurityPolicyReportOnly, cfg.ContentSecurityPolicy) @@ -98,13 +83,12 @@ func New(config ...Config) fiber.Handler { c.Set(fiber.HeaderContentSecurityPolicy, cfg.ContentSecurityPolicy) } } - if cfg.ReferrerPolicy != "" { - c.Set(fiber.HeaderReferrerPolicy, cfg.ReferrerPolicy) - } + + // Handle Permissions-Policy headers if cfg.PermissionPolicy != "" { c.Set(fiber.HeaderPermissionsPolicy, cfg.PermissionPolicy) - } + return c.Next() } } diff --git a/middleware/helmet/helmet_test.go b/middleware/helmet/helmet_test.go index 0b13b5a0d9..c9d4b1c457 100644 --- a/middleware/helmet/helmet_test.go +++ b/middleware/helmet/helmet_test.go @@ -1,7 +1,3 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://docs.gofiber.io/ -// 📝 Github Repository: https://github.com/gofiber/fiber - package helmet import ( @@ -21,22 +17,119 @@ func Test_Default(t *testing.T) { return c.SendString("Hello, World!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) - require.Equal(t, "1; mode=block", resp.Header.Get(fiber.HeaderXXSSProtection)) + require.Equal(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) require.Equal(t, "SAMEORIGIN", resp.Header.Get(fiber.HeaderXFrameOptions)) require.Equal(t, "", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) - require.Equal(t, "", resp.Header.Get(fiber.HeaderReferrerPolicy)) + require.Equal(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) require.Equal(t, "", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + require.Equal(t, "require-corp", resp.Header.Get("Cross-Origin-Embedder-Policy")) + require.Equal(t, "same-origin", resp.Header.Get("Cross-Origin-Opener-Policy")) + require.Equal(t, "same-origin", resp.Header.Get("Cross-Origin-Resource-Policy")) + require.Equal(t, "?1", resp.Header.Get("Origin-Agent-Cluster")) + require.Equal(t, "off", resp.Header.Get("X-DNS-Prefetch-Control")) + require.Equal(t, "noopen", resp.Header.Get("X-Download-Options")) + require.Equal(t, "none", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) +} + +func Test_CustomValues_AllHeaders(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + // Custom values for all headers + XSSProtection: "0", + ContentTypeNosniff: "custom-nosniff", + XFrameOptions: "DENY", + HSTSExcludeSubdomains: true, + ContentSecurityPolicy: "default-src 'none'", + CSPReportOnly: true, + HSTSPreloadEnabled: true, + ReferrerPolicy: "origin", + PermissionPolicy: "geolocation=(self)", + CrossOriginEmbedderPolicy: "custom-value", + CrossOriginOpenerPolicy: "custom-value", + CrossOriginResourcePolicy: "custom-value", + OriginAgentCluster: "custom-value", + XDNSPrefetchControl: "custom-control", + XDownloadOptions: "custom-options", + XPermittedCrossDomain: "custom-policies", + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + // Assertions for custom header values + require.Equal(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) + require.Equal(t, "custom-nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) + require.Equal(t, "DENY", resp.Header.Get(fiber.HeaderXFrameOptions)) + require.Equal(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly)) + require.Equal(t, "origin", resp.Header.Get(fiber.HeaderReferrerPolicy)) + require.Equal(t, "geolocation=(self)", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + require.Equal(t, "custom-value", resp.Header.Get("Cross-Origin-Embedder-Policy")) + require.Equal(t, "custom-value", resp.Header.Get("Cross-Origin-Opener-Policy")) + require.Equal(t, "custom-value", resp.Header.Get("Cross-Origin-Resource-Policy")) + require.Equal(t, "custom-value", resp.Header.Get("Origin-Agent-Cluster")) + require.Equal(t, "custom-control", resp.Header.Get("X-DNS-Prefetch-Control")) + require.Equal(t, "custom-options", resp.Header.Get("X-Download-Options")) + require.Equal(t, "custom-policies", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) +} + +func Test_RealWorldValues_AllHeaders(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + // Real-world values for all headers + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSExcludeSubdomains: false, + ContentSecurityPolicy: "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", + CSPReportOnly: false, + HSTSPreloadEnabled: true, + ReferrerPolicy: "no-referrer", + PermissionPolicy: "geolocation=(self)", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + // Assertions for real-world header values + require.Equal(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) + require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) + require.Equal(t, "SAMEORIGIN", resp.Header.Get(fiber.HeaderXFrameOptions)) + require.Equal(t, "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) + require.Equal(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) + require.Equal(t, "geolocation=(self)", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + require.Equal(t, "require-corp", resp.Header.Get("Cross-Origin-Embedder-Policy")) + require.Equal(t, "same-origin", resp.Header.Get("Cross-Origin-Opener-Policy")) + require.Equal(t, "same-origin", resp.Header.Get("Cross-Origin-Resource-Policy")) + require.Equal(t, "?1", resp.Header.Get("Origin-Agent-Cluster")) + require.Equal(t, "off", resp.Header.Get("X-DNS-Prefetch-Control")) + require.Equal(t, "noopen", resp.Header.Get("X-Download-Options")) + require.Equal(t, "none", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) } -func Test_Filter(t *testing.T) { +func Test_Next(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Filter: func(c fiber.Ctx) bool { - return c.Path() == "/filter" + Next: func(ctx fiber.Ctx) bool { + return ctx.Path() == "/next" }, ReferrerPolicy: "no-referrer", })) @@ -44,15 +137,15 @@ func Test_Filter(t *testing.T) { app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }) - app.Get("/filter", func(c fiber.Ctx) error { + app.Get("/next", func(c fiber.Ctx) error { return c.SendString("Skipped!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) - resp, err = app.Test(httptest.NewRequest("GET", "/filter", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/next", nil)) require.NoError(t, err) require.Equal(t, "", resp.Header.Get(fiber.HeaderReferrerPolicy)) } @@ -68,7 +161,7 @@ func Test_ContentSecurityPolicy(t *testing.T) { return c.SendString("Hello, World!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) } @@ -85,7 +178,7 @@ func Test_ContentSecurityPolicyReportOnly(t *testing.T) { return c.SendString("Hello, World!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly)) require.Equal(t, "", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) @@ -102,7 +195,7 @@ func Test_PermissionsPolicy(t *testing.T) { return c.SendString("Hello, World!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, "microphone=()", resp.Header.Get(fiber.HeaderPermissionsPolicy)) } diff --git a/middleware/idempotency/README.md b/middleware/idempotency/README.md deleted file mode 100644 index d2fb0965cf..0000000000 --- a/middleware/idempotency/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Idempotency Middleware - -Idempotency middleware for [Fiber](https://github.com/gofiber/fiber) allows for fault-tolerant APIs where duplicate requests — for example due to networking issues on the client-side — do not erroneously cause the same action performed multiple times on the server-side. - -Refer to https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02 for a better understanding. - -## Table of Contents - -- [Idempotency Middleware](#idempotency-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config](#default-config-1) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/idempotency" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -app.Use(idempotency.New()) -``` - -### Custom Config - -```go -app.Use(idempotency.New(idempotency.Config{ - Lifetime: 42 * time.Minute, - // ... -})) -``` - -### Config - -```go -type Config struct { - // Next defines a function to skip this middleware when returned true. - // - // Optional. Default: a function which skips the middleware on safe HTTP request method. - Next func(c fiber.Ctx) bool - - // Lifetime is the maximum lifetime of an idempotency key. - // - // Optional. Default: 30 * time.Minute - Lifetime time.Duration - - // KeyHeader is the name of the header that contains the idempotency key. - // - // Optional. Default: X-Idempotency-Key - KeyHeader string - // KeyHeaderValidate defines a function to validate the syntax of the idempotency header. - // - // Optional. Default: a function which ensures the header is 36 characters long (the size of an UUID). - KeyHeaderValidate func(string) error - - // KeepResponseHeaders is a list of headers that should be kept from the original response. - // - // Optional. Default: nil (to keep all headers) - KeepResponseHeaders []string - - // Lock locks an idempotency key. - // - // Optional. Default: an in-memory locker for this process only. - Lock Locker - - // Storage stores response data by idempotency key. - // - // Optional. Default: an in-memory storage for this process only. - Storage fiber.Storage -} -``` - -### Default Config - -```go -var ConfigDefault = Config{ - Next: func(c fiber.Ctx) bool { - // Skip middleware if the request was done using a safe HTTP method - return fiber.IsMethodSafe(c.Method()) - }, - - Lifetime: 30 * time.Minute, - - KeyHeader: "X-Idempotency-Key", - KeyHeaderValidate: func(k string) error { - if l, wl := len(k), 36; l != wl { // UUID length is 36 chars - return fmt.Errorf("%w: invalid length: %d != %d", ErrInvalidIdempotencyKey, l, wl) - } - - return nil - }, - - KeepResponseHeaders: nil, - - Lock: nil, // Set in configDefault so we don't allocate data here. - - Storage: nil, // Set in configDefault so we don't allocate data here. -} -``` diff --git a/middleware/idempotency/config.go b/middleware/idempotency/config.go index d5c11b962c..1856b42b02 100644 --- a/middleware/idempotency/config.go +++ b/middleware/idempotency/config.go @@ -9,9 +9,7 @@ import ( "github.com/gofiber/fiber/v3/internal/storage/memory" ) -var ( - ErrInvalidIdempotencyKey = errors.New("invalid idempotency key") -) +var ErrInvalidIdempotencyKey = errors.New("invalid idempotency key") // Config defines the config for middleware. type Config struct { diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 60adc8594b..8c92248423 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -2,10 +2,10 @@ package idempotency import ( "fmt" - "log" "strings" "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" ) @@ -92,7 +92,7 @@ func New(config ...Config) fiber.Handler { } defer func() { if err := cfg.Lock.Unlock(key); err != nil { - log.Printf("middleware/idempotency: failed to unlock key %q: %v", key, err) + log.Errorf("[IDEMPOTENCY] failed to unlock key %q: %v", key, err) } }() diff --git a/middleware/idempotency/idempotency_test.go b/middleware/idempotency/idempotency_test.go index d289124555..f06516fca6 100644 --- a/middleware/idempotency/idempotency_test.go +++ b/middleware/idempotency/idempotency_test.go @@ -1,3 +1,4 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package idempotency_test import ( @@ -86,7 +87,7 @@ func Test_Idempotency(t *testing.T) { if idempotencyKey != "" { req.Header.Set("X-Idempotency-Key", idempotencyKey) } - resp, err := app.Test(req, 3*lifetime) + resp, err := app.Test(req, 3*int(lifetime.Milliseconds())) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -171,5 +172,4 @@ func Benchmark_Idempotency(b *testing.B) { h(c) } }) - } diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go new file mode 100644 index 0000000000..4d722bbf60 --- /dev/null +++ b/middleware/keyauth/config.go @@ -0,0 +1,95 @@ +package keyauth + +import ( + "errors" + + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(fiber.Ctx) bool + + // SuccessHandler defines a function which is executed for a valid key. + // Optional. Default: nil + SuccessHandler fiber.Handler + + // ErrorHandler defines a function which is executed for an invalid key. + // It may be used to define a custom error. + // Optional. Default: 401 Invalid or expired key + ErrorHandler fiber.ErrorHandler + + // KeyLookup is a string in the form of ":" that is used + // to extract key from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "form:" + // - "param:" + // - "cookie:" + KeyLookup string + + // AuthScheme to be used in the Authorization header. + // Optional. Default value "Bearer". + AuthScheme string + + // Validator is a function to validate key. + Validator func(fiber.Ctx, string) (bool, error) + + // Context key to store the bearertoken from the token into context. + // Optional. Default: "token". + ContextKey string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + SuccessHandler: func(c fiber.Ctx) error { + return c.Next() + }, + ErrorHandler: func(c fiber.Ctx, err error) error { + if errors.Is(err, ErrMissingOrMalformedAPIKey) { + return c.Status(fiber.StatusUnauthorized).SendString(err.Error()) + } + return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") + }, + KeyLookup: "header:" + fiber.HeaderAuthorization, + AuthScheme: "Bearer", + ContextKey: "token", +} + +// 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.SuccessHandler == nil { + cfg.SuccessHandler = ConfigDefault.SuccessHandler + } + if cfg.ErrorHandler == nil { + cfg.ErrorHandler = ConfigDefault.ErrorHandler + } + if cfg.KeyLookup == "" { + cfg.KeyLookup = ConfigDefault.KeyLookup + // set AuthScheme as "Bearer" only if KeyLookup is set to default. + if cfg.AuthScheme == "" { + cfg.AuthScheme = ConfigDefault.AuthScheme + } + } + if cfg.Validator == nil { + panic("fiber: keyauth middleware requires a validator function") + } + if cfg.ContextKey == "" { + cfg.ContextKey = ConfigDefault.ContextKey + } + + return cfg +} diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index bf02dfdd9b..d471201c60 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -1,114 +1,47 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber // Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/key_auth.go package keyauth import ( "errors" + "net/url" "strings" "github.com/gofiber/fiber/v3" ) -var ( - // When there is no request of the key thrown ErrMissingOrMalformedAPIKey - ErrMissingOrMalformedAPIKey = errors.New("missing or malformed API Key") -) - -type Config struct { - // Filter defines a function to skip middleware. - // Optional. Default: nil - Filter func(fiber.Ctx) bool - - // SuccessHandler defines a function which is executed for a valid key. - // Optional. Default: nil - SuccessHandler fiber.Handler - - // ErrorHandler defines a function which is executed for an invalid key. - // It may be used to define a custom error. - // Optional. Default: 401 Invalid or expired key - ErrorHandler fiber.ErrorHandler - - // KeyLookup is a string in the form of ":" that is used - // to extract key from the request. - // Optional. Default value "header:Authorization". - // Possible values: - // - "header:" - // - "query:" - // - "form:" - // - "param:" - // - "cookie:" - KeyLookup string - - // AuthScheme to be used in the Authorization header. - // Optional. Default value "Bearer". - AuthScheme string +// When there is no request of the key thrown ErrMissingOrMalformedAPIKey +var ErrMissingOrMalformedAPIKey = errors.New("missing or malformed API Key") - // Validator is a function to validate key. - // Optional. Default: nil - Validator func(fiber.Ctx, string) (bool, error) - - // Context key to store the bearertoken from the token into context. - // Optional. Default: "token". - ContextKey string -} +const ( + query = "query" + form = "form" + param = "param" + cookie = "cookie" +) -// New ... +// New creates a new middleware handler func New(config ...Config) fiber.Handler { // Init config - var cfg Config - if len(config) > 0 { - cfg = config[0] - } - - if cfg.SuccessHandler == nil { - cfg.SuccessHandler = func(c fiber.Ctx) error { - return c.Next() - } - } - if cfg.ErrorHandler == nil { - cfg.ErrorHandler = func(c fiber.Ctx, err error) error { - if err == ErrMissingOrMalformedAPIKey { - return c.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") - } - } - if cfg.KeyLookup == "" { - cfg.KeyLookup = "header:" + fiber.HeaderAuthorization - // set AuthScheme as "Bearer" only if KeyLookup is set to default. - if cfg.AuthScheme == "" { - cfg.AuthScheme = "Bearer" - } - } - if cfg.Validator == nil { - cfg.Validator = func(c fiber.Ctx, t string) (bool, error) { - return true, nil - } - } - if cfg.ContextKey == "" { - cfg.ContextKey = "token" - } + cfg := configDefault(config...) // Initialize parts := strings.Split(cfg.KeyLookup, ":") extractor := keyFromHeader(parts[1], cfg.AuthScheme) switch parts[0] { - case "query": + case query: extractor = keyFromQuery(parts[1]) - case "form": + case form: extractor = keyFromForm(parts[1]) - case "param": + case param: extractor = keyFromParam(parts[1]) - case "cookie": + case cookie: extractor = keyFromCookie(parts[1]) } // Return middleware handler return func(c fiber.Ctx) error { // Filter request to skip middleware - if cfg.Filter != nil && cfg.Filter(c) { + if cfg.Next != nil && cfg.Next(c) { return c.Next() } @@ -129,7 +62,7 @@ func New(config ...Config) fiber.Handler { } // keyFromHeader returns a function that extracts api key from the request header. -func keyFromHeader(header string, authScheme string) func(c fiber.Ctx) (string, error) { +func keyFromHeader(header, authScheme string) func(c fiber.Ctx) (string, error) { return func(c fiber.Ctx) (string, error) { auth := c.Get(header) l := len(authScheme) @@ -168,8 +101,8 @@ func keyFromForm(param string) func(c fiber.Ctx) (string, error) { // keyFromParam returns a function that extracts api key from the url param string. func keyFromParam(param string) func(c fiber.Ctx) (string, error) { return func(c fiber.Ctx) (string, error) { - key := c.Params(param) - if key == "" { + key, err := url.PathUnescape(c.Params(param)) + if err != nil { return "", ErrMissingOrMalformedAPIKey } return key, nil diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 47dd09e001..a04f21fb89 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -1,5 +1,461 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package keyauth + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" +) + +const CorrectKey = "specials: !$%,.#\"!?~`<>@$^*(){}[]|/\\123" + +func TestAuthSources(t *testing.T) { + // define test cases + testSources := []string{"header", "cookie", "query", "param", "form"} + + tests := []struct { + route string + authTokenName string + description string + APIKey string + expectedCode int + expectedBody string + }{ + { + route: "/", + authTokenName: "access_token", + description: "auth with correct key", + APIKey: CorrectKey, + expectedCode: 200, + expectedBody: "Success!", + }, + { + route: "/", + authTokenName: "access_token", + description: "auth with no key", + APIKey: "", + expectedCode: 401, // 404 in case of param authentication + expectedBody: "missing or malformed API Key", + }, + { + route: "/", + authTokenName: "access_token", + description: "auth with wrong key", + APIKey: "WRONGKEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + } + + for _, authSource := range testSources { + t.Run(authSource, func(t *testing.T) { + for _, test := range tests { + // setup the fiber endpoint + // note that if UnescapePath: false (the default) + // escaped characters (such as `\"`) will not be handled correctly in the tests + app := fiber.New(fiber.Config{UnescapePath: true}) + + authMiddleware := New(Config{ + KeyLookup: authSource + ":" + test.authTokenName, + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + }) + + var route string + if authSource == param { + route = test.route + ":" + test.authTokenName + app.Use(route, authMiddleware) + } else { + route = test.route + app.Use(authMiddleware) + } + + app.Get(route, func(c fiber.Ctx) error { + return c.SendString("Success!") + }) + + // construct the test HTTP request + var req *http.Request + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, test.route, nil) + require.Equal(t, err, nil) + + // setup the apikey for the different auth schemes + if authSource == "header" { + req.Header.Set(test.authTokenName, test.APIKey) + } else if authSource == "cookie" { + req.Header.Set("Cookie", test.authTokenName+"="+test.APIKey) + } else if authSource == "query" || authSource == "form" { + q := req.URL.Query() + q.Add(test.authTokenName, test.APIKey) + req.URL.RawQuery = q.Encode() + } else if authSource == "param" { + r := req.URL.Path + r += url.PathEscape(test.APIKey) + req.URL.Path = r + } + + res, err := app.Test(req, -1) + + require.Equal(t, nil, err, test.description) + + // test the body of the request + body, err := io.ReadAll(res.Body) + // for param authentication, the route would be /:access_token + // when the access_token is empty, it leads to a 404 (not found) + // not a 401 (auth error) + if authSource == "param" && test.APIKey == "" { + test.expectedCode = 404 + test.expectedBody = "Cannot GET /" + } + require.Equal(t, test.expectedCode, res.StatusCode, test.description) + + // body + require.Equal(t, nil, err, test.description) + require.Equal(t, test.expectedBody, string(body), test.description) + + err = res.Body.Close() + require.Equal(t, err, nil) + } + }) + } +} + +func TestMultipleKeyAuth(t *testing.T) { + // setup the fiber endpoint + app := fiber.New() + + // setup keyauth for /auth1 + app.Use(New(Config{ + Next: func(c fiber.Ctx) bool { + return c.OriginalURL() != "/auth1" + }, + KeyLookup: "header:key", + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == "password1" { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // setup keyauth for /auth2 + app.Use(New(Config{ + Next: func(c fiber.Ctx) bool { + return c.OriginalURL() != "/auth2" + }, + KeyLookup: "header:key", + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == "password2" { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("No auth needed!") + }) + + app.Get("/auth1", func(c fiber.Ctx) error { + return c.SendString("Successfully authenticated for auth1!") + }) + + app.Get("/auth2", func(c fiber.Ctx) error { + return c.SendString("Successfully authenticated for auth2!") + }) + + // define test cases + tests := []struct { + route string + description string + APIKey string + expectedCode int + expectedBody string + }{ + // No auth needed for / + { + route: "/", + description: "No password needed", + APIKey: "", + expectedCode: 200, + expectedBody: "No auth needed!", + }, + + // auth needed for auth1 + { + route: "/auth1", + description: "Normal Authentication Case", + APIKey: "password1", + expectedCode: 200, + expectedBody: "Successfully authenticated for auth1!", + }, + { + route: "/auth1", + description: "Wrong API Key", + APIKey: "WRONG KEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + { + route: "/auth1", + description: "Wrong API Key", + APIKey: "", // NO KEY + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + + // Auth 2 has a different password + { + route: "/auth2", + description: "Normal Authentication Case for auth2", + APIKey: "password2", + expectedCode: 200, + expectedBody: "Successfully authenticated for auth2!", + }, + { + route: "/auth2", + description: "Wrong API Key", + APIKey: "WRONG KEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + { + route: "/auth2", + description: "Wrong API Key", + APIKey: "", // NO KEY + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + } + + // run the tests + for _, test := range tests { + var req *http.Request + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, test.route, nil) + require.Equal(t, err, nil) + if test.APIKey != "" { + req.Header.Set("key", test.APIKey) + } + + res, err := app.Test(req, -1) + + require.Equal(t, nil, err, test.description) + + // test the body of the request + body, err := io.ReadAll(res.Body) + require.Equal(t, test.expectedCode, res.StatusCode, test.description) + + // body + require.Equal(t, nil, err, test.description) + require.Equal(t, test.expectedBody, string(body), test.description) + } +} + +func TestCustomSuccessAndFailureHandlers(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + SuccessHandler: func(c fiber.Ctx) error { + return c.Status(fiber.StatusOK).SendString("API key is valid and request was handled by custom success handler") + }, + ErrorHandler: func(c fiber.Ctx, err error) error { + return c.Status(fiber.StatusUnauthorized).SendString("API key is invalid and request was handled by custom error handler") + }, + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler that should not be called + app.Get("/", func(c fiber.Ctx) error { + t.Error("Test handler should not be called") + return nil + }) + + // Create a request without an API key and send it to the app + res, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusUnauthorized) + require.Equal(t, string(body), "API key is invalid and request was handled by custom error handler") + + // Create a request with a valid API key in the Authorization header + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", CorrectKey)) + + // Send the request to the app + res, err = app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusOK) + require.Equal(t, string(body), "API key is valid and request was handled by custom success handler") +} + +func TestCustomNextFunc(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Next: func(c fiber.Ctx) bool { + return c.Path() == "/allowed" + }, + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/allowed", func(c fiber.Ctx) error { + return c.SendString("API key is valid and request was allowed by custom filter") + }) + + // Create a request with the "/allowed" path and send it to the app + req := httptest.NewRequest(fiber.MethodGet, "/allowed", nil) + res, err := app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusOK) + require.Equal(t, string(body), "API key is valid and request was allowed by custom filter") + + // Create a request with a different path and send it to the app without correct key + req = httptest.NewRequest(fiber.MethodGet, "/not-allowed", nil) + res, err = app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusUnauthorized) + require.Equal(t, string(body), ErrMissingOrMalformedAPIKey.Error()) + + // Create a request with a different path and send it to the app with correct key + req = httptest.NewRequest(fiber.MethodGet, "/not-allowed", nil) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", CorrectKey)) + + res, err = app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusUnauthorized) + require.Equal(t, string(body), ErrMissingOrMalformedAPIKey.Error()) +} + +func TestAuthSchemeToken(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + AuthScheme: "Token", + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("API key is valid") + }) + + // Create a request with a valid API key in the "Token" Authorization header + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Token %s", CorrectKey)) + + // Send the request to the app + res, err := app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusOK) + require.Equal(t, string(body), "API key is valid") +} + +func TestAuthSchemeBasic(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(c fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("API key is valid") + }) + + // Create a request without an API key and Send the request to the app + res, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusUnauthorized) + require.Equal(t, string(body), ErrMissingOrMalformedAPIKey.Error()) + + // Create a request with a valid API key in the "Authorization" header using the "Basic" scheme + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", CorrectKey)) + + // Send the request to the app + res, err = app.Test(req) + require.Equal(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + require.Equal(t, err, nil) + + // Check that the response has the expected status code and body + require.Equal(t, res.StatusCode, http.StatusOK) + require.Equal(t, string(body), "API key is valid") +} diff --git a/middleware/limiter/README.md b/middleware/limiter/README.md deleted file mode 100644 index 919764bf14..0000000000 --- a/middleware/limiter/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Limiter Middleware - -Limiter middleware for [Fiber](https://github.com/gofiber/fiber) that is used to limit repeat requests to public APIs and/or endpoints such as password reset. It is also useful for API clients, web crawling, or other tasks that need to be throttled. - -_NOTE: This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases._ - -**NOTE: this module does not share state with other processes/servers by default.** - -## Table of Contents - -- [Limiter Middleware](#limiter-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Custom Storage/Database](#custom-storagedatabase) - - [Config](#config) - - [Default Config](#default-config-1) - -## Signatures - -```go -func New(config ...Config) fiber.Handler -``` - -## Examples - -First import the middleware from Fiber, - -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/limiter" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Config - -```go -// Default middleware config -app.Use(limiter.New()) -``` - -### Custom Config - -```go -// Or extend your config for customization -app.Use(limiter.New(limiter.Config{ - Next: func(c fiber.Ctx) bool { - return c.IP() == "127.0.0.1" - }, - Max: 20, - Expiration: 30 * time.Second, - KeyGenerator: func(c fiber.Ctx) string{ - return "key" - } - LimitReached: func(c fiber.Ctx) error { - return c.SendFile("./toofast.html") - }, - Storage: myCustomStore{} -})) -``` - -### Custom Storage/Database - -You can use any storage from our [storage](https://github.com/gofiber/storage/) package. - -```go -storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 -app.Use(limiter.New(limiter.Config{ - Storage: storage, -})) -``` - -## Config - -```go -// 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 - - // Max number of recent connections during `Duration` seconds before sending a 429 response - // - // Default: 5 - Max int - - // KeyGenerator allows you to generate custom keys, by default c.IP() is used - // - // Default: func(c fiber.Ctx) string { - // return c.IP() - // } - KeyGenerator func(fiber.Ctx) string - - // Expiration is the time on how long to keep records of requests in memory - // - // Default: 1 * time.Minute - Expiration time.Duration - - // LimitReached is called when a request hits the limit - // - // Default: func(c fiber.Ctx) error { - // return c.SendStatus(fiber.StatusTooManyRequests) - // } - LimitReached fiber.Handler - - // When set to true, requests with StatusCode >= 400 won't be counted. - // - // Default: false - SkipFailedRequests bool - - // When set to true, requests with StatusCode < 400 won't be counted. - // - // Default: false - SkipSuccessfulRequests bool - - // Store is used to store the state of the middleware - // - // Default: an in memory store for this process only - Storage fiber.Storage -} -``` - -A custom store can be used if it implements the `Storage` interface - more details and an example can be found in `store.go`. - -### Default Config - -```go -var ConfigDefault = Config{ - Max: 5, - Expiration: 1 * time.Minute, - KeyGenerator: func(c fiber.Ctx) string { - return c.IP() - }, - LimitReached: func(c fiber.Ctx) error { - return c.SendStatus(fiber.StatusTooManyRequests) - }, - SkipFailedRequests: false, - SkipSuccessfulRequests: false, -} -``` diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go index 735eecaf03..1e2a1aa0e5 100644 --- a/middleware/limiter/limiter_fixed.go +++ b/middleware/limiter/limiter_fixed.go @@ -3,7 +3,6 @@ package limiter import ( "strconv" "sync" - "sync/atomic" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" @@ -43,7 +42,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + ts := uint64(utils.Timestamp()) // Set expiration if entry does not exist if e.exp == 0 { diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go index fcf1cb5f2b..a98593476e 100644 --- a/middleware/limiter/limiter_sliding.go +++ b/middleware/limiter/limiter_sliding.go @@ -3,7 +3,6 @@ package limiter import ( "strconv" "sync" - "sync/atomic" "time" "github.com/gofiber/fiber/v3" @@ -44,7 +43,7 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { e := manager.get(key) // Get timestamp - ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + ts := uint64(utils.Timestamp()) // Set expiration if entry does not exist if e.exp == 0 { @@ -117,9 +116,13 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { // Check for SkipFailedRequests and SkipSuccessfulRequests if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + // Lock entry mux.Lock() + e = manager.get(key) e.currHits-- remaining++ + manager.set(key, e, cfg.Expiration) + // Unlock entry mux.Unlock() } diff --git a/middleware/limiter/limiter_test.go b/middleware/limiter/limiter_test.go index 9cae095c11..383129115d 100644 --- a/middleware/limiter/limiter_test.go +++ b/middleware/limiter/limiter_test.go @@ -2,7 +2,6 @@ package limiter import ( "io" - "net/http" "net/http/httptest" "sync" "testing" @@ -16,6 +15,7 @@ import ( // go test -run Test_Limiter_Concurrency_Store -race -v func Test_Limiter_Concurrency_Store(t *testing.T) { + t.Parallel() // Test concurrency using a custom store app := fiber.New() @@ -33,7 +33,7 @@ func Test_Limiter_Concurrency_Store(t *testing.T) { var wg sync.WaitGroup singleRequest := func(wg *sync.WaitGroup) { defer wg.Done() - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -49,19 +49,20 @@ func Test_Limiter_Concurrency_Store(t *testing.T) { wg.Wait() - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) time.Sleep(3 * time.Second) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) } // go test -run Test_Limiter_Concurrency -race -v func Test_Limiter_Concurrency(t *testing.T) { + t.Parallel() // Test concurrency using a default store app := fiber.New() @@ -78,7 +79,7 @@ func Test_Limiter_Concurrency(t *testing.T) { var wg sync.WaitGroup singleRequest := func(wg *sync.WaitGroup) { defer wg.Done() - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -94,19 +95,20 @@ func Test_Limiter_Concurrency(t *testing.T) { wg.Wait() - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) time.Sleep(3 * time.Second) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) } -// go test -run Test_Limiter_No_Skip_Choices -v -func Test_Limiter_No_Skip_Choices(t *testing.T) { +// go test -run Test_Limiter_Fixed_Window_No_Skip_Choices -v +func Test_Limiter_Fixed_Window_No_Skip_Choices(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -114,6 +116,126 @@ func Test_Limiter_No_Skip_Choices(t *testing.T) { Expiration: 2 * time.Second, SkipFailedRequests: false, SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { //nolint:goconst // False positive + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices -v +func Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Storage: memory.New(), + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_No_Skip_Choices -v +func Test_Limiter_Sliding_Window_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices -v +func Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Storage: memory.New(), + LimiterMiddleware: SlidingWindow{}, })) app.Get("/:status", func(c fiber.Ctx) error { @@ -123,27 +245,35 @@ func Test_Limiter_No_Skip_Choices(t *testing.T) { return c.SendStatus(200) }) - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/fail", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) require.NoError(t, err) require.Equal(t, 400, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) } -// go test -run Test_Limiter_Skip_Failed_Requests -v -func Test_Limiter_Skip_Failed_Requests(t *testing.T) { +// go test -run Test_Limiter_Fixed_Window_Skip_Failed_Requests -v +func Test_Limiter_Fixed_Window_Skip_Failed_Requests(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Max: 1, Expiration: 2 * time.Second, SkipFailedRequests: true, + LimiterMiddleware: FixedWindow{}, })) app.Get("/:status", func(c fiber.Ctx) error { @@ -153,27 +283,144 @@ func Test_Limiter_Skip_Failed_Requests(t *testing.T) { return c.SendStatus(200) }) - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/fail", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) require.NoError(t, err) require.Equal(t, 400, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) time.Sleep(3 * time.Second) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) } -// go test -run Test_Limiter_Skip_Successful_Requests -v -func Test_Limiter_Skip_Successful_Requests(t *testing.T) { +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests -v +func Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipFailedRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Skip_Failed_Requests -v +func Test_Limiter_Sliding_Window_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipFailedRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests -v +func Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipFailedRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Skip_Successful_Requests -v +func Test_Limiter_Fixed_Window_Skip_Successful_Requests(t *testing.T) { + t.Parallel() // Test concurrency using a default store app := fiber.New() @@ -182,6 +429,7 @@ func Test_Limiter_Skip_Successful_Requests(t *testing.T) { Max: 1, Expiration: 2 * time.Second, SkipSuccessfulRequests: true, + LimiterMiddleware: FixedWindow{}, })) app.Get("/:status", func(c fiber.Ctx) error { @@ -191,21 +439,143 @@ func Test_Limiter_Skip_Successful_Requests(t *testing.T) { return c.SendStatus(200) }) - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/success", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/fail", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) require.NoError(t, err) require.Equal(t, 400, resp.StatusCode) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/fail", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) time.Sleep(3 * time.Second) - resp, err = app.Test(httptest.NewRequest(http.MethodGet, "/fail", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests -v +func Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipSuccessfulRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Skip_Successful_Requests -v +func Test_Limiter_Sliding_Window_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipSuccessfulRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests -v +func Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipSuccessfulRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + require.NoError(t, err) + require.Equal(t, 429, resp.StatusCode) + + time.Sleep(4 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) require.NoError(t, err) require.Equal(t, 400, resp.StatusCode) } @@ -227,7 +597,7 @@ func Benchmark_Limiter_Custom_Store(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") b.ResetTimer() @@ -239,6 +609,7 @@ func Benchmark_Limiter_Custom_Store(b *testing.B) { // go test -run Test_Limiter_Next func Test_Limiter_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -246,12 +617,13 @@ func Test_Limiter_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_Limiter_Headers(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ @@ -264,7 +636,7 @@ func Test_Limiter_Headers(t *testing.T) { }) fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") app.Handler()(fctx) @@ -294,7 +666,7 @@ func Benchmark_Limiter(b *testing.B) { h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") b.ResetTimer() @@ -306,6 +678,7 @@ func Benchmark_Limiter(b *testing.B) { // go test -run Test_Sliding_Window -race -v func Test_Sliding_Window(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Max: 10, @@ -319,7 +692,7 @@ func Test_Sliding_Window(t *testing.T) { }) singleRequest := func(shouldFail bool) { - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) if shouldFail { require.NoError(t, err) require.Equal(t, 429, resp.StatusCode) diff --git a/middleware/limiter/manager.go b/middleware/limiter/manager.go index 46d7c4b6af..4225cdd3ac 100644 --- a/middleware/limiter/manager.go +++ b/middleware/limiter/manager.go @@ -8,7 +8,8 @@ import ( "github.com/gofiber/fiber/v3/internal/memory" ) -//go:generate msgp -o=manager_msgp.go -io=false -unexported +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported type item struct { currHits int prevHits int @@ -43,7 +44,7 @@ func newManager(storage fiber.Storage) *manager { // acquire returns an *entry from the sync.Pool func (m *manager) acquire() *item { - return m.pool.Get().(*item) + return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool } // release and reset *entry to sync.Pool @@ -55,37 +56,33 @@ func (m *manager) release(e *item) { } // get data from storage or memory -func (m *manager) get(key string) (it *item) { +func (m *manager) get(key string) *item { + var it *item if m.storage != nil { it = m.acquire() - if raw, _ := m.storage.Get(key); raw != nil { + raw, err := m.storage.Get(key) + if err != nil { + return it + } + if raw != nil { if _, err := it.UnmarshalMsg(raw); err != nil { - return + return it } } - return + return it } - if it, _ = m.memory.Get(key).(*item); it == nil { + if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool it = m.acquire() + return it } - return -} - -// get raw data from storage or memory -func (m *manager) getRaw(key string) (raw []byte) { - if m.storage != nil { - raw, _ = m.storage.Get(key) - } else { - raw, _ = m.memory.Get(key).([]byte) - } - return + return it } // set data to storage or memory func (m *manager) set(key string, it *item, exp time.Duration) { if m.storage != nil { if raw, err := it.MarshalMsg(nil); err == nil { - _ = m.storage.Set(key, raw, exp) + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here } // we can release data because it's serialized to database m.release(it) @@ -93,21 +90,3 @@ func (m *manager) set(key string, it *item, exp time.Duration) { m.memory.Set(key, it, exp) } } - -// set data to storage or memory -func (m *manager) setRaw(key string, raw []byte, exp time.Duration) { - if m.storage != nil { - _ = m.storage.Set(key, raw, exp) - } else { - m.memory.Set(key, raw, exp) - } -} - -// delete data from storage or memory -func (m *manager) delete(key string) { - if m.storage != nil { - _ = m.storage.Delete(key) - } else { - m.memory.Delete(key) - } -} diff --git a/middleware/logger/README.md b/middleware/logger/README.md deleted file mode 100644 index 411dc06a81..0000000000 --- a/middleware/logger/README.md +++ /dev/null @@ -1,269 +0,0 @@ -# Logger Middleware -Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP request/response details. - -## Table of Contents -- [Logger Middleware](#logger-middleware) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Logging remote IP and Port](#logging-remote-ip-and-port) - - [Logging Request ID](#logging-request-id) - - [Changing TimeZone & TimeFormat](#changing-timezone--timeformat) - - [Custom File Writer](#custom-file-writer) - - [Logging with Zerolog](#logging-with-zerolog) - - [Add Custom Tags](#add-custom-tags) - - [Config](#config) - - [Default Config](#default-config-1) - - [Constants](#constants) - -## Signatures -```go -func New(config ...Config) fiber.Handler -``` - -## Examples -First ensure the appropriate packages are imported -```go -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/logger" -) -``` - -### Default Config -```go -// Default middleware config -app.Use(logger.New()) -``` - -### Logging remote IP and Port - -```go -app.Use(logger.New(logger.Config{ - Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", -})) -``` - -### Logging Request ID -```go -app.Use(requestid.New()) - -app.Use(logger.New(logger.Config{ - // For more options, see the Config section - Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}​\n", -})) -``` - -### Changing TimeZone & TimeFormat - -```go -app.Use(logger.New(logger.Config{ - Format: "${pid} ${status} - ${method} ${path}\n", - TimeFormat: "02-Jan-2006", - TimeZone: "America/New_York", -})) -``` - -### Custom File Writer -```go -file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) -if err != nil { - log.Fatalf("error opening file: %v", err) -} -defer file.Close() - -app.Use(logger.New(logger.Config{ - Output: file, -})) -``` -### Add Custom Tags -```go -app.Use(logger.New(logger.Config{ - CustomTags: map[string]logger.LogFunc{ - "custom_tag": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) { - return output.WriteString("it is a custom tag") - }, - }, -})) -``` - -### Callback after log is written - -```go -app.Use(logger.New(logger.Config{ - TimeFormat: time.RFC3339Nano, - TimeZone: "Asia/Shanghai", - Done: func(c fiber.Ctx, logString []byte) { - if c.Response().StatusCode() != fiber.StatusOK { - reporter.SendToSlack(logString) - } - }, -})) -``` - -### Logging with Zerolog -```go -package main - -import ( - "os" - - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/logger" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -func main() { - app := fiber.New() - - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - - app.Use(logger.New(logger.Config{LoggerFunc: func(c fiber.Ctx, data *logger.LoggerData, cfg logger.Config) error { - log.Info(). - Str("path", c.Path()). - Str("method", c.Method()). - Int("status", c.Response(). - StatusCode()). - Msg("new request") - - return nil - }})) - - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("test") - }) - - app.Listen(":3000") -} -``` - -## Config -```go -// 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 - - // Done is a function that is called after the log string for a request is written to Output, - // and pass the log string as parameter. - // - // Optional. Default: nil - Done func(c fiber.Ctx, logString []byte) - - // tagFunctions defines the custom tag action - // - // Optional. Default: map[string]LogFunc - CustomTags map[string]LogFunc - - // Format defines the logging tags - // - // Optional. Default: [${time}] ${status} - ${latency} ${method} ${path}\n - Format string - - // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html - // - // Optional. Default: 15:04:05 - TimeFormat string - - // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc - // - // Optional. Default: "Local" - TimeZone string - - // TimeInterval is the delay before the timestamp is updated - // - // Optional. Default: 500 * time.Millisecond - TimeInterval time.Duration - - // Output is a writer where logs are written - // - // Default: os.Stdout - Output io.Writer - - // You can define specific things before the returning the handler: colors, template, etc. - // - // Optional. Default: beforeHandlerFunc - BeforeHandlerFunc func(Config) - - // You can use custom loggers with Fiber by using this field. - // This field is really useful if you're using Zerolog, Zap, Logrus, apex/log etc. - // If you don't define anything for this field, it'll use classical logger of Fiber. - // - // Optional. Default: defaultLogger - LoggerFunc func(c fiber.Ctx, data *LoggerData, cfg Config) error -} - -type LogFunc func(buf logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) -``` - -## Default Config -```go -// ConfigDefault is the default config -var ConfigDefault = Config{ - Next: nil, - Done: nil, - Format: defaultFormat, - TimeFormat: "15:04:05", - TimeZone: "Local", - TimeInterval: 500 * time.Millisecond, - Output: os.Stdout, - BeforeHandlerFunc: beforeHandlerFunc, - LoggerFunc: defaultLogger, - enableColors: true, -} - -// default logging format for Fiber's default logger -var defaultFormat = "[${time}] ${status} - ${latency} ${method} ${path}\n" -``` - -## Constants -```go -// Logger variables -const ( - TagPid = "pid" - TagTime = "time" - TagReferer = "referer" - TagProtocol = "protocol" - TagPort = "port" - TagIP = "ip" - TagIPs = "ips" - TagHost = "host" - TagMethod = "method" - TagPath = "path" - TagURL = "url" - TagUA = "ua" - TagLatency = "latency" - TagStatus = "status" // response status - TagResBody = "resBody" // response body - TagReqHeaders = "reqHeaders" - TagQueryStringParams = "queryParams" // request query parameters - TagBody = "body" // request body - TagBytesSent = "bytesSent" - TagBytesReceived = "bytesReceived" - TagRoute = "route" - TagError = "error" - // DEPRECATED: Use TagReqHeader instead - TagHeader = "header:" // request header - TagReqHeader = "reqHeader:" // request header - TagRespHeader = "respHeader:" // response header - TagQuery = "query:" // request query - TagForm = "form:" // request form - TagCookie = "cookie:" // request cookie - TagLocals = "locals:" - - // colors - TagBlack = "black" - TagRed = "red" - TagGreen = "green" - TagYellow = "yellow" - TagBlue = "blue" - TagMagenta = "magenta" - TagCyan = "cyan" - TagWhite = "white" - TagReset = "reset" -) -``` diff --git a/middleware/logger/config.go b/middleware/logger/config.go index a30f3cd26c..060a5de813 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -3,7 +3,6 @@ package logger import ( "io" "os" - "strings" "time" "github.com/gofiber/fiber/v3" @@ -64,6 +63,11 @@ type Config struct { // Optional. Default: defaultLogger LoggerFunc func(c fiber.Ctx, data *Data, cfg Config) error + // DisableColors defines if the logs output should be colorized + // + // Default: false + DisableColors bool + enableColors bool enableLatency bool timeZoneLocation *time.Location @@ -107,17 +111,6 @@ var ConfigDefault = Config{ // default logging format for Fiber's default logger var defaultFormat = "[${time}] ${status} - ${latency} ${method} ${path}\n" -// Function to check if the logger format is compatible for coloring -func checkColorEnable(format string) bool { - validTemplates := []string{"${status}", "${method}"} - for _, template := range validTemplates { - if strings.Contains(format, template) { - return true - } - } - return false -} - // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided @@ -138,7 +131,6 @@ func configDefault(config ...Config) Config { if cfg.Format == "" { cfg.Format = ConfigDefault.Format } - if cfg.TimeZone == "" { cfg.TimeZone = ConfigDefault.TimeZone } @@ -161,7 +153,7 @@ func configDefault(config ...Config) Config { } // Enable colors if no custom format or output is given - if cfg.Output == nil && checkColorEnable(cfg.Format) { + if !cfg.DisableColors && cfg.Output == ConfigDefault.Output { cfg.enableColors = true } diff --git a/middleware/logger/data.go b/middleware/logger/data.go index fc30238835..2d5955dc51 100644 --- a/middleware/logger/data.go +++ b/middleware/logger/data.go @@ -1,13 +1,10 @@ package logger import ( - "sync" "sync/atomic" "time" ) -var DataPool = sync.Pool{New: func() any { return new(Data) }} - // Data is a struct to define some variables to use in custom logger function. type Data struct { Pid string diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index a6f665fad1..8d60a22c62 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/gofiber/fiber/v2/utils" "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "github.com/valyala/bytebufferpool" @@ -24,35 +24,52 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { // Get new buffer buf := bytebufferpool.Get() - // Put buffer back to pool - defer bytebufferpool.Put(buf) - // Default output when no custom Format or io.Writer is given - if cfg.enableColors && cfg.Format == defaultFormat { + if cfg.Format == defaultFormat { // Format error if exist formatErr := "" - if data.ChainErr != nil { - formatErr = colors.Red + " | " + data.ChainErr.Error() + colors.Reset + if cfg.enableColors { + if data.ChainErr != nil { + formatErr = colors.Red + " | " + data.ChainErr.Error() + colors.Reset + } + _, _ = buf.WriteString( //nolint:errcheck // This will never fail + fmt.Sprintf("%s |%s %3d %s| %7v | %15s |%s %-7s %s| %-"+data.ErrPaddingStr+"s %s\n", + data.Timestamp.Load().(string), + statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset, + data.Stop.Sub(data.Start).Round(time.Millisecond), + c.IP(), + methodColor(c.Method(), colors), c.Method(), colors.Reset, + c.Path(), + formatErr, + ), + ) + } else { + if data.ChainErr != nil { + formatErr = " | " + data.ChainErr.Error() + } + _, _ = buf.WriteString( //nolint:errcheck // This will never fail + fmt.Sprintf("%s | %3d | %7v | %15s | %-7s | %-"+data.ErrPaddingStr+"s %s\n", + data.Timestamp.Load().(string), + c.Response().StatusCode(), + data.Stop.Sub(data.Start).Round(time.Millisecond), + c.IP(), + c.Method(), + c.Path(), + formatErr, + ), + ) } - // Format log to buffer - _, _ = buf.WriteString(fmt.Sprintf("%s |%s %3d %s| %7v | %15s |%s %-7s %s| %-"+data.ErrPaddingStr+"s %s\n", - data.Timestamp.Load().(string), - statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset, - data.Stop.Sub(data.Start).Round(time.Millisecond), - c.IP(), - methodColor(c.Method(), colors), c.Method(), colors.Reset, - c.Path(), - formatErr, - )) - // Write buffer to output - _, _ = cfg.Output.Write(buf.Bytes()) + _, _ = cfg.Output.Write(buf.Bytes()) //nolint:errcheck // This will never fail if cfg.Done != nil { cfg.Done(c, buf.Bytes()) } + // Put buffer back to pool + bytebufferpool.Put(buf) + // End chain return nil } @@ -61,7 +78,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { // Loop over template parts execute dynamic parts and add fixed parts to the buffer for i, logFunc := range data.LogFuncChain { if logFunc == nil { - _, _ = buf.Write(data.TemplateChain[i]) + _, _ = buf.Write(data.TemplateChain[i]) //nolint:errcheck // This will never fail } else if data.TemplateChain[i] == nil { _, err = logFunc(buf, c, data, "") } else { @@ -74,7 +91,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { // Also write errors to the buffer if err != nil { - _, _ = buf.WriteString(err.Error()) + _, _ = buf.WriteString(err.Error()) //nolint:errcheck // This will never fail } mu.Lock() // Write buffer to output @@ -82,7 +99,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { // Write error to output if _, err := cfg.Output.Write([]byte(err.Error())); err != nil { // There is something wrong with the given io.Writer - fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) } } mu.Unlock() @@ -91,6 +108,9 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { cfg.Done(c, buf.Bytes()) } + // Put buffer back to pool + bytebufferpool.Put(buf) + return nil } diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 489c52f5b2..6d46ceb062 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -48,6 +48,8 @@ func New(config ...Config) fiber.Handler { var ( once sync.Once errHandler fiber.ErrorHandler + + dataPool = sync.Pool{New: func() interface{} { return new(Data) }} ) // Err padding @@ -66,7 +68,7 @@ func New(config ...Config) fiber.Handler { } // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() @@ -89,7 +91,7 @@ func New(config ...Config) fiber.Handler { }) // Logger data - data := DataPool.Get().(*Data) + data := dataPool.Get().(*Data) //nolint:forcetypeassert,errcheck // We store nothing else in the pool // no need for a reset, as long as we always override everything data.Pid = pid data.ErrPaddingStr = errPaddingStr @@ -97,7 +99,7 @@ func New(config ...Config) fiber.Handler { data.TemplateChain = templateChain data.LogFuncChain = logFunChain // put data back in the pool - defer DataPool.Put(data) + defer dataPool.Put(data) // Set latency start time if cfg.enableLatency { @@ -111,7 +113,7 @@ func New(config ...Config) fiber.Handler { // Manually call error handler if chainErr != nil { if err := errHandler(c, chainErr); err != nil { - _ = c.SendStatus(fiber.StatusInternalServerError) + _ = c.SendStatus(fiber.StatusInternalServerError) //nolint:errcheck // TODO: Explain why we ignore the error here } } @@ -121,10 +123,6 @@ func New(config ...Config) fiber.Handler { } // Logger instance & update some logger data fields - if err = cfg.LoggerFunc(c, data, cfg); err != nil { - return err - } - - return nil + return cfg.LoggerFunc(c, data, cfg) } } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 76f485b650..e9196ab995 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -1,6 +1,8 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package logger import ( + "bufio" "bytes" "errors" "fmt" @@ -10,6 +12,7 @@ import ( "os" "sync" "testing" + "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/requestid" @@ -20,6 +23,7 @@ import ( // go test -run Test_Logger func Test_Logger(t *testing.T) { + t.Parallel() app := fiber.New() buf := bytebufferpool.Get() @@ -34,7 +38,7 @@ func Test_Logger(t *testing.T) { return errors.New("some random error") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) require.Equal(t, "some random error", buf.String()) @@ -42,6 +46,7 @@ func Test_Logger(t *testing.T) { // go test -run Test_Logger_locals func Test_Logger_locals(t *testing.T) { + t.Parallel() app := fiber.New() buf := bytebufferpool.Get() @@ -66,21 +71,21 @@ func Test_Logger_locals(t *testing.T) { return c.SendStatus(fiber.StatusOK) }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "johndoe", buf.String()) buf.Reset() - resp, err = app.Test(httptest.NewRequest("GET", "/int", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/int", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "55", buf.String()) buf.Reset() - resp, err = app.Test(httptest.NewRequest("GET", "/empty", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/empty", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "", buf.String()) @@ -88,6 +93,7 @@ func Test_Logger_locals(t *testing.T) { // go test -run Test_Logger_Next func Test_Logger_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -95,26 +101,28 @@ func Test_Logger_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } // go test -run Test_Logger_Done func Test_Logger_Done(t *testing.T) { + t.Parallel() buf := bytes.NewBuffer(nil) app := fiber.New() app.Use(New(Config{ Done: func(c fiber.Ctx, logString []byte) { if c.Response().StatusCode() == fiber.StatusOK { - buf.Write(logString) + _, err := buf.Write(logString) + require.NoError(t, err) } }, })).Get("/logging", func(ctx fiber.Ctx) error { return ctx.SendStatus(fiber.StatusOK) }) - resp, err := app.Test(httptest.NewRequest("GET", "/logging", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/logging", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -123,12 +131,13 @@ func Test_Logger_Done(t *testing.T) { // go test -run Test_Logger_ErrorTimeZone func Test_Logger_ErrorTimeZone(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ TimeZone: "invalid", })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } @@ -140,23 +149,41 @@ func (o *fakeOutput) Write([]byte) (int, error) { return 0, errors.New("fake output") } +// go test -run Test_Logger_ErrorOutput_WithoutColor +func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) { + t.Parallel() + o := new(fakeOutput) + app := fiber.New() + app.Use(New(Config{ + Output: o, + DisableColors: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + require.Equal(t, 1, int(*o)) +} + // go test -run Test_Logger_ErrorOutput func Test_Logger_ErrorOutput(t *testing.T) { + t.Parallel() o := new(fakeOutput) app := fiber.New() app.Use(New(Config{ Output: o, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) - - require.Equal(t, 2, int(*o)) + require.Equal(t, 1, int(*o)) } // go test -run Test_Logger_All func Test_Logger_All(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -169,7 +196,7 @@ func Test_Logger_All(t *testing.T) { // Alias colors colors := app.Config().ColorScheme - resp, err := app.Test(httptest.NewRequest("GET", "/?foo=bar", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) @@ -179,6 +206,7 @@ func Test_Logger_All(t *testing.T) { // go test -run Test_Query_Params func Test_Query_Params(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -188,7 +216,7 @@ func Test_Query_Params(t *testing.T) { Output: buf, })) - resp, err := app.Test(httptest.NewRequest("GET", "/?foo=bar&baz=moz", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) @@ -198,6 +226,7 @@ func Test_Query_Params(t *testing.T) { // go test -run Test_Response_Body func Test_Response_Body(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -215,7 +244,7 @@ func Test_Response_Body(t *testing.T) { return c.Send([]byte("Post in test")) }) - _, err := app.Test(httptest.NewRequest("GET", "/", nil)) + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) expectedGetResponse := "Sample response body" @@ -223,7 +252,7 @@ func Test_Response_Body(t *testing.T) { buf.Reset() // Reset buffer to test POST - _, err = app.Test(httptest.NewRequest("POST", "/test", nil)) + _, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) require.NoError(t, err) expectedPostResponse := "Post in test" @@ -232,6 +261,7 @@ func Test_Response_Body(t *testing.T) { // go test -run Test_Logger_AppendUint func Test_Logger_AppendUint(t *testing.T) { + t.Parallel() app := fiber.New() buf := bytebufferpool.Get() @@ -246,7 +276,7 @@ func Test_Logger_AppendUint(t *testing.T) { return c.SendString("hello") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "0 5 200", buf.String()) @@ -254,12 +284,16 @@ func Test_Logger_AppendUint(t *testing.T) { // go test -run Test_Logger_Data_Race -race func Test_Logger_Data_Race(t *testing.T) { + t.Parallel() app := fiber.New() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) app.Use(New(ConfigDefault)) + app.Use(New(Config{ + Format: "${time} | ${pid} | ${locals:requestid} | ${status} | ${latency} | ${method} | ${path}\n", + })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") @@ -272,10 +306,10 @@ func Test_Logger_Data_Race(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) go func() { - resp1, err1 = app.Test(httptest.NewRequest("GET", "/", nil)) + resp1, err1 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) wg.Done() }() - resp2, err2 = app.Test(httptest.NewRequest("GET", "/", nil)) + resp2, err2 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) wg.Wait() require.Nil(t, err1) @@ -286,21 +320,23 @@ func Test_Logger_Data_Race(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Logger -benchmem -count=4 func Benchmark_Logger(b *testing.B) { - benchSetup := func(bb *testing.B, app *fiber.App) { + benchSetup := func(b *testing.B, app *fiber.App) { + b.Helper() + h := app.Handler() fctx := &fasthttp.RequestCtx{} - fctx.Request.Header.SetMethod("GET") + fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") - bb.ReportAllocs() - bb.ResetTimer() + b.ReportAllocs() + b.ResetTimer() - for n := 0; n < bb.N; n++ { + for n := 0; n < b.N; n++ { h(fctx) } - require.Equal(bb, 200, fctx.Response.Header.StatusCode()) + require.Equal(b, 200, fctx.Response.Header.StatusCode()) } b.Run("Base", func(bb *testing.B) { @@ -343,6 +379,7 @@ func Benchmark_Logger(b *testing.B) { // go test -run Test_Response_Header func Test_Response_Header(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -361,7 +398,7 @@ func Test_Response_Header(t *testing.T) { return c.SendString("Hello fiber!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -370,6 +407,7 @@ func Test_Response_Header(t *testing.T) { // go test -run Test_Req_Header func Test_Req_Header(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -381,10 +419,10 @@ func Test_Req_Header(t *testing.T) { app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") }) - headerReq := httptest.NewRequest("GET", "/", nil) + headerReq := httptest.NewRequest(fiber.MethodGet, "/", nil) headerReq.Header.Add("test", "Hello fiber!") - resp, err := app.Test(headerReq) + resp, err := app.Test(headerReq) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "Hello fiber!", buf.String()) @@ -392,6 +430,7 @@ func Test_Req_Header(t *testing.T) { // go test -run Test_ReqHeader_Header func Test_ReqHeader_Header(t *testing.T) { + t.Parallel() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) @@ -403,10 +442,10 @@ func Test_ReqHeader_Header(t *testing.T) { app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") }) - reqHeaderReq := httptest.NewRequest("GET", "/", nil) + reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil) reqHeaderReq.Header.Add("test", "Hello fiber!") - resp, err := app.Test(reqHeaderReq) + resp, err := app.Test(reqHeaderReq) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, "Hello fiber!", buf.String()) @@ -414,6 +453,7 @@ func Test_ReqHeader_Header(t *testing.T) { // go test -run Test_CustomTags func Test_CustomTags(t *testing.T) { + t.Parallel() customTag := "it is a custom tag" buf := bytebufferpool.Get() @@ -432,11 +472,67 @@ func Test_CustomTags(t *testing.T) { app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") }) - reqHeaderReq := httptest.NewRequest("GET", "/", nil) + reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil) reqHeaderReq.Header.Add("test", "Hello fiber!") - resp, err := app.Test(reqHeaderReq) + resp, err := app.Test(reqHeaderReq) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, customTag, buf.String()) } + +// go test -run Test_Logger_ByteSent_Streaming +func Test_Logger_ByteSent_Streaming(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status}", + Output: buf, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + var i int + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + err := w.Flush() + if err != nil { + break + } + if i == 10 { + break + } + } + }) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "0 0 200", buf.String()) +} + +// go test -run Test_Logger_EnableColors +func Test_Logger_EnableColors(t *testing.T) { + t.Parallel() + o := new(fakeOutput) + app := fiber.New() + app.Use(New(Config{ + Output: o, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + require.Equal(t, 1, int(*o)) +} diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index feb473ad87..afc0e34ad4 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -3,7 +3,6 @@ package logger import ( "fmt" "strings" - "time" "github.com/gofiber/fiber/v3" ) @@ -91,6 +90,9 @@ func createTagMap(cfg *Config) map[string]LogFunc { return appendInt(output, len(c.Request().Body())) }, TagBytesSent: func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error) { + if c.Response().Header.ContentLength() < 0 { + return appendInt(output, 0) + } return appendInt(output, len(c.Response().Body())) }, TagRoute: func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error) { @@ -193,11 +195,11 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.WriteString(data.Pid) }, TagLatency: func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error) { - latency := data.Stop.Sub(data.Start).Round(time.Millisecond) + latency := data.Stop.Sub(data.Start) return output.WriteString(fmt.Sprintf("%7v", latency)) }, TagTime: func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error) { - return output.WriteString(data.Timestamp.Load().(string)) + return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert // We always store a string in here }, } // merge with custom tags from user diff --git a/middleware/logger/template_chain.go b/middleware/logger/template_chain.go index ceb31a3356..00015bf33d 100644 --- a/middleware/logger/template_chain.go +++ b/middleware/logger/template_chain.go @@ -4,7 +4,7 @@ import ( "bytes" "errors" - "github.com/gofiber/fiber/v2/utils" + "github.com/gofiber/utils/v2" ) // buildLogFuncChain analyzes the template and creates slices with the functions for execution and @@ -14,13 +14,16 @@ import ( // funcChain contains for the parts which exist the functions for the dynamic parts // funcChain and fixParts always have the same length and contain nil for the parts where no data is required in the chain, // if a function exists for the part, a parameter for it can also exist in the fixParts slice -func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) (fixParts [][]byte, funcChain []LogFunc, err error) { +func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) ([][]byte, []LogFunc, error) { // process flow is copied from the fasttemplate flow https://github.com/valyala/fasttemplate/blob/2a2d1afadadf9715bfa19683cdaeac8347e5d9f9/template.go#L23-L62 templateB := utils.UnsafeBytes(cfg.Format) startTagB := utils.UnsafeBytes(startTag) endTagB := utils.UnsafeBytes(endTag) paramSeparatorB := utils.UnsafeBytes(paramSeparator) + var fixParts [][]byte + var funcChain []LogFunc + for { currentPos := bytes.Index(templateB, startTagB) if currentPos < 0 { @@ -42,13 +45,13 @@ func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) (fixParts [ // ## function block ## // first check for tags with parameters if index := bytes.Index(templateB[:currentPos], paramSeparatorB); index != -1 { - if logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:index+1])]; ok { - funcChain = append(funcChain, logFunc) - // add param to the fixParts - fixParts = append(fixParts, templateB[index+1:currentPos]) - } else { + logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:index+1])] + if !ok { return nil, nil, errors.New("No parameter found in \"" + utils.UnsafeString(templateB[:currentPos]) + "\"") } + funcChain = append(funcChain, logFunc) + // add param to the fixParts + fixParts = append(fixParts, templateB[index+1:currentPos]) } else if logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:currentPos])]; ok { // add functions without parameter funcChain = append(funcChain, logFunc) @@ -63,5 +66,5 @@ func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) (fixParts [ funcChain = append(funcChain, nil) fixParts = append(fixParts, templateB) - return + return fixParts, funcChain, nil } diff --git a/middleware/pprof/README.md b/middleware/pprof/README.md deleted file mode 100644 index 9d293b4459..0000000000 --- a/middleware/pprof/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Pprof -Pprof middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/. - -- [Signatures](#signatures) -- [Examples](#examples) - -### Signatures -```go -func New() 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/pprof" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -// Default middleware -app.Use(pprof.New()) -``` - -In systems where you have multiple ingress endpoints, it is common to add a URL prefix, like so: - -```go -// Default middleware -app.Use(pprof.New(pprof.Config{Prefix: "/endpoint-prefix"})) -``` - -This prefix will be added to the default path of "/debug/pprof/", for a resulting URL of: -"/endpoint-prefix/debug/pprof/". - -## Config - -```go -// 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 - - // Prefix defines a URL prefix added before "/debug/pprof". - // Note that it should start with (but not end with) a slash. - // Example: "/federated-fiber" - // - // Optional. Default: "" - Prefix string -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Next: nil, -} -``` diff --git a/middleware/pprof/config.go b/middleware/pprof/config.go index 618f5b658d..f763cc2481 100644 --- a/middleware/pprof/config.go +++ b/middleware/pprof/config.go @@ -1,6 +1,8 @@ package pprof -import "github.com/gofiber/fiber/v3" +import ( + "github.com/gofiber/fiber/v3" +) // Config defines the config for middleware. type Config struct { diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index aa55dc0181..373e42d6f0 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -8,26 +8,26 @@ import ( "github.com/valyala/fasthttp/fasthttpadaptor" ) -// Set pprof adaptors -var ( - pprofIndex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Index) - pprofCmdline = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Cmdline) - pprofProfile = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Profile) - pprofSymbol = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Symbol) - pprofTrace = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Trace) - pprofAllocs = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("allocs").ServeHTTP) - pprofBlock = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("block").ServeHTTP) - pprofGoroutine = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("goroutine").ServeHTTP) - pprofHeap = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("heap").ServeHTTP) - pprofMutex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("mutex").ServeHTTP) - pprofThreadcreate = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("threadcreate").ServeHTTP) -) - // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) + // Set pprof adaptors + var ( + pprofIndex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Index) + pprofCmdline = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Cmdline) + pprofProfile = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Profile) + pprofSymbol = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Symbol) + pprofTrace = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Trace) + pprofAllocs = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("allocs").ServeHTTP) + pprofBlock = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("block").ServeHTTP) + pprofGoroutine = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("goroutine").ServeHTTP) + pprofHeap = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("heap").ServeHTTP) + pprofMutex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("mutex").ServeHTTP) + pprofThreadcreate = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("threadcreate").ServeHTTP) + ) + // Return new handler return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true diff --git a/middleware/pprof/pprof_test.go b/middleware/pprof/pprof_test.go index c4274f1a85..5f25b42a7e 100644 --- a/middleware/pprof/pprof_test.go +++ b/middleware/pprof/pprof_test.go @@ -5,13 +5,13 @@ import ( "io" "net/http/httptest" "testing" - "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" ) func Test_Non_Pprof_Path(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -30,6 +30,7 @@ func Test_Non_Pprof_Path(t *testing.T) { } func Test_Non_Pprof_Path_WithPrefix(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Prefix: "/federated-fiber"})) @@ -39,15 +40,16 @@ func Test_Non_Pprof_Path_WithPrefix(t *testing.T) { }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) b, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "escaped", string(b)) } func Test_Pprof_Index(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -67,6 +69,7 @@ func Test_Pprof_Index(t *testing.T) { } func Test_Pprof_Index_WithPrefix(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Prefix: "/federated-fiber"})) @@ -76,16 +79,17 @@ func Test_Pprof_Index_WithPrefix(t *testing.T) { }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) b, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, true, bytes.Contains(b, []byte("/debug/pprof/"))) } func Test_Pprof_Subs(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -101,11 +105,12 @@ func Test_Pprof_Subs(t *testing.T) { for _, sub := range subs { t.Run(sub, func(t *testing.T) { + t.Parallel() target := "/debug/pprof/" + sub if sub == "profile" { target += "?seconds=1" } - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5000) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) }) @@ -113,6 +118,7 @@ func Test_Pprof_Subs(t *testing.T) { } func Test_Pprof_Subs_WithPrefix(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Prefix: "/federated-fiber"})) @@ -128,11 +134,12 @@ func Test_Pprof_Subs_WithPrefix(t *testing.T) { for _, sub := range subs { t.Run(sub, func(t *testing.T) { + t.Parallel() target := "/federated-fiber/debug/pprof/" + sub if sub == "profile" { target += "?seconds=1" } - resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5000) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode) }) @@ -140,6 +147,7 @@ func Test_Pprof_Subs_WithPrefix(t *testing.T) { } func Test_Pprof_Other(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -154,6 +162,7 @@ func Test_Pprof_Other(t *testing.T) { } func Test_Pprof_Other_WithPrefix(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{Prefix: "/federated-fiber"})) @@ -163,14 +172,13 @@ func Test_Pprof_Other_WithPrefix(t *testing.T) { }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/302", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, 302, resp.StatusCode) } // go test -run Test_Pprof_Next func Test_Pprof_Next(t *testing.T) { t.Parallel() - app := fiber.New() app.Use(New(Config{ @@ -187,7 +195,6 @@ func Test_Pprof_Next(t *testing.T) { // go test -run Test_Pprof_Next_WithPrefix func Test_Pprof_Next_WithPrefix(t *testing.T) { t.Parallel() - app := fiber.New() app.Use(New(Config{ @@ -198,6 +205,6 @@ func Test_Pprof_Next_WithPrefix(t *testing.T) { })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/", nil)) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, 404, resp.StatusCode) } diff --git a/middleware/proxy/README.md b/middleware/proxy/README.md deleted file mode 100644 index daa66245fc..0000000000 --- a/middleware/proxy/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# Proxy - -Proxy middleware for [Fiber](https://github.com/gofiber/fiber) that allows you to proxy requests to multiple servers. - -### Table of Contents - -- [Signatures](#signatures) -- [Examples](#examples) -- [Config](#config) -- [Default Config](#default-config) - -### Signatures - -```go -func Balancer(config Config) fiber.Handler -func Forward(addr string, clients ...*fasthttp.Client) fiber.Handler -func Do(c fiber.Ctx, addr string, clients ...*fasthttp.Client) error -``` - -### 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/proxy" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: - -```go -// if target https site uses a self-signed certificate, you should -// call WithTlsConfig before Do and Forward -proxy.WithTlsConfig(&tls.Config{ - InsecureSkipVerify: true, -}) - -// if you need to use global self-custom client, you should use proxy.WithClient. -proxy.WithClient(&fasthttp.Client{ - NoDefaultUserAgentHeader: true, - DisablePathNormalizing: true, -}) - -// Forward to url -app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif")) - -// Forward to url with local custom client -app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif", &fasthttp.Client{ - NoDefaultUserAgentHeader: true, - DisablePathNormalizing: true, -})) - -// Make request within handler -app.Get("/:id", func(c fiber.Ctx) error { - url := "https://i.imgur.com/"+c.Params("id")+".gif" - if err := proxy.Do(c, url); err != nil { - return err - } - // Remove Server header from response - c.Response().Header.Del(fiber.HeaderServer) - return nil -}) - -// Minimal round robin balancer -app.Use(proxy.Balancer(proxy.Config{ - Servers: []string{ - "http://localhost:3001", - "http://localhost:3002", - "http://localhost:3003", - }, -})) - -// Or extend your balancer for customization -app.Use(proxy.Balancer(proxy.Config{ - Servers: []string{ - "http://localhost:3001", - "http://localhost:3002", - "http://localhost:3003", - }, - ModifyRequest: func(c fiber.Ctx) error { - c.Request().Header.Add("X-Real-IP", c.IP()) - return nil - }, - ModifyResponse: func(c fiber.Ctx) error { - c.Response().Header.Del(fiber.HeaderServer) - return nil - }, -})) -``` - -### Config - -```go -// 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 - - // Servers defines a list of :// HTTP servers, - // - // which are used in a round-robin manner. - // i.e.: "https://foobar.com, http://www.foobar.com" - // - // Required - Servers []string - - // ModifyRequest allows you to alter the request - // - // Optional. Default: nil - ModifyRequest fiber.Handler - - // ModifyResponse allows you to alter the response - // - // Optional. Default: nil - ModifyResponse fiber.Handler - - // Timeout is the request timeout used when calling the proxy client - // - // Optional. Default: 1 second - Timeout time.Duration - - // Per-connection buffer size for requests' reading. - // This also limits the maximum header size. - // Increase this buffer if your clients send multi-KB RequestURIs - // and/or multi-KB headers (for example, BIG cookies). - ReadBufferSize int - - // Per-connection buffer size for responses' writing. - WriteBufferSize int - - // tls config for the http client. - TlsConfig *tls.Config - - // Client is custom client when client config is complex. - // Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TlsConfig - // will not be used if the client are set. - Client *fasthttp.LBClient -} -``` - -### Default Config - -```go -// ConfigDefault is the default config -var ConfigDefault = Config{ - Next: nil, - ModifyRequest: nil, - ModifyResponse: nil, - Timeout: fasthttp.DefaultLBClientTimeout, -} -``` diff --git a/middleware/proxy/config.go b/middleware/proxy/config.go index 0a482c6a55..18959748d2 100644 --- a/middleware/proxy/config.go +++ b/middleware/proxy/config.go @@ -48,7 +48,7 @@ type Config struct { WriteBufferSize int // tls config for the http client. - TlsConfig *tls.Config + TlsConfig *tls.Config //nolint:stylecheck,revive // TODO: Rename to "TLSConfig" in v3 // Client is custom client when client config is complex. // Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TlsConfig diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go index 6ce4ba0b61..50987a53b5 100644 --- a/middleware/proxy/proxy.go +++ b/middleware/proxy/proxy.go @@ -6,9 +6,11 @@ import ( "net/url" "strings" "sync" + "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" + "github.com/valyala/fasthttp" ) @@ -18,7 +20,7 @@ func Balancer(config Config) fiber.Handler { cfg := configDefault(config) // Load balanced client - var lbc = &fasthttp.LBClient{} + lbc := &fasthttp.LBClient{} // Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TlsConfig // will not be used if the client are set. if config.Client == nil { @@ -54,7 +56,7 @@ func Balancer(config Config) fiber.Handler { } // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() @@ -69,7 +71,7 @@ func Balancer(config Config) fiber.Handler { // Modify request if cfg.ModifyRequest != nil { - if err = cfg.ModifyRequest(c); err != nil { + if err := cfg.ModifyRequest(c); err != nil { return err } } @@ -77,7 +79,7 @@ func Balancer(config Config) fiber.Handler { req.SetRequestURI(utils.UnsafeString(req.RequestURI())) // Forward request - if err = lbc.Do(req, res); err != nil { + if err := lbc.Do(req, res); err != nil { return err } @@ -86,7 +88,7 @@ func Balancer(config Config) fiber.Handler { // Modify response if cfg.ModifyResponse != nil { - if err = cfg.ModifyResponse(c); err != nil { + if err := cfg.ModifyResponse(c); err != nil { return err } } @@ -106,6 +108,8 @@ var lock sync.RWMutex // WithTlsConfig update http client with a user specified tls.config // This function should be called before Do and Forward. // Deprecated: use WithClient instead. +// +//nolint:stylecheck,revive // TODO: Rename to "WithTLSConfig" in v3 func WithTlsConfig(tlsConfig *tls.Config) { client.TLSConfig = tlsConfig } @@ -129,16 +133,53 @@ func Forward(addr string, clients ...*fasthttp.Client) fiber.Handler { // Do performs the given http request and fills the given http response. // This method can be used within a fiber.Handler func Do(c fiber.Ctx, addr string, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.Do(req, resp) + }, clients...) +} + +// DoRedirects performs the given http request and fills the given http response, following up to maxRedirectsCount redirects. +// When the redirect count exceeds maxRedirectsCount, ErrTooManyRedirects is returned. +// This method can be used within a fiber.Handler +func DoRedirects(c fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoRedirects(req, resp, maxRedirectsCount) + }, clients...) +} + +// DoDeadline performs the given request and waits for response until the given deadline. +// This method can be used within a fiber.Handler +func DoDeadline(c fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoDeadline(req, resp, deadline) + }, clients...) +} + +// DoTimeout performs the given request and waits for response during the given timeout duration. +// This method can be used within a fiber.Handler +func DoTimeout(c fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoTimeout(req, resp, timeout) + }, clients...) +} + +func doAction( + c fiber.Ctx, + addr string, + action func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error, + clients ...*fasthttp.Client, +) error { var cli *fasthttp.Client + + // set local or global client if len(clients) != 0 { - // Set local client cli = clients[0] } else { - // Set global client lock.RLock() cli = client lock.RUnlock() } + req := c.Request() res := c.Response() originalURL := utils.CopyString(c.OriginalURL()) @@ -147,14 +188,13 @@ func Do(c fiber.Ctx, addr string, clients ...*fasthttp.Client) error { copiedURL := utils.CopyString(addr) req.SetRequestURI(copiedURL) // NOTE: if req.isTLS is true, SetRequestURI keeps the scheme as https. - // issue reference: - // https://github.com/gofiber/fiber/issues/1762 + // Reference: https://github.com/gofiber/fiber/issues/1762 if scheme := getScheme(utils.UnsafeBytes(copiedURL)); len(scheme) > 0 { req.URI().SetSchemeBytes(scheme) } req.Header.Del(fiber.HeaderConnection) - if err := cli.Do(req, res); err != nil { + if err := action(cli, req, res); err != nil { return err } res.Header.Del(fiber.HeaderConnection) @@ -168,3 +208,53 @@ func getScheme(uri []byte) []byte { } return uri[:i-1] } + +// DomainForward performs an http request based on the given domain and populates the given http response. +// This method will return an fiber.Handler +func DomainForward(hostname, addr string, clients ...*fasthttp.Client) fiber.Handler { + return func(c fiber.Ctx) error { + host := string(c.Request().Host()) + if host == hostname { + return Do(c, addr+c.OriginalURL(), clients...) + } + return nil + } +} + +type roundrobin struct { + sync.Mutex + + current int + pool []string +} + +// this method will return a string of addr server from list server. +func (r *roundrobin) get() string { + r.Lock() + defer r.Unlock() + + if r.current >= len(r.pool) { + r.current %= len(r.pool) + } + + result := r.pool[r.current] + r.current++ + return result +} + +// BalancerForward Forward performs the given http request with round robin algorithm to server and fills the given http response. +// This method will return an fiber.Handler +func BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler { + r := &roundrobin{ + current: 0, + pool: servers, + } + return func(c fiber.Ctx) error { + server := r.get() + if !strings.HasPrefix(server, "http") { + server = "http://" + server + } + c.Request().Header.Add("X-Real-IP", c.IP()) + return Do(c, server+c.OriginalURL(), clients...) + } +} diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go index 4d86e9552f..86dc2153e4 100644 --- a/middleware/proxy/proxy_test.go +++ b/middleware/proxy/proxy_test.go @@ -2,9 +2,9 @@ package proxy import ( "crypto/tls" + "errors" "io" "net" - "net/http" "net/http/httptest" "strings" "testing" @@ -12,12 +12,11 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/internal/tlstest" - "github.com/gofiber/utils/v2" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) -func createProxyTestServer(handler fiber.Handler, t *testing.T) (*fiber.App, string) { +func createProxyTestServer(t *testing.T, handler fiber.Handler) (*fiber.App, string) { t.Helper() target := fiber.New() @@ -52,6 +51,19 @@ func Test_Proxy_Empty_Upstream_Servers(t *testing.T) { app.Use(Balancer(Config{Servers: []string{}})) } +// go test -run Test_Proxy_Empty_Config +func Test_Proxy_Empty_Config(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + require.Equal(t, "Servers cannot be empty", r) + } + }() + app := fiber.New() + app.Use(Balancer(Config{})) +} + // go test -run Test_Proxy_Next func Test_Proxy_Next(t *testing.T) { t.Parallel() @@ -64,7 +76,7 @@ func Test_Proxy_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } @@ -73,11 +85,11 @@ func Test_Proxy_Next(t *testing.T) { func Test_Proxy(t *testing.T) { t.Parallel() - target, addr := createProxyTestServer( - func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) }, t, - ) + target, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) - resp, err := target.Test(httptest.NewRequest("GET", "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -85,7 +97,7 @@ func Test_Proxy(t *testing.T) { app.Use(Balancer(Config{Servers: []string{addr}})) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Host = addr resp, err = app.Test(req) require.NoError(t, err) @@ -111,7 +123,7 @@ func Test_Proxy_Balancer_WithTlsConfig(t *testing.T) { }) addr := ln.Addr().String() - clientTLSConf := &tls.Config{InsecureSkipVerify: true} + clientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine // disable certificate verification in Balancer app.Use(Balancer(Config{ @@ -134,11 +146,11 @@ func Test_Proxy_Balancer_WithTlsConfig(t *testing.T) { // go test -run Test_Proxy_Forward_WithTlsConfig_To_Http func Test_Proxy_Forward_WithTlsConfig_To_Http(t *testing.T) { - //t.Parallel() + t.Parallel() - _, targetAddr := createProxyTestServer(func(c fiber.Ctx) error { + _, targetAddr := createProxyTestServer(t, func(c fiber.Ctx) error { return c.SendString("hello from target") - }, t) + }) proxyServerTLSConf, _, err := tlstest.GetTLSConfigs() require.NoError(t, err) @@ -176,13 +188,13 @@ func Test_Proxy_Forward(t *testing.T) { app := fiber.New() - _, addr := createProxyTestServer( - func(c fiber.Ctx) error { return c.SendString("forwarded") }, t, - ) + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendString("forwarded") + }) app.Use(Forward("http://" + addr)) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -210,7 +222,7 @@ func Test_Proxy_Forward_WithTlsConfig(t *testing.T) { }) addr := ln.Addr().String() - clientTLSConf := &tls.Config{InsecureSkipVerify: true} + clientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine // disable certificate verification WithTlsConfig(clientTLSConf) @@ -233,9 +245,9 @@ func Test_Proxy_Forward_WithTlsConfig(t *testing.T) { func Test_Proxy_Modify_Response(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { return c.Status(500).SendString("not modified") - }, t) + }) app := fiber.New() app.Use(Balancer(Config{ @@ -246,7 +258,7 @@ func Test_Proxy_Modify_Response(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -259,10 +271,10 @@ func Test_Proxy_Modify_Response(t *testing.T) { func Test_Proxy_Modify_Request(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { b := c.Request().Body() return c.SendString(string(b)) - }, t) + }) app := fiber.New() app.Use(Balancer(Config{ @@ -273,7 +285,7 @@ func Test_Proxy_Modify_Request(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -286,10 +298,10 @@ func Test_Proxy_Modify_Request(t *testing.T) { func Test_Proxy_Timeout_Slow_Server(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { time.Sleep(2 * time.Second) return c.SendString("fiber is awesome") - }, t) + }) app := fiber.New() app.Use(Balancer(Config{ @@ -297,7 +309,7 @@ func Test_Proxy_Timeout_Slow_Server(t *testing.T) { Timeout: 3 * time.Second, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil), 5*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 5000) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) @@ -310,10 +322,10 @@ func Test_Proxy_Timeout_Slow_Server(t *testing.T) { func Test_Proxy_With_Timeout(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { time.Sleep(1 * time.Second) return c.SendString("fiber is awesome") - }, t) + }) app := fiber.New() app.Use(Balancer(Config{ @@ -321,7 +333,7 @@ func Test_Proxy_With_Timeout(t *testing.T) { Timeout: 100 * time.Millisecond, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil), 2*time.Second) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) @@ -334,16 +346,16 @@ func Test_Proxy_With_Timeout(t *testing.T) { func Test_Proxy_Buffer_Size_Response(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { long := strings.Join(make([]string, 5000), "-") c.Set("Very-Long-Header", long) return c.SendString("ok") - }, t) + }) app := fiber.New() app.Use(Balancer(Config{Servers: []string{addr}})) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) @@ -353,7 +365,7 @@ func Test_Proxy_Buffer_Size_Response(t *testing.T) { ReadBufferSize: 1024 * 8, })) - resp, err = app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) } @@ -361,33 +373,176 @@ func Test_Proxy_Buffer_Size_Response(t *testing.T) { // go test -race -run Test_Proxy_Do_RestoreOriginalURL func Test_Proxy_Do_RestoreOriginalURL(t *testing.T) { t.Parallel() + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendString("proxied") + }) + app := fiber.New() - app.Get("/proxy", func(c fiber.Ctx) error { - return c.SendString("ok") + app.Get("/test", func(c fiber.Ctx) error { + return Do(c, "http://"+addr) }) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, nil, err1) + require.Equal(t, "/test", resp.Request.URL.String()) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "proxied", string(body)) +} + +// go test -race -run Test_Proxy_Do_WithRealURL +func Test_Proxy_Do_WithRealURL(t *testing.T) { + t.Parallel() + app := fiber.New() app.Get("/test", func(c fiber.Ctx) error { - originalURL := utils.CopyString(c.OriginalURL()) - if err := Do(c, "/proxy"); err != nil { - return err - } - require.Equal(t, originalURL, c.OriginalURL()) - return c.SendString("ok") + return Do(c, "https://www.google.com") + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, nil, err1) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "/test", resp.Request.URL.String()) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, true, strings.Contains(string(body), "https://www.google.com/")) +} + +// go test -race -run Test_Proxy_Do_WithRedirect +func Test_Proxy_Do_WithRedirect(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return Do(c, "https://google.com") + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 1500) + require.Equal(t, nil, err1) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, true, strings.Contains(string(body), "https://www.google.com/")) + require.Equal(t, 301, resp.StatusCode) +} + +// go test -race -run Test_Proxy_DoRedirects_RestoreOriginalURL +func Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoRedirects(c, "http://google.com", 1) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 1500) + require.Equal(t, nil, err1) + _, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoRedirects_TooManyRedirects +func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoRedirects(c, "http://google.com", 0) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), 1500) + require.Equal(t, nil, err1) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "too many redirects detected when doing the request", string(body)) + require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) + require.Equal(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoTimeout_RestoreOriginalURL +func Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoTimeout(c, "http://"+addr, time.Second) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, nil, err1) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "proxied", string(body)) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoTimeout_Timeout +func Test_Proxy_DoTimeout_Timeout(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + time.Sleep(time.Second * 5) + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoTimeout(c, "http://"+addr, time.Second) + }) + + _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, errors.New("test: timeout error 1000ms"), err1) +} + +// go test -race -run Test_Proxy_DoDeadline_RestoreOriginalURL +func Test_Proxy_DoDeadline_RestoreOriginalURL(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoDeadline(c, "http://"+addr, time.Now().Add(time.Second)) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, nil, err1) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, "proxied", string(body)) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoDeadline_PastDeadline +func Test_Proxy_DoDeadline_PastDeadline(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + time.Sleep(time.Second * 5) + return c.SendString("proxied") }) - _, err1 := app.Test(httptest.NewRequest("GET", "/test", nil)) - // This test requires multiple requests due to zero allocation used in fiber - _, err2 := app.Test(httptest.NewRequest("GET", "/test", nil)) - require.Nil(t, err1) - require.Nil(t, err2) + app := fiber.New() + app.Get("/test", func(c fiber.Ctx) error { + return DoDeadline(c, "http://"+addr, time.Now().Add(time.Second)) + }) + + _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + require.Equal(t, errors.New("test: timeout error 1000ms"), err1) } // go test -race -run Test_Proxy_Do_HTTP_Prefix_URL func Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) { t.Parallel() - _, addr := createProxyTestServer(func(c fiber.Ctx) error { + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { return c.SendString("hello world") - }, t) + }) app := fiber.New() app.Get("/*", func(c fiber.Ctx) error { @@ -402,7 +557,7 @@ func Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) { return nil }) - resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/http://"+addr, nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/http://"+addr, nil)) require.NoError(t, err) s, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -451,9 +606,8 @@ func Test_Proxy_Forward_Local_Client(t *testing.T) { app.Use(Forward("http://"+addr+"/test_local_client", &fasthttp.Client{ NoDefaultUserAgentHeader: true, DisablePathNormalizing: true, - Dial: func(addr string) (net.Conn, error) { - return fasthttp.Dial(addr) - }, + + Dial: fasthttp.Dial, })) go func() { require.Nil(t, app.Listener(ln, fiber.ListenConfig{ @@ -471,11 +625,11 @@ func Test_Proxy_Forward_Local_Client(t *testing.T) { func Test_ProxyBalancer_Custom_Client(t *testing.T) { t.Parallel() - target, addr := createProxyTestServer( - func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) }, t, - ) + target, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) - resp, err := target.Test(httptest.NewRequest("GET", "/", nil), 2*time.Second) + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) @@ -492,9 +646,66 @@ func Test_ProxyBalancer_Custom_Client(t *testing.T) { Timeout: time.Second, }})) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Host = addr resp, err = app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) } + +// go test -run Test_Proxy_Domain_Forward_Local +func Test_Proxy_Domain_Forward_Local(t *testing.T) { + t.Parallel() + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + require.NoError(t, err) + app := fiber.New() + + // target server + ln1, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + require.NoError(t, err) + app1 := fiber.New() + + app1.Get("/test", func(c fiber.Ctx) error { + return c.SendString("test_local_client:" + c.Query("query_test")) + }) + + proxyAddr := ln.Addr().String() + targetAddr := ln1.Addr().String() + localDomain := strings.Replace(proxyAddr, "127.0.0.1", "localhost", 1) + app.Use(DomainForward(localDomain, "http://"+targetAddr, &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + + Dial: fasthttp.Dial, + })) + + go func() { require.NoError(t, app.Listener(ln)) }() + go func() { require.NoError(t, app1.Listener(ln1)) }() + + code, body, errs := fiber.Get("http://" + localDomain + "/test?query_test=true").String() + require.Equal(t, 0, len(errs)) + require.Equal(t, fiber.StatusOK, code) + require.Equal(t, "test_local_client:true", body) +} + +// go test -run Test_Proxy_Balancer_Forward_Local +func Test_Proxy_Balancer_Forward_Local(t *testing.T) { + t.Parallel() + + app := fiber.New() + + _, addr := createProxyTestServer(t, func(c fiber.Ctx) error { + return c.SendString("forwarded") + }) + + app.Use(BalancerForward([]string{addr})) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.Equal(t, string(b), "forwarded") +} diff --git a/middleware/recover/README.md b/middleware/recover/README.md deleted file mode 100644 index 991b755bef..0000000000 --- a/middleware/recover/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Recover -Recover middleware for [Fiber](https://github.com/gofiber/fiber) that recovers from panics anywhere in the stack chain and handles the control to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). - -### Table of Contents -- [Signatures](#signatures) -- [Examples](#examples) -- [Config](#config) -- [Default Config](#default-config) - - -### 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/recover" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -// Default middleware config -app.Use(recover.New()) - -// This panic will be caught by the middleware -app.Get("/", func(c fiber.Ctx) error { - panic("I'm an error") -}) -``` - -### Config -```go -// 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 - - // EnableStackTrace enables handling stack trace - // - // Optional. Default: false - EnableStackTrace bool - - // StackTraceHandler defines a function to handle stack trace - // - // Optional. Default: defaultStackTraceHandler - StackTraceHandler func(c fiber.Ctx, e any) -} -``` - -### Default Config -```go -var ConfigDefault = Config{ - Next: nil, - EnableStackTrace: false, - StackTraceHandler: defaultStackTraceHandler, -} -``` diff --git a/middleware/recover/config.go b/middleware/recover/config.go index c16a106d1f..a857ae5b94 100644 --- a/middleware/recover/config.go +++ b/middleware/recover/config.go @@ -1,4 +1,4 @@ -package recover +package recover //nolint:predeclared // TODO: Rename to some non-builtin import ( "github.com/gofiber/fiber/v3" diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go index 1f1e2aba60..4b9404c32a 100644 --- a/middleware/recover/recover.go +++ b/middleware/recover/recover.go @@ -1,4 +1,4 @@ -package recover +package recover //nolint:predeclared // TODO: Rename to some non-builtin import ( "fmt" @@ -8,8 +8,8 @@ import ( "github.com/gofiber/fiber/v3" ) -func defaultStackTraceHandler(_ fiber.Ctx, e any) { - _, _ = os.Stderr.WriteString(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack())) +func defaultStackTraceHandler(_ fiber.Ctx, e interface{}) { + _, _ = os.Stderr.WriteString(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack())) //nolint:errcheck // This will never fail } // New creates a new middleware handler @@ -18,7 +18,7 @@ func New(config ...Config) fiber.Handler { cfg := configDefault(config...) // Return new handler - return func(c fiber.Ctx) (err error) { + return func(c fiber.Ctx) (err error) { //nolint:nonamedreturns // Uses recover() to overwrite the error // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() diff --git a/middleware/recover/recover_test.go b/middleware/recover/recover_test.go index b837a9d46d..411c231431 100644 --- a/middleware/recover/recover_test.go +++ b/middleware/recover/recover_test.go @@ -1,4 +1,4 @@ -package recover +package recover //nolint:predeclared // TODO: Rename to some non-builtin import ( "net/http/httptest" @@ -10,6 +10,7 @@ import ( // go test -run Test_Recover func Test_Recover(t *testing.T) { + t.Parallel() app := fiber.New(fiber.Config{ ErrorHandler: func(c fiber.Ctx, err error) error { require.Equal(t, "Hi, I'm an error!", err.Error()) @@ -23,13 +24,14 @@ func Test_Recover(t *testing.T) { panic("Hi, I'm an error!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/panic", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) } // go test -run Test_Recover_Next func Test_Recover_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -37,12 +39,13 @@ func Test_Recover_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_Recover_EnableStackTrace(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ EnableStackTrace: true, @@ -52,7 +55,7 @@ func Test_Recover_EnableStackTrace(t *testing.T) { panic("Hi, I'm an error!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/panic", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/panic", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) } diff --git a/middleware/redirect/README.md b/middleware/redirect/README.md deleted file mode 100644 index cc7acf201a..0000000000 --- a/middleware/redirect/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Redirect - -![Release](https://img.shields.io/github/release/gofiber/redirect.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/redirect/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/redirect/workflows/Security/badge.svg) -![Linter](https://github.com/gofiber/redirect/workflows/Linter/badge.svg) - -### Install -``` -go get -u github.com/gofiber/fiber/v3 -go get -u github.com/gofiber/redirect/v2 -``` -### Example -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/redirect/v2" -) - -func main() { - app := fiber.New() - - app.Use(redirect.New(redirect.Config{ - Rules: map[string]string{ - "/old": "/new", - "/old/*": "/new/$1", - }, - StatusCode: 301, - })) - - app.Get("/new", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) - app.Get("/new/*", func(c fiber.Ctx) error { - return c.SendString("Wildcard: " + c.Params("*")) - }) - - app.Listen(":3000") -} -``` -### Test -```curl -curl http://localhost:3000/old -curl http://localhost:3000/old/hello -``` diff --git a/middleware/redirect/config.go b/middleware/redirect/config.go new file mode 100644 index 0000000000..bebc0c02f7 --- /dev/null +++ b/middleware/redirect/config.go @@ -0,0 +1,53 @@ +package redirect + +import ( + "regexp" + + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Filter defines a function to skip middleware. + // Optional. Default: nil + Next func(fiber.Ctx) bool + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Required. Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rules map[string]string + + // The status code when redirecting + // This is ignored if Redirect is disabled + // Optional. Default: 302 Temporary Redirect + StatusCode int + + rulesRegex map[*regexp.Regexp]string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + StatusCode: fiber.StatusFound, +} + +// 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.StatusCode == 0 { + cfg.StatusCode = ConfigDefault.StatusCode + } + + return cfg +} diff --git a/middleware/redirect/redirect.go b/middleware/redirect/redirect.go index 0dec5263a9..267f010e69 100644 --- a/middleware/redirect/redirect.go +++ b/middleware/redirect/redirect.go @@ -1,7 +1,3 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - package redirect import ( @@ -12,49 +8,22 @@ import ( "github.com/gofiber/fiber/v3" ) -// Config ... -type Config struct { - // Filter defines a function to skip middleware. - // Optional. Default: nil - Filter func(fiber.Ctx) bool - // Rules defines the URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Required. Example: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - Rules map[string]string - // The status code when redirecting - // This is ignored if Redirect is disabled - // Optional. Default: 302 Temporary Redirect - StatusCode int - - rulesRegex map[*regexp.Regexp]string -} - -// New ... +// New creates a new middleware handler func New(config ...Config) fiber.Handler { - // Init config - var cfg Config - if len(config) > 0 { - cfg = config[0] - } - if cfg.StatusCode == 0 { - cfg.StatusCode = 302 // Temporary Redirect - } - cfg = config[0] - cfg.rulesRegex = map[*regexp.Regexp]string{} + cfg := configDefault(config...) + // Initialize + cfg.rulesRegex = map[*regexp.Regexp]string{} for k, v := range cfg.Rules { - k = strings.Replace(k, "*", "(.*)", -1) - k = k + "$" + k = strings.ReplaceAll(k, "*", "(.*)") + k += "$" cfg.rulesRegex[regexp.MustCompile(k)] = v } + // Middleware function return func(c fiber.Ctx) error { - // Filter request to skip middleware - if cfg.Filter != nil && cfg.Filter(c) { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { return c.Next() } // Rewrite diff --git a/middleware/redirect/redirect_test.go b/middleware/redirect/redirect_test.go index 3b2491ca33..a03b1f65c5 100644 --- a/middleware/redirect/redirect_test.go +++ b/middleware/redirect/redirect_test.go @@ -1,14 +1,13 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package redirect import ( + "context" "net/http" "testing" "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" ) func Test_Redirect(t *testing.T) { @@ -18,32 +17,32 @@ func Test_Redirect(t *testing.T) { Rules: map[string]string{ "/default": "google.com", }, - StatusCode: 301, + StatusCode: fiber.StatusMovedPermanently, })) app.Use(New(Config{ Rules: map[string]string{ "/default/*": "fiber.wiki", }, - StatusCode: 307, + StatusCode: fiber.StatusTemporaryRedirect, })) app.Use(New(Config{ Rules: map[string]string{ "/redirect/*": "$1", }, - StatusCode: 303, + StatusCode: fiber.StatusSeeOther, })) app.Use(New(Config{ Rules: map[string]string{ "/pattern/*": "golang.org", }, - StatusCode: 302, + StatusCode: fiber.StatusFound, })) app.Use(New(Config{ Rules: map[string]string{ "/": "/swagger", }, - StatusCode: 301, + StatusCode: fiber.StatusMovedPermanently, })) app.Get("/api/*", func(c fiber.Ctx) error { @@ -61,66 +60,224 @@ func Test_Redirect(t *testing.T) { statusCode int }{ { - name: "should be returns status 302 without a wildcard", + name: "should be returns status StatusFound without a wildcard", url: "/default", redirectTo: "google.com", - statusCode: 301, + statusCode: fiber.StatusMovedPermanently, }, { - name: "should be returns status 307 using wildcard", + name: "should be returns status StatusTemporaryRedirect using wildcard", url: "/default/xyz", redirectTo: "fiber.wiki", - statusCode: 307, + statusCode: fiber.StatusTemporaryRedirect, }, { - name: "should be returns status 303 without set redirectTo to use the default", + name: "should be returns status StatusSeeOther without set redirectTo to use the default", url: "/redirect/github.com/gofiber/redirect", redirectTo: "github.com/gofiber/redirect", - statusCode: 303, + statusCode: fiber.StatusSeeOther, }, { name: "should return the status code default", url: "/pattern/xyz", redirectTo: "golang.org", - statusCode: 302, + statusCode: fiber.StatusFound, }, { name: "access URL without rule", url: "/new", - statusCode: 200, + statusCode: fiber.StatusOK, }, { name: "redirect to swagger route", url: "/", redirectTo: "/swagger", - statusCode: 301, + statusCode: fiber.StatusMovedPermanently, }, { name: "no redirect to swagger route", url: "/api/", - statusCode: 200, + statusCode: fiber.StatusOK, }, { name: "no redirect to swagger route #2", url: "/api/test", - statusCode: 200, + statusCode: fiber.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, _ := http.NewRequest("GET", tt.url, nil) + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, tt.url, nil) + require.Equal(t, err, nil) req.Header.Set("Location", "github.com/gofiber/redirect") resp, err := app.Test(req) - if err != nil { - t.Fatalf(`%s: %s`, t.Name(), err) - } - if resp.StatusCode != tt.statusCode { - t.Fatalf(`%s: StatusCode: got %v - expected %v`, t.Name(), resp.StatusCode, tt.statusCode) - } - if resp.Header.Get("Location") != tt.redirectTo { - t.Fatalf(`%s: Expecting Location: %s`, t.Name(), tt.redirectTo) - } + + require.Equal(t, err, nil) + require.Equal(t, tt.statusCode, resp.StatusCode) + require.Equal(t, tt.redirectTo, resp.Header.Get("Location")) }) } +} + +func Test_Next(t *testing.T) { + // Case 1 : Next function always returns true + app := *fiber.New() + app.Use(New(Config{ + Next: func(fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err := app.Test(req) + require.Equal(t, err, nil) + + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + // Case 2 : Next function always returns false + app = *fiber.New() + app.Use(New(Config{ + Next: func(fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + + require.Equal(t, fiber.StatusMovedPermanently, resp.StatusCode) + require.Equal(t, "google.com", resp.Header.Get("Location")) +} + +func Test_NoRules(t *testing.T) { + // Case 1: No rules with default route defined + app := *fiber.New() + + app.Use(New(Config{ + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err := app.Test(req) + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + // Case 2: No rules and no default route defined + app = *fiber.New() + + app.Use(New(Config{ + StatusCode: fiber.StatusMovedPermanently, + })) + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_DefaultConfig(t *testing.T) { + // Case 1: Default config and no default route + app := *fiber.New() + + app.Use(New()) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err := app.Test(req) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + // Case 2: Default config and default route + app = *fiber.New() + + app.Use(New()) + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) +} + +func Test_RegexRules(t *testing.T) { + // Case 1: Rules regex is empty + app := *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{}, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err := app.Test(req) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + // Case 2: Rules regex map contains valid regex and well-formed replacement URLs + app = *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusMovedPermanently, resp.StatusCode) + require.Equal(t, "google.com", resp.Header.Get("Location")) + + // Case 3: Test invalid regex throws panic + defer func() { + if r := recover(); r != nil { + t.Log("Recovered from invalid regex: ", r) + } + }() + + app = *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "(": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + t.Error("Expected panic, got nil") } diff --git a/middleware/requestid/README.md b/middleware/requestid/README.md deleted file mode 100644 index 5ef9e6b30a..0000000000 --- a/middleware/requestid/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# RequestID -RequestID middleware for [Fiber](https://github.com/gofiber/fiber) that adds an identifier to the response. - -### Table of Contents -- [Signatures](#signatures) -- [Examples](#examples) -- [Config](#config) -- [Default Config](#default-config) - - -### 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/requestid" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -// Default middleware config -app.Use(requestid.New()) - -// Or extend your config for customization -app.Use(requestid.New(requestid.Config{ - Header: "X-Custom-Header", - Generator: func() string { - return "static-id" - }, -})) -``` - -### Config -```go -// 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 - - // Header is the header key where to get/set the unique request ID - // - // Optional. Default: "X-Request-ID" - Header string - - // Generator defines a function to generate the unique identifier. - // - // Optional. Default: utils.UUID - Generator func() string - - // ContextKey defines the key used when storing the request ID in - // the locals for a specific request. - // - // Optional. Default: requestid - ContextKey string -} -``` - -### Default Config -```go -var ConfigDefault = Config{ - Next: nil, - Header: fiber.HeaderXRequestID, - Generator: func() string { - return utils.UUID() - }, - ContextKey: "requestid" -} -``` diff --git a/middleware/requestid/config.go b/middleware/requestid/config.go index d29a62a4fe..f1c88f2fd6 100644 --- a/middleware/requestid/config.go +++ b/middleware/requestid/config.go @@ -26,10 +26,13 @@ type Config struct { // the locals for a specific request. // // Optional. Default: requestid - ContextKey string + ContextKey interface{} } // ConfigDefault is the default config +// It uses a fast UUID generator which will expose the number of +// requests made to the server. To conceal this value for better +// privacy, use the "utils.UUIDv4" generator. var ConfigDefault = Config{ Next: nil, Header: fiber.HeaderXRequestID, diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 0b085e4f9b..d3c9ed9ed5 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -16,7 +16,10 @@ func New(config ...Config) fiber.Handler { return c.Next() } // Get id from request, else we generate one - rid := c.Get(cfg.Header, cfg.Generator()) + rid := c.Get(cfg.Header) + if rid == "" { + rid = cfg.Generator() + } // Set new id to response header c.Set(cfg.Header, rid) diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index f011373515..714cb84be4 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -10,6 +10,7 @@ import ( // go test -run Test_RequestID func Test_RequestID(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New()) @@ -18,14 +19,14 @@ func Test_RequestID(t *testing.T) { return c.SendString("Hello, World 👋!") }) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) reqid := resp.Header.Get(fiber.HeaderXRequestID) require.Equal(t, 36, len(reqid)) - req := httptest.NewRequest("GET", "/", nil) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Add(fiber.HeaderXRequestID, reqid) resp, err = app.Test(req) @@ -36,6 +37,7 @@ func Test_RequestID(t *testing.T) { // go test -run Test_RequestID_Next func Test_RequestID_Next(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { @@ -43,7 +45,7 @@ func Test_RequestID_Next(t *testing.T) { }, })) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, resp.Header.Get(fiber.HeaderXRequestID), "") require.Equal(t, fiber.StatusNotFound, resp.StatusCode) @@ -51,25 +53,27 @@ func Test_RequestID_Next(t *testing.T) { // go test -run Test_RequestID_Locals func Test_RequestID_Locals(t *testing.T) { - reqId := "ThisIsARequestId" - ctxKey := "ThisIsAContextKey" + t.Parallel() + reqID := "ThisIsARequestId" + type ContextKey int + const requestContextKey ContextKey = iota app := fiber.New() app.Use(New(Config{ Generator: func() string { - return reqId + return reqID }, - ContextKey: ctxKey, + ContextKey: requestContextKey, })) var ctxVal string app.Use(func(c fiber.Ctx) error { - ctxVal = c.Locals(ctxKey).(string) + ctxVal = c.Locals(requestContextKey).(string) //nolint:forcetypeassert,errcheck // We always store a string in here return c.Next() }) - _, err := app.Test(httptest.NewRequest("GET", "/", nil)) + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) - require.Equal(t, reqId, ctxVal) + require.Equal(t, reqID, ctxVal) } diff --git a/middleware/rewrite/README.md b/middleware/rewrite/README.md deleted file mode 100644 index cec9ec1c6a..0000000000 --- a/middleware/rewrite/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Rewrite - -![Release](https://img.shields.io/github/release/gofiber/rewrite.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/rewrite/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/rewrite/workflows/Security/badge.svg) -![Linter](https://github.com/gofiber/rewrite/workflows/Linter/badge.svg) - -### Install -``` -go get -u github.com/gofiber/fiber/v3 -go get -u github.com/gofiber/rewrite/v2 -``` -### Example -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/rewrite/v2" -) - -func main() { - app := fiber.New() - - app.Use(rewrite.New(rewrite.Config{ - Rules: map[string]string{ - "/old": "/new", - "/old/*": "/new/$1", - }, - })) - - app.Get("/new", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) - app.Get("/new/*", func(c fiber.Ctx) error { - return c.SendString("Wildcard: " + c.Params("*")) - }) - - app.Listen(":3000") -} - -``` -### Test -```curl -curl http://localhost:3000/old -curl http://localhost:3000/old/hello -``` diff --git a/middleware/rewrite/config.go b/middleware/rewrite/config.go new file mode 100644 index 0000000000..1f4f8efbf0 --- /dev/null +++ b/middleware/rewrite/config.go @@ -0,0 +1,38 @@ +package rewrite + +import ( + "regexp" + + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(fiber.Ctx) bool + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Required. Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rules map[string]string + + rulesRegex map[*regexp.Regexp]string +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return Config{} + } + + // Override default config + cfg := config[0] + + return cfg +} diff --git a/middleware/rewrite/rewrite.go b/middleware/rewrite/rewrite.go index 4824586cba..5a7ad88288 100644 --- a/middleware/rewrite/rewrite.go +++ b/middleware/rewrite/rewrite.go @@ -1,7 +1,3 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - package rewrite import ( @@ -12,52 +8,21 @@ import ( "github.com/gofiber/fiber/v3" ) -// Config ... -type Config struct { - // Filter defines a function to skip middleware. - // Optional. Default: nil - Filter func(fiber.Ctx) bool - // Rules defines the URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Required. Example: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - Rules map[string]string - // // Redirect determns if the client should be redirected - // // By default this is disabled and urls are rewritten on the server - // // Optional. Default: false - // Redirect bool - // // The status code when redirecting - // // This is ignored if Redirect is disabled - // // Optional. Default: 302 Temporary Redirect - // StatusCode int - rulesRegex map[*regexp.Regexp]string -} - -// New ... +// New creates a new middleware handler func New(config ...Config) fiber.Handler { - // Init config - var cfg Config - if len(config) > 0 { - cfg = config[0] - } - // if cfg.StatusCode == 0 { - // cfg.StatusCode = 302 // Temporary Redirect - // } - cfg = config[0] - cfg.rulesRegex = map[*regexp.Regexp]string{} + cfg := configDefault(config...) + // Initialize + cfg.rulesRegex = map[*regexp.Regexp]string{} for k, v := range cfg.Rules { - k = strings.Replace(k, "*", "(.*)", -1) - k = k + "$" + k = strings.ReplaceAll(k, "*", "(.*)") + k += "$" cfg.rulesRegex[regexp.MustCompile(k)] = v } // Middleware function return func(c fiber.Ctx) error { - // Filter request to skip middleware - if cfg.Filter != nil && cfg.Filter(c) { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { return c.Next() } // Rewrite diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go index 7370432121..ae4ef367c6 100644 --- a/middleware/rewrite/rewrite_test.go +++ b/middleware/rewrite/rewrite_test.go @@ -1,5 +1,173 @@ -// 🚀 Fiber is an Express inspired web framework written in Go with 💖 -// 📌 API Documentation: https://fiber.wiki -// 📝 Github Repository: https://github.com/gofiber/fiber - +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package rewrite + +import ( + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/require" +) + +func Test_New(t *testing.T) { + // Test with no config + m := New() + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } + + // Test with config + m = New(Config{ + Rules: map[string]string{ + "/old": "/new", + }, + }) + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } + + // Test with full config + m = New(Config{ + Next: func(fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } +} + +func Test_Rewrite(t *testing.T) { + // Case 1: Next function always returns true + app := fiber.New() + app.Use(New(Config{ + Next: func(fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + })) + + app.Get("/old", func(c fiber.Ctx) error { + return c.SendString("Rewrite Successful") + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/old", nil) + require.Equal(t, err, nil) + resp, err := app.Test(req) + require.Equal(t, err, nil) + body, err := io.ReadAll(resp.Body) + require.Equal(t, err, nil) + bodyString := string(body) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "Rewrite Successful", bodyString) + + // Case 2: Next function always returns false + app = fiber.New() + app.Use(New(Config{ + Next: func(fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/old": "/new", + }, + })) + + app.Get("/new", func(c fiber.Ctx) error { + return c.SendString("Rewrite Successful") + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/old", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + body, err = io.ReadAll(resp.Body) + require.Equal(t, err, nil) + bodyString = string(body) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "Rewrite Successful", bodyString) + + // Case 3: check for captured tokens in rewrite rule + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/users/123/orders/456", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + body, err = io.ReadAll(resp.Body) + require.Equal(t, err, nil) + bodyString = string(body) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "User ID: 123, Order ID: 456", bodyString) + + // Case 4: Send non-matching request, handled by default route + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/not-matching-any-rule", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + body, err = io.ReadAll(resp.Body) + require.Equal(t, err, nil) + bodyString = string(body) + + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "OK", bodyString) + + // Case 4: Send non-matching request, with no default route + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/not-matching-any-rule", nil) + require.Equal(t, err, nil) + resp, err = app.Test(req) + require.Equal(t, err, nil) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) +} diff --git a/middleware/session/README.md b/middleware/session/README.md deleted file mode 100644 index 085e621cd1..0000000000 --- a/middleware/session/README.md +++ /dev/null @@ -1,170 +0,0 @@ -# Session - -Session middleware for [Fiber](https://github.com/gofiber/fiber). - -_NOTE: This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases._ - -## Table of Contents - -- [Session](#session) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Configuration](#default-configuration) - - [Custom Storage/Database](#custom-storagedatabase) - - [Config](#config) - - [Default Config](#default-config) - -## Signatures - -```go -func New(config ...Config) *Store -func (s *Store) RegisterType(i any) -func (s *Store) Get(c fiber.Ctx) (*Session, error) -func (s *Store) Reset() error - -func (s *Session) Get(key string) any -func (s *Session) Set(key string, val any) -func (s *Session) Delete(key string) -func (s *Session) Destroy() error -func (s *Session) Regenerate() error -func (s *Session) Save() error -func (s *Session) Fresh() bool -func (s *Session) ID() string -func (s *Session) Keys() []string -func (s *Session) SetExpiry(time.Duration) -``` - -**⚠ _Storing `any` values are limited to built-ins Go types_** - -### 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/session" -) -``` - -Then create a Fiber app with `app := fiber.New()`. - -### Default Configuration - -```go -// This stores all of your app's sessions -// Default middleware config -store := session.New() - -// This panic will be catch by the middleware -app.Get("/", func(c fiber.Ctx) error { - // Get session from storage - sess, err := store.Get(c) - if err != nil { - panic(err) - } - - // Get value - name := sess.Get("name") - - // Set key/value - sess.Set("name", "john") - - // Get all Keys - keys := sess.Keys() - - // Delete key - sess.Delete("name") - - // Destroy session - if err := sess.Destroy(); err != nil { - panic(err) - } - - // Sets a specific expiration for this session - sess.SetExpiry(time.Second * 2) - - // Save session - if err := sess.Save(); err != nil { - panic(err) - } - - return c.SendString(fmt.Sprintf("Welcome %v", name)) -}) -``` - -### Custom Storage/Database - -You can use any storage from our [storage](https://github.com/gofiber/storage/) package. - -```go -storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 -store := session.New(session.Config{ - Storage: storage, -}) -``` - -To use the the store, see the above example. - -## Config - -```go -// Config defines the config for middleware. -type Config struct { - // Allowed session duration - // Optional. Default value 24 * time.Hour - Expiration time.Duration - - // Storage interface to store the session data - // Optional. Default value memory.New() - Storage fiber.Storage - - // KeyLookup is a string in the form of ":" that is used - // to extract session id from the request. - // Possible values: "header:", "query:" or "cookie:" - // Optional. Default value "cookie:session_id". - KeyLookup string - - // Domain of the cookie. - // Optional. Default value "". - CookieDomain string - - // Path of the cookie. - // Optional. Default value "". - CookiePath string - - // Indicates if cookie is secure. - // Optional. Default value false. - CookieSecure bool - - // Indicates if cookie is HTTP only. - // Optional. Default value false. - CookieHTTPOnly bool - - // Sets the cookie SameSite attribute. - // Optional. Default value "Lax". - CookieSameSite string - - // KeyGenerator generates the session key. - // Optional. Default value utils.UUID - KeyGenerator func() string - - // Deprecated, please use KeyLookup - CookieName string - - // Source defines where to obtain the session id - source Source - - // The session name - sessionName string -} -``` - -## Default Config - -```go -var ConfigDefault = Config{ - Expiration: 24 * time.Hour, - KeyLookup: "cookie:session_id", - KeyGenerator: utils.UUID, -} -``` diff --git a/middleware/session/config.go b/middleware/session/config.go index b304001ddc..b98eeb2553 100644 --- a/middleware/session/config.go +++ b/middleware/session/config.go @@ -24,19 +24,19 @@ type Config struct { // Optional. Default value "cookie:session_id". KeyLookup string - // Domain of the CSRF cookie. + // Domain of the cookie. // Optional. Default value "". CookieDomain string - // Path of the CSRF cookie. + // Path of the cookie. // Optional. Default value "". CookiePath string - // Indicates if CSRF cookie is secure. + // Indicates if cookie is secure. // Optional. Default value false. CookieSecure bool - // Indicates if CSRF cookie is HTTP only. + // Indicates if cookie is HTTP only. // Optional. Default value false. CookieHTTPOnly bool @@ -44,6 +44,11 @@ type Config struct { // Optional. Default value "Lax". CookieSameSite string + // Decides whether cookie should last for only the browser sesison. + // Ignores Expiration if set to true + // Optional. Default value false. + CookieSessionOnly bool + // KeyGenerator generates the session key. // Optional. Default value utils.UUIDv4 KeyGenerator func() string @@ -94,7 +99,8 @@ func configDefault(config ...Config) Config { } selectors := strings.Split(cfg.KeyLookup, ":") - if len(selectors) != 2 { + const numSelectors = 2 + if len(selectors) != numSelectors { panic("[session] KeyLookup must in the form of :") } switch Source(selectors[0]) { diff --git a/middleware/session/data.go b/middleware/session/data.go index baf502100d..d80c767ea1 100644 --- a/middleware/session/data.go +++ b/middleware/session/data.go @@ -4,6 +4,8 @@ import ( "sync" ) +// go:generate msgp +// msgp -file="data.go" -o="data_msgp.go" -tests=false -unexported type data struct { sync.RWMutex Data map[string]any @@ -18,7 +20,7 @@ var dataPool = sync.Pool{ } func acquireData() *data { - return dataPool.Get().(*data) + return dataPool.Get().(*data) //nolint:forcetypeassert // We store nothing else in the pool } func (d *data) Reset() { diff --git a/middleware/session/session.go b/middleware/session/session.go index ce145e5637..ab7ed74caf 100644 --- a/middleware/session/session.go +++ b/middleware/session/session.go @@ -3,6 +3,7 @@ package session import ( "bytes" "encoding/gob" + "fmt" "sync" "time" @@ -28,7 +29,7 @@ var sessionPool = sync.Pool{ } func acquireSession() *Session { - s := sessionPool.Get().(*Session) + s := sessionPool.Get().(*Session) //nolint:forcetypeassert,errcheck // We store nothing else in the pool if s.data == nil { s.data = acquireData() } @@ -153,7 +154,7 @@ func (s *Session) Save() error { encCache := gob.NewEncoder(s.byteBuffer) err := encCache.Encode(&s.data.Data) if err != nil { - return err + return fmt.Errorf("failed to encode data: %w", err) } // copy the data in buffer @@ -195,8 +196,12 @@ func (s *Session) setSession() { fcookie.SetValue(s.id) fcookie.SetPath(s.config.CookiePath) fcookie.SetDomain(s.config.CookieDomain) - fcookie.SetMaxAge(int(s.exp.Seconds())) - fcookie.SetExpire(time.Now().Add(s.exp)) + // Cookies are also session cookies if they do not specify the Expires or Max-Age attribute. + // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + if !s.config.CookieSessionOnly { + fcookie.SetMaxAge(int(s.exp.Seconds())) + fcookie.SetExpire(time.Now().Add(s.exp)) + } fcookie.SetSecure(s.config.CookieSecure) fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go index 891abb7f3c..7a125415d5 100644 --- a/middleware/session/session_test.go +++ b/middleware/session/session_test.go @@ -91,6 +91,8 @@ func Test_Session(t *testing.T) { } // go test -run Test_Session_Types +// +//nolint:forcetypeassert // TODO: Do not force-type assert func Test_Session_Types(t *testing.T) { t.Parallel() @@ -123,25 +125,27 @@ func Test_Session_Types(t *testing.T) { Name: "John", } // set value - var vbool = true - var vstring = "str" - var vint = 13 - var vint8 int8 = 13 - var vint16 int16 = 13 - var vint32 int32 = 13 - var vint64 int64 = 13 - var vuint uint = 13 - var vuint8 uint8 = 13 - var vuint16 uint16 = 13 - var vuint32 uint32 = 13 - var vuint64 uint64 = 13 - var vuintptr uintptr = 13 - var vbyte byte = 'k' - var vrune rune = 'k' - var vfloat32 float32 = 13 - var vfloat64 float64 = 13 - var vcomplex64 complex64 = 13 - var vcomplex128 complex128 = 13 + var ( + vbool = true + vstring = "str" + vint = 13 + vint8 int8 = 13 + vint16 int16 = 13 + vint32 int32 = 13 + vint64 int64 = 13 + vuint uint = 13 + vuint8 uint8 = 13 + vuint16 uint16 = 13 + vuint32 uint32 = 13 + vuint64 uint64 = 13 + vuintptr uintptr = 13 + vbyte byte = 'k' + vrune = 'k' + vfloat32 float32 = 13 + vfloat64 float64 = 13 + vcomplex64 complex64 = 13 + vcomplex128 complex128 = 13 + ) sess.Set("vuser", vuser) sess.Set("vbool", vbool) sess.Set("vstring", vstring) @@ -207,7 +211,8 @@ func Test_Session_Store_Reset(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) // make sure its new require.True(t, sess.Fresh()) // set value & save @@ -219,7 +224,8 @@ func Test_Session_Store_Reset(t *testing.T) { require.NoError(t, store.Reset()) // make sure the session is recreated - sess, _ = store.Get(ctx) + sess, err = store.Get(ctx) + require.NoError(t, err) require.True(t, sess.Fresh()) require.Nil(t, sess.Get("hello")) } @@ -237,12 +243,13 @@ func Test_Session_Save(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) // set value sess.Set("name", "john") // save session - err := sess.Save() + err = sess.Save() require.NoError(t, err) }) @@ -257,12 +264,13 @@ func Test_Session_Save(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) // set value sess.Set("name", "john") // save session - err := sess.Save() + err = sess.Save() require.NoError(t, err) require.Equal(t, store.getSessionID(ctx), string(ctx.Response().Header.Peek(store.sessionName))) require.Equal(t, store.getSessionID(ctx), string(ctx.Request().Header.Peek(store.sessionName))) @@ -273,6 +281,7 @@ func Test_Session_Save_Expiration(t *testing.T) { t.Parallel() t.Run("save to cookie", func(t *testing.T) { + t.Parallel() // session store store := New() // fiber instance @@ -281,7 +290,9 @@ func Test_Session_Save_Expiration(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) + // set value sess.Set("name", "john") @@ -289,18 +300,20 @@ func Test_Session_Save_Expiration(t *testing.T) { sess.SetExpiry(time.Second * 5) // save session - err := sess.Save() + err = sess.Save() require.NoError(t, err) // here you need to get the old session yet - sess, _ = store.Get(ctx) + sess, err = store.Get(ctx) + require.NoError(t, err) require.Equal(t, "john", sess.Get("name")) // just to make sure the session has been expired time.Sleep(time.Second * 5) // here you should get a new session - sess, _ = store.Get(ctx) + sess, err = store.Get(ctx) + require.NoError(t, err) require.Nil(t, sess.Get("name")) }) } @@ -310,6 +323,7 @@ func Test_Session_Reset(t *testing.T) { t.Parallel() t.Run("reset from cookie", func(t *testing.T) { + t.Parallel() // session store store := New() // fiber instance @@ -318,7 +332,8 @@ func Test_Session_Reset(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) sess.Set("name", "fenny") require.NoError(t, sess.Destroy()) @@ -327,6 +342,7 @@ func Test_Session_Reset(t *testing.T) { }) t.Run("reset from header", func(t *testing.T) { + t.Parallel() // session store store := New(Config{ KeyLookup: "header:session_id", @@ -337,14 +353,16 @@ func Test_Session_Reset(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) // set value & save sess.Set("name", "fenny") require.NoError(t, sess.Save()) - sess, _ = store.Get(ctx) + sess, err = store.Get(ctx) + require.NoError(t, err) - err := sess.Destroy() + err = sess.Destroy() require.NoError(t, err) require.Equal(t, "", string(ctx.Response().Header.Peek(store.sessionName))) require.Equal(t, "", string(ctx.Request().Header.Peek(store.sessionName))) @@ -374,7 +392,8 @@ func Test_Session_Cookie(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) require.NoError(t, sess.Save()) // cookie should be set on Save ( even if empty data ) @@ -391,12 +410,14 @@ func Test_Session_Cookie_In_Response(t *testing.T) { ctx := app.NewCtx(&fasthttp.RequestCtx{}) // get session - sess, _ := store.Get(ctx) + sess, err := store.Get(ctx) + require.NoError(t, err) sess.Set("id", "1") require.True(t, sess.Fresh()) require.NoError(t, sess.Save()) - sess, _ = store.Get(ctx) + sess, err = store.Get(ctx) + require.NoError(t, err) sess.Set("name", "john") require.True(t, sess.Fresh()) @@ -434,6 +455,7 @@ func Test_Session_Deletes_Single_Key(t *testing.T) { // go test -run Test_Session_Regenerate // Regression: https://github.com/gofiber/fiber/issues/1395 func Test_Session_Regenerate(t *testing.T) { + t.Parallel() // fiber instance app := fiber.New() t.Run("set fresh to be true when regenerating a session", func(t *testing.T) { @@ -484,7 +506,7 @@ func Benchmark_Session(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - sess, _ := store.Get(c) + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") err = sess.Save() } @@ -499,7 +521,7 @@ func Benchmark_Session(b *testing.B) { b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - sess, _ := store.Get(c) + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark sess.Set("john", "doe") err = sess.Save() } diff --git a/middleware/session/store.go b/middleware/session/store.go index 77537abe92..e8f6990966 100644 --- a/middleware/session/store.go +++ b/middleware/session/store.go @@ -2,6 +2,7 @@ package session import ( "encoding/gob" + "fmt" "sync" "github.com/gofiber/fiber/v3" @@ -31,7 +32,7 @@ func New(config ...Config) *Store { // RegisterType will allow you to encode/decode custom types // into any Storage provider -func (s *Store) RegisterType(i any) { +func (*Store) RegisterType(i any) { gob.Register(i) } @@ -70,11 +71,11 @@ func (s *Store) Get(c fiber.Ctx) (*Session, error) { if raw != nil && err == nil { mux.Lock() defer mux.Unlock() - _, _ = sess.byteBuffer.Write(raw) + _, _ = sess.byteBuffer.Write(raw) //nolint:errcheck // This will never fail encCache := gob.NewDecoder(sess.byteBuffer) err := encCache.Decode(&sess.data.Data) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decode session data: %w", err) } } else if err != nil { return nil, err diff --git a/middleware/session/store_test.go b/middleware/session/store_test.go index b579e59379..bb93ababc5 100644 --- a/middleware/session/store_test.go +++ b/middleware/session/store_test.go @@ -11,12 +11,14 @@ import ( // go test -run TestStore_getSessionID func TestStore_getSessionID(t *testing.T) { + t.Parallel() expectedID := "test-session-id" // fiber instance app := fiber.New() t.Run("from cookie", func(t *testing.T) { + t.Parallel() // session store store := New() // fiber context @@ -29,6 +31,7 @@ func TestStore_getSessionID(t *testing.T) { }) t.Run("from header", func(t *testing.T) { + t.Parallel() // session store store := New(Config{ KeyLookup: "header:session_id", @@ -43,6 +46,7 @@ func TestStore_getSessionID(t *testing.T) { }) t.Run("from url query", func(t *testing.T) { + t.Parallel() // session store store := New(Config{ KeyLookup: "query:session_id", @@ -60,10 +64,12 @@ func TestStore_getSessionID(t *testing.T) { // go test -run TestStore_Get // Regression: https://github.com/gofiber/fiber/issues/1408 func TestStore_Get(t *testing.T) { + t.Parallel() unexpectedID := "test-session-id" // fiber instance app := fiber.New() t.Run("session should persisted even session is invalid", func(t *testing.T) { + t.Parallel() // session store store := New() // fiber context diff --git a/middleware/skip/README.md b/middleware/skip/README.md deleted file mode 100644 index 7806211459..0000000000 --- a/middleware/skip/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Skip -Skip middleware for [Fiber](https://github.com/gofiber/fiber) that skips a wrapped handler is a predicate is true. - -### Table of Contents -- [Signatures](#signatures) -- [Examples](#examples) - - -### Signatures -```go -func New(handler fiber.Handler, exclude func(c fiber.Ctx) bool) 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/skip" -) -``` - -After you initiate your Fiber app, you can use the following possibilities: -```go -app.Use(skip.New(handler, func(c fiber.Ctx) bool { return ctx.Method() == fiber.MethodOptions })) -``` diff --git a/middleware/skip/skip.go b/middleware/skip/skip.go index c41504e3e3..debc10c17b 100644 --- a/middleware/skip/skip.go +++ b/middleware/skip/skip.go @@ -1,6 +1,8 @@ package skip -import "github.com/gofiber/fiber/v3" +import ( + "github.com/gofiber/fiber/v3" +) // New creates a middleware handler which skips the wrapped handler // if the exclude predicate returns true. diff --git a/middleware/skip/skip_test.go b/middleware/skip/skip_test.go index 4373e18e37..cfa95f8cbc 100644 --- a/middleware/skip/skip_test.go +++ b/middleware/skip/skip_test.go @@ -11,36 +11,39 @@ import ( // go test -run Test_Skip func Test_Skip(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(skip.New(errTeapotHandler, func(fiber.Ctx) bool { return true })) app.Get("/", helloWorldHandler) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) } // go test -run Test_SkipFalse func Test_SkipFalse(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(skip.New(errTeapotHandler, func(fiber.Ctx) bool { return false })) app.Get("/", helloWorldHandler) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) } // go test -run Test_SkipNilFunc func Test_SkipNilFunc(t *testing.T) { + t.Parallel() app := fiber.New() app.Use(skip.New(errTeapotHandler, nil)) app.Get("/", helloWorldHandler) - resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, fiber.StatusTeapot, resp.StatusCode) } diff --git a/middleware/timeout/timeout.go b/middleware/timeout/timeout.go index 5a9711ce22..7baa0e7056 100644 --- a/middleware/timeout/timeout.go +++ b/middleware/timeout/timeout.go @@ -3,13 +3,57 @@ package timeout import ( "context" "errors" + "sync" "time" + "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/fiber/v3" ) -// New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. -func New(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { +var once sync.Once + +// New wraps a handler and aborts the process of the handler if the timeout is reached. +// +// Deprecated: This implementation contains data race issues. Use NewWithContext instead. +// Find documentation and sample usage on https://docs.gofiber.io/api/middleware/timeout +func New(handler fiber.Handler, timeout time.Duration) fiber.Handler { + once.Do(func() { + log.Warn("[TIMEOUT] timeout contains data race issues, not ready for production!") + }) + + if timeout <= 0 { + return handler + } + + // logic is from fasthttp.TimeoutWithCodeHandler https://github.com/valyala/fasthttp/blob/master/server.go#L418 + return func(c fiber.Ctx) error { + ch := make(chan struct{}, 1) + + go func() { + defer func() { + if err := recover(); err != nil { + log.Errorf("[TIMEOUT] recover error %v", err) + } + }() + if err := handler(c); err != nil { + log.Errorf("[TIMEOUT] handler error %v", err) + } + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(timeout): + return fiber.ErrRequestTimeout + } + + return nil + } +} + +// NewWithContext implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. +func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { return func(ctx fiber.Ctx) error { timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) defer cancel() diff --git a/middleware/timeout/timeout_test.go b/middleware/timeout/timeout_test.go index 4d5d4aa1aa..87292c66f6 100644 --- a/middleware/timeout/timeout_test.go +++ b/middleware/timeout/timeout_test.go @@ -12,12 +12,14 @@ import ( "github.com/stretchr/testify/require" ) -// go test -run Test_Timeout -func Test_Timeout(t *testing.T) { +// go test -run Test_WithContextTimeout +func Test_WithContextTimeout(t *testing.T) { + t.Parallel() // fiber instance app := fiber.New() - h := New(func(c fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + h := NewWithContext(func(c fiber.Ctx) error { + sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") + require.NoError(t, err) if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) } @@ -25,13 +27,13 @@ func Test_Timeout(t *testing.T) { }, 100*time.Millisecond) app.Get("/test/:sleepTime", h) testTimeout := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - require.Equal(t, nil, err, "app.Test(req)") + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") } testSucces := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - require.Equal(t, nil, err, "app.Test(req)") + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") } testTimeout("300") @@ -42,12 +44,14 @@ func Test_Timeout(t *testing.T) { var ErrFooTimeOut = errors.New("foo context canceled") -// go test -run Test_TimeoutWithCustomError -func Test_TimeoutWithCustomError(t *testing.T) { +// go test -run Test_WithContextTimeoutWithCustomError +func Test_WithContextTimeoutWithCustomError(t *testing.T) { + t.Parallel() // fiber instance app := fiber.New() - h := New(func(c fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + h := NewWithContext(func(c fiber.Ctx) error { + sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") + require.NoError(t, err) if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { return fmt.Errorf("%w: execution error", err) } @@ -55,13 +59,13 @@ func Test_TimeoutWithCustomError(t *testing.T) { }, 100*time.Millisecond, ErrFooTimeOut) app.Get("/test/:sleepTime", h) testTimeout := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - require.Equal(t, nil, err, "app.Test(req)") + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") } testSucces := func(timeoutStr string) { - resp, err := app.Test(httptest.NewRequest("GET", "/test/"+timeoutStr, nil)) - require.Equal(t, nil, err, "app.Test(req)") + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + require.NoError(t, err, "app.Test(req)") require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code") } testTimeout("300") diff --git a/mount.go b/mount.go index 83b9229aa1..9eab8aca48 100644 --- a/mount.go +++ b/mount.go @@ -19,6 +19,8 @@ type mountFields struct { appListKeys []string // check added routes of sub-apps subAppsRoutesAdded sync.Once + // check mounted sub-apps + subAppsProcessed sync.Once // Prefix of app if it was mounted mountPath string } @@ -36,22 +38,26 @@ func newMountFields(app *App) *mountFields { // compose them as a single service using Mount. The fiber's error handler and // any of the fiber's sub apps are added to the application's error handlers // to be invoked on errors that happen within the prefix route. -func (app *App) mount(prefix string, fiber *App) Router { +func (app *App) mount(prefix string, subApp *App) Router { prefix = strings.TrimRight(prefix, "/") if prefix == "" { prefix = "/" } // Support for configs of mounted-apps and sub-mounted-apps - for mountedPrefixes, subApp := range fiber.mountFields.appList { + for mountedPrefixes, subApp := range subApp.mountFields.appList { path := getGroupPath(prefix, mountedPrefixes) subApp.mountFields.mountPath = path app.mountFields.appList[path] = subApp } + // register mounted group + mountGroup := &Group{Prefix: prefix, app: subApp} + app.register([]string{methodUse}, prefix, mountGroup, nil) + // Execute onMount hooks - if err := fiber.hooks.executeOnMountHooks(app); err != nil { + if err := subApp.hooks.executeOnMountHooks(app); err != nil { panic(err) } @@ -60,10 +66,8 @@ func (app *App) mount(prefix string, fiber *App) Router { // Mount attaches another app instance as a sub-router along a routing path. // It's very useful to split up a large API as many independent routers and -// compose them as a single service using Mount. The fiber's error handler and -// any of the fiber's sub apps are added to the application's error handlers -// to be invoked on errors that happen within the prefix route. -func (grp *Group) mount(prefix string, fiber *App) Router { +// compose them as a single service using Mount. +func (grp *Group) mount(prefix string, subApp *App) Router { groupPath := getGroupPath(grp.Prefix, prefix) groupPath = strings.TrimRight(groupPath, "/") if groupPath == "" { @@ -71,15 +75,19 @@ func (grp *Group) mount(prefix string, fiber *App) Router { } // Support for configs of mounted-apps and sub-mounted-apps - for mountedPrefixes, subApp := range fiber.mountFields.appList { + for mountedPrefixes, subApp := range subApp.mountFields.appList { path := getGroupPath(groupPath, mountedPrefixes) subApp.mountFields.mountPath = path grp.app.mountFields.appList[path] = subApp } + // register mounted group + mountGroup := &Group{Prefix: groupPath, app: subApp} + grp.app.register([]string{methodUse}, groupPath, mountGroup, nil) + // Execute onMount hooks - if err := fiber.hooks.executeOnMountHooks(grp.app); err != nil { + if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil { panic(err) } @@ -91,6 +99,26 @@ func (app *App) MountPath() string { return app.mountFields.mountPath } +// hasMountedApps Checks if there are any mounted apps in the current application. +func (app *App) hasMountedApps() bool { + return len(app.mountFields.appList) > 1 +} + +// mountStartupProcess Handles the startup process of mounted apps by appending sub-app routes, generating app list keys, and processing sub-app routes. +func (app *App) mountStartupProcess() { + if app.hasMountedApps() { + // add routes of sub-apps + app.mountFields.subAppsProcessed.Do(func() { + app.appendSubAppLists(app.mountFields.appList) + app.generateAppListKeys() + }) + // adds the routes of the sub-apps to the current application. + app.mountFields.subAppsRoutesAdded.Do(func() { + app.processSubAppsRoutes() + }) + } +} + // generateAppListKeys generates app list keys for Render, should work after appendSubAppLists func (app *App) generateAppListKeys() { for key := range app.mountFields.appList { @@ -104,14 +132,20 @@ func (app *App) generateAppListKeys() { // appendSubAppLists supports nested for sub apps func (app *App) appendSubAppLists(appList map[string]*App, parent ...string) { + // Optimize: Cache parent prefix + parentPrefix := "" + if len(parent) > 0 { + parentPrefix = parent[0] + } + for prefix, subApp := range appList { // skip real app if prefix == "" { continue } - if len(parent) > 0 { - prefix = getGroupPath(parent[0], prefix) + if parentPrefix != "" { + prefix = getGroupPath(parentPrefix, prefix) } if _, ok := app.mountFields.appList[prefix]; !ok { @@ -122,31 +156,75 @@ func (app *App) appendSubAppLists(appList map[string]*App, parent ...string) { if len(subApp.mountFields.appList) > 1 { app.appendSubAppLists(subApp.mountFields.appList, prefix) } - } } -// addSubAppsRoutes adds routes of sub apps nestedly when to start the server -func (app *App) addSubAppsRoutes(appList map[string]*App, parent ...string) { - for prefix, subApp := range appList { +// processSubAppsRoutes adds routes of sub-apps recursively when the server is started +func (app *App) processSubAppsRoutes() { + for prefix, subApp := range app.mountFields.appList { // skip real app if prefix == "" { continue } - - if len(parent) > 0 { - prefix = getGroupPath(parent[0], prefix) + // process the inner routes + if subApp.hasMountedApps() { + subApp.mountFields.subAppsRoutesAdded.Do(func() { + subApp.processSubAppsRoutes() + }) } + } + var handlersCount uint32 + var routePos uint32 + // Iterate over the stack of the parent app + for m := range app.stack { + // Iterate over each route in the stack + stackLen := len(app.stack[m]) + for i := 0; i < stackLen; i++ { + route := app.stack[m][i] + // Check if the route has a mounted app + if !route.mount { + routePos++ + // If not, update the route's position and continue + route.pos = routePos + if !route.use || (route.use && m == 0) { + handlersCount += uint32(len(route.Handlers)) + } + continue + } - // add routes - stack := subApp.stack - for m := range stack { - for r := range stack[m] { - route := app.copyRoute(stack[m][r]) - app.addRoute(route.Method, app.addPrefixToRoute(prefix, route), true) + // Create a slice to hold the sub-app's routes + subRoutes := make([]*Route, len(route.group.app.stack[m])) + + // Iterate over the sub-app's routes + for j, subAppRoute := range route.group.app.stack[m] { + // Clone the sub-app's route + subAppRouteClone := app.copyRoute(subAppRoute) + + // Add the parent route's path as a prefix to the sub-app's route + app.addPrefixToRoute(route.path, subAppRouteClone) + + // Add the cloned sub-app's route to the slice of sub-app routes + subRoutes[j] = subAppRouteClone } - } - atomic.AddUint32(&app.handlersCount, subApp.handlersCount) + // Insert the sub-app's routes into the parent app's stack + newStack := make([]*Route, len(app.stack[m])+len(subRoutes)-1) + copy(newStack[:i], app.stack[m][:i]) + copy(newStack[i:i+len(subRoutes)], subRoutes) + copy(newStack[i+len(subRoutes):], app.stack[m][i+1:]) + app.stack[m] = newStack + + // Decrease the parent app's route count to account for the mounted app's original route + atomic.AddUint32(&app.routesCount, ^uint32(0)) + i-- + // Increase the parent app's route count to account for the sub-app's routes + atomic.AddUint32(&app.routesCount, uint32(len(subRoutes))) + + // Mark the parent app's routes as refreshed + app.routesRefreshed = true + // update stackLen after appending subRoutes to app.stack[m] + stackLen = len(app.stack[m]) + } } + atomic.StoreUint32(&app.handlersCount, handlersCount) } diff --git a/mount_test.go b/mount_test.go index 7daebc3dd1..0ca9768e67 100644 --- a/mount_test.go +++ b/mount_test.go @@ -2,11 +2,13 @@ // 🤖 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package fiber import ( "errors" "io" + "net/http" "net/http/httptest" "testing" @@ -15,6 +17,7 @@ import ( // go test -run Test_App_Mount func Test_App_Mount(t *testing.T) { + t.Parallel() micro := New() micro.Get("/doe", func(c Ctx) error { return c.SendStatus(StatusOK) @@ -22,13 +25,14 @@ func Test_App_Mount(t *testing.T) { app := New() app.Use("/john", micro) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, uint32(1), app.handlersCount) } func Test_App_Mount_RootPath_Nested(t *testing.T) { + t.Parallel() app := New() dynamic := New() apiserver := New() @@ -41,7 +45,7 @@ func Test_App_Mount_RootPath_Nested(t *testing.T) { dynamic.Use("/api", apiserver) app.Use("/", dynamic) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/v1/home", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/v1/home", http.NoBody)) require.NoError(t, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, uint32(1), app.handlersCount) @@ -49,6 +53,7 @@ func Test_App_Mount_RootPath_Nested(t *testing.T) { // go test -run Test_App_Mount_Nested func Test_App_Mount_Nested(t *testing.T) { + t.Parallel() app := New() one := New() two := New() @@ -70,23 +75,145 @@ func Test_App_Mount_Nested(t *testing.T) { return c.SendStatus(StatusOK) }) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/one/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/one/doe", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") - resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/nested", nil)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/nested", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") - resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/three/test", nil)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/three/test", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, uint32(3), app.handlersCount) + require.Equal(t, uint32(3), app.routesCount) +} + +// go test -run Test_App_Mount_Express_Behavior +func Test_App_Mount_Express_Behavior(t *testing.T) { + t.Parallel() + createTestHandler := func(body string) func(c Ctx) error { + return func(c Ctx) error { + return c.SendString(body) + } + } + testEndpoint := func(app *App, route, expectedBody string, expectedStatusCode int) { + resp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody)) + require.Equal(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, expectedStatusCode, resp.StatusCode, "Status code") + require.Equal(t, expectedBody, string(body), "Unexpected response body") + } + + app := New() + subApp := New() + // app setup + { + subApp.Get("/hello", createTestHandler("subapp hello!")) + subApp.Get("/world", createTestHandler("subapp world!")) // <- wins + + app.Get("/hello", createTestHandler("app hello!")) // <- wins + app.Use("/", subApp) // <- subApp registration + app.Get("/world", createTestHandler("app world!")) + + app.Get("/bar", createTestHandler("app bar!")) + subApp.Get("/bar", createTestHandler("subapp bar!")) // <- wins + + subApp.Get("/foo", createTestHandler("subapp foo!")) // <- wins + app.Get("/foo", createTestHandler("app foo!")) + + // 404 Handler + app.Use(func(c Ctx) error { + return c.SendStatus(StatusNotFound) + }) + } + // expectation check + testEndpoint(app, "/world", "subapp world!", StatusOK) + testEndpoint(app, "/hello", "app hello!", StatusOK) + testEndpoint(app, "/bar", "subapp bar!", StatusOK) + testEndpoint(app, "/foo", "subapp foo!", StatusOK) + testEndpoint(app, "/unknown", ErrNotFound.Message, StatusNotFound) + + require.Equal(t, uint32(9), app.handlersCount) + require.Equal(t, uint32(17), app.routesCount) +} + +// go test -run Test_App_Mount_RoutePositions +func Test_App_Mount_RoutePositions(t *testing.T) { + t.Parallel() + testEndpoint := func(app *App, route, expectedBody string) { + resp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody)) + require.Equal(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, StatusOK, resp.StatusCode, "Status code") + require.Equal(t, expectedBody, string(body), "Unexpected response body") + } + + app := New() + subApp1 := New() + subApp2 := New() + // app setup + { + app.Use(func(c Ctx) error { + // set initial value + c.Locals("world", "world") + return c.Next() + }) + app.Use("/subApp1", subApp1) + app.Use(func(c Ctx) error { + return c.Next() + }) + app.Get("/bar", func(c Ctx) error { + return c.SendString("ok") + }) + app.Use(func(c Ctx) error { + // is overwritten in case the positioning is not correct + c.Locals("world", "hello") + return c.Next() + }) + methods := subApp2.Group("/subApp2") + methods.Get("/world", func(c Ctx) error { + v, ok := c.Locals("world").(string) + if !ok { + panic("unexpected data type") + } + return c.SendString(v) + }) + app.Use("", subApp2) + } + + testEndpoint(app, "/subApp2/world", "hello") + + routeStackGET := app.Stack()[0] + require.Equal(t, true, routeStackGET[0].use) + require.Equal(t, "/", routeStackGET[0].path) + + require.Equal(t, true, routeStackGET[1].use) + require.Equal(t, "/", routeStackGET[1].path) + require.Equal(t, true, routeStackGET[0].pos < routeStackGET[1].pos, "wrong position of route 0") + + require.Equal(t, false, routeStackGET[2].use) + require.Equal(t, "/bar", routeStackGET[2].path) + require.Equal(t, true, routeStackGET[1].pos < routeStackGET[2].pos, "wrong position of route 1") + + require.Equal(t, true, routeStackGET[3].use) + require.Equal(t, "/", routeStackGET[3].path) + require.Equal(t, true, routeStackGET[2].pos < routeStackGET[3].pos, "wrong position of route 2") + + require.Equal(t, false, routeStackGET[4].use) + require.Equal(t, "/subapp2/world", routeStackGET[4].path) + require.Equal(t, true, routeStackGET[3].pos < routeStackGET[4].pos, "wrong position of route 3") + + require.Equal(t, 5, len(routeStackGET)) } // go test -run Test_App_MountPath func Test_App_MountPath(t *testing.T) { + t.Parallel() app := New() one := New() two := New() @@ -103,6 +230,7 @@ func Test_App_MountPath(t *testing.T) { } func Test_App_ErrorHandler_GroupMount(t *testing.T) { + t.Parallel() micro := New(Config{ ErrorHandler: func(c Ctx, err error) error { require.Equal(t, "0: GET error", err.Error()) @@ -117,11 +245,12 @@ func Test_App_ErrorHandler_GroupMount(t *testing.T) { v1 := app.Group("/v1") v1.Use("/john", micro) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) testErrorResponse(t, err, resp, "1: custom error") } func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) { + t.Parallel() micro := New(Config{ ErrorHandler: func(c Ctx, err error) error { require.Equal(t, "0: GET error", err.Error()) @@ -136,12 +265,13 @@ func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) { v1 := app.Group("/v1") v1.Use("/", micro) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) testErrorResponse(t, err, resp, "1: custom error") } // go test -run Test_App_Group_Mount func Test_App_Group_Mount(t *testing.T) { + t.Parallel() micro := New() micro.Get("/doe", func(c Ctx) error { return c.SendStatus(StatusOK) @@ -151,13 +281,14 @@ func Test_App_Group_Mount(t *testing.T) { v1 := app.Group("/v1") v1.Use("/john", micro) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") require.Equal(t, uint32(1), app.handlersCount) } func Test_App_UseParentErrorHandler(t *testing.T) { + t.Parallel() app := New(Config{ ErrorHandler: func(ctx Ctx, err error) error { return ctx.Status(500).SendString("hi, i'm a custom error") @@ -171,11 +302,12 @@ func Test_App_UseParentErrorHandler(t *testing.T) { app.Use("/api", fiber) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) testErrorResponse(t, err, resp, "hi, i'm a custom error") } func Test_App_UseMountedErrorHandler(t *testing.T) { + t.Parallel() app := New() fiber := New(Config{ @@ -189,11 +321,12 @@ func Test_App_UseMountedErrorHandler(t *testing.T) { app.Use("/api", fiber) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) testErrorResponse(t, err, resp, "hi, i'm a custom error") } func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) { + t.Parallel() app := New() fiber := New(Config{ @@ -207,11 +340,12 @@ func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) { app.Use("/", fiber) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) testErrorResponse(t, err, resp, "hi, i'm a custom error") } func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { + t.Parallel() app := New() tsf := func(c Ctx, err error) error { @@ -248,7 +382,7 @@ func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { app.Use("/api", fiber) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", http.NoBody)) require.Equal(t, nil, err, "/api/sub req") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -256,7 +390,7 @@ func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { require.Equal(t, nil, err, "iotuil.ReadAll()") require.Equal(t, "hi, i'm a custom sub fiber error", string(b), "Response body") - resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", nil)) + resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", http.NoBody)) require.Equal(t, nil, err, "/api/sub/third req") require.Equal(t, 200, resp.StatusCode, "Status code") @@ -270,7 +404,8 @@ func Test_Ctx_Render_Mount(t *testing.T) { t.Parallel() engine := &testTemplateEngine{} - engine.Load() + err := engine.Load() + require.NoError(t, err) sub := New(Config{ Views: engine, @@ -285,12 +420,12 @@ func Test_Ctx_Render_Mount(t *testing.T) { app := New() app.Use("/hello", sub) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", http.NoBody)) require.Equal(t, StatusOK, resp.StatusCode, "Status code") require.Equal(t, nil, err, "app.Test(req)") body, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "

Hello a!

", string(body)) } @@ -300,15 +435,15 @@ func Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) { engine := &testTemplateEngine{} err := engine.Load() - require.Equal(t, nil, err) + require.NoError(t, err) engine2 := &testTemplateEngine{path: "testdata2"} err = engine2.Load() - require.Equal(t, nil, err) + require.NoError(t, err) engine3 := &testTemplateEngine{path: "testdata3"} err = engine3.Load() - require.Equal(t, nil, err) + require.NoError(t, err) sub := New(Config{ Views: engine3, @@ -341,37 +476,37 @@ func Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) { sub.Use("/bruh", sub2) app.Use("/hello", sub) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/world/a", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/world/a", http.NoBody)) require.Equal(t, StatusOK, resp.StatusCode, "Status code") require.Equal(t, nil, err, "app.Test(req)") body, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "

Hello a!

", string(body)) - resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody)) require.Equal(t, StatusOK, resp.StatusCode, "Status code") require.Equal(t, nil, err, "app.Test(req)") body, err = io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "

Hello, World!

", string(body)) - resp, err = app.Test(httptest.NewRequest(MethodGet, "/hello/bruh/moment", nil)) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/hello/bruh/moment", http.NoBody)) require.Equal(t, StatusOK, resp.StatusCode, "Status code") require.Equal(t, nil, err, "app.Test(req)") body, err = io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "

I'm Bruh

", string(body)) - } func Test_Ctx_Render_MountGroup(t *testing.T) { t.Parallel() engine := &testTemplateEngine{} - engine.Load() + err := engine.Load() + require.NoError(t, err) micro := New(Config{ Views: engine, @@ -387,11 +522,11 @@ func Test_Ctx_Render_MountGroup(t *testing.T) { v1 := app.Group("/v1") v1.Use("/john", micro) - resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", nil)) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) require.Equal(t, nil, err, "app.Test(req)") require.Equal(t, 200, resp.StatusCode, "Status code") body, err := io.ReadAll(resp.Body) - require.Equal(t, nil, err) + require.NoError(t, err) require.Equal(t, "

Hello doe!

", string(body)) } diff --git a/path.go b/path.go index 8936f90427..bfcfe68e6f 100644 --- a/path.go +++ b/path.go @@ -13,6 +13,7 @@ import ( "time" "unicode" + "github.com/gofiber/utils/v2" "github.com/google/uuid" ) @@ -45,7 +46,7 @@ type routeSegment struct { // different special routing signs const ( - wildcardParam byte = '*' // indicates a optional greedy parameter + wildcardParam byte = '*' // indicates an optional greedy parameter plusParam byte = '+' // indicates a required greedy parameter optionalParam byte = '?' // concludes a parameter by name and makes it optional paramStarterChar byte = ':' // start character for a parameter with name @@ -93,7 +94,7 @@ var ( routeDelimiter = []byte{slashDelimiter, '-', '.'} // list of greedy parameters greedyParameters = []byte{wildcardParam, plusParam} - // list of chars for the parameter recognising + // list of chars for the parameter recognizing parameterStartChars = []byte{wildcardParam, plusParam, paramStarterChar} // list of chars of delimiters and the starting parameter name char parameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...) @@ -113,6 +114,65 @@ var ( parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} ) +// RoutePatternMatch checks if a given path matches a Fiber route pattern. +func RoutePatternMatch(path, pattern string, cfg ...Config) bool { + // See logic in (*Route).match and (*App).register + var ctxParams [maxParams]string + + config := Config{} + if len(cfg) > 0 { + config = cfg[0] + } + + if path == "" { + path = "/" + } + + // Cannot have an empty pattern + if pattern == "" { + pattern = "/" + } + // Pattern always start with a '/' + if pattern[0] != '/' { + pattern = "/" + pattern + } + + patternPretty := pattern + + // Case-sensitive routing, all to lowercase + if !config.CaseSensitive { + patternPretty = utils.ToLower(patternPretty) + path = utils.ToLower(path) + } + // Strict routing, remove trailing slashes + if !config.StrictRouting && len(patternPretty) > 1 { + patternPretty = strings.TrimRight(patternPretty, "/") + } + + parser := parseRoute(patternPretty) + + if patternPretty == "/" && path == "/" { + return true + // '*' wildcard matches any path + } else if patternPretty == "/*" { + return true + } + + // Does this route have parameters + if len(parser.params) > 0 { + if match := parser.getMatch(path, path, &ctxParams, false); match { + return true + } + } + // Check for a simple match + patternPretty = RemoveEscapeChar(patternPretty) + if len(patternPretty) == len(path) && patternPretty == path { + return true + } + // No match + return false +} + // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes func parseRoute(pattern string) routeParser { @@ -169,7 +229,7 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { // check how often the compare part is in the following const parts if segs[i].IsParam { // check if parameter segments are directly after each other and if one of them is greedy - // in case the next parameter or the current parameter is not a wildcard its not greedy, we only want one character + // in case the next parameter or the current parameter is not a wildcard it's not greedy, we only want one character if segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy { segs[i].Length = 1 } @@ -209,7 +269,7 @@ func findNextParamPosition(pattern string) int { } // analyseConstantPart find the end of the constant part and create the route segment -func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { +func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { // handle the constant part processedPart := pattern if nextParamPosition != -1 { @@ -238,11 +298,12 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r parameterConstraintStart := -1 parameterConstraintEnd := -1 // handle wildcard end - if isWildCard || isPlusParam { + switch { + case isWildCard, isPlusParam: parameterEndPosition = 0 - } else if parameterEndPosition == -1 { + case parameterEndPosition == -1: parameterEndPosition = len(pattern) - 1 - } else if !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars) { + case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars): parameterEndPosition++ } @@ -271,15 +332,15 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r // Assign constraint if start != -1 && end != -1 { constraint := &Constraint{ - ID: getParamConstraintType(c[:start]), - Data: splitNonEscaped(c[start+1:end], string(parameterConstraintDataSeparatorChars)), + ID: getParamConstraintType(c[:start]), } // remove escapes from data if constraint.ID != regexConstraint { + constraint.Data = splitNonEscaped(c[start+1:end], string(parameterConstraintDataSeparatorChars)) if len(constraint.Data) == 1 { constraint.Data[0] = RemoveEscapeChar(constraint.Data[0]) - } else if len(constraint.Data) == 2 { + } else if len(constraint.Data) == 2 { // This is fine, we simply expect two parts constraint.Data[0] = RemoveEscapeChar(constraint.Data[0]) constraint.Data[1] = RemoveEscapeChar(constraint.Data[1]) } @@ -287,6 +348,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r // Precompile regex if has regex constraint if constraint.ID == regexConstraint { + constraint.Data = []string{c[start+1 : end]} constraint.RegexCompiler = regexp.MustCompile(constraint.Data[0]) } @@ -414,7 +476,7 @@ func splitNonEscaped(s, sep string) []string { } // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { +func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here var i, paramsIterator, partLen int for _, segment := range routeParser.segs { partLen = len(detectionPath) @@ -422,7 +484,7 @@ func (routeParser *routeParser) getMatch(detectionPath, path string, params *[ma if !segment.IsParam { i = segment.Length // is optional part or the const part must match with the given string - // check if the end of the segment is a optional slash + // check if the end of the segment is an optional slash if segment.HasOptionalSlash && partLen == i-1 && detectionPath == segment.Const[:i-1] { i-- } else if !(i <= partLen && detectionPath[:i] == segment.Const) { @@ -555,7 +617,7 @@ func getParamConstraintType(constraintPart string) TypeConstraint { return floatConstraint case ConstraintAlpha: return alphaConstraint - case ConstraintGuid: + case ConstraintGUID: return guidConstraint case ConstraintMinLen, ConstraintMinLenLower: return minLenConstraint @@ -578,9 +640,9 @@ func getParamConstraintType(constraintPart string) TypeConstraint { default: return noConstraint } - } +//nolint:errcheck // TODO: Properly check _all_ errors in here, log them & immediately return func (c *Constraint) CheckConstraint(param string) bool { var err error var num int @@ -603,6 +665,8 @@ func (c *Constraint) CheckConstraint(param string) bool { // check constraints switch c.ID { + case noConstraint: + // Nothing to check case intConstraint: _, err = strconv.Atoi(param) case boolConstraint: diff --git a/path_test.go b/path_test.go index b9ee575c86..cf9a2db064 100644 --- a/path_test.go +++ b/path_test.go @@ -13,6 +13,7 @@ import ( // go test -race -run Test_Path_parseRoute func Test_Path_parseRoute(t *testing.T) { + t.Parallel() var rp routeParser rp = parseRoute("/shop/product/::filter/color::color/size::size") @@ -131,479 +132,43 @@ func Test_Path_parseRoute(t *testing.T) { params: []string{"*1", "*2"}, wildCardCount: 2, }, rp) - } // go test -race -run Test_Path_matchParams func Test_Path_matchParams(t *testing.T) { t.Parallel() - type testparams struct { - url string - params []string - match bool - partialCheck bool - } var ctxParams [maxParams]string - testCase := func(r string, cases []testparams) { - parser := parseRoute(r) - for _, c := range cases { + testCaseFn := func(testCollection routeCaseCollection) { + parser := parseRoute(testCollection.pattern) + for _, c := range testCollection.testCases { match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck) - require.Equal(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + require.Equal(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) if match && len(c.params) > 0 { - require.Equal(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + require.Equal(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) } } } - testCase("/api/v1/:param/*", []testparams{ - {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:param/+", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/entity/", params: nil, match: false}, - {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:param?", []testparams{ - {url: "/api/v1", params: []string{""}, match: true}, - {url: "/api/v1/", params: []string{""}, match: true}, - {url: "/api/v1/optional", params: []string{"optional"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/xyz", params: nil, match: false}, - }) - testCase("/v1/some/resource/name\\:customVerb", []testparams{ - {url: "/v1/some/resource/name:customVerb", params: nil, match: true}, - {url: "/v1/some/resource/name:test", params: nil, match: false}, - }) - testCase("/v1/some/resource/:name\\:customVerb", []testparams{ - {url: "/v1/some/resource/test:customVerb", params: []string{"test"}, match: true}, - {url: "/v1/some/resource/test:test", params: nil, match: false}, - }) - testCase("/v1/some/resource/name\\\\:customVerb?\\?/:param/*", []testparams{ - {url: "/v1/some/resource/name:customVerb??/test/optionalWildCard/character", params: []string{"test", "optionalWildCard/character"}, match: true}, - {url: "/v1/some/resource/name:customVerb??/test", params: []string{"test", ""}, match: true}, - }) - testCase("/api/v1/*", []testparams{ - {url: "/api/v1", params: []string{""}, match: true}, - {url: "/api/v1/", params: []string{""}, match: true}, - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true}, - {url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/abc", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/entity/8728382", params: nil, match: false}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:param-:param2", []testparams{ - {url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true}, - {url: "/api/v1/entity/8728382", params: nil, match: false}, - {url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:filename.:extension", []testparams{ - {url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true}, - {url: "/api/v1/test/pdf", params: nil, match: false}, - {url: "/api/v1/test-pdf", params: nil, match: false}, - {url: "/api/v1/test_pdf", params: nil, match: false}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/const", []testparams{ - {url: "/api/v1/const", params: []string{}, match: true}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - {url: "/api/v1/something", params: nil, match: false}, - }) - testCase("/api/:param/fixedEnd", []testparams{ - {url: "/api/abc/fixedEnd", params: []string{"abc"}, match: true}, - {url: "/api/abc/def/fixedEnd", params: nil, match: false}, - }) - testCase("/shop/product/::filter/color::color/size::size", []testparams{ - {url: "/shop/product/:test/color:blue/size:xs", params: []string{"test", "blue", "xs"}, match: true}, - {url: "/shop/product/test/color:blue/size:xs", params: nil, match: false}, - }) - testCase("/::param?", []testparams{ - {url: "/:hello", params: []string{"hello"}, match: true}, - {url: "/:", params: []string{""}, match: true}, - {url: "/", params: nil, match: false}, - }) - // successive parameters, each take one character and the last parameter gets everything - testCase("/test:sign:param", []testparams{ - {url: "/test-abc", params: []string{"-", "abc"}, match: true}, - {url: "/test", params: nil, match: false}, - }) - // optional parameters are not greedy - testCase("/:param1:param2?:param3", []testparams{ - {url: "/abbbc", params: []string{"a", "b", "bbc"}, match: true}, - // {url: "/ac", params: []string{"a", "", "c"}, match: true}, // TODO: fix it - {url: "/test", params: []string{"t", "e", "st"}, match: true}, - }) - testCase("/test:optional?:mandatory", []testparams{ - // {url: "/testo", params: []string{"", "o"}, match: true}, // TODO: fix it - {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, - {url: "/test", params: nil, match: false}, - }) - testCase("/test:optional?:optional2?", []testparams{ - {url: "/testo", params: []string{"o", ""}, match: true}, - {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, - {url: "/test", params: []string{"", ""}, match: true}, - {url: "/tes", params: nil, match: false}, - }) - testCase("/foo:param?bar", []testparams{ - {url: "/foofaselbar", params: []string{"fasel"}, match: true}, - {url: "/foobar", params: []string{""}, match: true}, - {url: "/fooba", params: nil, match: false}, - {url: "/fobar", params: nil, match: false}, - }) - testCase("/foo*bar", []testparams{ - {url: "/foofaselbar", params: []string{"fasel"}, match: true}, - {url: "/foobar", params: []string{""}, match: true}, - {url: "/", params: nil, match: false}, - }) - testCase("/foo+bar", []testparams{ - {url: "/foofaselbar", params: []string{"fasel"}, match: true}, - {url: "/foobar", params: nil, match: false}, - {url: "/", params: nil, match: false}, - }) - testCase("/a*cde*g/", []testparams{ - {url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true}, - {url: "/acdeg", params: []string{"", ""}, match: true}, - {url: "/", params: nil, match: false}, - }) - testCase("/*v1*/proxy", []testparams{ - {url: "/customer/v1/cart/proxy", params: []string{"customer/", "/cart"}, match: true}, - {url: "/v1/proxy", params: []string{"", ""}, match: true}, - {url: "/v1/", params: nil, match: false}, - }) - // successive wildcard -> first wildcard is greedy - testCase("/foo***bar", []testparams{ - {url: "/foo*abar", params: []string{"*a", "", ""}, match: true}, - {url: "/foo*bar", params: []string{"*", "", ""}, match: true}, - {url: "/foobar", params: []string{"", "", ""}, match: true}, - {url: "/fooba", params: nil, match: false}, - }) - // chars in front of an parameter - testCase("/name::name", []testparams{ - {url: "/name:john", params: []string{"john"}, match: true}, - }) - testCase("/@:name", []testparams{ - {url: "/@john", params: []string{"john"}, match: true}, - }) - testCase("/-:name", []testparams{ - {url: "/-john", params: []string{"john"}, match: true}, - }) - testCase("/.:name", []testparams{ - {url: "/.john", params: []string{"john"}, match: true}, - }) - testCase("/api/v1/:param/abc/*", []testparams{ - {url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true}, - {url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true}, - {url: "/api/v1/well/abc", params: []string{"well", ""}, match: true}, - {url: "/api/v1/well/ttt", params: nil, match: false}, - }) - testCase("/api/:day/:month?/:year?", []testparams{ - {url: "/api/1", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/", params: []string{"1", "", ""}, match: true}, - {url: "/api/1//", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/-/", params: []string{"1", "-", ""}, match: true}, - {url: "/api/1-", params: []string{"1-", "", ""}, match: true}, - {url: "/api/1.", params: []string{"1.", "", ""}, match: true}, - {url: "/api/1/2", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/:day.:month?.:year?", []testparams{ - {url: "/api/1", params: nil, match: false}, - {url: "/api/1/", params: nil, match: false}, - {url: "/api/1.", params: nil, match: false}, - {url: "/api/1..", params: []string{"1", "", ""}, match: true}, - {url: "/api/1.2", params: nil, match: false}, - {url: "/api/1.2.", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/:day-:month?-:year?", []testparams{ - {url: "/api/1", params: nil, match: false}, - {url: "/api/1/", params: nil, match: false}, - {url: "/api/1-", params: nil, match: false}, - {url: "/api/1--", params: []string{"1", "", ""}, match: true}, - {url: "/api/1-/", params: nil, match: false}, - // {url: "/api/1-/-", params: nil, match: false}, // TODO: fix this part - {url: "/api/1-2", params: nil, match: false}, - {url: "/api/1-2-", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/*", []testparams{ - {url: "/api/", params: []string{""}, match: true}, - {url: "/api/joker", params: []string{"joker"}, match: true}, - {url: "/api", params: []string{""}, match: true}, - {url: "/api/v1/entity", params: []string{"v1/entity"}, match: true}, - {url: "/api2/v1/entity", params: nil, match: false}, - {url: "/api_ignore/v1/entity", params: nil, match: false}, - }) - testCase("/partialCheck/foo/bar/:param", []testparams{ - {url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true}, - {url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true}, - {url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true}, - {url: "/partiaFoo", params: nil, match: false, partialCheck: true}, - }) - testCase("/", []testparams{ - {url: "/api", params: nil, match: false}, - {url: "", params: []string{}, match: true}, - {url: "/", params: []string{}, match: true}, - }) - testCase("/config/abc.json", []testparams{ - {url: "/config/abc.json", params: []string{}, match: true}, - {url: "config/abc.json", params: nil, match: false}, - {url: "/config/efg.json", params: nil, match: false}, - {url: "/config", params: nil, match: false}, - }) - testCase("/config/*.json", []testparams{ - {url: "/config/abc.json", params: []string{"abc"}, match: true}, - {url: "/config/efg.json", params: []string{"efg"}, match: true}, - {url: "/config/.json", params: []string{""}, match: true}, - {url: "/config/efg.csv", params: nil, match: false}, - {url: "config/abc.json", params: nil, match: false}, - {url: "/config", params: nil, match: false}, - }) - testCase("/config/+.json", []testparams{ - {url: "/config/abc.json", params: []string{"abc"}, match: true}, - {url: "/config/.json", params: nil, match: false}, - {url: "/config/efg.json", params: []string{"efg"}, match: true}, - {url: "/config/efg.csv", params: nil, match: false}, - {url: "config/abc.json", params: nil, match: false}, - {url: "/config", params: nil, match: false}, - }) - testCase("/xyz", []testparams{ - {url: "xyz", params: nil, match: false}, - {url: "xyz/", params: nil, match: false}, - }) - testCase("/api/*/:param?", []testparams{ - {url: "/api/", params: []string{"", ""}, match: true}, - {url: "/api/joker", params: []string{"joker", ""}, match: true}, - {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, - {url: "/api/joker//batman", params: []string{"joker/", "batman"}, match: true}, - {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, - {url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true}, - {url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true}, - {url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true}, - {url: "/api", params: []string{"", ""}, match: true}, - }) - testCase("/api/*/:param", []testparams{ - {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, - {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, - {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, - {url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: nil, match: false}, - {url: "/api", params: nil, match: false}, - }) - testCase("/api/+/:param", []testparams{ - {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, - {url: "/api/joker", params: nil, match: false}, - {url: "/api", params: nil, match: false}, - }) - testCase("/api/*/:param/:param2", []testparams{ - {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, - {url: "/api/joker/batman", params: nil, match: false}, - {url: "/api/joker/batman-robin/1", params: []string{"joker", "batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: nil, match: false}, - {url: "/api/test/abc", params: nil, match: false}, - {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, - {url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true}, - {url: "/api", params: nil, match: false}, - {url: "/api/:test", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/true", params: []string{"true"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/#!?", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/123", params: nil, match: false}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/ent", params: []string{"ent"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/123", params: nil, match: false}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/ent", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/e", params: nil, match: false}, - {url: "/api/v1/en", params: []string{"en"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/e", params: nil, match: false}, - {url: "/api/v1/en", params: []string{"en"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/1", params: nil, match: false}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/1", params: []string{"1"}, match: true}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - {url: "/api/v1/15", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/9", params: []string{"9"}, match: true}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - {url: "/api/v1/15", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/15", params: nil, match: false}, - {url: "/api/v1/peach", params: []string{"peach"}, match: true}, - {url: "/api/v1/p34ch", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/15", params: nil, match: false}, - {url: "/api/v1/2022-08-27", params: []string{"2022-08-27"}, match: true}, - {url: "/api/v1/2022/08-27", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/87283827683", params: []string{"87283827683"}, match: true}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/true", params: []string{"true"}, match: true}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/1200", params: []string{"1200"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - testCase("/api/v1/:lang/videos/:page", []testparams{ - {url: "/api/v1/try/videos/200", params: nil, match: false}, - {url: "/api/v1/tr/videos/1800", params: nil, match: false}, - {url: "/api/v1/tr/videos/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/videos/10", params: nil, match: false}, - }) - testCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: nil, match: false}, - {url: "/api/v1/tr/1800", params: nil, match: false}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - testCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: []string{"try", "200"}, match: true}, - {url: "/api/v1/tr/1800", params: nil, match: false}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - testCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: nil, match: false}, - {url: "/api/v1/tr/1800", params: []string{"tr", "1800"}, match: true}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - testCase("/api/v1/:date/:regex", []testparams{ - {url: "/api/v1/2005-11-01/a", params: nil, match: false}, - {url: "/api/v1/2005-1101/paach", params: nil, match: false}, - {url: "/api/v1/2005-11-01/peach", params: []string{"2005-11-01", "peach"}, match: true}, - }) - testCase("/api/v1/:param?", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - {url: "/api/v1/", params: []string{""}, match: true}, - }) + for _, testCaseCollection := range routeTestCases { + testCaseFn(testCaseCollection) + } +} + +// go test -race -run Test_RoutePatternMatch +func Test_RoutePatternMatch(t *testing.T) { + t.Parallel() + testCaseFn := func(pattern string, cases []routeTestCase) { + for _, c := range cases { + // skip all cases for partial checks + if c.partialCheck { + continue + } + match := RoutePatternMatch(c.url, pattern) + require.Equal(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", pattern, c.url)) + } + } + for _, testCase := range routeTestCases { + testCaseFn(testCase.pattern, testCase.testCases) + } } func Test_Utils_GetTrimmedParam(t *testing.T) { @@ -632,236 +197,61 @@ func Test_Utils_RemoveEscapeChar(t *testing.T) { // go test -race -run Test_Path_matchParams func Benchmark_Path_matchParams(t *testing.B) { - type testparams struct { - url string - params []string - match bool - partialCheck bool - } var ctxParams [maxParams]string - benchCase := func(r string, cases []testparams) { - parser := parseRoute(r) - for _, c := range cases { + benchCaseFn := func(testCollection routeCaseCollection) { + parser := parseRoute(testCollection.pattern) + for _, c := range testCollection.testCases { var matchRes bool state := "match" if !c.match { state = "not match" } - t.Run(r+" | "+state+" | "+c.url, func(b *testing.B) { + t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { for i := 0; i <= b.N; i++ { if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match { - // Get params from the original path + // Get testCases from the original path matchRes = true } } - require.Equal(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + require.Equal(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) if matchRes && len(c.params) > 0 { - require.Equal(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + require.Equal(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) } }) + } + } + for _, testCollection := range benchmarkCases { + benchCaseFn(testCollection) + } +} + +// go test -race -run Test_RoutePatternMatch +func Benchmark_RoutePatternMatch(t *testing.B) { + benchCaseFn := func(testCollection routeCaseCollection) { + for _, c := range testCollection.testCases { + // skip all cases for partial checks + if c.partialCheck { + continue + } + var matchRes bool + state := "match" + if !c.match { + state = "not match" + } + t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { + for i := 0; i <= b.N; i++ { + if match := RoutePatternMatch(c.url, testCollection.pattern); match { + // Get testCases from the original path + matchRes = true + } + } + require.Equal(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + }) } } - benchCase("/api/:param/fixedEnd", []testparams{ - {url: "/api/abc/fixedEnd", params: []string{"abc"}, match: true}, - {url: "/api/abc/def/fixedEnd", params: nil, match: false}, - }) - benchCase("/api/v1/:param/*", []testparams{ - {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/entity/8728382", params: nil, match: false}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - benchCase("/api/v1", []testparams{ - {url: "/api/v1", params: []string{}, match: true}, - {url: "/api/v2", params: nil, match: false}, - }) - benchCase("/api/v1/:param/*", []testparams{ - {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/true", params: []string{"true"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/#!?", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/123", params: nil, match: false}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/ent", params: []string{"ent"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/123", params: nil, match: false}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/ent", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/e", params: nil, match: false}, - {url: "/api/v1/en", params: []string{"en"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/e", params: nil, match: false}, - {url: "/api/v1/en", params: []string{"en"}, match: true}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/12345", params: []string{"12345"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/1", params: nil, match: false}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/1", params: []string{"1"}, match: true}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - {url: "/api/v1/15", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/9", params: []string{"9"}, match: true}, - {url: "/api/v1/5", params: []string{"5"}, match: true}, - {url: "/api/v1/15", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/15", params: nil, match: false}, - {url: "/api/v1/peach", params: []string{"peach"}, match: true}, - {url: "/api/v1/p34ch", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/ent", params: nil, match: false}, - {url: "/api/v1/15", params: nil, match: false}, - {url: "/api/v1/2022-08-27", params: []string{"2022-08-27"}, match: true}, - {url: "/api/v1/2022/08-27", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/123", params: []string{"123"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/87283827683", params: []string{"87283827683"}, match: true}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/true", params: []string{"true"}, match: true}, - }) - benchCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/87283827683", params: nil, match: false}, - {url: "/api/v1/25", params: []string{"25"}, match: true}, - {url: "/api/v1/1200", params: []string{"1200"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - }) - benchCase("/api/v1/:lang/videos/:page", []testparams{ - {url: "/api/v1/try/videos/200", params: nil, match: false}, - {url: "/api/v1/tr/videos/1800", params: nil, match: false}, - {url: "/api/v1/tr/videos/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/videos/10", params: nil, match: false}, - }) - benchCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: nil, match: false}, - {url: "/api/v1/tr/1800", params: nil, match: false}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - benchCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: []string{"try", "200"}, match: true}, - {url: "/api/v1/tr/1800", params: nil, match: false}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - benchCase("/api/v1/:lang/:page", []testparams{ - {url: "/api/v1/try/200", params: nil, match: false}, - {url: "/api/v1/tr/1800", params: []string{"tr", "1800"}, match: true}, - {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, - {url: "/api/v1/e/10", params: nil, match: false}, - }) - benchCase("/api/v1/:date/:regex", []testparams{ - {url: "/api/v1/2005-11-01/a", params: nil, match: false}, - {url: "/api/v1/2005-1101/paach", params: nil, match: false}, - {url: "/api/v1/2005-11-01/peach", params: []string{"2005-11-01", "peach"}, match: true}, - }) - benchCase("/api/v1/:param?", []testparams{ - {url: "/api/v1/entity", params: nil, match: false}, - {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, - {url: "/api/v1/true", params: nil, match: false}, - {url: "/api/v1/", params: []string{""}, match: true}, - }) + + for _, testCollection := range benchmarkCases { + benchCaseFn(testCollection) + } } diff --git a/path_testcases_test.go b/path_testcases_test.go new file mode 100644 index 0000000000..5602a0284d --- /dev/null +++ b/path_testcases_test.go @@ -0,0 +1,718 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📝 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "strings" +) + +type routeTestCase struct { + url string + match bool + params []string + partialCheck bool +} + +type routeCaseCollection struct { + pattern string + testCases []routeTestCase +} + +var ( + benchmarkCases []routeCaseCollection + routeTestCases []routeCaseCollection +) + +func init() { + // smaller list for benchmark cases + benchmarkCases = []routeCaseCollection{ + { + pattern: "/api/v1/const", + testCases: []routeTestCase{ + {url: "/api/v1/const", params: []string{}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + {url: "/api/v1/something", params: nil, match: false}, + }, + }, + { + pattern: "/api/:param/fixedEnd", + testCases: []routeTestCase{ + {url: "/api/abc/fixedEnd", params: []string{"abc"}, match: true}, + {url: "/api/abc/def/fixedEnd", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param/*", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + } + + // combine benchmark cases and other cases + routeTestCases = benchmarkCases + routeTestCases = append( + routeTestCases, + []routeCaseCollection{ + { + pattern: "/api/v1/:param/+", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/entity/", params: nil, match: false}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param?", + testCases: []routeTestCase{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/optional", params: []string{"optional"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/xyz", params: nil, match: false}, + }, + }, + { + pattern: "/v1/some/resource/name\\:customVerb", + testCases: []routeTestCase{ + {url: "/v1/some/resource/name:customVerb", params: nil, match: true}, + {url: "/v1/some/resource/name:test", params: nil, match: false}, + }, + }, + { + pattern: "/v1/some/resource/:name\\:customVerb", + testCases: []routeTestCase{ + {url: "/v1/some/resource/test:customVerb", params: []string{"test"}, match: true}, + {url: "/v1/some/resource/test:test", params: nil, match: false}, + }, + }, + { + pattern: "/v1/some/resource/name\\\\:customVerb?\\?/:param/*", + testCases: []routeTestCase{ + {url: "/v1/some/resource/name:customVerb??/test/optionalWildCard/character", params: []string{"test", "optionalWildCard/character"}, match: true}, + {url: "/v1/some/resource/name:customVerb??/test", params: []string{"test", ""}, match: true}, + }, + }, + { + pattern: "/api/v1/*", + testCases: []routeTestCase{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true}, + {url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/abc", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param-:param2", + testCases: []routeTestCase{ + {url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:filename.:extension", + testCases: []routeTestCase{ + {url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true}, + {url: "/api/v1/test/pdf", params: nil, match: false}, + {url: "/api/v1/test-pdf", params: nil, match: false}, + {url: "/api/v1/test_pdf", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/shop/product/::filter/color::color/size::size", + testCases: []routeTestCase{ + {url: "/shop/product/:test/color:blue/size:xs", params: []string{"test", "blue", "xs"}, match: true}, + {url: "/shop/product/test/color:blue/size:xs", params: nil, match: false}, + }, + }, + { + pattern: "/::param?", + testCases: []routeTestCase{ + {url: "/:hello", params: []string{"hello"}, match: true}, + {url: "/:", params: []string{""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + // successive parameters, each take one character and the last parameter gets everything + { + pattern: "/test:sign:param", + testCases: []routeTestCase{ + {url: "/test-abc", params: []string{"-", "abc"}, match: true}, + {url: "/test", params: nil, match: false}, + }, + }, + // optional parameters are not greedy + { + pattern: "/:param1:param2?:param3", + testCases: []routeTestCase{ + {url: "/abbbc", params: []string{"a", "b", "bbc"}, match: true}, + // {url: "/ac", testCases: []string{"a", "", "c"}, match: true}, // TODO: fix it + {url: "/test", params: []string{"t", "e", "st"}, match: true}, + }, + }, + { + pattern: "/test:optional?:mandatory", + testCases: []routeTestCase{ + // {url: "/testo", testCases: []string{"", "o"}, match: true}, // TODO: fix it + {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, + {url: "/test", params: nil, match: false}, + }, + }, + { + pattern: "/test:optional?:optional2?", + testCases: []routeTestCase{ + {url: "/testo", params: []string{"o", ""}, match: true}, + {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, + {url: "/test", params: []string{"", ""}, match: true}, + {url: "/tes", params: nil, match: false}, + }, + }, + { + pattern: "/foo:param?bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: []string{""}, match: true}, + {url: "/fooba", params: nil, match: false}, + {url: "/fobar", params: nil, match: false}, + }, + }, + { + pattern: "/foo*bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: []string{""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/foo+bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: nil, match: false}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/a*cde*g/", + testCases: []routeTestCase{ + {url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true}, + {url: "/acdeg", params: []string{"", ""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/*v1*/proxy", + testCases: []routeTestCase{ + {url: "/customer/v1/cart/proxy", params: []string{"customer/", "/cart"}, match: true}, + {url: "/v1/proxy", params: []string{"", ""}, match: true}, + {url: "/v1/", params: nil, match: false}, + }, + }, + // successive wildcard -> first wildcard is greedy + { + pattern: "/foo***bar", + testCases: []routeTestCase{ + {url: "/foo*abar", params: []string{"*a", "", ""}, match: true}, + {url: "/foo*bar", params: []string{"*", "", ""}, match: true}, + {url: "/foobar", params: []string{"", "", ""}, match: true}, + {url: "/fooba", params: nil, match: false}, + }, + }, + // chars in front of an parameter + { + pattern: "/name::name", + testCases: []routeTestCase{ + {url: "/name:john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/@:name", + testCases: []routeTestCase{ + {url: "/@john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/-:name", + testCases: []routeTestCase{ + {url: "/-john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/.:name", + testCases: []routeTestCase{ + {url: "/.john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param/abc/*", + testCases: []routeTestCase{ + {url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true}, + {url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/abc", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/ttt", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day/:month?/:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/", params: []string{"1", "", ""}, match: true}, + {url: "/api/1//", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/-/", params: []string{"1", "-", ""}, match: true}, + {url: "/api/1-", params: []string{"1-", "", ""}, match: true}, + {url: "/api/1.", params: []string{"1.", "", ""}, match: true}, + {url: "/api/1/2", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day.:month?.:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: nil, match: false}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1.", params: nil, match: false}, + {url: "/api/1..", params: []string{"1", "", ""}, match: true}, + {url: "/api/1.2", params: nil, match: false}, + {url: "/api/1.2.", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day-:month?-:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: nil, match: false}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1-", params: nil, match: false}, + {url: "/api/1--", params: []string{"1", "", ""}, match: true}, + {url: "/api/1-/", params: nil, match: false}, + // {url: "/api/1-/-", testCases: nil, match: false}, // TODO: fix this part + {url: "/api/1-2", params: nil, match: false}, + {url: "/api/1-2-", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/*", + testCases: []routeTestCase{ + {url: "/api/", params: []string{""}, match: true}, + {url: "/api/joker", params: []string{"joker"}, match: true}, + {url: "/api", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"v1/entity"}, match: true}, + {url: "/api2/v1/entity", params: nil, match: false}, + {url: "/api_ignore/v1/entity", params: nil, match: false}, + }, + }, + { + pattern: "/partialCheck/foo/bar/:param", + testCases: []routeTestCase{ + {url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true}, + {url: "/partiaFoo", params: nil, match: false, partialCheck: true}, + }, + }, + { + pattern: "/", + testCases: []routeTestCase{ + {url: "/api", params: nil, match: false}, + {url: "", params: []string{}, match: true}, + {url: "/", params: []string{}, match: true}, + }, + }, + { + pattern: "/config/abc.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{}, match: true}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config/efg.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/config/*.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/.json", params: []string{""}, match: true}, + {url: "/config/efg.csv", params: nil, match: false}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/config/+.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/.json", params: nil, match: false}, + {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/efg.csv", params: nil, match: false}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/xyz", + testCases: []routeTestCase{ + {url: "xyz", params: nil, match: false}, + {url: "xyz/", params: nil, match: false}, + }, + }, + { + pattern: "/api/*/:param?", + testCases: []routeTestCase{ + {url: "/api/", params: []string{"", ""}, match: true}, + {url: "/api/joker", params: []string{"joker", ""}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker//batman", params: []string{"joker/", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true}, + {url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true}, + {url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true}, + {url: "/api", params: []string{"", ""}, match: true}, + }, + }, + { + pattern: "/api/*/:param", + testCases: []routeTestCase{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }, + }, + { + pattern: "/api/+/:param", + testCases: []routeTestCase{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }, + }, + { + pattern: "/api/*/:param/:param2", + testCases: []routeTestCase{ + {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, + {url: "/api/joker/batman", params: nil, match: false}, + {url: "/api/joker/batman-robin/1", params: []string{"joker", "batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api/test/abc", params: nil, match: false}, + {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true}, + {url: "/api", params: nil, match: false}, + {url: "/api/:test", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/#!?", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/123", params: nil, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/123", params: nil, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/ent", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/e", params: nil, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/e", params: nil, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/1", params: nil, match: false}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/9", params: []string{"9"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/15", params: nil, match: false}, + {url: "/api/v1/peach", params: []string{"peach"}, match: true}, + {url: "/api/v1/p34ch", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/12", params: nil, match: false}, + {url: "/api/v1/xy", params: nil, match: false}, + {url: "/api/v1/test", params: []string{"test"}, match: true}, + {url: "/api/v1/" + strings.Repeat("a", 64), params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/15", params: nil, match: false}, + {url: "/api/v1/2022-08-27", params: []string{"2022-08-27"}, match: true}, + {url: "/api/v1/2022/08-27", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/87283827683", params: []string{"87283827683"}, match: true}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/1200", params: []string{"1200"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/videos/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/videos/200", params: nil, match: false}, + {url: "/api/v1/tr/videos/1800", params: nil, match: false}, + {url: "/api/v1/tr/videos/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/videos/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: nil, match: false}, + {url: "/api/v1/tr/1800", params: nil, match: false}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: []string{"try", "200"}, match: true}, + {url: "/api/v1/tr/1800", params: nil, match: false}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: nil, match: false}, + {url: "/api/v1/tr/1800", params: []string{"tr", "1800"}, match: true}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:date/:regex", + testCases: []routeTestCase{ + {url: "/api/v1/2005-11-01/a", params: nil, match: false}, + {url: "/api/v1/2005-1101/paach", params: nil, match: false}, + {url: "/api/v1/2005-11-01/peach", params: []string{"2005-11-01", "peach"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param?", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + {url: "/api/v1/", params: []string{""}, match: true}, + }, + }, + }..., + ) +} diff --git a/prefork.go b/prefork.go index 71db86b8d7..5674124a14 100644 --- a/prefork.go +++ b/prefork.go @@ -2,6 +2,7 @@ package fiber import ( "crypto/tls" + "errors" "fmt" "net" "os" @@ -9,18 +10,23 @@ import ( "runtime" "strconv" "strings" + "sync/atomic" "time" + "github.com/gofiber/fiber/v2/log" "github.com/valyala/fasthttp/reuseport" ) const ( envPreforkChildKey = "FIBER_PREFORK_CHILD" envPreforkChildVal = "1" + sleepDuration = 100 * time.Millisecond ) -var testPreforkMaster = false -var testOnPrefork = false +var ( + testPreforkMaster = false + testOnPrefork = false +) // IsChild determines if the current process is a child of Prefork func IsChild() bool { @@ -28,19 +34,21 @@ func IsChild() bool { } // prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature -func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) (err error) { +func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) error { + var ln net.Listener + var err error + // 👶 child process 👶 if IsChild() { // use 1 cpu core per child process runtime.GOMAXPROCS(1) - var ln net.Listener // Linux will use SO_REUSEPORT and Windows falls back to SO_REUSEADDR // Only tcp4 or tcp6 is supported when preforking, both are not supported if ln, err = reuseport.Listen(cfg.ListenerNetwork, addr); err != nil { if !cfg.DisableStartupMessage { - time.Sleep(100 * time.Millisecond) // avoid colliding with startup message + time.Sleep(sleepDuration) // avoid colliding with startup message } - return fmt.Errorf("prefork: %v", err) + return fmt.Errorf("prefork: %w", err) } // wrap a tls config around the listener if provided if tlsConfig != nil { @@ -74,7 +82,11 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) (e // kill child procs when master exits defer func() { for _, proc := range childs { - _ = proc.Process.Kill() + if err := proc.Process.Kill(); err != nil { + if !errors.Is(err, os.ErrProcessDone) { + log.Errorf("prefork: failed to kill child: %v", err) + } + } } }() @@ -83,8 +95,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) (e // launch child procs for i := 0; i < max; i++ { - /* #nosec G204 */ - cmd := exec.Command(os.Args[0], os.Args[1:]...) // #nosec G204 + cmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint:gosec // It's fine to launch the same process again if testPreforkMaster { // When test prefork master, // just start the child process with a dummy cmd, @@ -100,7 +111,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) (e ) if err = cmd.Start(); err != nil { - return fmt.Errorf("failed to start a child prefork process, error: %v", err) + return fmt.Errorf("failed to start a child prefork process, error: %w", err) } // store child process @@ -123,6 +134,10 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) (e }() } + // Run onListen hooks + // Hooks have to be run here as different as non-prefork mode due to they should run as child or master + app.runOnListenHooks(app.prepareListenData(addr, tlsConfig != nil, cfg)) + // Print startup message if !cfg.DisableStartupMessage { app.startupMessage(addr, tlsConfig != nil, ","+strings.Join(pids, ","), cfg) @@ -144,27 +159,33 @@ func watchMaster() { // and waits for it to exit p, err := os.FindProcess(os.Getppid()) if err == nil { - _, _ = p.Wait() + _, _ = p.Wait() //nolint:errcheck // It is fine to ignore the error here } - os.Exit(1) + os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork } // if it is equal to 1 (init process ID), // it indicates that the master process has exited - for range time.NewTicker(time.Millisecond * 500).C { + const watchInterval = 500 * time.Millisecond + for range time.NewTicker(watchInterval).C { if os.Getppid() == 1 { - os.Exit(1) + os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork } } } -var dummyChildCmd = "go" +var ( + dummyPid = 1 + dummyChildCmd atomic.Value +) // dummyCmd is for internal prefork testing func dummyCmd() *exec.Cmd { + command := "go" + if storeCommand := dummyChildCmd.Load(); storeCommand != nil && storeCommand != "" { + command = storeCommand.(string) //nolint:forcetypeassert,errcheck // We always store a string in here + } if runtime.GOOS == "windows" { - return exec.Command("cmd", "/C", dummyChildCmd, "version") + return exec.Command("cmd", "/C", command, "version") } - return exec.Command(dummyChildCmd, "version") + return exec.Command(command, "version") } - -var dummyPid = 1 diff --git a/prefork_test.go b/prefork_test.go index a1a680c1bd..d33211c583 100644 --- a/prefork_test.go +++ b/prefork_test.go @@ -38,6 +38,7 @@ func Test_App_Prefork_Child_Process(t *testing.T) { if err != nil { require.NoError(t, err) } + //nolint:gosec // We're in a test so using old ciphers is fine config := &tls.Config{Certificates: []tls.Certificate{cer}} go func() { @@ -61,12 +62,12 @@ func Test_App_Prefork_Master_Process(t *testing.T) { require.Nil(t, app.prefork(":3000", nil, listenConfigDefault())) - dummyChildCmd = "invalid" + dummyChildCmd.Store("invalid") err := app.prefork("127.0.0.1:", nil, listenConfigDefault()) require.False(t, err == nil) - dummyChildCmd = "go" + dummyChildCmd.Store("go") } func Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) { diff --git a/redirect.go b/redirect.go index 13717ab233..a1fd29dd90 100644 --- a/redirect.go +++ b/redirect.go @@ -5,6 +5,7 @@ package fiber import ( + "fmt" "strings" "sync" @@ -13,17 +14,15 @@ import ( "github.com/valyala/fasthttp" ) -var ( - // Pool for redirection - redirectPool = sync.Pool{ - New: func() any { - return &Redirect{ - status: StatusFound, - oldInput: make(map[string]string, 0), - } - }, - } -) +// Pool for redirection +var redirectPool = sync.Pool{ + New: func() any { + return &Redirect{ + status: StatusFound, + oldInput: make(map[string]string, 0), + } + }, +} // Cookie name to send flash messages when to use redirection. const ( @@ -52,7 +51,12 @@ type RedirectConfig struct { // AcquireRedirect return default Redirect reference from the redirect pool func AcquireRedirect() *Redirect { - return redirectPool.Get().(*Redirect) + redirect, ok := redirectPool.Get().(*Redirect) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Redirect")) + } + + return redirect } // ReleaseRedirect returns c acquired via Redirect to redirect pool. @@ -86,7 +90,7 @@ func (r *Redirect) Status(code int) *Redirect { // They will be sent as a cookie. // You can get them by using: Redirect().Messages(), Redirect().Message() // Note: You must use escape char before using ',' and ':' chars to avoid wrong parsing. -func (r *Redirect) With(key string, value string) *Redirect { +func (r *Redirect) With(key, value string) *Redirect { r.messages = append(r.messages, key+CookieDataAssigner+value) return r @@ -196,7 +200,6 @@ func (r *Redirect) OldInput(key string) string { } } return "" - } // Redirect to the URL derived from the specified path, with specified status. @@ -229,10 +232,10 @@ func (r *Redirect) Route(name string, config ...RedirectConfig) error { // flash messages for i, message := range r.messages { - _, _ = messageText.WriteString(message) + _, _ = messageText.WriteString(message) //nolint:errcheck // Always return nil // when there are more messages or oldInput -> add a comma if len(r.messages)-1 != i || (len(r.messages)-1 == i && len(r.oldInput) > 0) { - _, _ = messageText.WriteString(CookieDataSeparator) + _, _ = messageText.WriteString(CookieDataSeparator) //nolint:errcheck // Always return nil } } r.messages = r.messages[:0] @@ -240,9 +243,9 @@ func (r *Redirect) Route(name string, config ...RedirectConfig) error { // old input data i := 1 for k, v := range r.oldInput { - _, _ = messageText.WriteString(OldInputDataPrefix + k + CookieDataAssigner + v) + _, _ = messageText.WriteString(OldInputDataPrefix + k + CookieDataAssigner + v) //nolint:errcheck // Always return nil if len(r.oldInput) != i { - _, _ = messageText.WriteString(CookieDataSeparator) + _, _ = messageText.WriteString(CookieDataSeparator) //nolint:errcheck // Always return nil } i++ } @@ -261,10 +264,10 @@ func (r *Redirect) Route(name string, config ...RedirectConfig) error { i := 1 for k, v := range cfg.Queries { - _, _ = queryText.WriteString(k + "=" + v) + _, _ = queryText.WriteString(k + "=" + v) //nolint:errcheck // Always return nil if i != len(cfg.Queries) { - _, _ = queryText.WriteString("&") + _, _ = queryText.WriteString("&") //nolint:errcheck // Always return nil } i++ } @@ -312,9 +315,10 @@ func (r *Redirect) setFlash() { r.c.ClearCookie(FlashCookieName) } -func parseMessage(raw string) (key, value string) { +func parseMessage(raw string) (string, string) { //nolint: revive // not necessary if i := findNextNonEscapedCharsetPosition(raw, []byte(CookieDataAssigner)); i != -1 { return RemoveEscapeChar(raw[:i]), RemoveEscapeChar(raw[i+1:]) } + return RemoveEscapeChar(raw), "" } diff --git a/redirect_test.go b/redirect_test.go index f603c6fc43..dee9b4193c 100644 --- a/redirect_test.go +++ b/redirect_test.go @@ -2,6 +2,7 @@ // 📝 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io +//nolint:wrapcheck // We must not wrap errors in tests package fiber import ( @@ -22,11 +23,13 @@ func Test_Redirect_To(t *testing.T) { app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().To("http://default.com") + err := c.Redirect().To("http://default.com") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "http://default.com", string(c.Response().Header.Peek(HeaderLocation))) - c.Redirect().Status(301).To("http://example.com") + err = c.Redirect().Status(301).To("http://example.com") + require.NoError(t, err) require.Equal(t, 301, c.Response().StatusCode()) require.Equal(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) } @@ -40,11 +43,12 @@ func Test_Redirect_Route_WithParams(t *testing.T) { }).Name("user") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Route("user", RedirectConfig{ + err := c.Redirect().Route("user", RedirectConfig{ Params: Map{ "name": "fiber", }, }) + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) } @@ -58,12 +62,13 @@ func Test_Redirect_Route_WithParams_WithQueries(t *testing.T) { }).Name("user") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Route("user", RedirectConfig{ + err := c.Redirect().Route("user", RedirectConfig{ Params: Map{ "name": "fiber", }, Queries: map[string]string{"data[0][name]": "john", "data[0][age]": "10", "test": "doe"}, }) + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) // analysis of query parameters with url parsing, since a map pass is always randomly ordered location, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation))) @@ -81,11 +86,12 @@ func Test_Redirect_Route_WithOptionalParams(t *testing.T) { }).Name("user") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Route("user", RedirectConfig{ + err := c.Redirect().Route("user", RedirectConfig{ Params: Map{ "name": "fiber", }, }) + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) } @@ -99,7 +105,8 @@ func Test_Redirect_Route_WithOptionalParamsWithoutValue(t *testing.T) { }).Name("user") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Route("user") + err := c.Redirect().Route("user") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user/", string(c.Response().Header.Peek(HeaderLocation))) } @@ -113,11 +120,12 @@ func Test_Redirect_Route_WithGreedyParameters(t *testing.T) { }).Name("user") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Route("user", RedirectConfig{ + err := c.Redirect().Route("user", RedirectConfig{ Params: Map{ "+": "test/routes", }, }) + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user/test/routes", string(c.Response().Header.Peek(HeaderLocation))) } @@ -131,11 +139,12 @@ func Test_Redirect_Back(t *testing.T) { }).Name("home") c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Redirect().Back("/") + err := c.Redirect().Back("/") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/", string(c.Response().Header.Peek(HeaderLocation))) - err := c.Redirect().Back() + err = c.Redirect().Back() require.Equal(t, 500, c.Response().StatusCode()) require.ErrorAs(t, ErrRedirectBackNoFallback, &err) } @@ -153,7 +162,8 @@ func Test_Redirect_Back_WithReferer(t *testing.T) { c := app.NewCtx(&fasthttp.RequestCtx{}) c.Request().Header.Set(HeaderReferer, "/back") - c.Redirect().Back("/") + err := c.Redirect().Back("/") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/back", c.Get(HeaderReferer)) require.Equal(t, "/back", string(c.Response().Header.Peek(HeaderLocation))) @@ -168,10 +178,10 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - - c.Redirect().With("success", "1").With("message", "test").Route("user") + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + err := c.Redirect().With("success", "1").With("message", "test").Route("user") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) @@ -191,11 +201,11 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().URI().SetQueryString("id=1&name=tom") - c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") - + err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) @@ -219,7 +229,7 @@ func Test_Redirect_setFlash(t *testing.T) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") @@ -326,13 +336,13 @@ func Benchmark_Redirect_Route(b *testing.B) { return c.JSON(c.Params("name")) }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c.Redirect().Route("user", RedirectConfig{ + c.Redirect().Route("user", RedirectConfig{ //nolint:errcheck,gosec,revive // we don't need to handle error here Params: Map{ "name": "fiber", }, @@ -350,13 +360,13 @@ func Benchmark_Redirect_Route_WithQueries(b *testing.B) { return c.JSON(c.Params("name")) }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c.Redirect().Route("user", RedirectConfig{ + c.Redirect().Route("user", RedirectConfig{ //nolint:errcheck,gosec,revive // we don't need to handle error here Params: Map{ "name": "fiber", }, @@ -379,13 +389,13 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - c.Redirect().With("success", "1").With("message", "test").Route("user") + c.Redirect().With("success", "1").With("message", "test").Route("user") //nolint:errcheck,gosec,revive // we don't need to handle error here } require.Equal(b, 302, c.Response().StatusCode()) @@ -405,7 +415,7 @@ func Benchmark_Redirect_setFlash(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") @@ -434,7 +444,7 @@ func Benchmark_Redirect_Messages(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") c.Redirect().setFlash() @@ -459,7 +469,7 @@ func Benchmark_Redirect_OldInputs(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") c.Redirect().setFlash() @@ -484,7 +494,7 @@ func Benchmark_Redirect_Message(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") c.Redirect().setFlash() @@ -509,7 +519,7 @@ func Benchmark_Redirect_OldInput(b *testing.B) { return c.SendString("user") }).Name("user") - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c.Request().Header.Set(HeaderCookie, "fiber_flash=success:1,message:test,old_input_data_name:tom,old_input_data_id:1") c.Redirect().setFlash() diff --git a/router.go b/router.go index 7f88c7b99d..87429d6213 100644 --- a/router.go +++ b/router.go @@ -43,9 +43,11 @@ type Router interface { // Route is a struct that holds all metadata for each registered handler. type Route struct { + // always keep in sync with the copy method "app.copyRoute" // Data for routing pos uint32 // Position in stack -> important for the sort of the matched routes use bool // USE matches path prefixes + mount bool // Indicated a mounted app on a specific route star bool // Path equals '*' root bool // Path equals '/' path string // Prettified path @@ -53,14 +55,15 @@ type Route struct { group *Group // Group instance. used for routes in groups // Public fields - Method string `json:"method"` // HTTP method - Name string `json:"name"` // Route's name + Method string `json:"method"` // HTTP method + Name string `json:"name"` // Route's name + //nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine Path string `json:"path"` // Original registered route path Params []string `json:"params"` // Case sensitive param keys Handlers []Handler `json:"-"` // Ctx handlers } -func (r *Route) match(detectionPath, path string, params *[maxParams]string) (match bool) { +func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { // root detectionPath check if r.root && detectionPath == "/" { return true @@ -95,7 +98,7 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) (ma return false } -func (app *App) nextCustom(c CustomCtx) (match bool, err error) { +func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint: unparam // bool param might be useful for testing // Get stack length tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] if !ok { @@ -112,7 +115,7 @@ func (app *App) nextCustom(c CustomCtx) (match bool, err error) { route := tree[c.getIndexRoute()] // Check if it matches the request path - match = route.match(c.getDetectionPath(), c.Path(), c.getValues()) + match := route.match(c.getDetectionPath(), c.Path(), c.getValues()) // No match, next route if !match { @@ -128,42 +131,48 @@ func (app *App) nextCustom(c CustomCtx) (match bool, err error) { // Execute first handler of route c.setIndexHandler(0) - err = route.Handlers[0](c) + err := route.Handlers[0](c) return match, err // Stop scanning the stack } // If c.Next() does not match, return 404 - err = NewError(StatusNotFound, "Cannot "+c.Method()+" "+c.getPathOriginal()) + err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+c.getPathOriginal()) // If no match, scan stack again if other methods match the request // Moved from app.handler because middleware may break the route chain if !c.getMatched() && app.methodExistCustom(c) { err = ErrMethodNotAllowed } - return + return false, err } -func (app *App) next(c *DefaultCtx) (match bool, err error) { +func (app *App) next(c *DefaultCtx) (bool, error) { // Get stack length tree, ok := app.treeStack[c.methodINT][c.treePath] if !ok { tree = app.treeStack[c.methodINT][""] } - lenr := len(tree) - 1 + lenTree := len(tree) - 1 // Loop over the route stack starting from previous index - for c.indexRoute < lenr { + for c.indexRoute < lenTree { // Increment route index c.indexRoute++ // Get *Route route := tree[c.indexRoute] + var match bool + var err error + // skip for mounted apps + if route.mount { + continue + } + // Check if it matches the request path match = route.match(c.detectionPath, c.path, &c.values) - - // No match, next route if !match { + // No match, next route continue } // Pass route reference and param values @@ -176,35 +185,43 @@ func (app *App) next(c *DefaultCtx) (match bool, err error) { // Execute first handler of route c.indexHandler = 0 - err = route.Handlers[0](c) + if len(route.Handlers) > 0 { + err = route.Handlers[0](c) + } return match, err // Stop scanning the stack } // If c.Next() does not match, return 404 - err = NewError(StatusNotFound, "Cannot "+c.method+" "+c.pathOriginal) - - // If no match, scan stack again if other methods match the request - // Moved from app.handler because middleware may break the route chain + err := NewError(StatusNotFound, "Cannot "+c.method+" "+c.pathOriginal) if !c.matched && app.methodExist(c) { + // If no match, scan stack again if other methods match the request + // Moved from app.handler because middleware may break the route chain err = ErrMethodNotAllowed } - return + return false, err } -func (app *App) handler(rctx *fasthttp.RequestCtx) { +func (app *App) requestHandler(rctx *fasthttp.RequestCtx) { // Handler for default ctxs var c CustomCtx + var ok bool if app.newCtxFunc != nil { - c = app.AcquireCtx().(CustomCtx) + c, ok = app.AcquireCtx().(CustomCtx) + if !ok { + panic(fmt.Errorf("failed to type-assert to CustomCtx")) + } } else { - c = app.AcquireCtx().(*DefaultCtx) + c, ok = app.AcquireCtx().(*DefaultCtx) + if !ok { + panic(fmt.Errorf("failed to type-assert to *DefaultCtx")) + } } c.Reset(rctx) defer app.ReleaseCtx(c) // handle invalid http method directly if app.methodInt(c.Method()) == -1 { - _ = c.SendStatus(StatusNotImplemented) + _ = c.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil return } @@ -222,15 +239,16 @@ func (app *App) handler(rctx *fasthttp.RequestCtx) { } if err != nil { if catch := c.App().ErrorHandler(c, err); catch != nil { - _ = c.SendStatus(StatusInternalServerError) + _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here } + // TODO: Do we need to return here? } } func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { prefixedPath := getGroupPath(prefix, route.Path) prettyPath := prefixedPath - // Case sensitive routing, all to lowercase + // Case-sensitive routing, all to lowercase if !app.config.CaseSensitive { prettyPath = utils.ToLower(prettyPath) } @@ -248,18 +266,22 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { return route } -func (app *App) copyRoute(route *Route) *Route { +func (*App) copyRoute(route *Route) *Route { return &Route{ // Router booleans - use: route.use, - star: route.star, - root: route.root, + use: route.use, + mount: route.mount, + star: route.star, + root: route.root, // Path data path: route.path, routeParser: route.routeParser, Params: route.Params, + // misc + pos: route.pos, + // Public data Path: route.Path, Method: route.Method, @@ -267,22 +289,23 @@ func (app *App) copyRoute(route *Route) *Route { } } -func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) Router { +func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) { handlers := middleware if handler != nil { handlers = append(handlers, handler) } for _, method := range methods { - // Uppercase HTTP methods method = utils.ToUpper(method) // Check if the HTTP method is valid unless it's USE if method != methodUse && app.methodInt(method) == -1 { panic(fmt.Sprintf("add: invalid http method %s\n", method)) } + // is mounted app + isMount := group != nil && group.app != app // A route requires atleast one ctx handler - if len(handlers) == 0 { + if len(handlers) == 0 && !isMount { panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw)) } // Cannot have an empty path @@ -295,7 +318,7 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler } // Create a stripped path in-case sensitive / trailing slashes pathPretty := pathRaw - // Case sensitive routing, all to lowercase + // Case-sensitive routing, all to lowercase if !app.config.CaseSensitive { pathPretty = utils.ToLower(pathPretty) } @@ -316,9 +339,10 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler // Create route metadata without pointer route := Route{ // Router booleans - use: isUse, - star: isStar, - root: isRoot, + use: isUse, + mount: isMount, + star: isStar, + root: isRoot, // Path data path: RemoveEscapeChar(pathPretty), @@ -342,19 +366,17 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler for _, m := range app.config.RequestMethods { // Create a route copy to avoid duplicates during compression r := route - app.addRoute(m, &r) + app.addRoute(m, &r, isMount) } } else { // Add route to stack - app.addRoute(method, &route) + app.addRoute(method, &route, isMount) } } - - return app } -func (app *App) registerStatic(prefix, root string, config ...Static) Router { - // For security we want to restrict to the current work directory. +func (app *App) registerStatic(prefix, root string, config ...Static) { + // For security, we want to restrict to the current work directory. if root == "" { root = "." } @@ -366,7 +388,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router { if prefix[0] != '/' { prefix = "/" + prefix } - // in case sensitive routing, all to lowercase + // in case-sensitive routing, all to lowercase if !app.config.CaseSensitive { prefix = utils.ToLower(prefix) } @@ -391,6 +413,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router { prefixLen-- prefix = prefix[:prefixLen] } + const cacheDuration = 10 * time.Second // Fileserver settings fs := &fasthttp.FS{ Root: root, @@ -399,7 +422,7 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router { AcceptByteRange: false, Compress: false, CompressedFileSuffix: app.config.CompressedFileSuffix, - CacheDuration: 10 * time.Second, + CacheDuration: cacheDuration, IndexNames: []string{"index.html"}, PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { path := fctx.Path() @@ -467,7 +490,6 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router { c.Context().SetContentType("") // Issue #420 c.Context().Response.SetStatusCode(StatusOK) c.Context().Response.SetBodyString("") - // Next middleware return c.Next() } @@ -489,7 +511,6 @@ func (app *App) registerStatic(prefix, root string, config ...Static) Router { app.addRoute(MethodGet, &route) // Add HEAD route app.addRoute(MethodHead, &route) - return app } func (app *App) addRoute(method string, route *Route, isMounted ...bool) { @@ -504,7 +525,7 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { // prevent identically route registration l := len(app.stack[m]) - if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use { + if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount { preRoute := app.stack[m][l-1] preRoute.Handlers = append(preRoute.Handlers, route.Handlers...) } else { diff --git a/router_test.go b/router_test.go index 613bb5d7f3..4c28cc841d 100644 --- a/router_test.go +++ b/router_test.go @@ -2,10 +2,9 @@ // 📃 Github Repository: https://github.com/gofiber/fiber // 📌 API Documentation: https://docs.gofiber.io +//nolint:bodyclose // Much easier to just ignore memory leaks in tests package fiber -// go test -v ./... -run=^$ -bench=Benchmark_Router -benchmem -count=2 - import ( "encoding/json" "errors" @@ -21,7 +20,7 @@ import ( "github.com/valyala/fasthttp" ) -var routesFixture = routeJSON{} +var routesFixture routeJSON func init() { dat, err := os.ReadFile("./.github/testdata/testRoutes.json") @@ -34,6 +33,8 @@ func init() { } func Test_Route_Match_SameLength(t *testing.T) { + t.Parallel() + app := New() app.Get("/:param", func(c Ctx) error { @@ -59,6 +60,8 @@ func Test_Route_Match_SameLength(t *testing.T) { } func Test_Route_Match_Star(t *testing.T) { + t.Parallel() + app := New() app.Get("/*", func(c Ctx) error { @@ -105,6 +108,8 @@ func Test_Route_Match_Star(t *testing.T) { } func Test_Route_Match_Root(t *testing.T) { + t.Parallel() + app := New() app.Get("/", func(c Ctx) error { @@ -121,6 +126,8 @@ func Test_Route_Match_Root(t *testing.T) { } func Test_Route_Match_Parser(t *testing.T) { + t.Parallel() + app := New() app.Get("/foo/:ParamName", func(c Ctx) error { @@ -148,6 +155,8 @@ func Test_Route_Match_Parser(t *testing.T) { } func Test_Route_Match_Middleware(t *testing.T) { + t.Parallel() + app := New() app.Use("/foo/*", func(c Ctx) error { @@ -173,6 +182,8 @@ func Test_Route_Match_Middleware(t *testing.T) { } func Test_Route_Match_UnescapedPath(t *testing.T) { + t.Parallel() + app := New(Config{UnescapePath: true}) app.Use("/créer", func(c Ctx) error { @@ -199,6 +210,8 @@ func Test_Route_Match_UnescapedPath(t *testing.T) { } func Test_Route_Match_WithEscapeChar(t *testing.T) { + t.Parallel() + app := New() // static route and escaped part app.Get("/v1/some/resource/name\\:customVerb", func(c Ctx) error { @@ -243,6 +256,8 @@ func Test_Route_Match_WithEscapeChar(t *testing.T) { } func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { + t.Parallel() + app := New() app.Use("/foo", func(c Ctx) error { @@ -259,6 +274,8 @@ func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { } func Test_Route_Match_Middleware_Root(t *testing.T) { + t.Parallel() + app := New() app.Use("/", func(c Ctx) error { @@ -275,6 +292,8 @@ func Test_Route_Match_Middleware_Root(t *testing.T) { } func Test_Router_Register_Missing_Handler(t *testing.T) { + t.Parallel() + app := New() defer func() { if err := recover(); err != nil { @@ -285,7 +304,9 @@ func Test_Router_Register_Missing_Handler(t *testing.T) { } func Test_Ensure_Router_Interface_Implementation(t *testing.T) { - var app any = (*App)(nil) + t.Parallel() + + var app interface{} = (*App)(nil) _, ok := app.(Router) require.True(t, ok) @@ -295,6 +316,8 @@ func Test_Ensure_Router_Interface_Implementation(t *testing.T) { } func Test_Router_Handler_Catch_Error(t *testing.T) { + t.Parallel() + app := New() app.config.ErrorHandler = func(c Ctx, err error) error { return errors.New("fake error") @@ -312,6 +335,8 @@ func Test_Router_Handler_Catch_Error(t *testing.T) { } func Test_Route_Static_Root(t *testing.T) { + t.Parallel() + dir := "./.github/testdata/fs/css" app := New() app.Static("/", dir, Static{ @@ -347,6 +372,8 @@ func Test_Route_Static_Root(t *testing.T) { } func Test_Route_Static_HasPrefix(t *testing.T) { + t.Parallel() + dir := "./.github/testdata/fs/css" app := New() app.Static("/static", dir, Static{ @@ -462,7 +489,7 @@ func Benchmark_App_MethodNotAllowed(b *testing.B) { } b.StopTimer() require.Equal(b, 405, c.Response.StatusCode()) - require.Equal(b, "GET", string(c.Response.Header.Peek("Allow"))) + require.Equal(b, MethodGet, string(c.Response.Header.Peek("Allow"))) require.Equal(b, utils.StatusMessage(StatusMethodNotAllowed), string(c.Response.Body())) } @@ -537,7 +564,7 @@ func Benchmark_Router_Chain(b *testing.B) { c := &fasthttp.RequestCtx{} - c.Request.Header.SetMethod("GET") + c.Request.Header.SetMethod(MethodGet) c.URI().SetPath("/") b.ResetTimer() for n := 0; n < b.N; n++ { @@ -561,7 +588,7 @@ func Benchmark_Router_WithCompression(b *testing.B) { appHandler := app.Handler() c := &fasthttp.RequestCtx{} - c.Request.Header.SetMethod("GET") + c.Request.Header.SetMethod(MethodGet) c.URI().SetPath("/") b.ResetTimer() for n := 0; n < b.N; n++ { @@ -591,7 +618,7 @@ func Benchmark_Router_Next(b *testing.B) { var res bool var err error - c := app.NewCtx(request).(*DefaultCtx) + c := app.NewCtx(request).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed b.ResetTimer() for n := 0; n < b.N; n++ { @@ -772,7 +799,7 @@ func Benchmark_Router_Github_API(b *testing.B) { for n := 0; n < b.N; n++ { c.URI().SetPath(routesFixture.TestRoutes[i].Path) - ctx := app.AcquireCtx().(*DefaultCtx) + ctx := app.AcquireCtx().(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed ctx.Reset(c) match, err = app.next(ctx) @@ -790,6 +817,6 @@ type testRoute struct { } type routeJSON struct { - TestRoutes []testRoute `json:"testRoutes"` - GithubAPI []testRoute `json:"githubAPI"` + TestRoutes []testRoute `json:"test_routes"` + GithubAPI []testRoute `json:"github_api"` }