diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index c8d7ee44..0dd0526c 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -71,11 +71,11 @@ Community leaders will follow these Community Impact Guidelines in determining t ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, -available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. +[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e35ce199..1ebcef7b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,9 +5,11 @@ Before making any changes to this repository, we kindly request you to initiate Please note: we have a [code of conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `Fiber` project. ## Pull Requests or Commits + Titles always we must use prefix according to below: > 🔥 Feature, ♻️ Refactor, 🩹 Fix, 🚨 Test, 📚 Doc, 🎨 Style + - 🔥 Feature: Add flow to add person - ♻️ Refactor: Rename file X to Y - 🩹 Fix: Improve flow @@ -17,7 +19,7 @@ Titles always we must use prefix according to below: All pull requests that contain a feature or fix are mandatory to have unit tests. Your PR is only to be merged if you respect this flow. -# 👍 Contribute +## 👍 Contribute If you want to say **thank you** and/or support the active development of `Fiber`: diff --git a/.github/README.md b/.github/README.md index a7cc2f75..68fbd9c9 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,4 +1,4 @@ -

+

@@ -12,7 +12,7 @@ - + Codecov @@ -24,7 +24,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.

@@ -39,7 +39,7 @@ Fiber v3 is currently in beta and under active development. While it offers exci ## ⚙️ Installation -Fiber requires **Go version `1.21` or higher** to run. If you need to install or upgrade Go, visit the [official Go download page](https://go.dev/dl/). To start setting up your project. Create a new directory for your project and navigate into it. Then, initialize your project with Go modules by executing the following command in your terminal: +Fiber requires **Go version `1.22` or higher** to run. If you need to install or upgrade Go, visit the [official Go download page](https://go.dev/dl/). To start setting up your project. Create a new directory for your project and navigate into it. Then, initialize your project with Go modules by executing the following command in your terminal: ```bash go mod init github.com/your/repo @@ -57,7 +57,7 @@ This command fetches the Fiber package and adds it to your project's dependencie ## ⚡️ Quickstart -Getting started with Fiber is easy. Here's a basic example to create a simple web server that responds with "Hello, World 👋!" on the root path. This example demonstrates initializing a new Fiber app, setting up a route, and starting the server. +Getting started with Fiber is easy. Here's a basic example to create a simple web server that responds with "Hello, World 👋!" on the root path. This example demonstrates initializing a new Fiber app, setting up a route, and starting the server. ```go package main @@ -100,19 +100,19 @@ These tests are performed by [TechEmpower](https://www.techempower.com/benchmark ## 🎯 Features -- 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/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/contrib/tree/main/websocket) -- [Socket.io Support](https://github.com/gofiber/contrib/tree/main/socketio) -- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) -- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) -- And much more, [explore Fiber](https://docs.gofiber.io/) +- 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/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/contrib/tree/main/websocket) +- [Socket.io Support](https://github.com/gofiber/contrib/tree/main/socketio) +- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- And much more, [explore Fiber](https://docs.gofiber.io/) ## 💡 Philosophy @@ -124,14 +124,14 @@ We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), ## ⚠️ Limitations -- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go versions 1.21 and 1.22. -- 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. +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go versions 1.22 and 1.23. +- 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). -#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) +### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) ```go func main() { @@ -615,7 +615,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team]( | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | | [contrib](https://github.com/gofiber/contrib) | Third party 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 9 template engines that can be used with Fiber `v3` Go version 1.21 or higher is required. | +| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3` Go version 1.22 or higher is required. | ## 🕶️ Awesome List @@ -634,14 +634,14 @@ If you want to say **Thank You** and/or support the active development of `Fiber To ensure your contributions are ready for a Pull Request, please use the following `Makefile` commands. These tools help maintain code quality, consistency. -* **make help**: Display available commands. -* **make audit**: Conduct quality checks. -* **make benchmark**: Benchmark code performance. -* **make coverage**: Generate test coverage report. -* **make format**: Automatically format code. -* **make lint**: Run lint checks. -* **make test**: Execute all tests. -* **make tidy**: Tidy dependencies. +- **make help**: Display available commands. +- **make audit**: Conduct quality checks. +- **make benchmark**: Benchmark code performance. +- **make coverage**: Generate test coverage report. +- **make format**: Automatically format code. +- **make lint**: Run lint checks. +- **make test**: Execute all tests. +- **make tidy**: Tidy dependencies. Run these commands to ensure your code adheres to project standards and best practices. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9d4826fe..a5919f86 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,6 +6,7 @@ 4. [Incident Response Process](#process) + ## Supported Versions The table below shows the supported versions for Fiber which include security updates. @@ -16,6 +17,7 @@ The table below shows the supported versions for Fiber which include security up | < 1.12.6 | :x: | + ## Reporting security problems to Fiber **DO NOT CREATE AN ISSUE** to report a security problem. Instead, please @@ -24,6 +26,7 @@ send us an e-mail at `team@gofiber.io` or join our discord server via to Fenny or any of the maintainers. + ## Security Point of Contact The security point of contact is [Fenny](https://github.com/Fenny). Fenny responds @@ -35,6 +38,7 @@ of contact are any of the [@maintainers](https://github.com/orgs/gofiber/teams/m The maintainers are the only other persons with administrative access to Fiber's source code. + ## Incident Response Process In case an incident is discovered or reported, we will follow the following @@ -73,4 +77,4 @@ for all of it's members. We learn about critical software updates and security threats from these sources 1. GitHub Security Alerts -2. GitHub: https://status.github.com/ & [@githubstatus](https://twitter.com/githubstatus) +2. GitHub: [https://status.github.com/](https://status.github.com/) & [@githubstatus](https://twitter.com/githubstatus) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 073b9603..c1382990 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ -## Description +# Description Please provide a clear and concise description of the changes you've made and the problem they address. Include the purpose of the change, any relevant issues it solves, and the benefits it brings to the project. If this change introduces new features or adjustments, highlight them here. diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index b0fcc599..7b2cdac1 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -5,6 +5,7 @@ on: types: [opened, edited, milestoned] pull_request_target: types: [opened] + permissions: contents: read issues: write diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 4a24c230..e8531e1c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,15 +3,19 @@ on: branches: - master - main - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" pull_request: - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" + +permissions: + # deployments permission to deploy GitHub pages website + deployments: write + # contents permission to update benchmark contents in gh-pages branch + contents: write + # allow posting comments to pull request + pull-requests: write name: Benchmark jobs: @@ -20,34 +24,88 @@ jobs: steps: - name: Fetch Repository uses: actions/checkout@v4 + with: + fetch-depth: 0 # to be able to retrieve the last commit in main - name: Install Go uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.21.x" + go-version: "1.22.x" - name: Run Benchmark run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt - - name: Get Previous Benchmark Results - uses: actions/cache@v4 + # NOTE: Benchmarks could change with different CPU types + - name: Get GitHub Runner System Information + uses: kenchan0130/actions-system-info@v1.3.0 + id: system-info + + - name: Get Main branch SHA + id: get-main-branch-sha + run: | + SHA=$(git rev-parse origin/main) + echo "sha=$SHA" >> $GITHUB_OUTPUT + + - name: Get Benchmark Results from main branch + id: cache + uses: actions/cache/restore@v4 with: path: ./cache - key: ${{ runner.os }}-benchmark + key: ${{ steps.get-main-branch-sha.outputs.sha }}-${{ runner.os }}-${{ steps.system-info.outputs.cpu-model }}-benchmark - - name: Save Benchmark Results + # This will only run if we have Benchmark Results from main branch + - name: Compare PR Benchmark Results with main branch uses: benchmark-action/github-action-benchmark@v1.20.3 + if: steps.cache.outputs.cache-hit == 'true' with: - tool: "go" + tool: 'go' output-file-path: output.txt - github-token: ${{ secrets.BENCHMARK_TOKEN }} - benchmark-data-dir-path: "benchmarks" + external-data-json-path: ./cache/benchmark-data.json + # Do not save the data (This allows comparing benchmarks) + save-data-file: false fail-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' }} + # Comment on the PR if the branch is not a fork + comment-on-alert: ${{ github.event.pull_request.head.repo.fork == false }} + github-token: ${{ secrets.GITHUB_TOKEN }} + summary-always: true + alert-threshold: "150%" + + - name: Store Benchmark Results for main branch + uses: benchmark-action/github-action-benchmark@v1.20.3 + if: ${{ github.ref_name == 'main' }} + with: + tool: 'go' + output-file-path: output.txt + external-data-json-path: ./cache/benchmark-data.json + # Save the data to external file (cache) + save-data-file: true + fail-on-alert: false + github-token: ${{ secrets.GITHUB_TOKEN }} + summary-always: true + alert-threshold: "150%" + + - name: Publish Benchmark Results to GitHub Pages + uses: benchmark-action/github-action-benchmark@v1.20.3 + if: ${{ github.ref_name == 'main' }} + with: + tool: 'go' + output-file-path: output.txt + benchmark-data-dir-path: "benchmarks" + fail-on-alert: false + github-token: ${{ secrets.GITHUB_TOKEN }} + comment-on-alert: true + summary-always: true + # Save the data to external file (GitHub Pages) + save-data-file: true + alert-threshold: "150%" # 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' }} + + - name: Update Benchmark Results cache + uses: actions/cache/save@v4 + if: ${{ github.ref_name == 'main' }} + with: + path: ./cache + key: ${{ steps.get-main-branch-sha.outputs.sha }}-${{ runner.os }}-${{ steps.system-info.outputs.cpu-model }}-benchmark diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 82036cc9..b145d1cd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,15 +5,11 @@ on: branches: - master - main - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" pull_request: - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" schedule: - cron: "0 3 * * 6" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 67a37b44..6c378e40 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -6,7 +6,11 @@ on: branches: - master - main + paths-ignore: + - "**/*.md" pull_request: + paths-ignore: + - "**/*.md" permissions: # Required: allow read access to the content for analysis. @@ -26,11 +30,11 @@ jobs: - uses: actions/setup-go@v5 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.21.x" + go-version: "1.22.x" cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: # NOTE: Keep this in sync with the version from .golangci.yml - version: v1.59.1 + version: v1.60.3 diff --git a/.github/workflows/manual-dependabot.yml b/.github/workflows/manual-dependabot.yml new file mode 100644 index 00000000..41e06303 --- /dev/null +++ b/.github/workflows/manual-dependabot.yml @@ -0,0 +1,46 @@ +# https://github.com/dependabot/dependabot-script/blob/main/manual-github-actions.yaml +# https://github.com/dependabot/dependabot-script?tab=readme-ov-file#github-actions-standalone +name: ManualDependabot + +on: + workflow_dispatch: + inputs: + package-manager: + description: 'The package manager to use' + required: true + default: 'gomod' + directory: + description: 'The directory to scan' + required: true + default: '/' + +permissions: + contents: read + +jobs: + dependabot: + permissions: + contents: write # for Git to git push + pull-requests: write # for repo-sync/pull-request to create pull requests + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Checkout dependabot + run: | + cd /tmp/ + git clone https://github.com/dependabot/dependabot-script + + - name: Build image + run: | + cd /tmp/dependabot-script + docker build -t "dependabot/dependabot-script" -f Dockerfile . + + - name: Run dependabot + env: + PACKAGE_MANAGER: ${{ github.event.inputs.package-manager }} + DIRECTORY: ${{ github.event.inputs.directory }} + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + docker run -v $PWD:/src -e PROJECT_PATH=$GITHUB_REPOSITORY -e PACKAGE_MANAGER=$PACKAGE_MANAGER -e DIRECTORY=$DIRECTORY -e GITHUB_ACCESS_TOKEN=$GITHUB_ACCESS_TOKEN -e OPTIONS="$OPTIONS" dependabot/dependabot-script diff --git a/.github/workflows/markdown.yml b/.github/workflows/markdown.yml new file mode 100644 index 00000000..cc131e7f --- /dev/null +++ b/.github/workflows/markdown.yml @@ -0,0 +1,22 @@ +name: markdownlint + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + markdownlint: + runs-on: ubuntu-latest + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Run markdownlint-cli2 + uses: DavidAnson/markdownlint-cli2-action@v16 + with: + globs: | + **/*.md + #vendor diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6706493..f7770993 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,22 +5,18 @@ on: branches: - master - main - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" pull_request: - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" jobs: unit: strategy: matrix: - go-version: [1.21.x, 1.22.x] - platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] + go-version: [1.22.x, 1.23.x] + platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: - name: Fetch Repository @@ -35,7 +31,7 @@ jobs: run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -shuffle=on - name: Upload coverage reports to Codecov - if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.22.x' }} + if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.23.x' }} uses: codecov/codecov-action@v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml index e6d29ccb..31c252eb 100644 --- a/.github/workflows/vulncheck.yml +++ b/.github/workflows/vulncheck.yml @@ -5,15 +5,11 @@ on: branches: - master - main - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" pull_request: - paths: - - "**" - - "!docs/**" - - "!**.md" + paths-ignore: + - "**/*.md" jobs: govulncheck-check: diff --git a/.golangci.yml b/.golangci.yml index f5e43b50..8b8d27b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -101,7 +101,6 @@ linters-settings: govet: enable-all: true disable: - - fieldalignment - shadow grouper: @@ -279,7 +278,7 @@ linters: - depguard - dogsled # - dupl - # - dupword # TODO: Enable + - dupword # TODO: Enable - durationcheck - errcheck - errchkjson @@ -288,7 +287,7 @@ linters: - exhaustive # - exhaustivestruct # - exhaustruct - - exportloopref + - copyloopvar - forbidigo - forcetypeassert # - funlen @@ -299,7 +298,7 @@ linters: # - gochecknoinits # TODO: Enable - gochecksumtype # - gocognit - # - goconst # TODO: Enable + - goconst # TODO: Enable - gocritic # - gocyclo # - godot @@ -308,9 +307,8 @@ linters: - gofmt - gofumpt # - goheader - # - goimports - # - golint - # - gomnd # TODO: Enable + - goimports + # - mnd # TODO: Enable - gomoddirectives # - gomodguard - goprintffuncname diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 00000000..71d7b309 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,248 @@ +# Example markdownlint configuration with all properties set to their default value + +# Default state for all rules +default: true + +# Path to configuration file to extend +extends: null + +# MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md001.md +MD001: true + +# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md003.md +MD003: + # Heading style + style: "consistent" + +# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md +MD004: + # List style + style: "consistent" + +# MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md005.md +MD005: true + +# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md007.md +MD007: + # Spaces for indent + indent: + # Whether to indent the first level of the list + start_indented: false + # Spaces for first level indent (when start_indented is set) + start_indent: 2 + +# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: true + +# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md +MD010: + # Include code blocks + code_blocks: true + # Fenced code languages to ignore + ignore_code_languages: [] + # Number of spaces for each hard tab + spaces_per_tab: 4 + +# MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md +MD011: true + +# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md +MD012: + # Consecutive blank lines + maximum: 1 + +# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md +MD013: false + +# MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md +MD014: true + +# MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md018.md +MD018: true + +# MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md019.md +MD019: true + +# MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md020.md +MD020: true + +# MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md021.md +MD021: true + +# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md +MD022: + # Blank lines above heading + lines_above: 1 + # Blank lines below heading + lines_below: 1 + +# MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md023.md +MD023: true + +# MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md +MD024: false + +# MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md025.md +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md026.md +MD026: + # Punctuation characters + punctuation: ".,;:!。,;:!" + +# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md +MD027: true + +# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md +MD028: true + +# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md +MD029: + # List style + style: "one_or_ordered" + +# MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md030.md +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md031.md +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md032.md +MD032: true + +# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md +MD033: false + +# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md +MD034: true + +# MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md035.md +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md036.md +MD036: + # Punctuation characters + punctuation: ".,;:!?。,;:!?" + +# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md037.md +MD037: true + +# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md +MD038: true + +# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md +MD039: true + +# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md +MD040: + # List of languages + allowed_languages: [] + # Require language only + language_only: false + +# MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md +MD042: true + +# MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md043.md +MD043: false + +# MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md044.md +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + # Include HTML elements + html_elements: true + +# MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md045.md +MD045: false + +# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md046.md +MD046: + # Block style + style: "fenced" + +# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md +MD047: true + +# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md048.md +MD048: + # Code fence style + style: "backtick" + +# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md +MD049: + # Emphasis style + style: "consistent" + +# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md +MD050: + # Strong style + style: "consistent" + +# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md +MD051: true + +# MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md052.md +MD052: + # Include shortcut syntax + shortcut_syntax: false + +# MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md053.md +MD053: + # Ignored definitions + ignored_definitions: + - "//" + +# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md054.md +MD054: + # Allow autolinks + autolink: false + # Allow inline links and images + inline: true + # Allow full reference links and images + full: true + # Allow collapsed reference links and images + collapsed: true + # Allow shortcut reference links and images + shortcut: true + # Allow URLs as inline links + url_inline: true + +# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md055.md +MD055: + # Table pipe style + style: "consistent" + +# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md056.md +MD056: true diff --git a/Makefile b/Makefile index 0d4be85f..1d7f57d0 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,15 @@ coverage: format: go run mvdan.cc/gofumpt@latest -w -l . +## markdown: 🎨 Find markdown format issues (Requires markdownlint-cli) +.PHONY: markdown +markdown: + markdownlint-cli2 "**/*.md" "#vendor" + ## lint: 🚨 Run lint checks .PHONY: lint lint: - go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1 run ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 run ./... ## test: 🚦 Execute all tests .PHONY: test @@ -46,3 +51,15 @@ longtest: .PHONY: tidy tidy: go mod tidy -v + +## betteralign: 📐 Optimize alignment of fields in structs +.PHONY: betteralign +betteralign: + go run github.com/dkorunic/betteralign/cmd/betteralign@latest -test_files -generated_files -apply ./... + +## generate: ⚡️ Generate msgp && interface implementations +.PHONY: generate +generate: + go install github.com/tinylib/msgp@latest + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c + go generate ./... diff --git a/addon/retry/README.md b/addon/retry/README.md index a07c7637..a3af4407 100644 --- a/addon/retry/README.md +++ b/addon/retry/README.md @@ -1,20 +1,20 @@ # Retry Addon Retry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network -operations. This addon uses exponential backoff algorithm with jitter. It calls the function multiple times and tries -to make it successful. If all calls are failed, then, it returns error. It adds a jitter at each retry step because adding -a jitter is a way to break synchronization across the client and avoid collision. +operations. This addon uses an exponential backoff algorithm with jitter. It calls the function multiple times and tries +to make it successful. If all calls are failed, then, it returns an error. It adds a jitter at each retry step because adding +a jitter is a way to break synchronization across the client and avoid collision. ## Table of Contents - [Retry Addon](#retry-addon) - - [Table of Contents](#table-of-contents) - - [Signatures](#signatures) - - [Examples](#examples) - - [Default Config](#default-config) - - [Custom Config](#custom-config) - - [Config](#config) - - [Default Config Example](#default-config-example) +- [Table of Contents](#table-of-contents) +- [Signatures](#signatures) +- [Examples](#examples) +- [Default Config](#default-config) +- [Custom Config](#custom-config) +- [Config](#config) +- [Default Config Example](#default-config-example) ## Signatures @@ -42,10 +42,10 @@ retry.NewExponentialBackoff() ```go retry.NewExponentialBackoff(retry.Config{ - InitialInterval: 2 * time.Second, - MaxBackoffTime: 64 * time.Second, - Multiplier: 2.0, - MaxRetryCount: 15, + InitialInterval: 2 * time.Second, + MaxBackoffTime: 64 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 15, }) ``` @@ -88,10 +88,10 @@ type Config struct { ```go // DefaultConfig is the default config for retry. var DefaultConfig = Config{ - InitialInterval: 1 * time.Second, - MaxBackoffTime: 32 * time.Second, - Multiplier: 2.0, - MaxRetryCount: 10, - currentInterval: 1 * time.Second, + InitialInterval: 1 * time.Second, + MaxBackoffTime: 32 * time.Second, + Multiplier: 2.0, + MaxRetryCount: 10, + currentInterval: 1 * time.Second, } -``` \ No newline at end of file +``` diff --git a/addon/retry/exponential_backoff_test.go b/addon/retry/exponential_backoff_test.go index 0961d4fa..844ed0df 100644 --- a/addon/retry/exponential_backoff_test.go +++ b/addon/retry/exponential_backoff_test.go @@ -11,10 +11,10 @@ import ( func Test_ExponentialBackoff_Retry(t *testing.T) { t.Parallel() tests := []struct { - name string + expErr error expBackoff *ExponentialBackoff f func() error - expErr error + name string }{ { name: "With default values - successful", @@ -51,7 +51,6 @@ func Test_ExponentialBackoff_Retry(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() err := tt.expBackoff.Retry(tt.f) @@ -106,7 +105,6 @@ func Test_ExponentialBackoff_Next(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() for i := 0; i < tt.expBackoff.MaxRetryCount; i++ { diff --git a/app.go b/app.go index 293f29ba..e0240d3c 100644 --- a/app.go +++ b/app.go @@ -80,29 +80,16 @@ type ErrorHandler = func(Ctx, error) error // Error represents an error that occurred while handling a request. type Error struct { - Code int `json:"code"` Message string `json:"message"` + Code int `json:"code"` } // App denotes the Fiber application. type App struct { - mutex sync.Mutex - // Route stack divided by HTTP methods - stack [][]*Route - // Route stack divided by HTTP methods and route prefixes - treeStack []map[string][]*Route - // contains the information if the route stack has been changed to build the optimized tree - routesRefreshed bool - // Amount of registered routes - routesCount uint32 - // Amount of registered handlers - handlersCount uint32 // Ctx pool pool sync.Pool // Fasthttp server server *fasthttp.Server - // App config - config Config // Converts string to a byte slice getBytes func(s string) (b []byte) // Converts byte slice to a string @@ -113,24 +100,37 @@ type App struct { latestRoute *Route // newCtxFunc newCtxFunc func(app *App) CustomCtx - // custom binders - customBinders []CustomBinder // TLS handler tlsHandler *TLSHandler // Mount fields mountFields *mountFields - // Indicates if the value was explicitly configured - configured Config + // Route stack divided by HTTP methods + stack [][]*Route + // Route stack divided by HTTP methods and route prefixes + treeStack []map[string][]*Route + // custom binders + customBinders []CustomBinder // customConstraints is a list of external constraints customConstraints []CustomConstraint // sendfiles stores configurations for handling ctx.SendFile operations sendfiles []*sendFileStore + // App config + config Config + // Indicates if the value was explicitly configured + configured Config // sendfilesMutex is a mutex used for sendfile operations sendfilesMutex sync.RWMutex + mutex sync.Mutex + // Amount of registered routes + routesCount uint32 + // Amount of registered handlers + handlersCount uint32 + // contains the information if the route stack has been changed to build the optimized tree + routesRefreshed bool } // Config is a struct holding the server settings. -type Config struct { +type Config struct { //nolint:govet // Aligning the struct fields is not necessary. betteralign:ignore // Enables the "Server: value" HTTP header. // // Default: "" diff --git a/bind_test.go b/bind_test.go index 9b291451..48f53f62 100644 --- a/bind_test.go +++ b/bind_test.go @@ -26,9 +26,9 @@ func Test_Bind_Query(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Query struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -53,14 +53,14 @@ func Test_Bind_Query(t *testing.T) { require.Empty(t, empty.Hobby) type Query2 struct { - Bool bool - ID int Name string Hobby string FavouriteDrinks []string Empty []string Alloc []string No []int64 + ID int + Bool bool } c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") @@ -237,8 +237,8 @@ func Test_Bind_Query_Schema(t *testing.T) { require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) type Node struct { - Value int `query:"val,required"` Next *Node `query:"next,required"` + Value int `query:"val,required"` } c.Request().URI().SetQueryString("val=1&next.val=3") n := new(Node) @@ -292,9 +292,9 @@ func Test_Bind_Header(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Header struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -318,14 +318,14 @@ func Test_Bind_Header(t *testing.T) { require.Empty(t, empty.Hobby) type Header2 struct { - Bool bool - ID int Name string Hobby string FavouriteDrinks []string Empty []string Alloc []string No []int64 + ID int + Bool bool } c.Request().Header.Add("id", "2") @@ -502,8 +502,8 @@ func Test_Bind_Header_Schema(t *testing.T) { require.Equal(t, "Nested.age is empty", c.Bind().Header(h2).Error()) type Node struct { - Value int `header:"Val,required"` Next *Node `header:"Next,required"` + Value int `header:"Val,required"` } c.Request().Header.Add("Val", "1") c.Request().Header.Add("Next.Val", "3") @@ -533,9 +533,9 @@ func Test_Bind_RespHeader(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Header struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -559,14 +559,14 @@ func Test_Bind_RespHeader(t *testing.T) { require.Empty(t, empty.Hobby) type Header2 struct { - Bool bool - ID int Name string Hobby string FavouriteDrinks []string Empty []string Alloc []string No []int64 + ID int + Bool bool } c.Response().Header.Add("id", "2") @@ -635,9 +635,9 @@ func Benchmark_Bind_Query(b *testing.B) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Query struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -708,9 +708,9 @@ func Benchmark_Bind_Query_Comma(b *testing.B) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Query struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -732,9 +732,9 @@ func Benchmark_Bind_Header(b *testing.B) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type ReqHeader struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -782,9 +782,9 @@ func Benchmark_Bind_RespHeader(b *testing.B) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type ReqHeader struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -1252,9 +1252,9 @@ func Test_Bind_Cookie(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Cookie struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") @@ -1278,14 +1278,14 @@ func Test_Bind_Cookie(t *testing.T) { require.Empty(t, empty.Hobby) type Cookie2 struct { - Bool bool - ID int Name string Hobby string FavouriteDrinks []string Empty []string Alloc []string No []int64 + ID int + Bool bool } c.Request().Header.SetCookie("id", "2") @@ -1463,8 +1463,8 @@ func Test_Bind_Cookie_Schema(t *testing.T) { require.Equal(t, "Nested.Age is empty", c.Bind().Cookie(h2).Error()) type Node struct { - Value int `cookie:"Val,required"` Next *Node `cookie:"Next,required"` + Value int `cookie:"Val,required"` } c.Request().Header.SetCookie("Val", "1") c.Request().Header.SetCookie("Next.Val", "3") @@ -1495,9 +1495,9 @@ func Benchmark_Bind_Cookie(b *testing.B) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Cookie struct { - ID int Name string Hobby []string + ID int } c.Request().SetBody([]byte(``)) c.Request().Header.SetContentType("") diff --git a/binder/README.md b/binder/README.md index d40cc7e5..676e1c9e 100644 --- a/binder/README.md +++ b/binder/README.md @@ -1,6 +1,7 @@ # Fiber Binders -Binder is new request/response binding feature for Fiber. By aganist old Fiber parsers, it supports custom binder registration, struct validation, **map[string]string**, **map[string][]string** and more. It's introduced in Fiber v3 and a replacement of: +Binder is a new request/response binding feature for Fiber. Against the old Fiber parsers, it supports custom binder registration, struct validation, `map[string]string`, `map[string][]string`, and more. It's introduced in Fiber v3 and a replacement of: + - BodyParser - ParamsParser - GetReqHeaders @@ -9,8 +10,8 @@ Binder is new request/response binding feature for Fiber. By aganist old Fiber p - QueryParser - ReqHeaderParser - ## Default Binders + - [Form](form.go) - [Query](query.go) - [URI](uri.go) @@ -23,7 +24,9 @@ Binder is new request/response binding feature for Fiber. By aganist old Fiber p ## Guides ### Binding into the Struct -Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example for it: + +Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example: + ```go // Field names should start with an uppercase letter type Person struct { @@ -32,16 +35,16 @@ type Person struct { } app.Post("/", func(c fiber.Ctx) error { - p := new(Person) + p := new(Person) - if err := c.Bind().Body(p); err != nil { - return err - } + if err := c.Bind().Body(p); err != nil { + return err + } - log.Println(p.Name) // john - log.Println(p.Pass) // doe + log.Println(p.Name) // john + log.Println(p.Pass) // doe - // ... + // ... }) // Run tests with the following curl commands: @@ -58,29 +61,34 @@ app.Post("/", func(c fiber.Ctx) error { ``` ### Binding into the Map -Fiber supports binding into the **map[string]string** or **map[string][]string**. Here's an example for it: + +Fiber supports binding into the `map[string]string` or `map[string][]string`. Here's an example: + ```go app.Get("/", func(c fiber.Ctx) error { - p := make(map[string][]string) + p := make(map[string][]string) - if err := c.Bind().Query(p); err != nil { - return err - } + if err := c.Bind().Query(p); err != nil { + return err + } - log.Println(p["name"]) // john - log.Println(p["pass"]) // doe - log.Println(p["products"]) // [shoe, hat] + 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" ``` + ### Behaviors of Should/Must -Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`. + +Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`. If there's an error it'll return error and 400 as HTTP status. Here's an example for it: + ```go // Field names should start with an uppercase letter type Person struct { @@ -89,106 +97,112 @@ type Person struct { } app.Get("/", func(c fiber.Ctx) error { - p := new(Person) + p := new(Person) - if err := c.Bind().Must().JSON(p); err != nil { - return err - // Status code: 400 - // Response: Bad request: name is empty - } + if err := c.Bind().Must().JSON(p); err != nil { + return err + // Status code: 400 + // Response: Bad request: name is empty + } - // ... + // ... }) // Run tests with the following curl command: // curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000 ``` + ### Defining Custom Binder -We didn't add much binder to make Fiber codebase minimal. But if you want to use your binders, it's easy to register and use them. Here's an example for TOML binder. + +We didn't add much binder to make Fiber codebase minimal. If you want to use your own binders, it's easy to register and use them. Here's an example for TOML binder. + ```go type Person struct { - Name string `toml:"name"` - Pass string `toml:"pass"` + Name string `toml:"name"` + Pass string `toml:"pass"` } type tomlBinding struct{} func (b *tomlBinding) Name() string { - return "toml" + return "toml" } func (b *tomlBinding) MIMETypes() []string { - return []string{"application/toml"} + return []string{"application/toml"} } func (b *tomlBinding) Parse(c fiber.Ctx, out any) error { - return toml.Unmarshal(c.Body(), out) + return toml.Unmarshal(c.Body(), out) } func main() { - app := fiber.New() - app.RegisterCustomBinder(&tomlBinding{}) + app := fiber.New() + app.RegisterCustomBinder(&tomlBinding{}) - app.Get("/", func(c fiber.Ctx) error { - out := new(Person) - if err := c.Bind().Body(out); err != nil { - return err - } + app.Get("/", func(c fiber.Ctx) error { + out := new(Person) + if err := c.Bind().Body(out); err != nil { + return err + } - // or you can use like: - // if err := c.Bind().Custom("toml", out); err != nil { - // return err - // } + // or you can use like: + // if err := c.Bind().Custom("toml", out); err != nil { + // return err + // } - return c.SendString(out.Pass) // test - }) + return c.SendString(out.Pass) // test + }) - app.Listen(":3000") + app.Listen(":3000") } // curl -X GET -H "Content-Type: application/toml" --data "name = 'bar' // pass = 'test'" localhost:3000 ``` + ### Defining Custom Validator + All Fiber binders supporting struct validation if you defined validator inside of the config. You can create own validator, or use [go-playground/validator](https://github.com/go-playground/validator), [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)... Here's an example of simple custom validator: + ```go type Query struct { - Name string `query:"name"` + Name string `query:"name"` } type structValidator struct{} func (v *structValidator) Engine() any { - return "" + return "" } func (v *structValidator) ValidateStruct(out any) error { - out = reflect.ValueOf(out).Elem().Interface() - sq := out.(Query) + out = reflect.ValueOf(out).Elem().Interface() + sq := out.(Query) - if sq.Name != "john" { - return errors.New("you should have entered right name!") - } + if sq.Name != "john" { + return errors.New("you should have entered right name!") + } - return nil + return nil } func main() { - app := fiber.New(fiber.Config{StructValidator: &structValidator{}}) + app := fiber.New(fiber.Config{StructValidator: &structValidator{}}) - app.Get("/", func(c fiber.Ctx) error { - out := new(Query) - if err := c.Bind().Query(out); err != nil { - return err // you should have entered right name! - } - return c.SendString(out.Name) - }) + app.Get("/", func(c fiber.Ctx) error { + out := new(Query) + if err := c.Bind().Query(out); err != nil { + return err // you should have entered right name! + } + return c.SendString(out.Name) + }) - app.Listen(":3000") + app.Listen(":3000") } // Run tests with the following curl command: // curl "http://localhost:3000/?name=efe" -``` \ No newline at end of file +``` diff --git a/binder/form.go b/binder/form.go index 0b469086..f45407fe 100644 --- a/binder/form.go +++ b/binder/form.go @@ -40,6 +40,10 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { } }) + if err != nil { + return err + } + return parse(b.Name(), out, data) } diff --git a/binder/mapping.go b/binder/mapping.go index 7a09140e..07af94a1 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -12,9 +12,9 @@ import ( // ParserConfig form decoder config for SetParserDecoder type ParserConfig struct { - IgnoreUnknownKeys bool SetAliasTag string ParserType []ParserType + IgnoreUnknownKeys bool ZeroEmpty bool } diff --git a/client/client.go b/client/client.go index e9c65e14..d9b9c84c 100644 --- a/client/client.go +++ b/client/client.go @@ -7,9 +7,7 @@ import ( "encoding/json" "encoding/xml" "errors" - "fmt" "io" - urlpkg "net/url" "os" "path/filepath" "sync" @@ -20,12 +18,10 @@ import ( "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpproxy" ) -var ( - ErrInvalidProxyURL = errors.New("invalid proxy url scheme") - ErrFailedToAppendCert = errors.New("failed to append certificate") -) +var ErrFailedToAppendCert = errors.New("failed to append certificate") // The Client is used to create a Fiber Client with // client-level settings that apply to all requests @@ -34,21 +30,29 @@ var ( // Fiber Client also provides an option to override // or merge most of the client settings at the request. type Client struct { - mu sync.RWMutex + // logger + logger log.CommonLogger fasthttp *fasthttp.Client + header *Header + params *QueryParam + cookies *Cookie + path *PathParam + + jsonMarshal utils.JSONMarshal + jsonUnmarshal utils.JSONUnmarshal + xmlMarshal utils.XMLMarshal + xmlUnmarshal utils.XMLUnmarshal + + cookieJar *CookieJar + + // retry + retryConfig *RetryConfig + baseURL string userAgent string referer string - header *Header - params *QueryParam - cookies *Cookie - path *PathParam - - debug bool - - timeout time.Duration // user defined request hooks userRequestHooks []RequestHook @@ -62,21 +66,11 @@ type Client struct { // client package defined response hooks builtinResponseHooks []ResponseHook - jsonMarshal utils.JSONMarshal - jsonUnmarshal utils.JSONUnmarshal - xmlMarshal utils.XMLMarshal - xmlUnmarshal utils.XMLUnmarshal + timeout time.Duration - cookieJar *CookieJar + mu sync.RWMutex - // proxy - proxyURL string - - // retry - retryConfig *RetryConfig - - // logger - logger log.CommonLogger + debug bool } // R raise a request from the client. @@ -228,16 +222,7 @@ func (c *Client) SetRootCertificateFromString(pem string) *Client { // SetProxyURL sets proxy url in client. It will apply via core to hostclient. func (c *Client) SetProxyURL(proxyURL string) error { - pURL, err := urlpkg.Parse(proxyURL) - if err != nil { - return fmt.Errorf("client: %w", err) - } - - if pURL.Scheme != "http" && pURL.Scheme != "https" { - return fmt.Errorf("client: %w", ErrInvalidProxyURL) - } - - c.proxyURL = pURL.String() + c.fasthttp.Dial = fasthttpproxy.FasthttpHTTPDialer(proxyURL) return nil } @@ -581,7 +566,6 @@ func (c *Client) Reset() { c.timeout = 0 c.userAgent = "" c.referer = "" - c.proxyURL = "" c.retryConfig = nil c.debug = false @@ -604,19 +588,20 @@ func (c *Client) Reset() { type Config struct { Ctx context.Context //nolint:containedctx // It's needed to be stored in the config. - UserAgent string - Referer string + Body any Header map[string]string Param map[string]string Cookie map[string]string PathParam map[string]string + FormData map[string]string + + UserAgent string + Referer string + File []*File + Timeout time.Duration MaxRedirects int - - Body any - FormData map[string]string - File []*File } // setConfigToRequest Set the parameters passed via Config to Request. @@ -672,7 +657,7 @@ func setConfigToRequest(req *Request, config ...Config) { return } - if cfg.File != nil && len(cfg.File) != 0 { + if len(cfg.File) != 0 { req.AddFiles(cfg.File...) return } diff --git a/client/client_test.go b/client/client_test.go index 0f81dda9..b8dd39bb 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" ) func startTestServerWithPort(t *testing.T, beforeStarting func(app *fiber.App)) (*fiber.App, string) { @@ -835,8 +836,8 @@ func Test_Client_Cookie(t *testing.T) { t.Run("set cookies with struct", func(t *testing.T) { t.Parallel() type args struct { - CookieInt int `cookie:"int"` CookieString string `cookie:"string"` + CookieInt int `cookie:"int"` } req := New().SetCookiesWithStruct(&args{ @@ -1087,12 +1088,12 @@ func Test_Client_QueryParam(t *testing.T) { t.Parallel() type args struct { - TInt int TString string - TFloat float64 - TBool bool TSlice []string TIntSlice []int `param:"int_slice"` + TInt int + TFloat float64 + TBool bool } p := New() @@ -1195,8 +1196,8 @@ func Test_Client_PathParam(t *testing.T) { t.Run("set path params with struct", func(t *testing.T) { t.Parallel() type args struct { - CookieInt int `path:"int"` CookieString string `path:"string"` + CookieInt int `path:"int"` } req := New().SetPathParamsWithStruct(&args{ @@ -1422,7 +1423,7 @@ func Test_Set_Config_To_Request(t *testing.T) { key := struct{}{} ctx := context.Background() - ctx = context.WithValue(ctx, key, "v1") + ctx = context.WithValue(ctx, key, "v1") //nolint: staticcheck // not needed for tests req := AcquireRequest() @@ -1539,11 +1540,53 @@ func Test_Client_SetProxyURL(t *testing.T) { app, dial, start := createHelperServer(t) app.Get("/", func(c fiber.Ctx) error { - return c.SendString("hello world") + return c.SendString(c.Get("isProxy")) }) go start() + fasthttpClient := &fasthttp.Client{ + Dial: dial, + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + } + + // Create a simple proxy sever + proxyServer := fiber.New() + + proxyServer.Use("*", func(c fiber.Ctx) error { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + + req.SetRequestURI(c.BaseURL()) + req.Header.SetMethod(fasthttp.MethodGet) + + c.Request().Header.VisitAll(func(key, value []byte) { + req.Header.AddBytesKV(key, value) + }) + + req.Header.Set("isProxy", "true") + + if err := fasthttpClient.Do(req, resp); err != nil { + return err + } + + c.Status(resp.StatusCode()) + c.Context().SetBody(resp.Body()) + + return nil + }) + + addrChan := make(chan string) + go func() { + assert.NoError(t, proxyServer.Listen(":0", fiber.ListenConfig{ + DisableStartupMessage: true, + ListenerAddrFunc: func(addr net.Addr) { + addrChan <- addr.String() + }, + })) + }() + t.Cleanup(func() { require.NoError(t, app.Shutdown()) }) @@ -1552,31 +1595,27 @@ func Test_Client_SetProxyURL(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() - client := New().SetDial(dial) - err := client.SetProxyURL("http://test.com") - require.NoError(t, err) - - _, err = client.Get("http://localhost:3000") - - require.NoError(t, err) - }) - - t.Run("wrong url", func(t *testing.T) { - t.Parallel() client := New() + err := client.SetProxyURL(<-addrChan) - err := client.SetProxyURL(":this is not a url") + require.NoError(t, err) - require.Error(t, err) + resp, err := client.Get("http://localhost:3000") + require.NoError(t, err) + + require.Equal(t, 200, resp.StatusCode()) + require.Equal(t, "true", string(resp.Body())) }) t.Run("error", func(t *testing.T) { t.Parallel() client := New() - err := client.SetProxyURL("htgdftp://test.com") + err := client.SetProxyURL(":this is not a proxy") + require.NoError(t, err) + _, err = client.Get("http://localhost:3000") require.Error(t, err) }) } diff --git a/client/cookiejar.go b/client/cookiejar.go index c66d5f3b..834357fb 100644 --- a/client/cookiejar.go +++ b/client/cookiejar.go @@ -36,8 +36,8 @@ func ReleaseCookieJar(c *CookieJar) { // CookieJar manages cookie storage. It is used by the client to store cookies. type CookieJar struct { - mu sync.Mutex hostCookies map[string][]*fasthttp.Cookie + mu sync.Mutex } // Get returns the cookies stored from a specific domain. diff --git a/client/core_test.go b/client/core_test.go index c985784c..36e8eb78 100644 --- a/client/core_test.go +++ b/client/core_test.go @@ -22,8 +22,8 @@ func Test_AddMissing_Port(t *testing.T) { } tests := []struct { name string - args args want string + args args }{ { name: "do anything", @@ -49,7 +49,6 @@ func Test_AddMissing_Port(t *testing.T) { }, } for _, tt := range tests { - tt := tt // create a new 'tt' variable for the goroutine t.Run(tt.name, func(t *testing.T) { t.Parallel() require.Equal(t, tt.want, addMissingPort(tt.args.addr, tt.args.isTLS)) diff --git a/client/hooks_test.go b/client/hooks_test.go index dfea361d..359e3184 100644 --- a/client/hooks_test.go +++ b/client/hooks_test.go @@ -35,7 +35,6 @@ func Test_Rand_String(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := randString(tt.args) @@ -188,7 +187,7 @@ func Test_Parser_Request_URL(t *testing.T) { flag1 = true case "foo2": flag2 = true - case "foo": + case "foo": //nolint:goconst // test flag3 = true } } diff --git a/client/request.go b/client/request.go index ebddb0d0..61b5798c 100644 --- a/client/request.go +++ b/client/request.go @@ -40,28 +40,30 @@ var ErrClientNil = errors.New("client can not be nil") // Request is a struct which contains the request data. type Request struct { - url string - method string - userAgent string - boundary string - referer string - ctx context.Context //nolint:containedctx // It's needed to be stored in the request. - header *Header - params *QueryParam - cookies *Cookie - path *PathParam + ctx context.Context //nolint:containedctx // It's needed to be stored in the request. + + body any + header *Header + params *QueryParam + cookies *Cookie + path *PathParam + + client *Client + + formData *FormData + + RawRequest *fasthttp.Request + url string + method string + userAgent string + boundary string + referer string + files []*File timeout time.Duration maxRedirects int - client *Client - - body any - formData *FormData - files []*File bodyType bodyType - - RawRequest *fasthttp.Request } // Method returns http method in request. @@ -782,10 +784,10 @@ func (f *FormData) Reset() { // File is a struct which support send files via request. type File struct { + reader io.ReadCloser name string fieldName string path string - reader io.ReadCloser } // SetName method sets file name. diff --git a/client/request_test.go b/client/request_test.go index e5369fbb..0593d048 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -84,7 +84,7 @@ func Test_Request_Context(t *testing.T) { require.Nil(t, ctx.Value(key)) - ctx = context.WithValue(ctx, key, "string") + ctx = context.WithValue(ctx, key, "string") //nolint: staticcheck // not needed for tests req.SetContext(ctx) ctx = req.Context() @@ -222,12 +222,12 @@ func Test_Request_QueryParam(t *testing.T) { t.Parallel() type args struct { - TInt int TString string - TFloat float64 - TBool bool TSlice []string TIntSlice []int `param:"int_slice"` + TInt int + TFloat float64 + TBool bool } p := AcquireRequest() @@ -334,8 +334,8 @@ func Test_Request_Cookie(t *testing.T) { t.Run("set cookies with struct", func(t *testing.T) { t.Parallel() type args struct { - CookieInt int `cookie:"int"` CookieString string `cookie:"string"` + CookieInt int `cookie:"int"` } req := AcquireRequest().SetCookiesWithStruct(&args{ @@ -396,8 +396,8 @@ func Test_Request_PathParam(t *testing.T) { t.Run("set path params with struct", func(t *testing.T) { t.Parallel() type args struct { - CookieInt int `path:"int"` CookieString string `path:"string"` + CookieInt int `path:"int"` } req := AcquireRequest().SetPathParamsWithStruct(&args{ @@ -510,12 +510,12 @@ func Test_Request_FormData(t *testing.T) { t.Parallel() type args struct { - TInt int TString string - TFloat float64 - TBool bool TSlice []string TIntSlice []int `form:"int_slice"` + TInt int + TFloat float64 + TBool bool } p := AcquireRequest() @@ -1299,13 +1299,13 @@ func Test_SetValWithStruct(t *testing.T) { // test SetValWithStruct vai QueryParam struct. type args struct { - unexport int - TInt int TString string - TFloat float64 - TBool bool TSlice []string TIntSlice []int `param:"int_slice"` + unexport int + TInt int + TFloat float64 + TBool bool } t.Run("the struct should be applied", func(t *testing.T) { @@ -1340,7 +1340,7 @@ func Test_SetValWithStruct(t *testing.T) { require.True(t, func() bool { for _, v := range p.PeekMulti("TSlice") { - if string(v) == "bar" { + if string(v) == "bar" { //nolint:goconst // test return true } } @@ -1453,13 +1453,13 @@ func Test_SetValWithStruct(t *testing.T) { func Benchmark_SetValWithStruct(b *testing.B) { // test SetValWithStruct vai QueryParam struct. type args struct { - unexport int - TInt int TString string - TFloat float64 - TBool bool TSlice []string TIntSlice []int `param:"int_slice"` + unexport int + TInt int + TFloat float64 + TBool bool } b.Run("the struct should be applied", func(b *testing.B) { @@ -1603,8 +1603,8 @@ func Benchmark_SetValWithStruct(b *testing.B) { require.Empty(b, string(p.Peek("TInt"))) require.Empty(b, string(p.Peek("TString"))) require.Empty(b, string(p.Peek("TFloat"))) - require.Empty(b, len(p.PeekMulti("TSlice"))) - require.Empty(b, len(p.PeekMulti("int_slice"))) + require.Empty(b, p.PeekMulti("TSlice")) + require.Empty(b, p.PeekMulti("int_slice")) }) b.Run("error type should ignore", func(b *testing.B) { diff --git a/client/response.go b/client/response.go index adb70ac4..a8a032b6 100644 --- a/client/response.go +++ b/client/response.go @@ -8,7 +8,6 @@ import ( "io/fs" "os" "path/filepath" - "strings" "sync" "github.com/gofiber/utils/v2" @@ -19,9 +18,9 @@ import ( type Response struct { client *Client request *Request - cookie []*fasthttp.Cookie RawResponse *fasthttp.Response + cookie []*fasthttp.Cookie } // setClient method sets client object in response instance. @@ -68,7 +67,7 @@ func (r *Response) Body() []byte { // String method returns the body of the server response as String. func (r *Response) String() string { - return strings.TrimSpace(string(r.Body())) + return utils.Trim(string(r.Body()), ' ') } // JSON method will unmarshal body to json. diff --git a/ctx.go b/ctx.go index 78eedc19..4d7417ee 100644 --- a/ctx.go +++ b/ctx.go @@ -50,24 +50,24 @@ const userContextKey contextKey = 0 // __local_user_context__ type DefaultCtx struct { app *App // Reference to *App route *Route // Reference to *Route - indexRoute int // Index of the current route - indexHandler int // Index of the current handler - method string // HTTP method - methodINT int // HTTP method INT equivalent - baseURI string // HTTP base uri - path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer - pathBuffer []byte // HTTP path buffer - detectionPath string // Route detection path -> string copy from detectionPathBuffer - detectionPathBuffer []byte // HTTP detectionPath buffer - treePath string // Path for the search in the tree - pathOriginal string // Original HTTP path - values [maxParams]string // Route parameter values fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - matched bool // Non use route matched - viewBindMap sync.Map // Default view map to bind template engine bind *Bind // Default bind reference redirect *Redirect // Default redirect reference - redirectionMessages []string // Messages of the previous redirect + values [maxParams]string // Route parameter values + viewBindMap sync.Map // Default view map to bind template engine + method string // HTTP method + baseURI string // HTTP base uri + path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer + detectionPath string // Route detection path -> string copy from detectionPathBuffer + treePath string // Path for the search in the tree + pathOriginal string // Original HTTP path + pathBuffer []byte // HTTP path buffer + detectionPathBuffer []byte // HTTP detectionPath buffer + flashMessages redirectionMsgs // Flash messages + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + methodINT int // HTTP method INT equivalent + matched bool // Non use route matched } // SendFile defines configuration options when to transfer file with SendFile. @@ -112,8 +112,8 @@ type SendFile struct { // sendFileStore is used to keep the SendFile configuration and the handler. type sendFileStore struct { handler fasthttp.RequestHandler - config SendFile cacheControlValue string + config SendFile } // compareConfig compares the current SendFile config with the new one @@ -175,15 +175,15 @@ type RangeSet struct { // Cookie data for c.Cookie type Cookie struct { + Expires time.Time `json:"expires"` // The expiration date of the cookie Name string `json:"name"` // The name of the cookie Value string `json:"value"` // The value of the cookie Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie + SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie - Expires time.Time `json:"expires"` // The expiration date of the cookie Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol - SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie } @@ -196,8 +196,8 @@ type Views interface { // ResFmt associates a Content Type to a fiber.Handler for c.Format type ResFmt struct { - MediaType string Handler func(Ctx) error + MediaType string } // Accepts checks if the specified extensions or content types are acceptable. @@ -273,10 +273,7 @@ func (c *DefaultCtx) BaseURL() string { // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. func (c *DefaultCtx) BodyRaw() []byte { - if c.app.config.Immutable { - return utils.CopyBytes(c.fasthttp.Request.Body()) - } - return c.fasthttp.Request.Body() + return c.getBody() } func (c *DefaultCtx) tryDecodeBodyInOrder( @@ -339,21 +336,19 @@ func (c *DefaultCtx) Body() []byte { encodingOrder = []string{"", "", ""} ) - // faster than peek - c.Request().Header.VisitAll(func(key, value []byte) { - if c.app.getString(key) == HeaderContentEncoding { - headerEncoding = c.app.getString(value) - } - }) + // Get Content-Encoding header + headerEncoding = utils.UnsafeString(c.Request().Header.ContentEncoding()) + + // If no encoding is provided, return the original body + if len(headerEncoding) == 0 { + return c.getBody() + } // Split and get the encodings list, in order to attend the // rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5 encodingOrder = getSplicedStrList(headerEncoding, encodingOrder) if len(encodingOrder) == 0 { - if c.app.config.Immutable { - return utils.CopyBytes(c.fasthttp.Request.Body()) - } - return c.fasthttp.Request.Body() + return c.getBody() } var decodesRealized uint8 @@ -778,7 +773,7 @@ iploop: i++ } - s := strings.TrimRight(headerValue[i:j], " ") + s := utils.TrimRight(headerValue[i:j], ' ') if c.app.config.EnableIPValidation { // Skip validation if IP is clearly not IPv4/IPv6, otherwise validate without allocations @@ -828,7 +823,7 @@ func (c *DefaultCtx) extractIPFromHeader(header string) string { i++ } - s := strings.TrimRight(headerValue[i:j], " ") + s := utils.TrimRight(headerValue[i:j], ' ') if c.app.config.EnableIPValidation { if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) { @@ -862,7 +857,7 @@ func (c *DefaultCtx) Is(extension string) bool { } return strings.HasPrefix( - strings.TrimLeft(utils.UnsafeString(c.fasthttp.Request.Header.ContentType()), " "), + utils.TrimLeft(utils.UnsafeString(c.fasthttp.Request.Header.ContentType()), ' '), extensionHeader, ) } @@ -939,7 +934,7 @@ func (c *DefaultCtx) Links(link ...string) { bb.WriteString(`; rel="` + link[i] + `",`) } } - c.setCanonical(HeaderLink, strings.TrimRight(c.app.getString(bb.Bytes()), ",")) + c.setCanonical(HeaderLink, utils.TrimRight(c.app.getString(bb.Bytes()), ',')) bytebufferpool.Put(bb) } @@ -1023,19 +1018,20 @@ func (c *DefaultCtx) ClientHelloInfo() *tls.ClientHelloInfo { func (c *DefaultCtx) Next() error { // Increment handler index c.indexHandler++ - 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) - } else { - // Continue handler stack - if c.app.newCtxFunc != nil { - _, err = c.app.nextCustom(c) - } else { - _, err = c.app.next(c) - } + return c.route.Handlers[c.indexHandler](c) } + + // Continue handler stack + if c.app.newCtxFunc != nil { + _, err := c.app.nextCustom(c) + return err + } + + _, err := c.app.next(c) return err } @@ -1284,8 +1280,8 @@ func (c *DefaultCtx) Range(size int) (Range, error) { Start int End int }{ - start, - end, + Start: start, + End: end, }) } if len(rangeData.Ranges) < 1 { @@ -1809,7 +1805,7 @@ func (c *DefaultCtx) configDependentPaths() { } // If StrictRouting is disabled, we strip all trailing slashes if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { - c.detectionPathBuffer = bytes.TrimRight(c.detectionPathBuffer, "/") + c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/') } c.detectionPath = c.app.getString(c.detectionPathBuffer) @@ -1900,7 +1896,7 @@ func (c *DefaultCtx) release() { c.route = nil c.fasthttp = nil c.bind = nil - c.redirectionMessages = c.redirectionMessages[:0] + c.flashMessages = c.flashMessages[:0] c.viewBindMap = sync.Map{} if c.redirect != nil { ReleaseRedirect(c.redirect) @@ -1908,6 +1904,14 @@ func (c *DefaultCtx) release() { } } +func (c *DefaultCtx) getBody() []byte { + if c.app.config.Immutable { + return utils.CopyBytes(c.fasthttp.Request.Body()) + } + + return c.fasthttp.Request.Body() +} + // Methods to use with next stack. func (c *DefaultCtx) getMethodINT() int { return c.methodINT diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index bffbe79d..7709f7c9 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -330,6 +330,7 @@ type Ctx interface { Reset(fctx *fasthttp.RequestCtx) // Release is a method to reset context fields when to use ReleaseCtx() release() + getBody() []byte // Methods to use with next stack. getMethodINT() int getIndexRoute() int diff --git a/ctx_test.go b/ctx_test.go index e27aa421..6dcd7410 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -356,6 +356,26 @@ func Test_Ctx_Body(t *testing.T) { require.Equal(t, []byte("john=doe"), c.Body()) } +// go test -run Test_Ctx_BodyRaw +func Test_Ctx_BodyRaw(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + c.Request().SetBodyRaw([]byte("john=doe")) + require.Equal(t, []byte("john=doe"), c.BodyRaw()) +} + +// go test -run Test_Ctx_BodyRaw_Immutable +func Test_Ctx_BodyRaw_Immutable(t *testing.T) { + t.Parallel() + app := New(Config{Immutable: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + c.Request().SetBodyRaw([]byte("john=doe")) + require.Equal(t, []byte("john=doe"), c.BodyRaw()) +} + // go test -v -run=^$ -bench=Benchmark_Ctx_Body -benchmem -count=4 func Benchmark_Ctx_Body(b *testing.B) { const input = "john=doe" @@ -373,6 +393,40 @@ func Benchmark_Ctx_Body(b *testing.B) { require.Equal(b, []byte(input), c.Body()) } +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyRaw -benchmem -count=4 +func Benchmark_Ctx_BodyRaw(b *testing.B) { + const input = "john=doe" + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + c.Request().SetBodyRaw([]byte(input)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = c.BodyRaw() + } + + require.Equal(b, []byte(input), c.BodyRaw()) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyRaw_Immutable -benchmem -count=4 +func Benchmark_Ctx_BodyRaw_Immutable(b *testing.B) { + const input = "john=doe" + + app := New(Config{Immutable: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + c.Request().SetBodyRaw([]byte(input)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = c.BodyRaw() + } + + require.Equal(b, []byte(input), c.BodyRaw()) +} + // go test -run Test_Ctx_Body_Immutable func Test_Ctx_Body_Immutable(t *testing.T) { t.Parallel() @@ -509,8 +563,8 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) { } ) compressionTests := []struct { - contentEncoding string compressWriter func([]byte) ([]byte, error) + contentEncoding string }{ { contentEncoding: "gzip", @@ -702,8 +756,8 @@ func Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) { } ) compressionTests := []struct { - contentEncoding string compressWriter func([]byte) ([]byte, error) + contentEncoding string }{ { contentEncoding: "gzip", @@ -813,7 +867,7 @@ func Test_Ctx_UserContext(t *testing.T) { t.Parallel() testKey := struct{}{} testValue := "Test Value" - ctx := context.WithValue(context.Background(), testKey, testValue) + ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests require.Equal(t, testValue, ctx.Value(testKey)) }) } @@ -826,7 +880,7 @@ func Test_Ctx_SetUserContext(t *testing.T) { testKey := struct{}{} testValue := "Test Value" - ctx := context.WithValue(context.Background(), testKey, testValue) + ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests c.SetUserContext(ctx) require.Equal(t, testValue, c.UserContext().Value(testKey)) } @@ -846,7 +900,7 @@ func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { } input := utils.CopyString(Query(c, "input", "NO_VALUE")) - ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) + ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint: staticcheck // not needed for tests c.SetUserContext(ctx) return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input)) @@ -854,7 +908,6 @@ func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { // Consecutive Requests for i := 1; i <= 10; i++ { - i := i t.Run(fmt.Sprintf("request_%d", i), func(t *testing.T) { t.Parallel() resp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf("/?input=%d", i), nil)) @@ -966,7 +1019,7 @@ func Test_Ctx_Format(t *testing.T) { fmts := []ResFmt{} for _, t := range types { t := utils.CopyString(t) - fmts = append(fmts, ResFmt{t, func(_ Ctx) error { + fmts = append(fmts, ResFmt{MediaType: t, Handler: func(_ Ctx) error { accepted = t return nil }}) @@ -988,11 +1041,11 @@ func Test_Ctx_Format(t *testing.T) { require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) myError := errors.New("this is an error") - err = c.Format(ResFmt{"text/html", func(_ Ctx) error { return myError }}) + err = c.Format(ResFmt{MediaType: "text/html", Handler: func(_ Ctx) error { return myError }}) require.ErrorIs(t, err, myError) c.Request().Header.Set(HeaderAccept, "application/json") - err = c.Format(ResFmt{"text/html", func(c Ctx) error { return c.SendStatus(StatusOK) }}) + err = c.Format(ResFmt{MediaType: "text/html", Handler: func(c Ctx) error { return c.SendStatus(StatusOK) }}) require.Equal(t, StatusNotAcceptable, c.Response().StatusCode()) require.NoError(t, err) @@ -1022,10 +1075,10 @@ func Benchmark_Ctx_Format(b *testing.B) { b.Run("with arg allocation", func(b *testing.B) { for n := 0; n < b.N; n++ { err = c.Format( - ResFmt{"application/xml", fail}, - ResFmt{"text/html", fail}, - ResFmt{"text/plain;format=fixed", fail}, - ResFmt{"text/plain;format=flowed", ok}, + ResFmt{MediaType: "application/xml", Handler: fail}, + ResFmt{MediaType: "text/html", Handler: fail}, + ResFmt{MediaType: "text/plain;format=fixed", Handler: fail}, + ResFmt{MediaType: "text/plain;format=flowed", Handler: ok}, ) } require.NoError(b, err) @@ -1033,10 +1086,10 @@ func Benchmark_Ctx_Format(b *testing.B) { b.Run("pre-allocated args", func(b *testing.B) { offers := []ResFmt{ - {"application/xml", fail}, - {"text/html", fail}, - {"text/plain;format=fixed", fail}, - {"text/plain;format=flowed", ok}, + {MediaType: "application/xml", Handler: fail}, + {MediaType: "text/html", Handler: fail}, + {MediaType: "text/plain;format=fixed", Handler: fail}, + {MediaType: "text/plain;format=flowed", Handler: ok}, } for n := 0; n < b.N; n++ { err = c.Format(offers...) @@ -1047,8 +1100,8 @@ func Benchmark_Ctx_Format(b *testing.B) { c.Request().Header.Set("Accept", "text/plain") b.Run("text/plain", func(b *testing.B) { offers := []ResFmt{ - {"application/xml", fail}, - {"text/plain", ok}, + {MediaType: "application/xml", Handler: fail}, + {MediaType: "text/plain", Handler: ok}, } for n := 0; n < b.N; n++ { err = c.Format(offers...) @@ -1059,9 +1112,9 @@ func Benchmark_Ctx_Format(b *testing.B) { c.Request().Header.Set("Accept", "json") b.Run("json", func(b *testing.B) { offers := []ResFmt{ - {"xml", fail}, - {"html", fail}, - {"json", ok}, + {MediaType: "xml", Handler: fail}, + {MediaType: "html", Handler: fail}, + {MediaType: "json", Handler: ok}, } for n := 0; n < b.N; n++ { err = c.Format(offers...) @@ -1123,9 +1176,9 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Message struct { - Recipients []string Sender string `xml:"sender,attr"` - Urgency int `xml:"urgency,attr"` + Recipients []string + Urgency int `xml:"urgency,attr"` } data := Message{ Recipients: []string{"Alice", "Bob"}, @@ -1137,7 +1190,7 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) { err := c.AutoFormat(data) require.NoError(t, err) require.Equal(t, - `{"Recipients":["Alice","Bob"],"Sender":"Carol","Urgency":3}`, + `{"Sender":"Carol","Recipients":["Alice","Bob"],"Urgency":3}`, string(c.Response().Body()), ) @@ -1370,11 +1423,11 @@ func Test_Ctx_Binders(t *testing.T) { } type TestStruct struct { + Name string + NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" header:"Name2"` TestEmbeddedStruct - Name string Class int - NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" header:"Name2"` - ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` + ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` } withValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) { @@ -2141,11 +2194,11 @@ func Test_Ctx_Locals_GenericCustomStruct(t *testing.T) { app := New() app.Use(func(c Ctx) error { - Locals[User](c, "user", User{"john", 18}) + Locals[User](c, "user", User{name: "john", age: 18}) return c.Next() }) app.Use("/test", func(c Ctx) error { - require.Equal(t, User{"john", 18}, Locals[User](c, "user")) + require.Equal(t, User{name: "john", age: 18}, Locals[User](c, "user")) return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) @@ -2697,13 +2750,13 @@ func Test_Ctx_Range(t *testing.T) { testRange("bytes=") testRange("bytes=500=") testRange("bytes=500-300") - testRange("bytes=a-700", RangeSet{300, 999}) - testRange("bytes=500-b", RangeSet{500, 999}) - testRange("bytes=500-1000", RangeSet{500, 999}) - testRange("bytes=500-700", RangeSet{500, 700}) - testRange("bytes=0-0,2-1000", RangeSet{0, 0}, RangeSet{2, 999}) - testRange("bytes=0-99,450-549,-100", RangeSet{0, 99}, RangeSet{450, 549}, RangeSet{900, 999}) - testRange("bytes=500-700,601-999", RangeSet{500, 700}, RangeSet{601, 999}) + testRange("bytes=a-700", RangeSet{Start: 300, End: 999}) + testRange("bytes=500-b", RangeSet{Start: 500, End: 999}) + testRange("bytes=500-1000", RangeSet{Start: 500, End: 999}) + testRange("bytes=500-700", RangeSet{Start: 500, End: 700}) + testRange("bytes=0-0,2-1000", RangeSet{Start: 0, End: 0}, RangeSet{Start: 2, End: 999}) + testRange("bytes=0-99,450-549,-100", RangeSet{Start: 0, End: 99}, RangeSet{Start: 450, End: 549}, RangeSet{Start: 900, End: 999}) + testRange("bytes=500-700,601-999", RangeSet{Start: 500, End: 700}, RangeSet{Start: 601, End: 999}) } // go test -v -run=^$ -bench=Benchmark_Ctx_Range -benchmem -count=4 @@ -2717,10 +2770,10 @@ func Benchmark_Ctx_Range(b *testing.B) { start int end int }{ - {"bytes=-700", 300, 999}, - {"bytes=500-", 500, 999}, - {"bytes=500-1000", 500, 999}, - {"bytes=0-700,800-1000", 0, 700}, + {str: "bytes=-700", start: 300, end: 999}, + {str: "bytes=500-", start: 500, end: 999}, + {str: "bytes=500-1000", start: 500, end: 999}, + {str: "bytes=0-700,800-1000", start: 0, end: 700}, } for _, tc := range testCases { @@ -3068,8 +3121,6 @@ func Test_Static_Compress(t *testing.T) { // Note: deflate is not supported by fasthttp.FS algorithms := []string{"zstd", "gzip", "br"} for _, algo := range algorithms { - algo := algo - t.Run(algo+"_compression", func(t *testing.T) { t.Parallel() @@ -3099,23 +3150,25 @@ func Test_Ctx_SendFile_Compress_CheckCompressed(t *testing.T) { expectedFileContent, err := io.ReadAll(f) require.NoError(t, err) - sendFileBodyReader := func(compression string) []byte { - reqCtx := &fasthttp.RequestCtx{} - reqCtx.Request.Header.Add(HeaderAcceptEncoding, compression) + sendFileBodyReader := func(compression string) ([]byte, error) { + t.Helper() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Add(HeaderAcceptEncoding, compression) - c := app.AcquireCtx(reqCtx) - err = c.SendFile("./ctx.go", SendFile{ + err := c.SendFile("./ctx.go", SendFile{ Compress: true, }) - require.NoError(t, err) - return c.Response().Body() + return c.Response().Body(), err } t.Run("gzip", func(t *testing.T) { t.Parallel() - body, err := fasthttp.AppendGunzipBytes(nil, sendFileBodyReader("gzip")) + b, err := sendFileBodyReader("gzip") + require.NoError(t, err) + body, err := fasthttp.AppendGunzipBytes(nil, b) require.NoError(t, err) require.Equal(t, expectedFileContent, body) @@ -3124,7 +3177,9 @@ func Test_Ctx_SendFile_Compress_CheckCompressed(t *testing.T) { t.Run("zstd", func(t *testing.T) { t.Parallel() - body, err := fasthttp.AppendUnzstdBytes(nil, sendFileBodyReader("zstd")) + b, err := sendFileBodyReader("zstd") + require.NoError(t, err) + body, err := fasthttp.AppendUnzstdBytes(nil, b) require.NoError(t, err) require.Equal(t, expectedFileContent, body) @@ -3133,7 +3188,9 @@ func Test_Ctx_SendFile_Compress_CheckCompressed(t *testing.T) { t.Run("br", func(t *testing.T) { t.Parallel() - body, err := fasthttp.AppendUnbrotliBytes(nil, sendFileBodyReader("br")) + b, err := sendFileBodyReader("br") + require.NoError(t, err) + body, err := fasthttp.AppendUnbrotliBytes(nil, b) require.NoError(t, err) require.Equal(t, expectedFileContent, body) @@ -3223,12 +3280,12 @@ func Test_Ctx_SendFile_Multiple(t *testing.T) { body string contentDisposition string }{ - {"/test?file=1", "type DefaultCtx struct", ""}, - {"/test?file=2", "type App struct", ""}, - {"/test?file=3", "type DefaultCtx struct", "attachment"}, - {"/test?file=4", "Test_App_MethodNotAllowed", ""}, - {"/test2", "type DefaultCtx struct", "attachment"}, - {"/test2", "type DefaultCtx struct", "attachment"}, + {url: "/test?file=1", body: "type DefaultCtx struct", contentDisposition: ""}, + {url: "/test?file=2", body: "type App struct", contentDisposition: ""}, + {url: "/test?file=3", body: "type DefaultCtx struct", contentDisposition: "attachment"}, + {url: "/test?file=4", body: "Test_App_MethodNotAllowed", contentDisposition: ""}, + {url: "/test2", body: "type DefaultCtx struct", contentDisposition: "attachment"}, + {url: "/test2", body: "type DefaultCtx struct", contentDisposition: "attachment"}, } for _, tc := range testCases { @@ -3242,6 +3299,8 @@ func Test_Ctx_SendFile_Multiple(t *testing.T) { require.Contains(t, string(body), tc.body) } + app.sendfilesMutex.RLock() + defer app.sendfilesMutex.RUnlock() require.Len(t, app.sendfiles, 3) } @@ -3276,7 +3335,6 @@ func Test_Ctx_SendFile_Immutable(t *testing.T) { } for _, endpoint := range endpointsForTest { - endpoint := endpoint t.Run(endpoint, func(t *testing.T) { t.Parallel() // 1st try diff --git a/docs/api/app.md b/docs/api/app.md index 164b1edc..ef9c2ea0 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -43,19 +43,19 @@ func (app *App) MountPath() string ```go title="Examples" func main() { - app := fiber.New() - one := fiber.New() - two := fiber.New() - three := fiber.New() + app := fiber.New() + one := fiber.New() + two := fiber.New() + three := fiber.New() - two.Use("/three", three) - one.Use("/two", two) - app.Use("/one", one) + two.Use("/three", three) + one.Use("/two", two) + app.Use("/one", one) - one.MountPath() // "/one" - two.MountPath() // "/one/two" - three.MountPath() // "/one/two/three" - app.MountPath() // "" + one.MountPath() // "/one" + two.MountPath() // "/one/two" + three.MountPath() // "/one/two/three" + app.MountPath() // "" } ``` @@ -91,7 +91,7 @@ func main() { ### Route -Returns an instance of a single route, which you can then use to handle HTTP verbs with optional middleware. +Returns an instance of a single route, which you can then use to handle HTTP verbs with optional middleware. Similar to [`Express`](https://expressjs.com/de/api.html#app.route). @@ -120,6 +120,7 @@ type Register interface { Route(path string) Register } ``` + ```go title="Examples" @@ -222,6 +223,7 @@ func main() { ] ] ``` + ### Name @@ -328,6 +330,7 @@ func main() { null ] ``` + ### GetRoute @@ -347,10 +350,10 @@ func main() { app.Get("/", handler).Name("index") data, _ := json.MarshalIndent(app.GetRoute("index"), "", " ") - fmt.Print(string(data)) + fmt.Print(string(data)) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -365,6 +368,7 @@ func main() { "params": null } ``` + ### GetRoutes @@ -376,14 +380,15 @@ 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)) + 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)) } ``` @@ -400,6 +405,7 @@ func main() { } ] ``` + ## Config @@ -436,12 +442,12 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) ```go title="Examples" type CustomCtx struct { - DefaultCtx + DefaultCtx } // Custom method func (c *CustomCtx) Params(key string, defaultValue ...string) string { - return "prefix_" + c.DefaultCtx.Params(key) + return "prefix_" + c.DefaultCtx.Params(key) } app := New() @@ -521,7 +527,6 @@ func (app *App) RegisterCustomConstraint(constraint CustomConstraint) See [Custom Constraint](../guide/routing.md#custom-constraint) section for more information. - ## SetTLSHandler Use SetTLSHandler to set [ClientHelloInfo](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) when using TLS with Listener. @@ -568,3 +573,31 @@ Hooks is a method to return [hooks](./hooks.md) property. ```go title="Signature" func (app *App) Hooks() *Hooks ``` + +## RebuildTree + +The RebuildTree method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the App instance. + +```go title="Signature" +func (app *App) RebuildTree() *App +``` + +**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in development mode. Avoid using it concurrently. + +### Example Usage + +Here’s an example of how to define and register routes dynamically: + +```go +app.Get("/define", func(c Ctx) error { // Define a new route dynamically + app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() // Rebuild the route tree to register the new route + + return c.SendStatus(http.StatusOK) +}) +``` + +In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. diff --git a/docs/api/bind.md b/docs/api/bind.md index f955202e..927d423b 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -15,14 +15,13 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. ::: - ## Binders - [Body](#body) - - [Form](#form) - - [JSON](#json) - - [MultipartForm](#multipartform) - - [XML](#xml) + - [Form](#form) + - [JSON](#json) + - [MultipartForm](#multipartform) + - [XML](#xml) - [Cookie](#cookie) - [Header](#header) - [Query](#query) @@ -80,7 +79,6 @@ app.Post("/", func(c fiber.Ctx) error { // curl -X POST "http://localhost:3000/?name=john&pass=doe" ``` - **The methods for the various bodies can also be used directly:** #### Form @@ -89,7 +87,6 @@ Binds the request form 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 Form body with a field called Pass, you would use a struct field of `form:"pass"`. - ```go title="Signature" func (b *Bind) Form(out any) error ``` @@ -125,7 +122,6 @@ Binds the request json 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"`. - ```go title="Signature" func (b *Bind) JSON(out any) error ``` @@ -162,7 +158,6 @@ Binds the request multipart form 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 MultipartForm body with a field called Pass, you would use a struct field of `form:"pass"`. - ```go title="Signature" func (b *Bind) MultipartForm(out any) error ``` @@ -197,8 +192,7 @@ app.Post("/", func(c fiber.Ctx) error { Binds the request xml form 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 XML body with a field called Pass, you would use a struct field of `xml:"pass"`. - +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse an XML body with a field called Pass, you would use a struct field of `xml:"pass"`. ```go title="Signature" func (b *Bind) XML(out any) error @@ -229,7 +223,6 @@ app.Post("/", func(c fiber.Ctx) error { // curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 ``` - ### Cookie This method is similar to [Body-Binding](#body), but for cookie parameters. @@ -262,7 +255,6 @@ app.Get("/", func(c fiber.Ctx) error { // curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/ ``` - ### Header This method is similar to [Body-Binding](#body), but for request headers. @@ -298,7 +290,6 @@ app.Get("/", func(c fiber.Ctx) error { // curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" ``` - ### Query This method is similar to [Body-Binding](#body), but for query parameters. @@ -342,7 +333,6 @@ app.Get("/", func(c fiber.Ctx) error { For more parser settings please look here [Config](fiber.md#enablesplittingonparsers) ::: - ### RespHeader This method is similar to [Body-Binding](#body), but for response headers. @@ -467,7 +457,6 @@ It's default behavior of binder. func (b *Bind) Should() *Bind ``` - ## SetParserDecoder Allow you to config BodyParser/QueryParser decoder, base on schema's options, providing possibility to add custom type for parsing. @@ -542,7 +531,6 @@ app.Get("/query", func(c fiber.Ctx) error { ``` - ## Validation Validation is also possible with the binding methods. You can specify your validation rules using the `validate` struct tag. @@ -585,6 +573,3 @@ app.Post("/", func(c fiber.Ctx) error { } }) ``` - - - diff --git a/docs/api/constants.md b/docs/api/constants.md index a9ee6d5a..9fbff533 100644 --- a/docs/api/constants.md +++ b/docs/api/constants.md @@ -5,7 +5,7 @@ description: Some constants for Fiber. sidebar_position: 8 --- -### HTTP methods were copied from net/http. +### HTTP methods were copied from net/http ```go const ( @@ -26,94 +26,94 @@ const ( ```go const ( - MIMETextXML = "text/xml" - MIMETextHTML = "text/html" - MIMETextPlain = "text/plain" - MIMETextJavaScript = "text/javascript" - MIMETextCSS = "text/css" - MIMEApplicationXML = "application/xml" + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMETextJavaScript = "text/javascript" + MIMETextCSS = "text/css" + MIMEApplicationXML = "application/xml" MIMEApplicationJSON = "application/json" - MIMEApplicationJavaScript = "application/javascript" - MIMEApplicationForm = "application/x-www-form-urlencoded" - MIMEOctetStream = "application/octet-stream" - MIMEMultipartForm = "multipart/form-data" + 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" - MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" - MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" - MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" - MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" - MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" + MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" + MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" + MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" + MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" + MIMETextCSSCharsetUTF8 = "text/css; charset=utf-8" + MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" + MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" + MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" )``` ### HTTP status codes were copied from net/http. ```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 + 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 ) ``` @@ -121,46 +121,46 @@ const ( ```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 + 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 ) ``` @@ -168,127 +168,127 @@ 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" + 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" ) ``` diff --git a/docs/api/ctx.md b/docs/api/ctx.md index 0434671c..2640512a 100644 --- a/docs/api/ctx.md +++ b/docs/api/ctx.md @@ -165,7 +165,6 @@ Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US The supported content types are `text/html`, `text/plain`, `application/json`, and `application/xml`. For more flexible content negotiation, use [Format](ctx.md#format). - :::info If the header is **not** specified or there is **no** proper format, **text/plain** is used. ::: @@ -375,17 +374,17 @@ func (c Ctx) Cookie(cookie *Cookie) ```go type Cookie struct { - Name string `json:"name"` // The name of the cookie - Value string `json:"value"` // The value of the cookie - Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie - Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie - MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie - Expires time.Time `json:"expires"` // The expiration date of the cookie - Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection - HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol - SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests - Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar - SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie + Name string `json:"name"` // The name of the cookie + Value string `json:"value"` // The value of the cookie + Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie + Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie + MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie + Expires time.Time `json:"expires"` // The expiration date of the cookie + Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection + HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol + SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests + Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar + SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie } ``` @@ -1001,7 +1000,7 @@ app.Get("/admin", func(c fiber.Ctx) error { }) ``` -An alternative version of the Locals method that takes advantage of Go's generics feature is also available. This version +An alternative version of the Locals method that takes advantage of Go's generics feature is also available. This version allows for the manipulation and retrieval of local values within a request's context with a more specific data type. ```go title="Signature" @@ -1023,7 +1022,7 @@ app.Get("/test", func(c Ctx) error { }) ```` -Make sure to understand and correctly implement the Locals method in both its standard and generic form for better control +Make sure to understand and correctly implement the Locals method in both its standard and generic form for better control over route-specific data within your application. ## Location @@ -1206,9 +1205,8 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. ::: - -In certain scenarios, it can be useful to have an alternative approach to handle different types of parameters, not -just strings. This can be achieved using a generic Query function known as `Params[V GenericType](c Ctx, key string, defaultValue ...V) V`. +In certain scenarios, it can be useful to have an alternative approach to handle different types of parameters, not +just strings. This can be achieved using a generic Query function known as `Params[V GenericType](c Ctx, key string, defaultValue ...V) V`. This function is capable of parsing a query string and returning a value of a type that is assumed and specified by `V GenericType`. ```go title="Signature" @@ -1227,6 +1225,7 @@ app.Get("/user/:id", func(c fiber.Ctx) error{ ``` The generic Params function supports returning the following data types based on V GenericType: + - Integer: int, int8, int16, int32, int64 - Unsigned integer: uint, uint8, uint16, uint32, uint64 - Floating-point numbers: float32, float64 @@ -1234,7 +1233,6 @@ The generic Params function supports returning the following data types based on - String: string - Byte array: []byte - ## 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). @@ -1303,11 +1301,11 @@ func (c Ctx) Queries() map[string]string // 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"] // "" - // ... + m := c.Queries() + m["name"] // "alex" + m["want_pizza"] // "false" + m["id"] // "" + // ... }) ``` @@ -1315,9 +1313,9 @@ app.Get("/", func(c fiber.Ctx) error { // 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 + m := c.Queries() + m["field1"] // "value2" + m["field2"] // value3 }) ``` @@ -1325,10 +1323,10 @@ app.Get("/", func (c fiber.Ctx) error { // 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" + m := c.Queries() + m["list_a"] // "3" + m["list_b[]"] // "3" + m["list_c"] // "1,2,3" }) ``` @@ -1336,9 +1334,9 @@ app.Get("/", func(c fiber.Ctx) error { // 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 + m := c.Queries() + m["filters.author.name"] // John + m["filters.category.name"] // Technology }) ``` @@ -1346,12 +1344,12 @@ app.Get("/", func(c fiber.Ctx) error { // 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 + 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 }) ``` @@ -1386,8 +1384,8 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more.. ::: -In certain scenarios, it can be useful to have an alternative approach to handle different types of query parameters, not -just strings. This can be achieved using a generic Query function known as `Query[V GenericType](c Ctx, key string, defaultValue ...V) V`. +In certain scenarios, it can be useful to have an alternative approach to handle different types of query parameters, not +just strings. This can be achieved using a generic Query function known as `Query[V GenericType](c Ctx, key string, defaultValue ...V) V`. This function is capable of parsing a query string and returning a value of a type that is assumed and specified by `V GenericType`. Here is the signature for the generic Query function: @@ -1410,12 +1408,13 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` -In this case, `Query[V GenericType](c Ctx, key string, defaultValue ...V) V` can retrieve 'page' as an integer, 'brand' -as a string, and 'new' as a boolean. The function uses the appropriate parsing function for each specified type to ensure -the correct type is returned. This simplifies the retrieval process of different types of query parameters, making your +In this case, `Query[V GenericType](c Ctx, key string, defaultValue ...V) V` can retrieve 'page' as an integer, 'brand' +as a string, and 'new' as a boolean. The function uses the appropriate parsing function for each specified type to ensure +the correct type is returned. This simplifies the retrieval process of different types of query parameters, making your controller actions cleaner. The generic Query function supports returning the following data types based on V GenericType: + - Integer: int, int8, int16, int32, int64 - Unsigned integer: uint, uint8, uint16, uint32, uint64 - Floating-point numbers: float32, float64 @@ -1464,7 +1463,6 @@ app.Get("/teapot", func(c fiber.Ctx) error { }) ``` - ## 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). @@ -1746,7 +1744,7 @@ type SendFile struct { // The value for the Cache-Control HTTP-header // that is set on the file response. MaxAge is defined in seconds. // - // Optional. Default value 0. + // Optional. Default value 0. MaxAge int `json:"max_age"` } ``` diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 08d8b6ae..16f23b9e 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -82,7 +82,6 @@ app := fiber.New(fiber.Config{ | 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` | - ## Server listening ### Config @@ -112,10 +111,9 @@ app.Listen(":8080", fiber.ListenConfig{ | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `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. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | -| OnShutdownSuccess | `func()` | Allows to customize success behavior when gracefully shutting down the server by given signal. | `nil` | +| OnShutdownSuccess | `func()` | Allows customizing success behavior when gracefully shutting down the server by given signal. | `nil` | | TLSConfigFunc | `func(tlsConfig *tls.Config)` | Allows customizing `tls.Config` as you want. | `nil` | - ### Listen Listen serves HTTP requests from the given address. @@ -215,7 +213,6 @@ func (app *App) ShutdownWithTimeout(timeout time.Duration) error func (app *App) ShutdownWithContext(ctx context.Context) error ``` - ## Helper functions ### NewError diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 8717ba58..2ef3a461 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -7,7 +7,8 @@ sidebar_position: 7 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: +With Fiber v2.30.0, you can execute custom user functions when to run some methods. Here is a list of these hooks: + - [OnRoute](#onroute) - [OnName](#onname) - [OnGroup](#ongroup) @@ -18,6 +19,7 @@ With Fiber v2.30.0, you can execute custom user functions when to run some metho - [OnMount](#onmount) ## Constants + ```go // Handlers define a function to create hooks for Fiber. type OnRouteHandler = func(Route) error @@ -57,45 +59,46 @@ func (h *Hooks) OnName(handler ...OnNameHandler) package main import ( - "fmt" + "fmt" - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/", func(c fiber.Ctx) error { - return c.SendString(c.Route().Name) - }).Name("index") + 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 + ", ") + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Name: " + r.Name + ", ") - return nil - }) + return nil + }) - app.Hooks().OnName(func(r fiber.Route) error { - fmt.Print("Method: " + r.Method + "\n") + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Method: " + r.Method + "\n") - return nil - }) + return nil + }) - app.Get("/add/user", func(c fiber.Ctx) error { - return c.SendString(c.Route().Name) - }).Name("addUser") + 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.Delete("/destroy/user", func(c fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("destroyUser") - app.Listen(":5000") + app.Listen(":5000") } // Results: // Name: addUser, Method: GET // Name: destroyUser, Method: DELETE ``` + @@ -137,7 +140,7 @@ app := fiber.New(fiber.Config{ app.Hooks().OnListen(func(listenData fiber.ListenData) error { if fiber.IsChild() { - return nil + return nil } scheme := "http" if data.TLS { @@ -184,26 +187,26 @@ func (h *Hooks) OnMount(handler ...OnMountHandler) package main import ( - "fmt" + "fmt" - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := New() - app.Get("/", testSimpleHandler).Name("x") + app := New() + app.Get("/", testSimpleHandler).Name("x") - subApp := New() - subApp.Get("/test", testSimpleHandler) + subApp := New() + subApp.Get("/test", testSimpleHandler) - subApp.Hooks().OnMount(func(parent *fiber.App) error { - fmt.Print("Mount path of parent app: "+parent.MountPath()) - // ... + subApp.Hooks().OnMount(func(parent *fiber.App) error { + fmt.Print("Mount path of parent app: "+parent.MountPath()) + // ... - return nil - }) + return nil + }) - app.Mount("/sub", subApp) + app.Mount("/sub", subApp) } // Result: @@ -213,6 +216,5 @@ func main() { - :::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. +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/api/log.md b/docs/api/log.md index 9289967d..508be9a6 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -13,13 +13,13 @@ Fiber offers a default mechanism for logging to standard output. Additionally, i ```go const ( - LevelTrace Level = iota - LevelDebug - LevelInfo - LevelWarn - LevelError - LevelFatal - LevelPanic + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarn + LevelError + LevelFatal + LevelPanic ) ``` @@ -42,9 +42,11 @@ type AllLogger interface { ``` ## Print log + Note: The Fatal level method will terminate the program after printing the log message. Please use it with caution. ### Basic Logging + Logs of different levels can be directly printed. These will be entered into `messageKey`, with the default key being `msg`. ```go @@ -58,6 +60,7 @@ log.Panic("The system is down.") ``` ### Formatted Logging + Logs of different levels can be formatted before printing. All such methods end with an `f`. ```go @@ -69,6 +72,7 @@ log.Fatalf("So Long, and Thanks for All the %s.", "banana") ``` ### Key-Value Logging + Print a message with key-value pairs. If the key and value are not paired correctly, the log will output `KEYVALS UNPAIRED`. ```go @@ -80,6 +84,7 @@ log.Fatalw("", "fruit", "banana") ``` ## Global log + For projects that require a simple, global logging function to print messages at any time, Fiber provides a global log. ```go @@ -113,6 +118,7 @@ fiberlog.SetLogger(customLogger) ``` ## Set Level + `log.SetLevel` sets the minimum level of logs that will be output. The default log level is `LevelTrace`. Note that this method is not **concurrent-safe**. diff --git a/docs/api/redirect.md b/docs/api/redirect.md index e1403b73..79faa36d 100644 --- a/docs/api/redirect.md +++ b/docs/api/redirect.md @@ -190,7 +190,6 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` - #### OldInputs Get old input data. Check [WithInput](#withinput) for more information. diff --git a/docs/client/examples.md b/docs/client/examples.md index 2ea79609..86e73b65 100644 --- a/docs/client/examples.md +++ b/docs/client/examples.md @@ -18,28 +18,29 @@ import TabItem from '@theme/TabItem'; package main import ( - "encoding/base64" - "fmt" + "encoding/base64" + "fmt" - "github.com/gofiber/fiber/v3/client" + "github.com/gofiber/fiber/v3/client" ) func main() { - cc := client.New() + cc := client.New() - out := base64.StdEncoding.EncodeToString([]byte("john:doe")) - resp, err := cc.Get("http://localhost:3000", client.Config{ - Header: map[string]string{ - "Authorization": "Basic " + out, - }, - }) - if err != nil { - panic(err) - } + out := base64.StdEncoding.EncodeToString([]byte("john:doe")) + resp, err := cc.Get("http://localhost:3000", client.Config{ + Header: map[string]string{ + "Authorization": "Basic " + out, + }, + }) + if err != nil { + panic(err) + } - fmt.Print(string(resp.Body())) + fmt.Print(string(resp.Body())) } ``` + @@ -47,27 +48,28 @@ func main() { package main import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/basicauth" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/basicauth" ) func main() { - app := fiber.New() - app.Use( - basicauth.New(basicauth.Config{ - Users: map[string]string{ - "john": "doe", - }, - }), - ) + app := fiber.New() + app.Use( + basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + }, + }), + ) - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` + @@ -80,40 +82,41 @@ func main() { package main import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" + "crypto/tls" + "crypto/x509" + "fmt" + "os" - "github.com/gofiber/fiber/v3/client" + "github.com/gofiber/fiber/v3/client" ) func main() { - cc := client.New() + cc := client.New() - certPool, err := x509.SystemCertPool() - if err != nil { - panic(err) - } + certPool, err := x509.SystemCertPool() + if err != nil { + panic(err) + } - cert, err := os.ReadFile("ssl.cert") - if err != nil { - panic(err) - } + cert, err := os.ReadFile("ssl.cert") + if err != nil { + panic(err) + } - certPool.AppendCertsFromPEM(cert) - cc.SetTLSConfig(&tls.Config{ - RootCAs: certPool, - }) + certPool.AppendCertsFromPEM(cert) + cc.SetTLSConfig(&tls.Config{ + RootCAs: certPool, + }) - resp, err := cc.Get("https://localhost:3000") - if err != nil { - panic(err) - } + resp, err := cc.Get("https://localhost:3000") + if err != nil { + panic(err) + } - fmt.Print(string(resp.Body())) + fmt.Print(string(resp.Body())) } ``` + @@ -121,25 +124,26 @@ func main() { package main import ( - "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) - err := app.Listen(":3000", fiber.ListenConfig{ - CertFile: "ssl.cert", - CertKeyFile: "ssl.key", - }) - if err != nil { - panic(err) - } + err := app.Listen(":3000", fiber.ListenConfig{ + CertFile: "ssl.cert", + CertKeyFile: "ssl.key", + }) + if err != nil { + panic(err) + } } ``` + @@ -149,20 +153,20 @@ func main() { ```go func main() { - jar := client.AcquireCookieJar() - defer client.ReleaseCookieJar(jar) + jar := client.AcquireCookieJar() + defer client.ReleaseCookieJar(jar) - cc := client.New() - cc.SetCookieJar(jar) + cc := client.New() + cc.SetCookieJar(jar) - jar.SetKeyValueBytes("httpbin.org", []byte("john"), []byte("doe")) + jar.SetKeyValueBytes("httpbin.org", []byte("john"), []byte("doe")) - resp, err := cc.Get("https://httpbin.org/cookies") - if err != nil { - panic(err) - } + resp, err := cc.Get("https://httpbin.org/cookies") + if err != nil { + panic(err) + } - fmt.Println(string(resp.Body())) + fmt.Println(string(resp.Body())) } ``` @@ -183,23 +187,23 @@ func main() { ```go func main() { - jar := client.AcquireCookieJar() - defer client.ReleaseCookieJar(jar) + jar := client.AcquireCookieJar() + defer client.ReleaseCookieJar(jar) - cc := client.New() - cc.SetCookieJar(jar) + cc := client.New() + cc.SetCookieJar(jar) - _, err := cc.Get("https://httpbin.org/cookies/set/john/doe") - if err != nil { - panic(err) - } + _, err := cc.Get("https://httpbin.org/cookies/set/john/doe") + if err != nil { + panic(err) + } - uri := fasthttp.AcquireURI() - defer fasthttp.ReleaseURI(uri) + uri := fasthttp.AcquireURI() + defer fasthttp.ReleaseURI(uri) - uri.SetHost("httpbin.org") - uri.SetPath("/cookies") - fmt.Println(jar.Get(uri)) + uri.SetHost("httpbin.org") + uri.SetPath("/cookies") + fmt.Println(jar.Get(uri)) } ``` @@ -216,23 +220,23 @@ func main() { ```go func main() { - jar := client.AcquireCookieJar() - defer client.ReleaseCookieJar(jar) + jar := client.AcquireCookieJar() + defer client.ReleaseCookieJar(jar) - cc := client.New() - cc.SetCookieJar(jar) + cc := client.New() + cc.SetCookieJar(jar) - _, err := cc.Get("https://httpbin.org/cookies/set/john/doe") - if err != nil { - panic(err) - } + _, err := cc.Get("https://httpbin.org/cookies/set/john/doe") + if err != nil { + panic(err) + } - resp, err := cc.Get("https://httpbin.org/cookies") - if err != nil { - panic(err) - } + resp, err := cc.Get("https://httpbin.org/cookies") + if err != nil { + panic(err) + } - fmt.Println(resp.String()) + fmt.Println(resp.String()) } ``` diff --git a/docs/client/hooks.md b/docs/client/hooks.md index 3ae510f3..8616eac7 100644 --- a/docs/client/hooks.md +++ b/docs/client/hooks.md @@ -18,43 +18,43 @@ You need to use `RequestHook func(*Client, *Request) error` function signature w ```go type Repository struct { - Name string `json:"name"` - FullName string `json:"full_name"` - Description string `json:"description"` - Homepage string `json:"homepage"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + Homepage string `json:"homepage"` - Owner struct { - Login string `json:"login"` - } `json:"owner"` + Owner struct { + Login string `json:"login"` + } `json:"owner"` } func main() { - cc := client.New() + cc := client.New() - cc.AddRequestHook(func(c *client.Client, r *client.Request) error { - r.SetURL("https://api.github.com/" + r.URL()) + cc.AddRequestHook(func(c *client.Client, r *client.Request) error { + r.SetURL("https://api.github.com/" + r.URL()) - return nil - }) + return nil + }) - resp, err := cc.Get("repos/gofiber/fiber") - if err != nil { - panic(err) - } + resp, err := cc.Get("repos/gofiber/fiber") + if err != nil { + panic(err) + } - var repo Repository - if err := resp.JSON(&repo); err != nil { - panic(err) - } + var repo Repository + if err := resp.JSON(&repo); err != nil { + panic(err) + } - fmt.Printf("Status code: %d\n", resp.StatusCode()) + fmt.Printf("Status code: %d\n", resp.StatusCode()) - fmt.Printf("Repository: %s\n", repo.FullName) - fmt.Printf("Description: %s\n", repo.Description) - fmt.Printf("Homepage: %s\n", repo.Homepage) - fmt.Printf("Owner: %s\n", repo.Owner.Login) - fmt.Printf("Name: %s\n", repo.Name) - fmt.Printf("Full Name: %s\n", repo.FullName) + fmt.Printf("Repository: %s\n", repo.FullName) + fmt.Printf("Description: %s\n", repo.Description) + fmt.Printf("Homepage: %s\n", repo.Homepage) + fmt.Printf("Owner: %s\n", repo.Owner.Login) + fmt.Printf("Name: %s\n", repo.Name) + fmt.Printf("Full Name: %s\n", repo.FullName) } ``` @@ -87,22 +87,22 @@ If any error returns from request hook execution, it will interrupt the request ```go func main() { - cc := client.New() + cc := client.New() - cc.AddRequestHook(func(c *client.Client, r *client.Request) error { - fmt.Println("Hook 1") - return errors.New("error") - }) + cc.AddRequestHook(func(c *client.Client, r *client.Request) error { + fmt.Println("Hook 1") + return errors.New("error") + }) - cc.AddRequestHook(func(c *client.Client, r *client.Request) error { - fmt.Println("Hook 2") - return nil - }) + cc.AddRequestHook(func(c *client.Client, r *client.Request) error { + fmt.Println("Hook 2") + return nil + }) - _, err := cc.Get("https://example.com/") - if err != nil { - panic(err) - } + _, err := cc.Get("https://example.com/") + if err != nil { + panic(err) + } } ``` @@ -129,24 +129,24 @@ You need to use `ResponseHook func(*Client, *Response, *Request) error` function ```go func main() { - cc := client.New() + cc := client.New() - cc.AddResponseHook(func(c *client.Client, resp *client.Response, req *client.Request) error { - fmt.Printf("Response Status Code: %d\n", resp.StatusCode()) - fmt.Printf("HTTP protocol: %s\n\n", resp.Protocol()) + cc.AddResponseHook(func(c *client.Client, resp *client.Response, req *client.Request) error { + fmt.Printf("Response Status Code: %d\n", resp.StatusCode()) + fmt.Printf("HTTP protocol: %s\n\n", resp.Protocol()) - fmt.Println("Response Headers:") - resp.RawResponse.Header.VisitAll(func(key, value []byte) { - fmt.Printf("%s: %s\n", key, value) - }) + fmt.Println("Response Headers:") + resp.RawResponse.Header.VisitAll(func(key, value []byte) { + fmt.Printf("%s: %s\n", key, value) + }) - return nil - }) + return nil + }) - _, err := cc.Get("https://example.com/") - if err != nil { - panic(err) - } + _, err := cc.Get("https://example.com/") + if err != nil { + panic(err) + } } ``` @@ -185,27 +185,27 @@ If any error is returned from executing the response hook, it will return the er ```go func main() { - cc := client.New() + cc := client.New() - cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { - fmt.Println("Hook 1") - return nil - }) + cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { + fmt.Println("Hook 1") + return nil + }) - cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { - fmt.Println("Hook 2") - return errors.New("error") - }) + cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { + fmt.Println("Hook 2") + return errors.New("error") + }) - cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { - fmt.Println("Hook 3") - return nil - }) + cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error { + fmt.Println("Hook 3") + return nil + }) - _, err := cc.Get("https://example.com/") - if err != nil { - panic(err) - } + _, err := cc.Get("https://example.com/") + if err != nil { + panic(err) + } } ``` @@ -231,22 +231,22 @@ Hooks work as FIFO (first-in-first-out). You need to check the order while addin ```go func main() { - cc := client.New() + cc := client.New() - cc.AddRequestHook(func(c *client.Client, r *client.Request) error { - fmt.Println("Hook 1") - return nil - }) + cc.AddRequestHook(func(c *client.Client, r *client.Request) error { + fmt.Println("Hook 1") + return nil + }) - cc.AddRequestHook(func(c *client.Client, r *client.Request) error { - fmt.Println("Hook 2") - return nil - }) + cc.AddRequestHook(func(c *client.Client, r *client.Request) error { + fmt.Println("Hook 2") + return nil + }) - _, err := cc.Get("https://example.com/") - if err != nil { - panic(err) - } + _, err := cc.Get("https://example.com/") + if err != nil { + panic(err) + } } ``` diff --git a/docs/client/request.md b/docs/client/request.md index a5a025bd..d1126dae 100644 --- a/docs/client/request.md +++ b/docs/client/request.md @@ -18,32 +18,31 @@ This structure is designed to be flexible and efficient, allowing users to easil ```go type Request struct { - url string - method string - userAgent string - boundary string - referer string - ctx context.Context - header *Header - params *QueryParam - cookies *Cookie - path *PathParam + url string + method string + userAgent string + boundary string + referer string + ctx context.Context + header *Header + params *QueryParam + cookies *Cookie + path *PathParam - timeout time.Duration - maxRedirects int + timeout time.Duration + maxRedirects int - client *Client + client *Client - body any - formData *FormData - files []*File - bodyType bodyType + body any + formData *FormData + files []*File + bodyType bodyType - RawRequest *fasthttp.Request + RawRequest *fasthttp.Request } ``` - ## REST Methods ### Get @@ -207,6 +206,7 @@ func (r *Request) SetContext(ctx context.Context) *Request ## Header Header method returns header value via key, this method will visit all field in the header. + ```go title="Signature" func (r *Request) Header(key string) []string ``` @@ -229,7 +229,7 @@ req.AddHeader("Test", "654321") resp, err := req.Get("https://httpbin.org/headers") if err != nil { - panic(err) + panic(err) } fmt.Println(resp.String()) @@ -271,7 +271,7 @@ req.SetHeader("Test", "654321") resp, err := req.Get("https://httpbin.org/headers") if err != nil { - panic(err) + panic(err) } fmt.Println(resp.String()) @@ -338,7 +338,7 @@ req.AddParam("hobbies", "basketball") resp, err := req.Get("https://httpbin.org/response-headers") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -401,19 +401,19 @@ req := client.AcquireRequest() defer client.ReleaseRequest(req) req.SetParamsWithStruct(struct { - Name string `json:"name"` - Hobbies []string `json:"hobbies"` + Name string `json:"name"` + Hobbies []string `json:"hobbies"` }{ - Name: "John Doe", - Hobbies: []string{ - "Football", - "Basketball", - }, + Name: "John Doe", + Hobbies: []string{ + "Football", + "Basketball", + }, }) resp, err := req.Get("https://httpbin.org/response-headers") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -525,13 +525,13 @@ req := client.AcquireRequest() defer client.ReleaseRequest(req) req.SetCookies(map[string]string{ - "cookie1": "value1", - "cookie2": "value2", + "cookie1": "value1", + "cookie2": "value2", }) resp, err := req.Get("https://httpbin.org/cookies") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -592,7 +592,7 @@ req.SetPathParam("base64", "R29maWJlcg==") resp, err := req.Get("https://httpbin.org/base64/:base64") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -691,7 +691,7 @@ req.AddFormData("points", "100") resp, err := req.Post("https://httpbin.org/post") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -735,7 +735,7 @@ req.SetFormData("email", "john@doe.com") resp, err := req.Post("https://httpbin.org/post") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -832,7 +832,7 @@ req.AddFile("test.txt") resp, err := req.Post("https://httpbin.org/post") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -872,7 +872,7 @@ req.AddFileWithReader("test.txt", io.NopCloser(buf)) resp, err := req.Post("https://httpbin.org/post") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -928,7 +928,7 @@ req.SetTimeout(5 * time.Second) resp, err := req.Get("https://httpbin.org/delay/4") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -957,7 +957,7 @@ req.SetTimeout(5 * time.Second) resp, err := req.Get("https://httpbin.org/delay/6") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -1016,7 +1016,7 @@ Header is a wrapper which wrap http.Header, the header in client and request wil ```go type Header struct { - *fasthttp.RequestHeader + *fasthttp.RequestHeader } ``` @@ -1050,7 +1050,7 @@ QueryParam is a wrapper which wrap url.Values, the query string and formdata in ```go type QueryParam struct { - *fasthttp.Args + *fasthttp.Args } ``` @@ -1229,7 +1229,7 @@ FormData is a wrapper of fasthttp.Args and it is used for url encode body and fi ```go type FormData struct { - *fasthttp.Args + *fasthttp.Args } ``` @@ -1295,10 +1295,10 @@ File is a struct which support send files via request. ```go type File struct { - name string - fieldName string - path string - reader io.ReadCloser + name string + fieldName string + path string + reader io.ReadCloser } ``` diff --git a/docs/client/response.md b/docs/client/response.md index 85ea79c4..2256d400 100644 --- a/docs/client/response.md +++ b/docs/client/response.md @@ -17,11 +17,11 @@ This structure allows users to easily access and manage the data returned by the ```go type Response struct { - client *Client - request *Request - cookie []*fasthttp.Cookie + client *Client + request *Request + cookie []*fasthttp.Cookie - RawResponse *fasthttp.Response + RawResponse *fasthttp.Response } ``` @@ -71,7 +71,7 @@ func (r *Response) Protocol() string ```go title="Example" resp, err := client.Get("https://httpbin.org/get") if err != nil { - panic(err) + panic(err) } fmt.Println(resp.Protocol()) @@ -80,7 +80,7 @@ fmt.Println(resp.Protocol())
Click here to see the result -``` +```text HTTP/1.1 ``` @@ -105,19 +105,19 @@ func (r *Response) Cookies() []*fasthttp.Cookie ```go title="Example" resp, err := client.Get("https://httpbin.org/cookies/set/go/fiber") if err != nil { - panic(err) + panic(err) } cookies := resp.Cookies() for _, cookie := range cookies { - fmt.Printf("%s => %s\n", string(cookie.Key()), string(cookie.Value())) + fmt.Printf("%s => %s\n", string(cookie.Key()), string(cookie.Value())) } ```
Click here to see the result -``` +```text go => fiber ``` @@ -149,22 +149,22 @@ func (r *Response) JSON(v any) error ```go title="Example" type Body struct { - Slideshow struct { - Author string `json:"author"` - Date string `json:"date"` - Title string `json:"title"` - } `json:"slideshow"` + Slideshow struct { + Author string `json:"author"` + Date string `json:"date"` + Title string `json:"title"` + } `json:"slideshow"` } var out Body resp, err := client.Get("https://httpbin.org/json") if err != nil { - panic(err) + panic(err) } err = resp.JSON(&out) if err != nil { - panic(err) + panic(err) } fmt.Printf("%+v\n", out) @@ -173,7 +173,7 @@ fmt.Printf("%+v\n", out)
Click here to see the result -``` +```text {Slideshow:{Author:Yours Truly Date:date of publication Title:Sample Slide Show}} ``` diff --git a/docs/client/rest.md b/docs/client/rest.md index 9b34b80f..3590407e 100644 --- a/docs/client/rest.md +++ b/docs/client/rest.md @@ -10,36 +10,38 @@ toc_max_heading_level: 5 The Fiber Client for Fiber v3 is a powerful HTTP client optimized for high performance and ease of use in server-side applications. Built on top of the robust FastHTTP library, it inherits FastHTTP's high-speed HTTP protocol implementation. The client is designed to make HTTP requests both internally within services or externally to other web services. ## Features + - **Lightweight & Fast**: Leveraging the minimalistic design of FastHTTP, the Fiber Client is lightweight and extremely fast. - **Flexible Configuration**: Configure client-level settings such as timeouts, headers, and more, which apply to all requests. Specific requests can further override or merge these settings. - **Connection Pooling**: Manages a pool of persistent connections that reduce the overhead of repeatedly establishing connections. - **Timeouts & Retries**: Supports setting request timeouts and retry mechanisms to handle transient failures. ## Usage + To use the Fiber Client, instantiate it with the desired configuration. Here's a simple example: ```go package main import ( - "fmt" - "time" + "fmt" + "time" - "github.com/gofiber/fiber/v3/client" + "github.com/gofiber/fiber/v3/client" ) func main() { - cc := client.New() - cc.SetTimeout(10 * time.Second) + cc := client.New() + cc.SetTimeout(10 * time.Second) - // Get request - resp, err := cc.Get("https://httpbin.org/get") - if err != nil { - panic(err) - } + // Get request + resp, err := cc.Get("https://httpbin.org/get") + if err != nil { + panic(err) + } - fmt.Printf("Status: %d\n", resp.StatusCode()) - fmt.Printf("Body: %s\n", string(resp.Body())) + fmt.Printf("Status: %d\n", resp.StatusCode()) + fmt.Printf("Body: %s\n", string(resp.Body())) } ``` @@ -47,49 +49,49 @@ You can check out [examples](examples.md) for more examples! ```go type Client struct { - mu sync.RWMutex + mu sync.RWMutex - fasthttp *fasthttp.Client + fasthttp *fasthttp.Client - baseURL string - userAgent string - referer string - header *Header - params *QueryParam - cookies *Cookie - path *PathParam + baseURL string + userAgent string + referer string + header *Header + params *QueryParam + cookies *Cookie + path *PathParam - debug bool + debug bool - timeout time.Duration + timeout time.Duration - // user defined request hooks - userRequestHooks []RequestHook + // user defined request hooks + userRequestHooks []RequestHook - // client package defined request hooks - builtinRequestHooks []RequestHook + // client package defined request hooks + builtinRequestHooks []RequestHook - // user defined response hooks - userResponseHooks []ResponseHook + // user defined response hooks + userResponseHooks []ResponseHook - // client package defined response hooks - builtinResponseHooks []ResponseHook + // client package defined response hooks + builtinResponseHooks []ResponseHook - jsonMarshal utils.JSONMarshal - jsonUnmarshal utils.JSONUnmarshal - xmlMarshal utils.XMLMarshal - xmlUnmarshal utils.XMLUnmarshal + jsonMarshal utils.JSONMarshal + jsonUnmarshal utils.JSONUnmarshal + xmlMarshal utils.XMLMarshal + xmlUnmarshal utils.XMLUnmarshal - cookieJar *CookieJar + cookieJar *CookieJar - // proxy - proxyURL string + // proxy + proxyURL string - // retry - retryConfig *RetryConfig + // retry + retryConfig *RetryConfig - // logger - logger log.CommonLogger + // logger + logger log.CommonLogger } ``` @@ -175,21 +177,21 @@ It can be used to configure request data while sending requests using Get, Post, ```go type Config struct { - Ctx context.Context + Ctx context.Context - UserAgent string - Referer string - Header map[string]string - Param map[string]string - Cookie map[string]string - PathParam map[string]string + UserAgent string + Referer string + Header map[string]string + Param map[string]string + Cookie map[string]string + PathParam map[string]string - Timeout time.Duration - MaxRedirects int + Timeout time.Duration + MaxRedirects int - Body any - FormData map[string]string - File []*File + Body any + FormData map[string]string + File []*File } ``` @@ -393,7 +395,7 @@ cc.SetBaseURL("https://httpbin.org/") resp, err := cc.Get("/get") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -402,12 +404,13 @@ fmt.Println(string(resp.Body()))
Click here to see the result -``` +```json { "args": {}, ... } ``` +
### Header @@ -441,7 +444,7 @@ func (c *Client) SetHeader(key, val string) *Client #### AddHeaders AddHeaders method adds multiple headers field and its values at one go in the client instance. -These headers will be applied to all requests raised from this client instance. +These headers will be applied to all requests raised from this client instance. Also it can be overridden at request level headers options. ```go title="Signature" @@ -451,7 +454,7 @@ func (c *Client) AddHeaders(h map[string][]string) *Client #### SetHeaders SetHeaders method sets multiple headers field and its values at one go in the client instance. -These headers will be applied to all requests raised from this client instance. +These headers will be applied to all requests raised from this client instance. Also it can be overridden at request level headers options. ```go title="Signature" @@ -489,7 +492,7 @@ func (c *Client) SetParam(key, val string) *Client #### AddParams AddParams method adds multiple query params field and its values at one go in the client instance. -These params will be applied to all requests raised from this client instance. +These params will be applied to all requests raised from this client instance. Also it can be overridden at request level params options. ```go title="Signature" @@ -499,7 +502,7 @@ func (c *Client) AddParams(m map[string][]string) *Client #### SetParams SetParams method sets multiple params field and its values at one go in the client instance. -These params will be applied to all requests raised from this client instance. +These params will be applied to all requests raised from this client instance. Also it can be overridden at request level params options. ```go title="Signature" @@ -509,7 +512,7 @@ func (c *Client) SetParams(m map[string]string) *Client #### SetParamsWithStruct SetParamsWithStruct method sets multiple params field and its values at one go in the client instance. -These params will be applied to all requests raised from this client instance. +These params will be applied to all requests raised from this client instance. Also it can be overridden at request level params options. ```go title="Signature" @@ -566,7 +569,7 @@ func (c *Client) SetPathParam(key, val string) *Client #### SetPathParams SetPathParams method sets multiple path params field and its values at one go in the client instance. -These path params will be applied to all requests raised from this client instance. +These path params will be applied to all requests raised from this client instance. Also it can be overridden at request level path params options. ```go title="Signature" @@ -576,7 +579,7 @@ func (c *Client) SetPathParams(m map[string]string) *Client #### SetPathParamsWithStruct SetPathParamsWithStruct method sets multiple path params field and its values at one go in the client instance. -These path params will be applied to all requests raised from this client instance. +These path params will be applied to all requests raised from this client instance. Also it can be overridden at request level path params options. ```go title="Signature" @@ -616,7 +619,7 @@ cc.SetCookie("john", "doe") resp, err := cc.Get("https://httpbin.org/cookies") if err != nil { - panic(err) + panic(err) } fmt.Println(string(resp.Body())) @@ -625,19 +628,20 @@ fmt.Println(string(resp.Body()))
Click here to see the result -``` +```json { "cookies": { "john": "doe" } } ``` +
#### SetCookies SetCookies method sets multiple cookies field and its values at one go in the client instance. -These cookies will be applied to all requests raised from this client instance. +These cookies will be applied to all requests raised from this client instance. Also it can be overridden at request level cookie options. ```go title="Signature" diff --git a/docs/extra/faq.md b/docs/extra/faq.md index eaea2a2c..3e48c89e 100644 --- a/docs/extra/faq.md +++ b/docs/extra/faq.md @@ -20,7 +20,7 @@ Routes and other application-specific logic can live in as many files as you wis ## 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.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: @@ -32,12 +32,13 @@ app.Use(func(c fiber.Ctx) error { ## 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. +[Air](https://github.com/air-verse/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: +* Install Air by downloading the appropriate binary for your operating system from the GitHub release page or by building the tool directly from source. +* 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 = "." @@ -50,7 +51,9 @@ tmp_dir = "tmp" 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: + +* Start your Fiber application using Air by running the following command in the terminal: + ```sh air ``` @@ -59,7 +62,6 @@ As you make changes to your source code, Air will detect them and automatically 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#errorhandler) when initiating a new [Fiber instance](../api/fiber.md#new). @@ -92,78 +94,80 @@ To learn more about using Templates in Fiber, see [Templates](../guide/templates ## 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. +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: +Yes we do, here are some examples: This example works v2 + ```go package main import ( - "log" + "log" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/logger" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/logger" ) type Host struct { - Fiber *fiber.App + 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 + 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")) + 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/error-handling.md b/docs/guide/error-handling.md index 584f28ed..17fe4d75 100644 --- a/docs/guide/error-handling.md +++ b/docs/guide/error-handling.md @@ -24,6 +24,7 @@ app.Get("/", func(c fiber.Ctx) error { return c.SendFile("file-does-not-exist") }) ``` + diff --git a/docs/guide/faster-fiber.md b/docs/guide/faster-fiber.md index d4c2df1b..90d2a358 100644 --- a/docs/guide/faster-fiber.md +++ b/docs/guide/faster-fiber.md @@ -5,13 +5,13 @@ 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: + +Since Fiber v2.32.0, we have adopted `encoding/json` as the default JSON library for its stability and reliability. However, the standard library can be slower than some third-party alternatives. If you find the performance of `encoding/json` unsatisfactory, we suggest considering 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 @@ -20,12 +20,12 @@ import "github.com/gofiber/fiber/v3" import "github.com/goccy/go-json" func main() { - app := fiber.New(fiber.Config{ - JSONEncoder: json.Marshal, - JSONDecoder: json.Unmarshal, - }) + app := fiber.New(fiber.Config{ + JSONEncoder: json.Marshal, + JSONDecoder: json.Unmarshal, + }) - # ... + # ... } ``` diff --git a/docs/guide/grouping.md b/docs/guide/grouping.md index 4ed4622c..6374f102 100644 --- a/docs/guide/grouping.md +++ b/docs/guide/grouping.md @@ -10,23 +10,23 @@ In general, the Group functionality in Fiber behaves similarly to ExpressJS. Gro ## Paths -Like **Routing**, groups can also have paths that belong to a cluster. +Like `Routing`, groups can also have paths that belong to a cluster. ```go func main() { - app := fiber.New() + app := fiber.New() - api := app.Group("/api", middleware) // /api + 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 + 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 + 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")) + log.Fatal(app.Listen(":3000")) } ``` @@ -34,19 +34,19 @@ A **Group** of paths can have an optional handler. ```go func main() { - app := fiber.New() + app := fiber.New() - api := app.Group("/api") // /api + 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 + 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 + 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")) + log.Fatal(app.Listen(":3000")) } ``` diff --git a/docs/guide/routing.md b/docs/guide/routing.md index 7dcc2b61..450932cc 100644 --- a/docs/guide/routing.md +++ b/docs/guide/routing.md @@ -20,12 +20,12 @@ import RoutingHandler from './../partials/routing/handler.md'; 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** +### 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") + return c.SendString("root") }) // This route path will match requests to "/about": @@ -56,7 +56,7 @@ 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** +### Example of define routes with route parameters ```go // Parameters @@ -143,6 +143,7 @@ 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 introduced 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 @@ -172,7 +173,7 @@ Constraints aren't validation for parameters. If constraints aren't valid for a ```go app.Get("/:test", func(c fiber.Ctx) error { - return c.SendString(c.Params("test")) + return c.SendString(c.Params("test")) }) // curl -X GET http://localhost:3000/12 @@ -181,13 +182,15 @@ app.Get("/:test", func(c fiber.Ctx) error { // 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")) + return c.SendString(c.Params("test")) }) // curl -X GET http://localhost:3000/120000 @@ -199,13 +202,15 @@ app.Get("/:test", func(c fiber.Ctx) error { // 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")) + return c.SendString(c.Params("date")) }) // curl -X GET http://localhost:3000/125 @@ -245,21 +250,21 @@ app.Get("/:test?", func(c fiber.Ctx) error { Custom constraints can be added to Fiber using the `app.RegisterCustomConstraint` method. Your constraints have to be compatible with the `CustomConstraint` interface. -It is a good idea to add external constraints to your project once you want to add more specific rules to your routes. +It is a good idea to add external constraints to your project once you want to add more specific rules to your routes. For example, you can add a constraint to check if a parameter is a valid ULID. ```go // CustomConstraint is an interface for custom constraints type CustomConstraint interface { - // Name returns the name of the constraint. - // This name is used in the constraint matching. - Name() string + // Name returns the name of the constraint. + // This name is used in the constraint matching. + Name() string - // Execute executes the constraint. - // It returns true if the constraint is matched and right. - // param is the parameter value to check. - // args are the constraint arguments. - Execute(param string, args ...string) bool + // Execute executes the constraint. + // It returns true if the constraint is matched and right. + // param is the parameter value to check. + // args are the constraint arguments. + Execute(param string, args ...string) bool } ``` @@ -267,30 +272,30 @@ You can check the example below: ```go type UlidConstraint struct { - fiber.CustomConstraint + fiber.CustomConstraint } func (*UlidConstraint) Name() string { - return "ulid" + return "ulid" } func (*UlidConstraint) Execute(param string, args ...string) bool { - _, err := ulid.Parse(param) - return err == nil + _, err := ulid.Parse(param) + return err == nil } func main() { - app := fiber.New() - app.RegisterCustomConstraint(&UlidConstraint{}) + app := fiber.New() + app.RegisterCustomConstraint(&UlidConstraint{}) - app.Get("/login/:id", func(c fiber.Ctx) error { - return c.SendString("...") - }) + app.Get("/login/:id", func(c fiber.Ctx) error { + return c.SendString("...") + }) - app.Listen(":3000") + app.Listen(":3000") - // /login/01HK7H9ZE5BFMK348CPYP14S0Z -> 200 - // /login/12345 -> 404 + // /login/01HK7H9ZE5BFMK348CPYP14S0Z -> 200 + // /login/12345 -> 404 } ``` @@ -302,15 +307,15 @@ Functions that are designed to make changes to the request or response are calle ```go app.Use(func(c fiber.Ctx) error { - // Set a custom header on all responses: - c.Set("X-Custom-Header", "Hello, World") + // Set a custom header on all responses: + c.Set("X-Custom-Header", "Hello, World") - // Go to next middleware: - return c.Next() + // Go to next middleware: + return c.Next() }) app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") + return c.SendString("Hello, World!") }) ``` @@ -322,26 +327,25 @@ app.Get("/", func(c fiber.Ctx) error { Adding routes dynamically after the application has started is not supported due to design and performance considerations. Make sure to define all your routes before the application starts. ::: - ## Grouping If you have many endpoints, you can organize your routes using `Group`. ```go func main() { - app := fiber.New() + app := fiber.New() - api := app.Group("/api", middleware) // /api + 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 + 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 + 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")) + log.Fatal(app.Listen(":3000")) } ``` diff --git a/docs/guide/templates.md b/docs/guide/templates.md index 2bd441fe..af2ea4e4 100644 --- a/docs/guide/templates.md +++ b/docs/guide/templates.md @@ -8,47 +8,29 @@ sidebar_position: 3 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -## Template interfaces +Templates are a great tool to render dynamic content without using a separate frontend framework. -Fiber provides a Views interface to provide your own template engine: +## Template Engines - - +Fiber allows you to provide a custom template engine at app initialization. ```go -type Views interface { - Load() error - Render(out io.Writer, name string, binding any, layout ...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{ + // Pass in Views Template Engine Views: engine, - // Views Layout is the global layout for all template render until override on Render function. - ViewsLayout: "layouts/main" + + // Default global path to search for views (can be overriden when calling Render()) + ViewsLayout: "layouts/main", + + // Enables/Disables access to `ctx.Locals()` entries in rendered views + // (defaults to false) + PassLocalsToViews: false, }) ``` -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. +### Supported Engines -```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: +The Fiber team maintains a [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/) @@ -60,6 +42,174 @@ Fiber team maintains [templates](https://docs.gofiber.io/template) package that * [pug](https://docs.gofiber.io/template/pug) * [slim](https://docs.gofiber.io/template/slim) +:::info +Custom template engines can implement the `Views` interface to be supported in Fiber. +::: + +```go title="Views interface" +type Views interface { + // Fiber executes Load() on app initialization to load/parse the templates + Load() error + + // Outputs a template to the provided buffer using the provided template, + // template name, and binded data + Render(io.Writer, string, interface{}, ...string) error +} +``` + +:::note +The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. +::: + +## Rendering Templates + +Once an engine is set up, a route handler can call the [**ctx.Render\(\)**](../api/ctx.md#render) function with a template name and binded data to send the rendered template. + +```go title="Signature" +func (c Ctx) Render(name string, bind Map, layouts ...string) error +``` + +:::info +By default, [**ctx.Render\(\)**](../api/ctx.md#render) searches for the template name in the `ViewsLayout` path. To override this setting, provide the path(s) in the `layouts` argument. +::: + + + + +```go +app.Get("/", func(c fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Title": "Hello, World!", + }) + +}) +``` + + + + + +```html + + + +

{{.Title}}

+ + +``` + +
+ +
+ +:::caution +If the Fiber config option `PassLocalsToViews` is enabled, then all locals set using `ctx.Locals(key, value)` will be passed to the template. It is important to avoid clashing keys when using this setting. +::: + +## Advanced Templating + +### Custom Functions + +Fiber supports adding custom functions to templates. + +#### AddFunc + +Adds a global function to all templates. + +```go title="Signature" +func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore +``` + + + + +```go +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFunc("ToUpper", func(s string) string { + return strings.ToUpper(s) +} + +// Initialize Fiber App +app := fiber.New(fiber.Config{ + Views: engine, +}) + +app.Get("/", func (c fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Content": "hello, World!" + }) +}) +``` + + + + +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` + +
+
+ +#### AddFuncMap + +Adds a Map of functions (keyed by name) to all templates. + +```go title="Signature" +func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore +``` + + + + +```go +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFuncMap(map[string]interface{}{ + "ToUpper": func(s string) string { + return strings.ToUpper(s) + }, +}) + +// Initialize Fiber App +app := fiber.New(fiber.Config{ + Views: engine, +}) + +app.Get("/", func (c fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Content": "hello, world!" + }) +}) +``` + + + + +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` + +
+
+ +* For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template). + +## Full Example + @@ -75,9 +225,10 @@ import ( func main() { // Initialize standard Go html template engine engine := html.New("./views", ".html") - // If you want other engine, just replace with following + // If you want to use another engine, + // just replace with following: // Create a new engine with django - // engine := django.New("./views", ".django") + // engine := django.New("./views", ".django") app := fiber.New(fiber.Config{ Views: engine, @@ -85,22 +236,32 @@ func main() { app.Get("/", func(c fiber.Ctx) error { // Render index template return c.Render("index", fiber.Map{ - "Title": "Hello, World!", - }) + "Title": "Go Fiber Template Example", + "Description": "An example template", + "Greeting": "Hello, World!", + }); }) log.Fatal(app.Listen(":3000")) } ``` + -```markup +```html + + + {{.Title}} + +

{{.Title}}

+

{{.Greeting}}

``` +
diff --git a/docs/guide/utils.md b/docs/guide/utils.md index 3dbff1ab..24ed2362 100644 --- a/docs/guide/utils.md +++ b/docs/guide/utils.md @@ -19,21 +19,20 @@ func Convert[T any](value string, convertor func(string) (T, error), defaultValu ```go title="Example" // GET http://example.com/id/bb70ab33-d455-4a03-8d78-d3c1dacae9ff app.Get("/id/:id", func(c fiber.Ctx) error { - fiber.Convert(c.Params("id"), uuid.Parse) // UUID(bb70ab33-d455-4a03-8d78-d3c1dacae9ff), nil + fiber.Convert(c.Params("id"), uuid.Parse) // UUID(bb70ab33-d455-4a03-8d78-d3c1dacae9ff), nil // GET http://example.com/search?id=65f6f54221fb90e6a6b76db7 app.Get("/search", func(c fiber.Ctx) error) { - fiber.Convert(c.Query("id"), mongo.ParseObjectID) // objectid(65f6f54221fb90e6a6b76db7), nil - fiber.Convert(c.Query("id"), uuid.Parse) // uuid.Nil, error(cannot parse given uuid) - fiber.Convert(c.Query("id"), uuid.Parse, mongo.NewObjectID) // new object id generated and return nil as error. + fiber.Convert(c.Query("id"), mongo.ParseObjectID) // objectid(65f6f54221fb90e6a6b76db7), nil + fiber.Convert(c.Query("id"), uuid.Parse) // uuid.Nil, error(cannot parse given uuid) + fiber.Convert(c.Query("id"), uuid.Parse, mongo.NewObjectID) // new object id generated and return nil as error. } // ... }) ``` - ### GetReqHeader GetReqHeader function utilizing Go's generics feature. @@ -45,11 +44,11 @@ func GetReqHeader[V any](c Ctx, key string, defaultValue ...V) V ```go title="Example" app.Get("/search", func(c fiber.Ctx) error { - // curl -X GET http://example.com/search -H "X-Request-ID: 12345" -H "X-Request-Name: John" - GetReqHeader[int](c, "X-Request-ID") // => returns 12345 as integer. - GetReqHeader[string](c, "X-Request-Name") // => returns "John" as string. - GetReqHeader[string](c, "unknownParam", "default") // => returns "default" as string. - // ... + // curl -X GET http://example.com/search -H "X-Request-ID: 12345" -H "X-Request-Name: John" + GetReqHeader[int](c, "X-Request-ID") // => returns 12345 as integer. + GetReqHeader[string](c, "X-Request-Name") // => returns "John" as string. + GetReqHeader[string](c, "unknownParam", "default") // => returns "default" as string. + // ... }) ``` @@ -69,20 +68,20 @@ func Locals[V any](c Ctx, key any, value ...V) V ```go title="Example" app.Use("/user/:user/:id", func(c fiber.Ctx) error { - // set local values - fiber.Locals[string](c, "user", "john") - fiber.Locals[int](c, "id", 25) - // ... - - return c.Next() + // set local values + fiber.Locals[string](c, "user", "john") + fiber.Locals[int](c, "id", 25) + // ... + + return c.Next() }) app.Get("/user/*", func(c fiber.Ctx) error { - // get local values - name := fiber.Locals[string](c, "user") // john - age := fiber.Locals[int](c, "id") // 25 - // ... + // get local values + name := fiber.Locals[string](c, "user") // john + age := fiber.Locals[int](c, "id") // 25 + // ... }) ``` @@ -97,11 +96,11 @@ func Params[V any](c Ctx, key string, defaultValue ...V) V ```go title="Example" app.Get("/user/:user/:id", func(c fiber.Ctx) error { - // http://example.com/user/john/25 - Params[int](c, "id") // => returns 25 as integer. - Params[int](c, "unknownParam", 99) // => returns the default 99 as integer. - // ... - return c.SendString("Hello, " + fiber.Params[string](c, "user")) + // http://example.com/user/john/25 + Params[int](c, "id") // => returns 25 as integer. + Params[int](c, "unknownParam", 99) // => returns the default 99 as integer. + // ... + return c.SendString("Hello, " + fiber.Params[string](c, "user")) }) ``` @@ -116,10 +115,10 @@ func Query[V any](c Ctx, key string, defaultValue ...V) V ```go title="Example" app.Get("/search", func(c fiber.Ctx) error { - // http://example.com/search?name=john&age=25 - Query[string](c, "name") // => returns "john" - Query[int](c, "age") // => returns 25 as integer. - Query[string](c, "unknownParam", "default") // => returns "default" as string. - // ... + // http://example.com/search?name=john&age=25 + Query[string](c, "name") // => returns "john" + Query[int](c, "age") // => returns 25 as integer. + Query[string](c, "unknownParam", "default") // => returns "default" as string. + // ... }) ``` diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 45025526..7226347f 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -27,18 +27,18 @@ app := fiber.New(fiber.Config{ }) type User struct { - Name string `json:"name" form:"name" query:"name" validate:"required"` - Age int `json:"age" form:"age" query:"age" validate:"gte=0,lte=100"` + Name string `json:"name" form:"name" query:"name" validate:"required"` + Age int `json:"age" form:"age" query:"age" validate:"gte=0,lte=100"` } app.Post("/", func(c fiber.Ctx) error { - user := new(User) - - // Works with all bind methods - Body, Query, Form, ... - if err := c.Bind().Body(user); err != nil { // <- here you receive the validation errors - return err - } - - return c.JSON(user) + user := new(User) + + // Works with all bind methods - Body, Query, Form, ... + if err := c.Bind().Body(user); err != nil { // <- here you receive the validation errors + return err + } + + return c.JSON(user) }) ``` diff --git a/docs/intro.md b/docs/intro.md index 9c651b38..7e0e1798 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -12,7 +12,7 @@ These docs are for **Fiber v3**, which was released on **March XX, 2024**. ### Installation -First of all, [download](https://go.dev/dl/) and install Go. `1.21` or higher is required. +First of all, [download](https://go.dev/dl/) and install Go. `1.22` 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: @@ -54,10 +54,10 @@ We created a custom `CopyString` function that does the above and is available u ```go app.Get("/:foo", func(c fiber.Ctx) error { - // Variable is now immutable - result := utils.CopyString(c.Params("foo")) + // Variable is now immutable + result := utils.CopyString(c.Params("foo")) - // ... + // ... }) ``` @@ -65,13 +65,13 @@ Alternatively, you can also use the `Immutable` setting. It will make all values ```go app := fiber.New(fiber.Config{ - Immutable: true, + Immutable: true, }) ``` For more information, please check [**\#426**](https://github.com/gofiber/fiber/issues/426), [**\#185**](https://github.com/gofiber/fiber/issues/185) and [**\#3012**](https://github.com/gofiber/fiber/issues/3012). -### Hello, World! +### Hello, World Embedded below is essentially the most straightforward **Fiber** app you can create: @@ -81,13 +81,13 @@ package main import "github.com/gofiber/fiber/v3" func main() { - app := fiber.New() + app := fiber.New() - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") - }) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello, World!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -115,48 +115,48 @@ app.Method(path string, ...func(fiber.Ctx) error) - `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** +#### Simple route ```go // Respond with "Hello, World!" on root path, "/" app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Hello, World!") + return c.SendString("Hello, World!") }) ``` -**Parameters** +#### 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 + return c.SendString("value: " + c.Params("value")) + // => Get request with value: hello world }) ``` -**Optional parameter** +#### 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?") + if c.Params("name") != "" { + return c.SendString("Hello " + c.Params("name")) + // => Hello john + } + return c.SendString("Where is john?") }) ``` -**Wildcards** +#### 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 + return c.SendString("API path: " + c.Params("*")) + // => API path: user/john }) ``` @@ -183,8 +183,3 @@ 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/middleware/adaptor.md b/docs/middleware/adaptor.md index 39fc1895..3103635d 100644 --- a/docs/middleware/adaptor.md +++ b/docs/middleware/adaptor.md @@ -7,6 +7,7 @@ id: 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 @@ -21,149 +22,154 @@ Converter for net/http handlers to/from Fiber request handlers, special thanks t ## Examples ### net/http to Fiber + ```go package main import ( - "fmt" - "net/http" + "fmt" + "net/http" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/adaptor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // New fiber app - app := fiber.New() + // New fiber app + app := fiber.New() - // http.Handler -> fiber.Handler - app.Get("/", adaptor.HTTPHandler(handler(greet))) + // http.Handler -> fiber.Handler + app.Get("/", adaptor.HTTPHandler(handler(greet))) - // http.HandlerFunc -> fiber.Handler - app.Get("/func", adaptor.HTTPHandlerFunc(greet)) + // http.HandlerFunc -> fiber.Handler + app.Get("/func", adaptor.HTTPHandlerFunc(greet)) - // Listen on port 3000 - app.Listen(":3000") + // Listen on port 3000 + app.Listen(":3000") } func handler(f http.HandlerFunc) http.Handler { - return http.HandlerFunc(f) + return http.HandlerFunc(f) } func greet(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "Hello World!") + fmt.Fprint(w, "Hello World!") } ``` ### net/http middleware to Fiber + ```go package main import ( - "log" - "net/http" + "log" + "net/http" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/adaptor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // New fiber app - app := fiber.New() + // New fiber app + app := fiber.New() - // http middleware -> fiber.Handler - app.Use(adaptor.HTTPMiddleware(logMiddleware)) + // http middleware -> fiber.Handler + app.Use(adaptor.HTTPMiddleware(logMiddleware)) - // Listen on port 3000 - app.Listen(":3000") + // 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) - }) + 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" + "net/http" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/adaptor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - // fiber.Handler -> http.Handler - http.Handle("/", adaptor.FiberHandler(greet)) + // fiber.Handler -> http.Handler + http.Handle("/", adaptor.FiberHandler(greet)) - // fiber.Handler -> http.HandlerFunc - http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet)) + // fiber.Handler -> http.HandlerFunc + http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet)) - // Listen on port 3000 - http.ListenAndServe(":3000", nil) + // Listen on port 3000 + http.ListenAndServe(":3000", nil) } func greet(c fiber.Ctx) error { - return c.SendString("Hello World!") + return c.SendString("Hello World!") } ``` ### Fiber App to net/http + ```go package main import ( - "net/http" + "net/http" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/adaptor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/greet", greet) + app.Get("/greet", greet) - // Listen on port 3000 - http.ListenAndServe(":3000", adaptor.FiberApp(app)) + // Listen on port 3000 + http.ListenAndServe(":3000", adaptor.FiberApp(app)) } func greet(c fiber.Ctx) error { - return c.SendString("Hello World!") + return c.SendString("Hello World!") } ``` ### Fiber Context to (net/http).Request + ```go package main import ( - "net/http" + "net/http" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/adaptor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/adaptor" ) func main() { - app := fiber.New() + app := fiber.New() - app.Get("/greet", greetWithHTTPReq) + app.Get("/greet", greetWithHTTPReq) - // Listen on port 3000 - http.ListenAndServe(":3000", adaptor.FiberApp(app)) + // 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 - } + httpReq, err := adaptor.ConvertRequest(c, false) + if err != nil { + return err + } - return c.SendString("Request URL: " + httpReq.URL.String()) + return c.SendString("Request URL: " + httpReq.URL.String()) } ``` diff --git a/docs/middleware/basicauth.md b/docs/middleware/basicauth.md index 246945ce..080f1283 100644 --- a/docs/middleware/basicauth.md +++ b/docs/middleware/basicauth.md @@ -20,8 +20,8 @@ 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/basicauth" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/basicauth" ) ``` diff --git a/docs/middleware/cache.md b/docs/middleware/cache.md index f62ab9ff..0723c615 100644 --- a/docs/middleware/cache.md +++ b/docs/middleware/cache.md @@ -52,7 +52,7 @@ app.Use(cache.New(cache.Config{ return time.Second * time.Duration(newCacheTime) }, KeyGenerator: func(c fiber.Ctx) string { - return utils.CopyString(c.Path()) + return utils.CopyString(c.Path()) }, })) diff --git a/docs/middleware/compress.md b/docs/middleware/compress.md index 5a119ce2..aac21681 100644 --- a/docs/middleware/compress.md +++ b/docs/middleware/compress.md @@ -22,8 +22,8 @@ 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/compress" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/compress" ) ``` @@ -40,10 +40,10 @@ app.Use(compress.New(compress.Config{ // 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 + Next: func(c fiber.Ctx) bool { + return c.Path() == "/dont_compress" + }, + Level: compress.LevelBestSpeed, // 1 })) ``` diff --git a/docs/middleware/cors.md b/docs/middleware/cors.md index 4442d4b4..6c6d31cd 100644 --- a/docs/middleware/cors.md +++ b/docs/middleware/cors.md @@ -26,8 +26,8 @@ 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/cors" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/cors" ) ``` @@ -49,8 +49,8 @@ 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", + AllowOrigins: []string{"https://gofiber.io", "https://gofiber.net"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, })) ``` @@ -69,25 +69,25 @@ If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `"*"` ```go // dbCheckOrigin checks if the origin is in the list of allowed origins in the database. func dbCheckOrigin(db *sql.DB, origin string) bool { - // Placeholder query - adjust according to your database schema and query needs - query := "SELECT COUNT(*) FROM allowed_origins WHERE origin = $1" - - var count int - err := db.QueryRow(query, origin).Scan(&count) - if err != nil { - // Handle error (e.g., log it); for simplicity, we return false here - return false - } - - return count > 0 + // Placeholder query - adjust according to your database schema and query needs + query := "SELECT COUNT(*) FROM allowed_origins WHERE origin = $1" + + var count int + err := db.QueryRow(query, origin).Scan(&count) + if err != nil { + // Handle error (e.g., log it); for simplicity, we return false here + return false + } + + return count > 0 } // ... app.Use(cors.New(cors.Config{ - AllowOriginsFunc: func(origin string) bool { - return dbCheckOrigin(db, origin) - }, + AllowOriginsFunc: func(origin string) bool { + return dbCheckOrigin(db, origin) + }, })) ``` @@ -104,7 +104,7 @@ app.Use(cors.New(cors.Config{ This will result in the following panic: -``` +```text panic: [CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin '*'. Please specify allowed origins explicitly or adjust 'AllowCredentials' setting. ``` @@ -130,22 +130,22 @@ If AllowOrigins is a zero value `[]string{}`, and AllowOriginsFunc is provided, ```go var ConfigDefault = Config{ - Next: nil, - AllowOriginsFunc: nil, - AllowOrigins: []string{"*"}, - AllowMethods: []string{ - fiber.MethodGet, - fiber.MethodPost, - fiber.MethodHead, - fiber.MethodPut, - fiber.MethodDelete, - fiber.MethodPatch, - }, - AllowHeaders: []string{}, - AllowCredentials: false, - ExposeHeaders: []string{}, - MaxAge: 0, - AllowPrivateNetwork: false, + Next: nil, + AllowOriginsFunc: nil, + AllowOrigins: []string{"*"}, + AllowMethods: []string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, + AllowHeaders: []string{}, + AllowCredentials: false, + ExposeHeaders: []string{}, + MaxAge: 0, + AllowPrivateNetwork: false, } ``` @@ -163,7 +163,7 @@ app.Use(cors.New(cors.Config{ })) ``` -# How It Works +## How It Works The CORS middleware works by adding the necessary CORS headers to responses from your Fiber application. These headers tell browsers what origins, methods, and headers are allowed for cross-origin requests. @@ -189,7 +189,7 @@ The `AllowMethods` option controls which HTTP methods are allowed. For example, The `AllowHeaders` option specifies which headers are allowed in the actual request. The middleware sets the Access-Control-Allow-Headers response header to the value of `AllowHeaders`. This informs the client which headers it can use in the actual request. -The `AllowCredentials` option indicates whether the response to the request can be exposed when the credentials flag is true. If `AllowCredentials` is set to `true`, the middleware adds the header `Access-Control-Allow-Credentials: true` to the response. To prevent security vulnerabilities, `AllowCredentials` cannot be set to `true` if `AllowOrigins` is set to a wildcard (`*`). +The `AllowCredentials` option indicates whether the response to the request can be exposed when the credentials flag is true. If `AllowCredentials` is set to `true`, the middleware adds the header `Access-Control-Allow-Credentials: true` to the response. To prevent security vulnerabilities, `AllowCredentials` cannot be set to `true` if `AllowOrigins` is set to a wildcard (`*`). The `ExposeHeaders` option defines a whitelist of headers that clients are allowed to access. If `ExposeHeaders` is set to `"X-Custom-Header"`, the middleware adds the header `Access-Control-Expose-Headers: X-Custom-Header` to the response. diff --git a/docs/middleware/csrf.md b/docs/middleware/csrf.md index 73980250..a034f9df 100644 --- a/docs/middleware/csrf.md +++ b/docs/middleware/csrf.md @@ -18,8 +18,8 @@ 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/csrf" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/csrf" ) ``` @@ -31,12 +31,12 @@ 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.UUIDv4, - Extractor: func(c fiber.Ctx) (string, error) { ... }, + KeyLookup: "header:X-Csrf-Token", + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + Extractor: func(c fiber.Ctx) (string, error) { ... }, })) ``` @@ -48,27 +48,27 @@ Getting the CSRF token in a handler: ```go func handler(c fiber.Ctx) error { - handler := csrf.HandlerFromContext(c) - token := csrf.TokenFromContext(c) - if handler == nil { - panic("csrf middleware handler not registered") - } - cfg := handler.Config - if cfg == nil { - panic("csrf middleware handler has no config") - } - if !strings.Contains(cfg.KeyLookup, ":") { - panic("invalid KeyLookup format") - } - formKey := strings.Split(cfg.KeyLookup, ":")[1] - - tmpl := fmt.Sprintf(`
- - - -
`, formKey, token) - c.Set("Content-Type", "text/html") - return c.SendString(tmpl) + handler := csrf.HandlerFromContext(c) + token := csrf.TokenFromContext(c) + if handler == nil { + panic("csrf middleware handler not registered") + } + cfg := handler.Config + if cfg == nil { + panic("csrf middleware handler has no config") + } + if !strings.Contains(cfg.KeyLookup, ":") { + panic("invalid KeyLookup format") + } + formKey := strings.Split(cfg.KeyLookup, ":")[1] + + tmpl := fmt.Sprintf(`
+ + + +
`, formKey, token) + c.Set("Content-Type", "text/html") + return c.SendString(tmpl) } ``` @@ -78,11 +78,11 @@ There are two basic use cases for the CSRF middleware: 1. **Without Sessions**: This is the simplest way to use the middleware. It uses the Double Submit Cookie Pattern and does not require a user session. - - See GoFiber recipe [CSRF](https://github.com/gofiber/recipes/tree/master/csrf) for an example of using the CSRF middleware without a user session. + - See GoFiber recipe [CSRF](https://github.com/gofiber/recipes/tree/master/csrf) for an example of using the CSRF middleware without a user session. 2. **With Sessions**: This is generally considered more secure. It uses the Synchronizer Token Pattern and requires a user session, and the use of pre-session, which prevents login CSRF attacks. - - See GoFiber recipe [CSRF with Session](https://github.com/gofiber/recipes/tree/master/csrf-with-session) for an example of using the CSRF middleware with a user session. + - See GoFiber recipe [CSRF with Session](https://github.com/gofiber/recipes/tree/master/csrf-with-session) for an example of using the CSRF middleware with a user session. ## Signatures @@ -94,7 +94,6 @@ func HandlerFromContext(c fiber.Ctx) *Handler func (h *Handler) DeleteToken(c fiber.Ctx) error ``` - ## Config | Property | Type | Description | Default | @@ -122,14 +121,14 @@ func (h *Handler) DeleteToken(c fiber.Ctx) error ```go var ConfigDefault = Config{ - KeyLookup: "header:" + HeaderName, - CookieName: "csrf_", - CookieSameSite: "Lax", - Expiration: 1 * time.Hour, - KeyGenerator: utils.UUIDv4, - ErrorHandler: defaultErrorHandler, - Extractor: FromHeader(HeaderName), - SessionKey: "csrfToken", + KeyLookup: "header:" + HeaderName, + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + ErrorHandler: defaultErrorHandler, + Extractor: FromHeader(HeaderName), + SessionKey: "csrfToken", } ``` @@ -139,18 +138,18 @@ It's recommended to use this middleware with [fiber/middleware/session](https:// ```go var ConfigDefault = Config{ - KeyLookup: "header:" + HeaderName, - CookieName: "__Host-csrf_", - CookieSameSite: "Lax", - CookieSecure: true, - CookieSessionOnly: true, - CookieHTTPOnly: true, - Expiration: 1 * time.Hour, - KeyGenerator: utils.UUIDv4, - ErrorHandler: defaultErrorHandler, - Extractor: FromHeader(HeaderName), - Session: session.Store, - SessionKey: "csrfToken", + KeyLookup: "header:" + HeaderName, + CookieName: "__Host-csrf_", + CookieSameSite: "Lax", + CookieSecure: true, + CookieSessionOnly: true, + CookieHTTPOnly: true, + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + ErrorHandler: defaultErrorHandler, + Extractor: FromHeader(HeaderName), + Session: session.Store, + SessionKey: "csrfToken", } ``` @@ -166,7 +165,7 @@ In the following example, the CSRF middleware will allow requests from `trusted. ```go app.Use(csrf.New(csrf.Config{ - TrustedOrigins: []string{"https://trusted.example.com"}, + TrustedOrigins: []string{"https://trusted.example.com"}, })) ``` @@ -176,7 +175,7 @@ In the following example, the CSRF middleware will allow requests from any subdo ```go app.Use(csrf.New(csrf.Config{ - TrustedOrigins: []string{"https://*.example.com"}, + TrustedOrigins: []string{"https://*.example.com"}, })) ``` @@ -216,19 +215,19 @@ Example, returning a JSON response for API requests and rendering an error page ```go app.Use(csrf.New(csrf.Config{ - ErrorHandler: func(c fiber.Ctx, err error) error { - accepts := c.Accepts("html", "json") - path := c.Path() - if accepts == "json" || strings.HasPrefix(path, "/api/") { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Forbidden", - }) - } - return c.Status(fiber.StatusForbidden).Render("error", fiber.Map{ - "Title": "Forbidden", - "Status": fiber.StatusForbidden, - }, "layouts/main") - }, + ErrorHandler: func(c fiber.Ctx, err error) error { + accepts := c.Accepts("html", "json") + path := c.Path() + if accepts == "json" || strings.HasPrefix(path, "/api/") { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Forbidden", + }) + } + return c.Status(fiber.StatusForbidden).Render("error", fiber.Map{ + "Title": "Forbidden", + "Status": fiber.StatusForbidden, + }, "layouts/main") + }, })) ``` @@ -239,17 +238,17 @@ You can use any storage from our [storage](https://github.com/gofiber/storage/) ```go storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 app.Use(csrf.New(csrf.Config{ - Storage: storage, + Storage: storage, })) ``` -# How It Works +## How It Works -## Token Generation +### Token Generation CSRF tokens are generated on 'safe' requests and when the existing token has expired or hasn't been set yet. If `SingleUseToken` is `true`, a new token is generated after each use. Retrieve the CSRF token using `csrf.TokenFromContext(c)`. -## Security Considerations +### Security Considerations This middleware is designed to protect against CSRF attacks but does not protect against other attack vectors, such as XSS. It should be used in combination with other security measures. @@ -257,9 +256,9 @@ This middleware is designed to protect against CSRF attacks but does not protect Never use 'safe' methods to mutate data, for example, never use a GET request to modify a resource. This middleware will not protect against CSRF attacks on 'safe' methods. ::: -### Token Validation Patterns +## Token Validation Patterns -#### Double Submit Cookie Pattern (Default) +### Double Submit Cookie Pattern (Default) By default, the middleware generates and stores tokens using the `fiber.Storage` interface. These tokens are not linked to any particular user session, and they are validated using the Double Submit Cookie pattern. The token is stored in a cookie, and then sent as a header on requests. The middleware compares the cookie value with the header value to validate the token. This is a secure pattern that does not require a user session. @@ -273,7 +272,7 @@ When using this pattern, it's important to set the `CookieSameSite` option to `L When using this pattern, this middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for Storage saves data to memory. See [Custom Storage/Database](#custom-storagedatabase) for customizing the storage. ::: -#### Synchronizer Token Pattern (with Session) +### Synchronizer Token Pattern (with Session) When using this middleware with a user session, the middleware can be configured to store the token within the session. This method is recommended when using a user session, as it is generally more secure than the Double Submit Cookie Pattern. @@ -283,7 +282,7 @@ When using this pattern it's important to regenerate the session when the author Pre-sessions are required and will be created automatically if not present. Use a session value to indicate authentication instead of relying on the presence of a session. ::: -### Defense In Depth +## Defense In Depth When using this middleware, it's recommended to serve your pages over HTTPS, set the `CookieSecure` option to `true`, and set the `CookieSameSite` option to `Lax` or `Strict`. This ensures that the cookie is only sent over HTTPS and not on requests from external sites. @@ -293,7 +292,7 @@ Cookie prefixes `__Host-` and `__Secure-` can be used to further secure the cook To use these prefixes, set the `CookieName` option to `__Host-csrf_` or `__Secure-csrf_`. ::: -### Referer Checking +## Referer Checking For HTTPS requests, this middleware performs strict referer checking. Even if a subdomain can set or modify cookies on your domain, it can't force a user to post to your application, since that request won't come from your own exact domain. @@ -303,11 +302,11 @@ When HTTPS requests are protected by CSRF, referer checking is always carried ou The Referer header is automatically included in requests by all modern browsers, including those made using the JS Fetch API. However, if you're making use of this middleware with a custom client, it's important to ensure that the client sends a valid Referer header. ::: -### Token Lifecycle +## Token Lifecycle Tokens are valid until they expire or until they are deleted. By default, tokens are valid for 1 hour, and each subsequent request extends the expiration by 1 hour. The token only expires if the user doesn't make a request for the duration of the expiration time. -#### Token Reuse +### Token Reuse By default, tokens may be used multiple times. If you want to delete the token after it has been used, you can set the `SingleUseToken` option to `true`. This will delete the token after it has been used, and a new token will be generated on the next request. @@ -315,16 +314,16 @@ By default, tokens may be used multiple times. If you want to delete the token a Using `SingleUseToken` comes with usability trade-offs and is not enabled by default. For example, it can interfere with the user experience if the user has multiple tabs open or uses the back button. ::: -#### Deleting Tokens +### Deleting Tokens When the authorization status changes, the CSRF token MUST be deleted, and a new one generated. This can be done by calling `handler.DeleteToken(c)`. ```go handler := csrf.HandlerFromContext(ctx) if handler != nil { - if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil { - // handle error - } + if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil { + // handle error + } } ``` @@ -332,6 +331,6 @@ if handler != nil { If you are using this middleware with the fiber session middleware, then you can simply call `session.Destroy()`, `session.Regenerate()`, or `session.Reset()` to delete the session and the token stored therein. ::: -### BREACH +## BREACH It's important to note that the token is sent as a header on every request. If you include the token in a page that is vulnerable to [BREACH](https://en.wikipedia.org/wiki/BREACH), an attacker may be able to extract the token. To mitigate this, ensure your pages are served over HTTPS, disable HTTP compression, and implement rate limiting for requests. diff --git a/docs/middleware/earlydata.md b/docs/middleware/earlydata.md index a5ce3219..b0e39b2f 100644 --- a/docs/middleware/earlydata.md +++ b/docs/middleware/earlydata.md @@ -11,8 +11,8 @@ Make sure to enable fiber's `EnableTrustedProxyCheck` config option before using 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/ +- [datatracker](https://datatracker.ietf.org/doc/html/rfc8446#section-8) +- [trailofbits](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. @@ -30,8 +30,8 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/earlydata" ) ``` @@ -43,8 +43,8 @@ app.Use(earlydata.New()) // Or extend your config for customization app.Use(earlydata.New(earlydata.Config{ - Error: fiber.ErrTooEarly, - // ... + Error: fiber.ErrTooEarly, + // ... })) ``` @@ -61,13 +61,13 @@ app.Use(earlydata.New(earlydata.Config{ ```go 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, + 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, } ``` @@ -75,7 +75,7 @@ var ConfigDefault = Config{ ```go const ( - DefaultHeaderName = "Early-Data" - DefaultHeaderTrueValue = "1" + DefaultHeaderName = "Early-Data" + DefaultHeaderTrueValue = "1" ) ``` diff --git a/docs/middleware/encryptcookie.md b/docs/middleware/encryptcookie.md index 3054becb..004b4bee 100644 --- a/docs/middleware/encryptcookie.md +++ b/docs/middleware/encryptcookie.md @@ -4,7 +4,7 @@ id: encryptcookie # Encrypt Cookie -Encrypt Cookie is a middleware for [Fiber](https://github.com/gofiber/fiber) that secures your cookie values through encryption. +Encrypt Cookie is a middleware for [Fiber](https://github.com/gofiber/fiber) that secures your cookie values through encryption. :::note This middleware encrypts cookie values and not the cookie names. @@ -28,8 +28,8 @@ To use the Encrypt Cookie middleware, first, import the middleware package as pa ```go import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/encryptcookie" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/encryptcookie" ) ``` @@ -75,11 +75,11 @@ To generate a 32 char key, use `openssl rand -base64 32` or `encryptcookie.Gener ```go var ConfigDefault = Config{ - Next: nil, - Except: []string{}, - Key: "", - Encryptor: EncryptCookie, - Decryptor: DecryptCookie, + Next: nil, + Except: []string{}, + Key: "", + Encryptor: EncryptCookie, + Decryptor: DecryptCookie, } ``` @@ -91,20 +91,21 @@ You may also choose to exclude certain cookies from encryption. For instance, if ```go app.Use(encryptcookie.New(encryptcookie.Config{ - Key: "secret-thirty-2-character-string", - Except: []string{csrf.ConfigDefault.CookieName}, // exclude CSRF cookie + Key: "secret-thirty-2-character-string", + Except: []string{csrf.ConfigDefault.CookieName}, // exclude CSRF cookie })) app.Use(csrf.New(csrf.Config{ - KeyLookup: "header:" + csrf.HeaderName, - CookieSameSite: "Lax", - CookieSecure: true, - CookieHTTPOnly: false, + KeyLookup: "header:" + csrf.HeaderName, + CookieSameSite: "Lax", + CookieSecure: true, + CookieHTTPOnly: false, })) ``` ## Encryption Algorithms The default Encryptor and Decryptor functions use `AES-256-GCM` for encryption and decryption. If you need to use `AES-128` or `AES-192` instead, you can do so by changing the length of the key when calling `encryptcookie.GenerateKey(length)` or by providing a key of one of the following lengths: + - AES-128 requires a 16-byte key. - AES-192 requires a 24-byte key. - AES-256 requires a 32-byte key. @@ -119,4 +120,4 @@ And for AES-192: ```go key := encryptcookie.GenerateKey(24) -``` \ No newline at end of file +``` diff --git a/docs/middleware/envvar.md b/docs/middleware/envvar.md index 3740c38a..4467b734 100644 --- a/docs/middleware/envvar.md +++ b/docs/middleware/envvar.md @@ -18,8 +18,8 @@ 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/envvar" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/envvar" ) ``` @@ -31,10 +31,10 @@ 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": ""}, - }), + envvar.Config{ + ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, + ExcludeVars: map[string]string{"excludeKey": ""}, + }), ) ``` @@ -45,7 +45,8 @@ You will need to provide a path to use the envvar middleware. ## Response Http response contract: -``` + +```json { "vars": { "someEnvVariable": "someValue", diff --git a/docs/middleware/etag.md b/docs/middleware/etag.md index 28cf6919..6c60df8b 100644 --- a/docs/middleware/etag.md +++ b/docs/middleware/etag.md @@ -18,8 +18,8 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/etag" ) ``` diff --git a/docs/middleware/expvar.md b/docs/middleware/expvar.md index 8299b8fd..890ee235 100644 --- a/docs/middleware/expvar.md +++ b/docs/middleware/expvar.md @@ -18,12 +18,13 @@ Import the middleware package that is part of the Fiber web framework ```go import ( - "github.com/gofiber/fiber/v3" - expvarmw "github.com/gofiber/fiber/v3/middleware/expvar" + "github.com/gofiber/fiber/v3" + expvarmw "github.com/gofiber/fiber/v3/middleware/expvar" ) ``` After you initiate your Fiber app, you can use the following possibilities: + ```go var count = expvar.NewInt("count") diff --git a/docs/middleware/favicon.md b/docs/middleware/favicon.md index ddb0a7a7..7a4b2266 100644 --- a/docs/middleware/favicon.md +++ b/docs/middleware/favicon.md @@ -22,8 +22,8 @@ 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/favicon" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/favicon" ) ``` @@ -55,9 +55,9 @@ app.Use(favicon.New(favicon.Config{ ```go var ConfigDefault = Config{ - Next: nil, - File: "", - URL: fPath, - CacheControl: "public, max-age=31536000", + Next: nil, + File: "", + URL: fPath, + CacheControl: "public, max-age=31536000", } ``` diff --git a/docs/middleware/healthcheck.md b/docs/middleware/healthcheck.md index d99e0851..2837c550 100644 --- a/docs/middleware/healthcheck.md +++ b/docs/middleware/healthcheck.md @@ -4,7 +4,7 @@ id: healthcheck # Health Check -Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides two endpoints for checking the liveness and readiness state of HTTP applications. +Liveness, readiness and startup probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides three endpoints for checking the liveness, readiness, and startup state of HTTP applications. ## Overview @@ -16,6 +16,10 @@ Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/ - **Default Endpoint**: `/readyz` - **Behavior**: By default returns `true` immediately when the server is operational. +- **Startup Probe**: Checks if the application has completed its startup sequence and is ready to proceed with initialization and readiness checks. + - **Default Endpoint**: `/startupz` + - **Behavior**: By default returns `true` immediately when the server is operational. + - **HTTP Status Codes**: - `200 OK`: Returned when the checker function evaluates to `true`. - `503 Service Unavailable`: Returned when the checker function evaluates to `false`. @@ -29,6 +33,7 @@ func NewHealthChecker(config Config) fiber.Handler ## Examples Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework + ```go import( "github.com/gofiber/fiber/v3" @@ -43,6 +48,8 @@ After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can u app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker()) // Provide a minimal config for readiness check app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) +// Provide a minimal config for startup check +app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker()) // Provide a minimal config for check with custom endpoint app.Get("/live", healthcheck.NewHealthChecker()) @@ -58,6 +65,12 @@ app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker(healt return true }, })) +// And it works the same for startup, just change the route +app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ + Probe: func(c fiber.Ctx) bool { + return true + }, +})) // With a custom route and custom probe app.Get("/live", healthcheck.NewHealthChecker(healthcheck.Config{ Probe: func(c fiber.Ctx) bool { @@ -79,29 +92,34 @@ app.All(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker(healt ```go type Config struct { - // Next defines a function to skip this middleware when returned true. If this function returns true + // Next defines a function to skip this middleware when returned true. If this function returns true // and no other handlers are defined for the route, Fiber will return a status 404 Not Found, since // no other handlers were defined to return a different status. - // - // Optional. Default: nil - Next func(fiber.Ctx) bool + // + // Optional. Default: nil + Next func(fiber.Ctx) bool - // Function used for checking the liveness of the application. Returns true if the application - // is running and false if it is not. The liveness probe is typically used to indicate if - // the application is in a state where it can handle requests (e.g., the server is up and running). - // - // Optional. Default: func(c fiber.Ctx) bool { return true } - Probe HealthChecker + // Function used for checking the liveness of the application. Returns true if the application + // is running and false if it is not. The liveness probe is typically used to indicate if + // the application is in a state where it can handle requests (e.g., the server is up and running). + // The readiness probe is typically used to indicate if the application is ready to start accepting traffic (e.g., all necessary components + // are initialized and dependent services are available) and the startup probe typically used to + // indicate if the application has completed its startup sequence and is ready to proceed with + // initialization and readiness checks + // + // Optional. Default: func(c fiber.Ctx) bool { return true } + Probe HealthChecker } ``` ## Default Config The default configuration used by this middleware is defined as follows: + ```go func defaultProbe(fiber.Ctx) bool { return true } var ConfigDefault = Config{ - Probe: defaultProbe, + Probe: defaultProbe, } ``` diff --git a/docs/middleware/helmet.md b/docs/middleware/helmet.md index b0dca52d..bc101350 100644 --- a/docs/middleware/helmet.md +++ b/docs/middleware/helmet.md @@ -13,30 +13,31 @@ func New(config ...Config) fiber.Handler ``` ## Examples + ```go package main import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/helmet" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/helmet" ) func main() { - app := fiber.New() + app := fiber.New() - app.Use(helmet.New()) + app.Use(helmet.New()) - app.Get("/", func(c fiber.Ctx) error { - return c.SendString("Welcome!") - }) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Welcome!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` -**Test:** +## Test -```curl +```bash curl -I http://localhost:3000 ``` @@ -67,16 +68,16 @@ curl -I http://localhost:3000 ```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", + 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/middleware/idempotency.md b/docs/middleware/idempotency.md index 475ec9df..59d47f8b 100644 --- a/docs/middleware/idempotency.md +++ b/docs/middleware/idempotency.md @@ -6,7 +6,7 @@ id: 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. +Refer to [datatracker](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02) for a better understanding. ## Signatures @@ -22,8 +22,8 @@ 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/idempotency" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/idempotency" ) ``` @@ -39,8 +39,8 @@ app.Use(idempotency.New()) ```go app.Use(idempotency.New(idempotency.Config{ - Lifetime: 42 * time.Minute, - // ... + Lifetime: 42 * time.Minute, + // ... })) ``` @@ -60,26 +60,26 @@ app.Use(idempotency.New(idempotency.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()) - }, + 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, + 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) - } + 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 - }, + return nil + }, - KeepResponseHeaders: nil, + KeepResponseHeaders: nil, - Lock: nil, // Set in configDefault so we don't allocate data here. + Lock: nil, // Set in configDefault so we don't allocate data here. - Storage: 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/middleware/keyauth.md b/docs/middleware/keyauth.md index 9907a5da..ce39b820 100644 --- a/docs/middleware/keyauth.md +++ b/docs/middleware/keyauth.md @@ -19,44 +19,44 @@ func TokenFromContext(c fiber.Ctx) string package main import ( - "crypto/sha256" - "crypto/subtle" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/keyauth" + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/keyauth" ) var ( - apiKey = "correct horse battery staple" + apiKey = "correct horse battery staple" ) func validateAPIKey(c fiber.Ctx, key string) (bool, error) { - hashedAPIKey := sha256.Sum256([]byte(apiKey)) - hashedKey := sha256.Sum256([]byte(key)) + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) - if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { - return true, nil - } - return false, keyauth.ErrMissingOrMalformedAPIKey + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey } func main() { - app := fiber.New() + 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, - })) + // 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.Get("/", func(c fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` -**Test:** +## Test ```bash # No api-key specified -> 400 missing @@ -72,7 +72,6 @@ curl --cookie "access_token=Clearly A Wrong Key" http://localhost:3000 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 @@ -81,63 +80,63 @@ If you want to authenticate only certain endpoints, you can use the `Config` of package main import ( - "crypto/sha256" - "crypto/subtle" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/keyauth" - "regexp" - "strings" + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/keyauth" + "regexp" + "strings" ) var ( - apiKey = "correct horse battery staple" - protectedURLs = []*regexp.Regexp{ - regexp.MustCompile("^/authenticated$"), - regexp.MustCompile("^/auth2$"), - } + 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)) + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) - if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { - return true, nil - } - return false, keyauth.ErrMissingOrMalformedAPIKey + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey } func authFilter(c fiber.Ctx) bool { - originalURL := strings.ToLower(c.OriginalURL()) + originalURL := strings.ToLower(c.OriginalURL()) - for _, pattern := range protectedURLs { - if pattern.MatchString(originalURL) { - return false - } - } - return true + for _, pattern := range protectedURLs { + if pattern.MatchString(originalURL) { + return false + } + } + return true } func main() { - app := fiber.New() + app := fiber.New() - app.Use(keyauth.New(keyauth.Config{ - Next: authFilter, - KeyLookup: "cookie:access_token", - Validator: validateAPIKey, - })) + 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.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") + app.Listen(":3000") } ``` @@ -163,10 +162,10 @@ curl --cookie "access_token=correct horse battery staple" http://localhost:3000/ package main import ( - "crypto/sha256" - "crypto/subtle" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/keyauth" + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/keyauth" ) const ( @@ -174,29 +173,29 @@ const ( ) func main() { - app := fiber.New() + 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)) + 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 - }, - }) + 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("/", func(c fiber.Ctx) error { + return c.SendString("Welcome") + }) - app.Get("/allowed", authMiddleware, func(c fiber.Ctx) error { - return c.SendString("Successfully authenticated!") - }) + app.Get("/allowed", authMiddleware, func(c fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) - app.Listen(":3000") + app.Listen(":3000") } ``` @@ -228,23 +227,24 @@ curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000 ```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, - CustomKeyLookup: nil, - AuthScheme: "Bearer", + 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, + CustomKeyLookup: nil, + AuthScheme: "Bearer", } ``` ## CustomKeyLookup Two public utility functions are provided that may be useful when creating custom extraction: + * `DefaultKeyLookup(keyLookup string, authScheme string)`: This is the function that implements the default `KeyLookup` behavior, exposed to be used as a component of custom parsing logic * `MultipleKeySourceLookup(keyLookups []string, authScheme string)`: Creates a CustomKeyLookup function that checks each listed source using the above function until a key is found or the options are all exhausted. For example, `MultipleKeySourceLookup([]string{"header:Authorization", "header:x-api-key", "cookie:apikey"}, "Bearer")` would first check the standard Authorization header, checks the `x-api-key` header next, and finally checks for a cookie named `apikey`. If any of these contain a valid API key, the request continues. Otherwise, an error is returned. diff --git a/docs/middleware/limiter.md b/docs/middleware/limiter.md index 30986251..ab976205 100644 --- a/docs/middleware/limiter.md +++ b/docs/middleware/limiter.md @@ -26,8 +26,8 @@ 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/limiter" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/limiter" ) ``` @@ -43,6 +43,9 @@ app.Use(limiter.New(limiter.Config{ return c.IP() == "127.0.0.1" }, Max: 20, + MaxFunc: func(c fiber.Ctx) int { + return 20 + }, Expiration: 30 * time.Second, KeyGenerator: func(c fiber.Ctx) string { return c.Get("x-forwarded-for") @@ -68,10 +71,26 @@ app.Use(limiter.New(limiter.Config{ })) ``` -This means that every window will take into account the previous window(if there was any). The given formula for the rate is: +This means that every window will consider the previous window (if there was any). The given formula for the rate is: + +```text +weightOfPreviousWindow = previous window's amount request * (whenNewWindow / Expiration) +rate = weightOfPreviousWindow + current window's amount request. ``` -weightOfPreviousWindpw = previous window's amount request * (whenNewWindow / Expiration) -rate = weightOfPreviousWindpw + current window's amount request. + +## Dynamic limit + +You can also calculate the limit dynamically using the MaxFunc parameter. It's a function that receives the request's context as a parameter and allow you to calculate a different limit for each request separately. + +Example: + +```go +app.Use(limiter.New(limiter.Config{ + MaxFunc: func(c fiber.Ctx) int { + return getUserLimit(ctx.Param("id")) + }, + Expiration: 30 * time.Second, +})) ``` ## Config @@ -80,6 +99,7 @@ rate = weightOfPreviousWindpw + current window's amount request. |:-----------------------|:--------------------------|:--------------------------------------------------------------------------------------------|:-----------------------------------------| | 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 | +| MaxFunc | `func(fiber.Ctx) int` | A function to calculate the max number of recent connections during `Expiration` seconds before sending a 429 response. | A function which returns the cfg.Max | | 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 | @@ -100,6 +120,9 @@ A custom store can be used if it implements the `Storage` interface - more detai ```go var ConfigDefault = Config{ Max: 5, + MaxFunc: func(c fiber.Ctx) int { + return 5 + }, Expiration: 1 * time.Minute, KeyGenerator: func(c fiber.Ctx) string { return c.IP() @@ -119,7 +142,8 @@ You can use any storage from our [storage](https://github.com/gofiber/storage/) ```go storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 + app.Use(limiter.New(limiter.Config{ - Storage: storage, + Storage: storage, })) ``` diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index 7c147ba0..f9972ec2 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -7,9 +7,11 @@ id: 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 @@ -88,7 +90,7 @@ app.Use(logger.New(logger.Config{ })) ``` -:::tip +:::tip Writing to os.File is goroutine-safe, but if you are using a custom Output that is not goroutine-safe, make sure to implement locking to properly serialize writes. ::: @@ -112,6 +114,7 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output that | 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, @@ -126,6 +129,7 @@ var ConfigDefault = Config{ ``` ## Constants + ```go // Logger variables const ( diff --git a/docs/middleware/monitor.md b/docs/middleware/monitor.md index b314c66b..aa3b1c89 100644 --- a/docs/middleware/monitor.md +++ b/docs/middleware/monitor.md @@ -14,22 +14,25 @@ Monitor is still in beta, API might change in the future! ![](https://i.imgur.com/nHAtBpJ.gif) -### Signatures +## Signatures + ```go func New() fiber.Handler ``` -### Examples +## 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/monitor" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/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()) @@ -39,43 +42,53 @@ app.Get("/metrics", monitor.New()) // 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 }} +{ + "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" | +| 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 | "[fonts.googleapis.com](https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap)" | +| ChartJsURL | `string` | ChartJsURL for specify ChartJS library path or URL | "[cdn.jsdelivr.net](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, - }), + 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/middleware/pprof.md b/docs/middleware/pprof.md index bbac32fd..146b6391 100644 --- a/docs/middleware/pprof.md +++ b/docs/middleware/pprof.md @@ -18,8 +18,8 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/pprof" ) ``` diff --git a/docs/middleware/proxy.md b/docs/middleware/proxy.md index 08d00b35..8404efe2 100644 --- a/docs/middleware/proxy.md +++ b/docs/middleware/proxy.md @@ -7,6 +7,7 @@ id: proxy Proxy middleware for [Fiber](https://github.com/gofiber/fiber) that allows you to proxy requests to multiple servers. ## Signatures + // BalancerForward performs the given http request based on a round-robin balancer and fills the given http response. ```go @@ -154,18 +155,18 @@ app.Use(proxy.Balancer(proxy.Config{ ## 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` | -| DialDualStack | `bool` | Client will attempt to connect to both IPv4 and IPv6 host addresses if set to true. | `false` | -| Client | `*fasthttp.LBClient` | Client is a custom client when client config is complex. | `nil` | +| 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](https://foobar.com), [http://www.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` | +| DialDualStack | `bool` | Client will attempt to connect to both IPv4 and IPv6 host addresses if set to true. | `false` | +| Client | `*fasthttp.LBClient` | Client is a custom client when client config is complex. | `nil` | ## Default Config diff --git a/docs/middleware/recover.md b/docs/middleware/recover.md index 8d0a601e..e8b4870a 100644 --- a/docs/middleware/recover.md +++ b/docs/middleware/recover.md @@ -18,8 +18,8 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/recover" ) ``` diff --git a/docs/middleware/redirect.md b/docs/middleware/redirect.md index c7976a7a..0b2551aa 100644 --- a/docs/middleware/redirect.md +++ b/docs/middleware/redirect.md @@ -18,35 +18,35 @@ func New(config ...Config) fiber.Handler package main import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/fiber/v3/middleware/redirect" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/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") + 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:** +## Test -```curl +```bash curl http://localhost:3000/old curl http://localhost:3000/old/hello ``` @@ -63,6 +63,6 @@ curl http://localhost:3000/old/hello ```go var ConfigDefault = Config{ - StatusCode: fiber.StatusFound, + StatusCode: fiber.StatusFound, } ``` diff --git a/docs/middleware/requestid.md b/docs/middleware/requestid.md index 36abb201..739a4a61 100644 --- a/docs/middleware/requestid.md +++ b/docs/middleware/requestid.md @@ -19,8 +19,8 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/requestid" ) ``` @@ -58,6 +58,7 @@ func handler(c fiber.Ctx) error { | Generator | `func() string` | Generator defines a function to generate the unique identifier. | utils.UUID | ## 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. @@ -66,6 +67,6 @@ requests made to the server. To conceal this value for better privacy, use the var ConfigDefault = Config{ Next: nil, Header: fiber.HeaderXRequestID, - Generator: utils.UUID, + Generator: utils.UUID, } ``` diff --git a/docs/middleware/rewrite.md b/docs/middleware/rewrite.md index d7ca4a22..99c2acf8 100644 --- a/docs/middleware/rewrite.md +++ b/docs/middleware/rewrite.md @@ -20,39 +20,40 @@ func New(config ...Config) fiber.Handler | 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/v3" - "github.com/gofiber/fiber/v3/middleware/rewrite" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/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") + 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:** +## Test -```curl +```bash curl http://localhost:3000/old curl http://localhost:3000/old/hello ``` diff --git a/docs/middleware/session.md b/docs/middleware/session.md index db62b53c..39b9ccc8 100644 --- a/docs/middleware/session.md +++ b/docs/middleware/session.md @@ -37,11 +37,13 @@ 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" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/session" ) ``` @@ -76,15 +78,15 @@ app.Get("/", func(c fiber.Ctx) error { panic(err) } - // Sets a specific expiration for this session - sess.SetExpiry(time.Second * 2) + // Sets a specific expiration for this session + sess.SetExpiry(time.Second * 2) // Save session if err := sess.Save(); err != nil { - panic(err) - } + panic(err) + } - return c.SendString(fmt.Sprintf("Welcome %v", name)) + return c.SendString(fmt.Sprintf("Welcome %v", name)) }) ``` @@ -108,11 +110,11 @@ app.Get("/", func(c fiber.Ctx) error { ```go var ConfigDefault = Config{ - Expiration: 24 * time.Hour, - KeyLookup: "cookie:session_id", - KeyGenerator: utils.UUIDv4, - source: "cookie", - sessionName: "session_id", + Expiration: 24 * time.Hour, + KeyLookup: "cookie:session_id", + KeyGenerator: utils.UUIDv4, + source: "cookie", + sessionName: "session_id", } ``` @@ -120,9 +122,9 @@ var ConfigDefault = Config{ ```go const ( - SourceCookie Source = "cookie" - SourceHeader Source = "header" - SourceURLQuery Source = "query" + SourceCookie Source = "cookie" + SourceHeader Source = "header" + SourceURLQuery Source = "query" ) ``` @@ -132,8 +134,9 @@ You can use any storage from our [storage](https://github.com/gofiber/storage/) ```go storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 + store := session.New(session.Config{ - Storage: storage, + Storage: storage, }) ``` diff --git a/docs/middleware/skip.md b/docs/middleware/skip.md index 07df603b..5d702dc8 100644 --- a/docs/middleware/skip.md +++ b/docs/middleware/skip.md @@ -7,16 +7,19 @@ id: 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/v3" - "github.com/gofiber/fiber/v3/middleware/skip" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/skip" ) ``` @@ -24,21 +27,21 @@ After you initiate your Fiber app, you can use the following possibilities: ```go func main() { - app := fiber.New() + app := fiber.New() - app.Use(skip.New(BasicHandler, func(ctx fiber.Ctx) bool { - return ctx.Method() == fiber.MethodGet - })) + 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!") - }) + app.Get("/", func(ctx fiber.Ctx) error { + return ctx.SendString("It was a GET request!") + }) - log.Fatal(app.Listen(":3000")) + log.Fatal(app.Listen(":3000")) } func BasicHandler(ctx fiber.Ctx) error { - return ctx.SendString("It was not a GET request!") + return ctx.SendString("It was not a GET request!") } ``` diff --git a/docs/middleware/static.md b/docs/middleware/static.md index 6dd4e3cf..a8c7ec60 100644 --- a/docs/middleware/static.md +++ b/docs/middleware/static.md @@ -19,6 +19,7 @@ func New(root string, cfg ...Config) fiber.Handler ## Examples Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework + ```go import( "github.com/gofiber/fiber/v3" @@ -166,7 +167,7 @@ You can set `CacheDuration` config property to `-1` to disable caching. ```go var ConfigDefault = Config{ - Index: []string{"index.html"}, - CacheDuration: 10 * time.Second, + Index: []string{"index.html"}, + CacheDuration: 10 * time.Second, } ``` diff --git a/docs/middleware/timeout.md b/docs/middleware/timeout.md index e5f77546..8f94f056 100644 --- a/docs/middleware/timeout.md +++ b/docs/middleware/timeout.md @@ -6,13 +6,12 @@ id: timeout There exist two distinct implementations of timeout middleware [Fiber](https://github.com/gofiber/fiber). -**New** +## New + +As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. -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 does not cancel long running executions. Underlying executions must handle timeout by using `context.Context` parameter. ## Signatures @@ -27,8 +26,8 @@ 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/timeout" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/timeout" ) ``` @@ -36,31 +35,31 @@ After you initiate your Fiber app, you can use the following possibilities: ```go func main() { - app := fiber.New() - h := func(c fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil - } + app := fiber.New() + h := func(c fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } - app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second)) - log.Fatal(app.Listen(":3000")) + app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second)) + log.Fatal(app.Listen(":3000")) } func sleepWithContext(ctx context.Context, d time.Duration) error { - timer := time.NewTimer(d) + timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return context.DeadlineExceeded - case <-timer.C: - } - return nil + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return context.DeadlineExceeded + case <-timer.C: + } + return nil } ``` @@ -82,30 +81,30 @@ Use with custom error: var ErrFooTimeOut = errors.New("foo context canceled") func main() { - app := fiber.New() - h := func(c fiber.Ctx) error { - sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") - if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { - return fmt.Errorf("%w: execution error", err) - } - return nil - } + app := fiber.New() + h := func(c fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } - app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second, ErrFooTimeOut)) - log.Fatal(app.Listen(":3000")) + app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second, ErrFooTimeOut)) + log.Fatal(app.Listen(":3000")) } func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error { - timer := time.NewTimer(d) - select { - case <-ctx.Done(): - if !timer.Stop() { - <-timer.C - } - return ErrFooTimeOut - case <-timer.C: - } - return nil + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ErrFooTimeOut + case <-timer.C: + } + return nil } ``` @@ -113,24 +112,24 @@ Sample usage with a DB call: ```go func main() { - app := fiber.New() - db, _ := gorm.Open(postgres.Open("postgres://localhost/foodb"), &gorm.Config{}) + 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 - } + 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 - } + return nil + } - app.Get("/foo", timeout.New(handler, 10*time.Second)) - log.Fatal(app.Listen(":3000")) + app.Get("/foo", timeout.New(handler, 10*time.Second)) + log.Fatal(app.Listen(":3000")) } ``` diff --git a/docs/partials/routing/handler.md b/docs/partials/routing/handler.md index 5cff828d..2b94a07c 100644 --- a/docs/partials/routing/handler.md +++ b/docs/partials/routing/handler.md @@ -30,12 +30,12 @@ func (app *App) All(path string, handler Handler, middlewares ...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!") + 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!") + return c.SendString("I'm a POST request!") }) ``` @@ -71,7 +71,7 @@ app.Use([]string{"/api", "/home"}, func(c fiber.Ctx) error { // Attach multiple handlers app.Use("/api", func(c fiber.Ctx) error { - c.Set("X-Custom-Header", random.String(32)) + c.Set("X-Custom-Header", random.String(32)) return c.Next() }, func(c fiber.Ctx) error { return c.Next() diff --git a/docs/whats_new.md b/docs/whats_new.md index 934d94c7..963d1dae 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -7,7 +7,7 @@ toc_max_heading_level: 3 :::caution -Its a draft, not finished yet. +It's a draft, not finished yet. ::: @@ -20,6 +20,7 @@ We are excited to announce the release of Fiber v3! 🚀 In this guide, we'll walk you through the most important changes in Fiber `v3` and show you how to migrate your existing Fiber `v2` applications to Fiber `v3`. Here's a quick overview of the changes in Fiber `v3`: + - [🚀 App](#-app) - [🗺️ Router](#-router) - [🧠 Context](#-context) @@ -32,11 +33,12 @@ Here's a quick overview of the changes in Fiber `v3`: - [Session](#session) - [Filesystem](#filesystem) - [Monitor](#monitor) + - [Healthcheck](#healthcheck) - [📋 Migration guide](#-migration-guide) ## Drop for old Go versions -Fiber `v3` drops support for Go versions below `1.21`. We recommend upgrading to Go `1.21` or higher to use Fiber `v3`. +Fiber `v3` drops support for Go versions below `1.22`. We recommend upgrading to Go `1.22` or higher to use Fiber `v3`. ## 🚀 App @@ -46,33 +48,33 @@ DRAFT section We have made several changes to the Fiber app, including: -* Listen -> unified with config -* Static -> has been removed and moved to [static middleware](./middleware/static.md) -* app.Config properties moved to listen config - * DisableStartupMessage - * EnablePrefork -> previously Prefork - * EnablePrintRoutes - * ListenerNetwork -> previously Network +- Listen -> unified with config +- Static -> has been removed and moved to [static middleware](./middleware/static.md) +- app.Config properties moved to listen config + - DisableStartupMessage + - EnablePrefork -> previously Prefork + - EnablePrintRoutes + - ListenerNetwork -> previously Network ### new methods -* RegisterCustomBinder -* RegisterCustomConstraint -* NewCtxFunc +- RegisterCustomBinder +- RegisterCustomConstraint +- NewCtxFunc ### removed methods -* Mount -> Use app.Use() instead -* ListenTLS -> Use app.Listen() with tls.Config -* ListenTLSWithCertificate -> Use app.Listen() with tls.Config -* ListenMutualTLS -> Use app.Listen() with tls.Config -* ListenMutualTLSWithCertificate -> Use app.Listen() with tls.Config +- Mount -> Use app.Use() instead +- ListenTLS -> Use app.Listen() with tls.Config +- ListenTLSWithCertificate -> Use app.Listen() with tls.Config +- ListenMutualTLS -> Use app.Listen() with tls.Config +- ListenMutualTLSWithCertificate -> Use app.Listen() with tls.Config ### Methods changes -* Test -> timeout changed to 1 second -* Listen -> has a config parameter -* Listener -> has a config parameter +- Test -> timeout changed to 1 second +- Listen -> has a config parameter +- Listener -> has a config parameter ### CTX interface + customizable @@ -141,6 +143,7 @@ app.Route("/api").Route("/user/:id?") }) }) ``` +
[Here](./api/app#route) you can find more information. @@ -169,6 +172,7 @@ api.Get("/user", func(c *fiber.Ctx) error { // register subapp app.Use("/api", api) ``` +
To enable the routing changes above we had to slightly adjust the signature of the `Add` method. @@ -192,37 +196,37 @@ DRAFT section ### new methods -* AutoFormat -> ExpressJs like -* Host -> ExpressJs like -* Port -> ExpressJs like -* IsProxyTrusted -* Reset -* Schema -> ExpressJs like -* SendStream -> ExpressJs like -* SendString -> ExpressJs like -* String -> ExpressJs like -* ViewBind -> instead of Bind +- AutoFormat -> ExpressJs like +- Host -> ExpressJs like +- Port -> ExpressJs like +- IsProxyTrusted +- Reset +- Schema -> ExpressJs like +- SendStream -> ExpressJs like +- SendString -> ExpressJs like +- String -> ExpressJs like +- ViewBind -> instead of Bind ### removed methods -* AllParams -> c.Bind().URL() ? -* ParamsInt -> Params Generic -* QueryBool -> Query Generic -* QueryFloat -> Query Generic -* QueryInt -> Query Generic -* BodyParser -> c.Bind().Body() -* CookieParser -> c.Bind().Cookie() -* ParamsParser -> c.Bind().URL() -* RedirectToRoute -> c.Redirect().Route() -* RedirectBack -> c.Redirect().Back() -* ReqHeaderParser -> c.Bind().Header() +- AllParams -> c.Bind().URL() ? +- ParamsInt -> Params Generic +- QueryBool -> Query Generic +- QueryFloat -> Query Generic +- QueryInt -> Query Generic +- BodyParser -> c.Bind().Body() +- CookieParser -> c.Bind().Cookie() +- ParamsParser -> c.Bind().URL() +- RedirectToRoute -> c.Redirect().Route() +- RedirectBack -> c.Redirect().Back() +- ReqHeaderParser -> c.Bind().Header() ### changed methods -* Bind -> for Binding instead of View, us c.ViewBind() -* Format -> Param: body interface{} -> handlers ...ResFmt -* Redirect -> c.Redirect().To() -* SendFile now supports different configurations using the config parameter. +- Bind -> for Binding instead of View, us c.ViewBind() +- Format -> Param: body interface{} -> handlers ...ResFmt +- Redirect -> c.Redirect().To() +- SendFile now supports different configurations using the config parameter. --- @@ -230,7 +234,7 @@ DRAFT section The Gofiber client has been completely rebuilt. It includes numerous new features such as Cookiejar, request/response hooks, and more. You can take a look to [client docs](./client/rest.md) to see what's new with the client. - + ## 📎 Binding :::caution @@ -243,7 +247,6 @@ DRAFT section DRAFT section ::: - ## 🧰 Generic functions :::caution @@ -252,6 +255,34 @@ DRAFT section ## 🧬 Middlewares +### Adaptor + +The adaptor middleware has been significantly optimized for performance and efficiency. Key improvements include reduced response times, lower memory usage, and fewer memory allocations. These changes make the middleware more reliable and capable of handling higher loads effectively. Enhancements include the introduction of a `sync.Pool` for managing `fasthttp.RequestCtx` instances and better HTTP request and response handling between net/http and fasthttp contexts. + +| Payload Size | Metric | V2 | V3 | Percent Change | +|--------------|------------------|-----------|----------|-------------------| +| 100KB | Execution Time | 1056 ns/op| 588.6 ns/op | -44.25% | +| | Memory Usage | 2644 B/op | 254 B/op | -90.39% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 500KB | Execution Time | 1061 ns/op| 562.9 ns/op | -46.94% | +| | Memory Usage | 2644 B/op | 248 B/op | -90.62% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 1MB | Execution Time | 1080 ns/op| 629.7 ns/op | -41.68% | +| | Memory Usage | 2646 B/op | 267 B/op | -89.91% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 5MB | Execution Time | 1093 ns/op| 540.3 ns/op | -50.58% | +| | Memory Usage | 2654 B/op | 254 B/op | -90.43% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 10MB | Execution Time | 1044 ns/op| 533.1 ns/op | -48.94% | +| | Memory Usage | 2665 B/op | 258 B/op | -90.32% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 25MB | Execution Time | 1069 ns/op| 540.7 ns/op | -49.42% | +| | Memory Usage | 2706 B/op | 289 B/op | -89.32% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | +| 50MB | Execution Time | 1137 ns/op| 554.6 ns/op | -51.21% | +| | Memory Usage | 2734 B/op | 298 B/op | -89.10% | +| | Allocations | 16 allocs/op | 5 allocs/op | -68.75% | + ### Cache We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries. @@ -261,9 +292,11 @@ We are excited to introduce a new option in our caching middleware: Cache Invali We've made some changes to the CORS middleware to improve its functionality and flexibility. Here's what's new: #### New Struct Fields + - `Config.AllowPrivateNetwork`: This new field is a boolean that allows you to control whether private networks are allowed. This is related to the [Private Network Access (PNA)](https://wicg.github.io/private-network-access/) specification from the Web Incubator Community Group (WICG). When set to `true`, the CORS middleware will allow CORS preflight requests from private networks and respond with the `Access-Control-Allow-Private-Network: true` header. This could be useful in development environments or specific use cases, but should be done with caution due to potential security risks. #### Updated Struct Fields + We've updated several fields from a single string (containing comma-separated values) to slices, allowing for more explicit declaration of multiple values. Here are the updated fields: - `Config.AllowOrigins`: Now accepts a slice of strings, each representing an allowed origin. @@ -287,7 +320,7 @@ DRAFT section ### Filesystem -We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. +We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. Now, static middleware can do everything that filesystem middleware and static do. You can check out [static middleware](./middleware/static.md) or [migration guide](#-migration-guide) to see what has been changed. ### Monitor @@ -298,6 +331,25 @@ DRAFT section Monitor middleware is now in Contrib package. +### Healthcheck + +The Healthcheck middleware has been enhanced to support more than two routes, with default endpoints for liveliness, readiness, and startup checks. Here's a detailed breakdown of the changes and how to use the new features. + +1. **Support for More Than Two Routes**: + - The updated middleware now supports multiple routes beyond the default liveliness and readiness endpoints. This allows for more granular health checks, such as startup probes. + +2. **Default Endpoints**: + - Three default endpoints are now available: + - **Liveness**: `/livez` + - **Readiness**: `/readyz` + - **Startup**: `/startupz` + - These endpoints can be customized or replaced with user-defined routes. + +3. **Simplified Configuration**: + - The configuration for each health check endpoint has been simplified. Each endpoint can be configured separately, allowing for more flexibility and readability. + +Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes. + ## 📋 Migration guide - [🚀 App](#-app-1) @@ -308,7 +360,6 @@ Monitor middleware is now in Contrib package. - [🌎 Client package](#-client-package-1) - [🧬 Middlewares](#-middlewares-1) - ### 🚀 App #### Static @@ -386,6 +437,31 @@ app.Route("/api").Route("/user/:id?") }); ``` +### 🗺 RebuildTree + +We have added a new method that allows the route tree stack to be rebuilt in runtime, with it, you can add a route while your application is running and rebuild the route tree stack to make it registered and available for calls. + +You can find more reference on it in the [app](./api/app.md#rebuildtree): + +#### Example Usage + +```go +app.Get("/define", func(c Ctx) error { // Define a new route dynamically + app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() // Rebuild the route tree to register the new route + + return c.SendStatus(http.StatusOK) +}) +``` + +In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. + +**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in +development mode. Avoid using it concurrently. + ### 🧠 Context ### 📎 Parser @@ -449,3 +525,48 @@ app.Use(static.New("", static.Config{ MaxAge: 3600, })) ``` + +### Healthcheck + +Previously, the Healthcheck middleware was configured with a combined setup for liveliness and readiness probes: + +```go +//before +app.Use(healthcheck.New(healthcheck.Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, + ReadinessEndpoint: "/ready", +})) +``` + +With the new version, each health check endpoint is configured separately, allowing for more flexibility: + +```go +// after + +// Default liveness endpoint configuration +app.Get(healthcheck.DefaultLivenessEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ + Probe: func(c *fiber.Ctx) bool { + return true + }, +})) + +// Default readiness endpoint configuration +app.Get(healthcheck.DefaultReadinessEndpoint, healthcheck.NewHealthChecker()) + +// New default startup endpoint configuration +// Default endpoint is /startupz +app.Get(healthcheck.DefaultStartupEndpoint, healthcheck.NewHealthChecker(healthcheck.Config{ + Probe: func(c *fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, +})) + +// Custom liveness endpoint configuration +app.Get("/live", healthcheck.NewHealthChecker()) +``` diff --git a/go.mod b/go.mod index 1edd9f34..dd88f5b1 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/gofiber/fiber/v3 -go 1.21 +go 1.22 require ( - github.com/gofiber/utils/v2 v2.0.0-beta.4 + github.com/gofiber/utils/v2 v2.0.0-beta.6 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 @@ -20,6 +20,8 @@ require ( 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/net v0.26.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bc31777a..1c4e23e7 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 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/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI= +github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -36,6 +36,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL 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/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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= @@ -56,6 +58,8 @@ 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/group.go b/group.go index fe2ac97a..4142b0ba 100644 --- a/group.go +++ b/group.go @@ -11,12 +11,12 @@ import ( // Group struct type Group struct { - app *App - parentGroup *Group - name string - anyRouteDefined bool + app *App + parentGroup *Group + name string - Prefix string + Prefix string + anyRouteDefined bool } // Name Assign name to specific route or group itself. diff --git a/helpers.go b/helpers.go index 203f51da..90621b1c 100644 --- a/helpers.go +++ b/helpers.go @@ -31,11 +31,11 @@ import ( // along with quality, specificity, parameters, and order. // Used for sorting accept headers. type acceptedType struct { + params headerParams spec string quality float64 specificity int order int - params headerParams } type headerParams map[string][]byte @@ -222,7 +222,7 @@ func getGroupPath(prefix, path string) string { path = "/" + path } - return strings.TrimRight(prefix, "/") + path + return utils.TrimRight(prefix, '/') + path } // acceptsOffer This function determines if an offer matches a given specification. @@ -298,7 +298,7 @@ func paramsMatch(specParamStr headerParams, offerParams string) bool { for specParam, specVal := range specParamStr { foundParam := false fasthttp.VisitHeaderParams(utils.UnsafeBytes(offerParams), func(key, value []byte) bool { - if utils.EqualFold(specParam, string(key)) { + if utils.EqualFold(specParam, utils.UnsafeString(key)) { foundParam = true allSpecParamsMatch = utils.EqualFold(specVal, value) return false @@ -326,7 +326,7 @@ func getSplicedStrList(headerValue string, dst []string) []string { var ( index int character rune - lastElementEndsAt uint8 + lastElementEndsAt int insertIndex int ) for index, character = range headerValue + "$" { @@ -336,8 +336,8 @@ func getSplicedStrList(headerValue string, dst []string) []string { dst = make([]string, len(dst)+(len(dst)>>1)+2) copy(dst, oldSlice) } - dst[insertIndex] = strings.TrimLeft(headerValue[lastElementEndsAt:index], " ") - lastElementEndsAt = uint8(index + 1) + dst[insertIndex] = utils.TrimLeft(headerValue[lastElementEndsAt:index], ' ') + lastElementEndsAt = index + 1 insertIndex++ } } @@ -356,7 +356,7 @@ func forEachMediaRange(header []byte, functor func([]byte)) { for len(header) > 0 { n := 0 - header = bytes.TrimLeft(header, " ") + header = utils.TrimLeft(header, ' ') quotes := 0 escaping := false @@ -423,29 +423,31 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head forEachMediaRange(header, func(accept []byte) { order++ spec, quality := accept, 1.0 - var params headerParams if i := bytes.IndexByte(accept, ';'); i != -1 { spec = accept[:i] - // The vast majority of requests will have only the q parameter with - // no whitespace. Check this first to see if we can skip - // the more involved parsing. - if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[i+3:], ';') == -1 { - if q, err := fasthttp.ParseUfloat(bytes.TrimRight(accept[i+3:], " ")); err == nil { + // Optimized quality parsing + qIndex := i + 3 + if bytes.HasPrefix(accept[i:], []byte(";q=")) && bytes.IndexByte(accept[qIndex:], ';') == -1 { + if q, err := fasthttp.ParseUfloat(accept[qIndex:]); err == nil { quality = q } } else { params, _ = headerParamPool.Get().(headerParams) //nolint:errcheck // only contains headerParams + for k := range params { + delete(params, k) + } fasthttp.VisitHeaderParams(accept[i:], func(key, value []byte) bool { - if string(key) == "q" { + if len(key) == 1 && key[0] == 'q' { if q, err := fasthttp.ParseUfloat(value); err == nil { quality = q } return false } - params[utils.UnsafeString(utils.ToLowerBytes(key))] = value + lowerKey := utils.UnsafeString(utils.ToLowerBytes(key)) + params[lowerKey] = value return true }) } @@ -457,13 +459,16 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head } } - spec = bytes.TrimRight(spec, " ") + spec = utils.Trim(spec, ' ') - // Get specificity + // Determine specificity var specificity int + // check for wildcard this could be a mime */* or a wildcard character * switch { - case string(spec) == "*/*" || string(spec) == "*": + case len(spec) == 1 && spec[0] == '*': + specificity = 1 + case bytes.Equal(spec, []byte("*/*")): specificity = 1 case bytes.HasSuffix(spec, []byte("/*")): specificity = 2 @@ -474,7 +479,13 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head } // Add to accepted types - acceptedTypes = append(acceptedTypes, acceptedType{utils.UnsafeString(spec), quality, specificity, order, params}) + acceptedTypes = append(acceptedTypes, acceptedType{ + spec: utils.UnsafeString(spec), + quality: quality, + specificity: specificity, + order: order, + params: params, + }) }) if len(acceptedTypes) > 1 { @@ -483,30 +494,24 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head } // Find the first offer that matches the accepted types - ret := "" - done := false for _, acceptedType := range acceptedTypes { - if !done { - for _, offer := range offers { - if offer == "" { - continue - } - if isAccepted(acceptedType.spec, offer, acceptedType.params) { - ret = offer - done = true - break + for _, offer := range offers { + if offer == "" { + continue + } + if isAccepted(acceptedType.spec, offer, acceptedType.params) { + if acceptedType.params != nil { + headerParamPool.Put(acceptedType.params) } + return offer } } if acceptedType.params != nil { - for p := range acceptedType.params { - delete(acceptedType.params, p) - } headerParamPool.Put(acceptedType.params) } } - return ret + return "" } // sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements @@ -761,6 +766,7 @@ func genericParseBool[V GenericType](str string, parser func(bool) V, defaultVal return genericParseDefault[V](err, func() V { return parser(result) }, defaultValue...) } +//nolint:gosec // Casting in this function is not a concern func genericParseType[V GenericType](str string, v V, defaultValue ...V) V { switch any(v).(type) { case int: diff --git a/helpers_test.go b/helpers_test.go index 21344d22..ddee4340 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -5,7 +5,6 @@ package fiber import ( - "fmt" "strings" "testing" "time" @@ -138,6 +137,8 @@ func Benchmark_Utils_GetOffer(b *testing.B) { }, } + b.ReportAllocs() + b.ResetTimer() for _, tc := range testCases { accept := []byte(tc.accept) b.Run(tc.description, func(b *testing.B) { @@ -205,6 +206,8 @@ func Benchmark_Utils_ParamsMatch(b *testing.B) { "appLe": []byte("orange"), "param": []byte("foo"), } + b.ReportAllocs() + b.ResetTimer() for n := 0; n < b.N; n++ { match = paramsMatch(specParams, `;param=foo; apple=orange`) } @@ -317,6 +320,8 @@ func Benchmark_Utils_GetSplicedStrList(b *testing.B) { destination := make([]string, 5) result := destination const input = `deflate, gzip,br,brotli,zstd` + b.ReportAllocs() + b.ResetTimer() for n := 0; n < b.N; n++ { result = getSplicedStrList(input, destination) } @@ -359,6 +364,8 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) { // go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4 func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { acceptedTypes := make([]acceptedType, 3) + b.ReportAllocs() + b.ResetTimer() 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} @@ -373,6 +380,8 @@ func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4 func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { acceptedTypes := make([]acceptedType, 11) + b.ReportAllocs() + b.ResetTimer() 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} @@ -452,9 +461,10 @@ func Test_Utils_getGroupPath(t *testing.T) { } // go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3 - func Benchmark_Utils_getGroupPath(b *testing.B) { var res string + b.ReportAllocs() + b.ResetTimer() for n := 0; n < b.N; n++ { _ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome") _ = getGroupPath("/v1", "/") @@ -467,7 +477,8 @@ func Benchmark_Utils_getGroupPath(b *testing.B) { func Benchmark_Utils_Unescape(b *testing.B) { unescaped := "" dst := make([]byte, 0) - + b.ReportAllocs() + b.ResetTimer() for n := 0; n < b.N; n++ { source := "/cr%C3%A9er" pathBytes := utils.UnsafeBytes(source) @@ -483,9 +494,9 @@ func Test_Utils_Parse_Address(t *testing.T) { testCases := []struct { addr, host, port string }{ - {"[::1]:3000", "[::1]", "3000"}, - {"127.0.0.1:3000", "127.0.0.1", "3000"}, - {"/path/to/unix/socket", "/path/to/unix/socket", ""}, + {addr: "[::1]:3000", host: "[::1]", port: "3000"}, + {addr: "127.0.0.1:3000", host: "127.0.0.1", port: "3000"}, + {addr: "/path/to/unix/socket", host: "/path/to/unix/socket", port: ""}, } for _, c := range testCases { @@ -509,26 +520,27 @@ func Test_Utils_IsNoCache(t *testing.T) { string bool }{ - {"public", false}, - {"no-cache", true}, - {"public, no-cache, max-age=30", true}, - {"public,no-cache", true}, - {"public,no-cacheX", false}, - {"no-cache, public", true}, - {"Xno-cache, public", false}, - {"max-age=30, no-cache,public", true}, + {string: "public", bool: false}, + {string: "no-cache", bool: true}, + {string: "public, no-cache, max-age=30", bool: true}, + {string: "public,no-cache", bool: true}, + {string: "public,no-cacheX", bool: false}, + {string: "no-cache, public", bool: true}, + {string: "Xno-cache, public", bool: false}, + {string: "max-age=30, no-cache,public", bool: true}, } for _, c := range testCases { ok := isNoCache(c.string) - require.Equal(t, c.bool, ok, - fmt.Sprintf("want %t, got isNoCache(%s)=%t", c.bool, c.string, ok)) + require.Equal(t, c.bool, ok, "want %t, got isNoCache(%s)=%t", c.bool, c.string, ok) } } // go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4 func Benchmark_Utils_IsNoCache(b *testing.B) { var ok bool + b.ReportAllocs() + b.ResetTimer() for i := 0; i < b.N; i++ { _ = isNoCache("public") _ = isNoCache("no-cache") @@ -544,7 +556,10 @@ func Benchmark_Utils_IsNoCache(b *testing.B) { func Benchmark_SlashRecognition(b *testing.B) { search := "wtf/1234" var result bool + b.Run("indexBytes", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() result = false for i := 0; i < b.N; i++ { if strings.IndexByte(search, slashDelimiter) != -1 { @@ -554,6 +569,8 @@ func Benchmark_SlashRecognition(b *testing.B) { require.True(b, result) }) b.Run("forEach", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() result = false c := int32(slashDelimiter) for i := 0; i < b.N; i++ { @@ -567,6 +584,8 @@ func Benchmark_SlashRecognition(b *testing.B) { require.True(b, result) }) b.Run("IndexRune", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() result = false c := int32(slashDelimiter) for i := 0; i < b.N; i++ { diff --git a/internal/memory/memory.go b/internal/memory/memory.go index cf2b3cac..3b552c58 100644 --- a/internal/memory/memory.go +++ b/internal/memory/memory.go @@ -10,14 +10,14 @@ import ( ) type Storage struct { - sync.RWMutex data map[string]item // data + sync.RWMutex } type item struct { + v any // val // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 e uint32 // exp - v any // val } func New() *Storage { @@ -46,7 +46,7 @@ func (s *Storage) Set(key string, val any, ttl time.Duration) { if ttl > 0 { exp = uint32(ttl.Seconds()) + utils.Timestamp() } - i := item{exp, val} + i := item{e: exp, v: val} s.Lock() s.data[key] = i s.Unlock() diff --git a/internal/schema/cache.go b/internal/schema/cache.go index 3d77ec07..85e28f17 100644 --- a/internal/schema/cache.go +++ b/internal/schema/cache.go @@ -26,10 +26,10 @@ func newCache() *cache { // cache caches meta-data about a struct. type cache struct { - l sync.RWMutex m map[reflect.Type]*structInfo regconv map[reflect.Type]Converter tag string + l sync.RWMutex } // registerConverter registers a converter function for a custom type. diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go index 410ad631..310b783e 100644 --- a/internal/schema/decoder.go +++ b/internal/schema/decoder.go @@ -462,10 +462,10 @@ type unmarshaler struct { // ConversionError stores information about a failed conversion. type ConversionError struct { - Key string // key from the source map. Type reflect.Type // expected type of elem - Index int // index for multi-value fields; -1 for single-value fields. Err error // low-level error (when it exists) + Key string // key from the source map. + Index int // index for multi-value fields; -1 for single-value fields. } func (e ConversionError) Error() string { diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go index e2f305d2..c22ab92b 100644 --- a/internal/storage/memory/memory.go +++ b/internal/storage/memory/memory.go @@ -11,10 +11,10 @@ import ( // Storage interface that is implemented by storage providers type Storage struct { - mux sync.RWMutex db map[string]entry - gcInterval time.Duration done chan struct{} + gcInterval time.Duration + mux sync.RWMutex } type entry struct { @@ -69,7 +69,7 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { expire = uint32(exp.Seconds()) + utils.Timestamp() } - e := entry{val, expire} + e := entry{data: val, expiry: expire} s.mux.Lock() s.db[key] = e s.mux.Unlock() diff --git a/listen.go b/listen.go index e643db20..0df4e1a0 100644 --- a/listen.go +++ b/listen.go @@ -40,6 +40,36 @@ const ( // // TODO: Add timeout for graceful shutdown. type ListenConfig struct { + // GracefulContext is a field to shutdown Fiber by given context gracefully. + // + // Default: nil + GracefulContext context.Context `json:"graceful_context"` //nolint:containedctx // It's needed to set context inside Listen. + + // TLSConfigFunc allows customizing tls.Config as you want. + // + // Default: nil + TLSConfigFunc func(tlsConfig *tls.Config) `json:"tls_config_func"` + + // ListenerFunc allows accessing and customizing net.Listener. + // + // Default: nil + ListenerAddrFunc func(addr net.Addr) `json:"listener_addr_func"` + + // BeforeServeFunc allows customizing and accessing fiber app before serving the app. + // + // Default: nil + BeforeServeFunc func(app *App) error `json:"before_serve_func"` + + // OnShutdownError allows to customize error behavior when to graceful shutdown server by given signal. + // + // Print error with log.Fatalf() by default. + // Default: nil + OnShutdownError func(err error) + + // OnShutdownSuccess allows to customize success behavior when to graceful shutdown server by given signal. + // + // Default: nil + OnShutdownSuccess func() // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. // @@ -64,26 +94,6 @@ type ListenConfig struct { // Default : "" CertClientFile string `json:"cert_client_file"` - // GracefulContext is a field to shutdown Fiber by given context gracefully. - // - // Default: nil - GracefulContext context.Context `json:"graceful_context"` //nolint:containedctx // It's needed to set context inside Listen. - - // TLSConfigFunc allows customizing tls.Config as you want. - // - // Default: nil - TLSConfigFunc func(tlsConfig *tls.Config) `json:"tls_config_func"` - - // ListenerFunc allows accessing and customizing net.Listener. - // - // Default: nil - ListenerAddrFunc func(addr net.Addr) `json:"listener_addr_func"` - - // BeforeServeFunc allows customizing and accessing fiber app before serving the app. - // - // Default: nil - BeforeServeFunc func(app *App) error `json:"before_serve_func"` - // When set to true, it will not print out the «Fiber» ASCII art and listening address. // // Default: false @@ -98,17 +108,6 @@ type ListenConfig struct { // // Default: false EnablePrintRoutes bool `json:"enable_print_routes"` - - // OnShutdownError allows to customize error behavior when to graceful shutdown server by given signal. - // - // Print error with log.Fatalf() by default. - // Default: nil - OnShutdownError func(err error) - - // OnShutdownSuccess allows to customize success behavior when to graceful shutdown server by given signal. - // - // Default: nil - OnShutdownSuccess func() } // listenConfigDefault is a function to set default values of ListenConfig. @@ -352,7 +351,7 @@ func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg ListenC } fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset)) //nolint:errcheck,revive // ignore error - fmt.Fprintf(out, strings.Repeat("-", 50)+"\n") //nolint:errcheck,revive // ignore error + fmt.Fprintf(out, strings.Repeat("-", 50)+"\n") //nolint:errcheck,revive,govet // ignore error if host == "0.0.0.0" { //nolint:errcheck,revive // ignore error diff --git a/listen_test.go b/listen_test.go index da60aa75..c828a911 100644 --- a/listen_test.go +++ b/listen_test.go @@ -79,10 +79,10 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) { } testCases := []struct { - Time time.Duration - ExpectedBody string - ExpectedStatusCode int ExpectedErr error + ExpectedBody string + Time time.Duration + ExpectedStatusCode int }{ {Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil}, {Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: errors.New("InmemoryListener is already closed: use of closed network connection")}, diff --git a/log/default_test.go b/log/default_test.go index 2d2e4f8f..4a6ff82e 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -121,11 +121,11 @@ func Test_CtxLogger(t *testing.T) { func Test_LogfKeyAndValues(t *testing.T) { tests := []struct { name string - level Level format string + wantOutput string fmtArgs []any keysAndValues []any - wantOutput string + level Level }{ { name: "test logf with debug level and key-values", @@ -310,9 +310,9 @@ func Test_Tracew(t *testing.T) { func Benchmark_LogfKeyAndValues(b *testing.B) { tests := []struct { name string - level Level format string keysAndValues []any + level Level }{ { name: "test logf with debug level and key-values", @@ -368,9 +368,9 @@ func Benchmark_LogfKeyAndValues(b *testing.B) { func Benchmark_LogfKeyAndValues_Parallel(b *testing.B) { tests := []struct { name string - level Level format string keysAndValues []any + level Level }{ { name: "debug level with key-values", diff --git a/middleware/adaptor/README.md b/middleware/adaptor/README.md deleted file mode 100644 index abc0184a..00000000 --- a/middleware/adaptor/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Adaptor - -![Release](https://img.shields.io/github/release/gofiber/adaptor.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/adaptor/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/adaptor/workflows/Security/badge.svg) -![Linter](https://github.com/gofiber/adaptor/workflows/Linter/badge.svg) - -Converter for net/http handlers to/from Fiber request handlers, special thanks to [@arsmn](https://github.com/arsmn)! - -### Install -``` -go get -u github.com/gofiber/fiber/v3 -go get -u github.com/gofiber/adaptor/v2 -``` - -### Functions -| 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 - -### net/http to Fiber -```go -package main - -import ( - "fmt" - "net/http" - - "github.com/gofiber/adaptor/v2" - "github.com/gofiber/fiber/v3" -) - -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/adaptor/v2" - "github.com/gofiber/fiber/v3" -) - -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/adaptor/v2" - "github.com/gofiber/fiber/v3" -) - -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 ( - "github.com/gofiber/adaptor/v2" - "github.com/gofiber/fiber/v3" - "net/http" -) -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!") -} -``` diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go index 82059cf1..539aa3e2 100644 --- a/middleware/adaptor/adaptor.go +++ b/middleware/adaptor/adaptor.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "reflect" + "sync" "unsafe" "github.com/gofiber/fiber/v3" @@ -13,6 +14,12 @@ import ( "github.com/valyala/fasthttp/fasthttpadaptor" ) +var ctxPool = sync.Pool{ + New: func() any { + return new(fasthttp.RequestCtx) + }, +} + // HTTPHandlerFunc wraps net/http handler func to fiber handler func HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler { return HTTPHandler(h) @@ -82,12 +89,13 @@ func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { return func(c fiber.Ctx) error { var next bool nextHandler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { - next = true // Convert again in case request may modify by middleware + next = true c.Request().Header.SetMethod(r.Method) c.Request().SetRequestURI(r.RequestURI) c.Request().SetHost(r.Host) c.Request().Header.SetHost(r.Host) + for key, val := range r.Header { for _, v := range val { c.Request().Header.Set(key, v) @@ -124,9 +132,9 @@ func FiberApp(app *fiber.App) http.HandlerFunc { func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // New fasthttp request req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) + // Convert net/http -> fasthttp request if r.Body != nil { n, err := io.Copy(req.BodyWriter(), r.Body) @@ -141,26 +149,35 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { req.SetRequestURI(r.RequestURI) req.SetHost(r.Host) req.Header.SetHost(r.Host) + for key, val := range r.Header { for _, v := range val { req.Header.Set(key, v) } } + 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) if err != nil { http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) return } - // New fasthttp Ctx - var fctx fasthttp.RequestCtx + // New fasthttp Ctx from pool + fctx := ctxPool.Get().(*fasthttp.RequestCtx) //nolint:forcetypeassert,errcheck // overlinting + fctx.Response.Reset() + fctx.Request.Reset() + defer ctxPool.Put(fctx) fctx.Init(req, remoteAddr, nil) + if len(h) > 0 { // New fiber Ctx - ctx := app.AcquireCtx(&fctx) + ctx := app.AcquireCtx(fctx) + defer app.ReleaseCtx(ctx) + // Execute fiber Ctx err := h[0](ctx) if err != nil { @@ -168,10 +185,10 @@ func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { } } else { // Execute fasthttp Ctx though app.Handler - app.Handler()(&fctx) + app.Handler()(fctx) } - // Convert fasthttp Ctx > net/http + // Convert fasthttp Ctx -> net/http fctx.Response.Header.VisitAll(func(k, v []byte) { w.Header().Add(string(k), string(v)) }) diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go index b96259f3..32abc64d 100644 --- a/middleware/adaptor/adaptor_test.go +++ b/middleware/adaptor/adaptor_test.go @@ -274,7 +274,7 @@ func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.A var r http.Request r.Method = expectedMethod - r.Body = &netHTTPBody{[]byte(expectedBody)} + r.Body = &netHTTPBody{b: []byte(expectedBody)} r.RequestURI = expectedRequestURI r.ContentLength = int64(expectedContentLength) r.Host = expectedHost @@ -355,9 +355,9 @@ func (r *netHTTPBody) Close() error { } type netHTTPResponseWriter struct { - statusCode int h http.Header body []byte + statusCode int } func (w *netHTTPResponseWriter) StatusCode() int { diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 421fcfd8..b47f557e 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -47,9 +47,9 @@ func Test_Middleware_BasicAuth(t *testing.T) { tests := []struct { url string - statusCode int username string password string + statusCode int }{ { url: "/testauth", diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go index 1aa050a0..8668fad8 100644 --- a/middleware/basicauth/config.go +++ b/middleware/basicauth/config.go @@ -19,13 +19,6 @@ type Config struct { // 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 @@ -40,6 +33,13 @@ type Config struct { // // Optional. Default: nil Unauthorized fiber.Handler + + // 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 } // ConfigDefault is the default config diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index c107b212..69c3fd5c 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -117,46 +117,49 @@ func New(config ...Config) fiber.Handler { // Get timestamp ts := atomic.LoadUint64(×tamp) - // Invalidate cache if requested - if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) && e != nil { - e.exp = ts - 1 - } - - // Check if entry is expired - if e.exp != 0 && ts >= e.exp { - deleteKey(key) - if cfg.MaxBytes > 0 { - _, size := heap.remove(e.heapidx) - storedBytes -= size - } - } else if e.exp != 0 && !hasRequestDirective(c, noCache) { - // Separate body value to avoid msgp serialization - // We can store raw bytes with Storage 👍 - if cfg.Storage != nil { - e.body = manager.getRaw(key + "_body") - } - // Set response headers from cache - c.Response().SetBodyRaw(e.body) - c.Response().SetStatusCode(e.status) - c.Response().Header.SetContentTypeBytes(e.ctype) - if len(e.cencoding) > 0 { - c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) - } - for k, v := range e.headers { - c.Response().Header.SetBytesV(k, v) - } - // Set Cache-Control header if enabled - if cfg.CacheControl { - maxAge := strconv.FormatUint(e.exp-ts, 10) - c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge) + // Cache Entry not found + if e != nil { + // Invalidate cache if requested + if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) { + e.exp = ts - 1 } - c.Set(cfg.CacheHeader, cacheHit) + // Check if entry is expired + if e.exp != 0 && ts >= e.exp { + deleteKey(key) + if cfg.MaxBytes > 0 { + _, size := heap.remove(e.heapidx) + storedBytes -= size + } + } else if e.exp != 0 && !hasRequestDirective(c, noCache) { + // Separate body value to avoid msgp serialization + // We can store raw bytes with Storage 👍 + if cfg.Storage != nil { + e.body = manager.getRaw(key + "_body") + } + // Set response headers from cache + c.Response().SetBodyRaw(e.body) + c.Response().SetStatusCode(e.status) + c.Response().Header.SetContentTypeBytes(e.ctype) + if len(e.cencoding) > 0 { + c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) + } + for k, v := range e.headers { + c.Response().Header.SetBytesV(k, v) + } + // Set Cache-Control header if enabled + if cfg.CacheControl { + maxAge := strconv.FormatUint(e.exp-ts, 10) + c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge) + } - mux.Unlock() + c.Set(cfg.CacheHeader, cacheHit) - // Return response - return nil + mux.Unlock() + + // Return response + return nil + } } // make sure we're not blocking concurrent requests - do unlock @@ -193,6 +196,7 @@ func New(config ...Config) fiber.Handler { } } + e = manager.acquire() // Cache response e.body = utils.CopyBytes(c.Response().Body()) e.status = c.Response().StatusCode() diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index d529ccd9..8f00f1f1 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -47,9 +47,10 @@ func Test_Cache_Expired(t *testing.T) { t.Parallel() app := fiber.New() app.Use(New(Config{Expiration: 2 * time.Second})) - + count := 0 app.Get("/", func(c fiber.Ctx) error { - return c.SendString(strconv.FormatInt(time.Now().UnixNano(), 10)) + count++ + return c.SendString(strconv.Itoa(count)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -86,9 +87,10 @@ func Test_Cache(t *testing.T) { app := fiber.New() app.Use(New()) + count := 0 app.Get("/", func(c fiber.Ctx) error { - now := strconv.FormatInt(time.Now().UnixNano(), 10) - return c.SendString(now) + count++ + return c.SendString(strconv.Itoa(count)) }) req := httptest.NewRequest(fiber.MethodGet, "/", nil) @@ -305,9 +307,10 @@ func Test_Cache_Invalid_Expiration(t *testing.T) { cache := New(Config{Expiration: 0 * time.Second}) app.Use(cache) + count := 0 app.Get("/", func(c fiber.Ctx) error { - now := strconv.FormatInt(time.Now().UnixNano(), 10) - return c.SendString(now) + count++ + return c.SendString(strconv.Itoa(count)) }) req := httptest.NewRequest(fiber.MethodGet, "/", nil) @@ -414,8 +417,10 @@ func Test_Cache_NothingToCache(t *testing.T) { app.Use(New(Config{Expiration: -(time.Second * 1)})) + count := 0 app.Get("/", func(c fiber.Ctx) error { - return c.SendString(time.Now().String()) + count++ + return c.SendString(strconv.Itoa(count)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -447,12 +452,16 @@ func Test_Cache_CustomNext(t *testing.T) { CacheControl: true, })) + count := 0 app.Get("/", func(c fiber.Ctx) error { - return c.SendString(time.Now().String()) + count++ + return c.SendString(strconv.Itoa(count)) }) + errorCount := 0 app.Get("/error", func(c fiber.Ctx) error { - return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) + errorCount++ + return c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(errorCount)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -508,9 +517,11 @@ func Test_CustomExpiration(t *testing.T) { return time.Second * time.Duration(newCacheTime) }})) + count := 0 app.Get("/", func(c fiber.Ctx) error { + count++ c.Response().Header.Add("Cache-Time", "1") - return c.SendString(strconv.FormatInt(time.Now().UnixNano(), 10)) + return c.SendString(strconv.Itoa(count)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -588,8 +599,11 @@ func Test_CacheHeader(t *testing.T) { return c.SendString(fiber.Query[string](c, "cache")) }) + count := 0 app.Get("/error", func(c fiber.Ctx) error { - return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) + count++ + c.Response().Header.Add("Cache-Time", "1") + return c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(count)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -615,10 +629,13 @@ func Test_Cache_WithHead(t *testing.T) { app := fiber.New() app.Use(New()) + count := 0 handler := func(c fiber.Ctx) error { - now := strconv.FormatInt(time.Now().UnixNano(), 10) - return c.SendString(now) + count++ + c.Response().Header.Add("Cache-Time", "1") + return c.SendString(strconv.Itoa(count)) } + app.Route("/").Get(handler).Head(handler) req := httptest.NewRequest(fiber.MethodHead, "/", nil) @@ -708,8 +725,10 @@ func Test_CacheInvalidation(t *testing.T) { }, })) + count := 0 app.Get("/", func(c fiber.Ctx) error { - return c.SendString(time.Now().String()) + count++ + return c.SendString(strconv.Itoa(count)) }) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -731,6 +750,93 @@ func Test_CacheInvalidation(t *testing.T) { require.NotEqual(t, body, bodyInvalidate) } +func Test_CacheInvalidation_noCacheEntry(t *testing.T) { + t.Parallel() + t.Run("Cache Invalidator should not be called if no cache entry exist ", func(t *testing.T) { + t.Parallel() + app := fiber.New() + cacheInvalidatorExecuted := false + app.Use(New(Config{ + CacheControl: true, + CacheInvalidator: func(c fiber.Ctx) bool { + cacheInvalidatorExecuted = true + return fiber.Query[bool](c, "invalidate") + }, + MaxBytes: 10 * 1024 * 1024, + })) + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?invalidate=true", nil)) + require.NoError(t, err) + require.False(t, cacheInvalidatorExecuted) + }) +} + +func Test_CacheInvalidation_removeFromHeap(t *testing.T) { + t.Parallel() + t.Run("Invalidate and remove from the heap", func(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + CacheControl: true, + CacheInvalidator: func(c fiber.Ctx) bool { + return fiber.Query[bool](c, "invalidate") + }, + MaxBytes: 10 * 1024 * 1024, + })) + + count := 0 + app.Get("/", func(c fiber.Ctx) error { + count++ + return c.SendString(strconv.Itoa(count)) + }) + + 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(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.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl)) + + respInvalidate, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?invalidate=true", nil)) + require.NoError(t, err) + bodyInvalidate, err := io.ReadAll(respInvalidate.Body) + require.NoError(t, err) + require.NotEqual(t, body, bodyInvalidate) + }) +} + +func Test_CacheStorage_CustomHeaders(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + CacheControl: true, + Storage: memory.New(), + MaxBytes: 10 * 1024 * 1024, + })) + + app.Get("/", func(c fiber.Ctx) error { + c.Response().Header.Set("Content-Type", "text/xml") + c.Response().Header.Set("Content-Encoding", "utf8") + return c.Send([]byte("Test")) + }) + + 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(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.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl)) +} + // Because time points are updated once every X milliseconds, entries in tests can often have // equal expiration times and thus be in an random order. This closure hands out increasing // time intervals to maintain strong ascending order of expiration @@ -774,7 +880,7 @@ func Test_Cache_MaxBytesOrder(t *testing.T) { for idx, tcase := range cases { 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)) + require.Equal(t, tcase[1], rsp.Header.Get("X-Cache"), "Case %v", idx) } } @@ -808,7 +914,7 @@ func Test_Cache_MaxBytesSizes(t *testing.T) { for idx, tcase := range cases { 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)) + require.Equal(t, tcase[1], rsp.Header.Get("X-Cache"), "Case %v", idx) } } diff --git a/middleware/cache/config.go b/middleware/cache/config.go index b32e9e8c..b19be897 100644 --- a/middleware/cache/config.go +++ b/middleware/cache/config.go @@ -9,28 +9,16 @@ import ( // Config defines the config for middleware. type Config struct { + // Store is used to store the state of the middleware + // + // Default: an in memory store for this process only + Storage fiber.Storage + // 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 - // CacheInvalidator defines a function to invalidate the cache when returned true // // Optional. Default: nil @@ -48,15 +36,23 @@ type Config struct { // Default: nil ExpirationGenerator func(fiber.Ctx, *Config) time.Duration - // Store is used to store the state of the middleware + // CacheHeader header on response header, indicate cache status, with the following possible return value // - // Default: an in memory store for this process only - Storage fiber.Storage + // hit, miss, unreachable + // + // Optional. Default: X-Cache + CacheHeader string - // allows you to store additional headers generated by next middlewares & handler + // You can specify HTTP methods to cache. + // The middleware just caches the routes of its methods in this slice. // - // Default: false - StoreResponseHeaders bool + // Default: []string{fiber.MethodGet, fiber.MethodHead} + Methods []string + + // Expiration is the time that an cached response will live + // + // Optional. Default: 1 * time.Minute + Expiration time.Duration // 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. @@ -65,11 +61,15 @@ type Config struct { // Default: 0 MaxBytes uint - // You can specify HTTP methods to cache. - // The middleware just caches the routes of its methods in this slice. + // CacheControl enables client side caching if set to true // - // Default: []string{fiber.MethodGet, fiber.MethodHead} - Methods []string + // Optional. Default: false + CacheControl bool + + // allows you to store additional headers generated by next middlewares & handler + // + // Default: false + StoreResponseHeaders bool } // ConfigDefault is the default config diff --git a/middleware/cache/heap.go b/middleware/cache/heap.go index fa978715..c5715392 100644 --- a/middleware/cache/heap.go +++ b/middleware/cache/heap.go @@ -15,7 +15,7 @@ type heapEntry struct { // elements in constant time. It does so by handing out special indices // and tracking entry movement. // -// indexdedHeap is used for quickly finding entries with the lowest +// indexedHeap is used for quickly finding entries with the lowest // expiration timestamp and deleting arbitrary entries. type indexedHeap struct { // Slice the heap is built on diff --git a/middleware/cache/manager.go b/middleware/cache/manager.go index c6ae5428..3a796c77 100644 --- a/middleware/cache/manager.go +++ b/middleware/cache/manager.go @@ -8,15 +8,16 @@ import ( "github.com/gofiber/fiber/v3/internal/memory" ) -// go:generate msgp -// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported +// msgp -file="manager.go" -o="manager_msgp.go" -tests=true -unexported +// +//go:generate msgp -o=manager_msgp.go -tests=true -unexported type item struct { + headers map[string][]byte body []byte ctype []byte cencoding []byte status int exp uint64 - headers map[string][]byte // used for finding the item in an indexed heap heapidx int } @@ -83,8 +84,7 @@ func (m *manager) get(key string) *item { return it } if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool - it = m.acquire() - return it + return nil } return it } diff --git a/middleware/cache/manager_msgp.go b/middleware/cache/manager_msgp.go index e053416c..bf5d6152 100644 --- a/middleware/cache/manager_msgp.go +++ b/middleware/cache/manager_msgp.go @@ -6,12 +6,202 @@ import ( "github.com/tinylib/msgp/msgp" ) +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "headers": + var zb0002 uint32 + zb0002, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err, "headers") + return + } + if z.headers == nil { + z.headers = make(map[string][]byte, zb0002) + } else if len(z.headers) > 0 { + for key := range z.headers { + delete(z.headers, key) + } + } + for zb0002 > 0 { + zb0002-- + var za0001 string + var za0002 []byte + za0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "headers") + return + } + za0002, err = dc.ReadBytes(za0002) + if err != nil { + err = msgp.WrapError(err, "headers", za0001) + return + } + z.headers[za0001] = za0002 + } + case "body": + z.body, err = dc.ReadBytes(z.body) + if err != nil { + err = msgp.WrapError(err, "body") + return + } + case "ctype": + z.ctype, err = dc.ReadBytes(z.ctype) + if err != nil { + err = msgp.WrapError(err, "ctype") + return + } + case "cencoding": + z.cencoding, err = dc.ReadBytes(z.cencoding) + if err != nil { + err = msgp.WrapError(err, "cencoding") + return + } + case "status": + z.status, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "status") + return + } + case "exp": + z.exp, err = dc.ReadUint64() + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + case "heapidx": + z.heapidx, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "heapidx") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 7 + // write "headers" + err = en.Append(0x87, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + if err != nil { + return + } + err = en.WriteMapHeader(uint32(len(z.headers))) + if err != nil { + err = msgp.WrapError(err, "headers") + return + } + for za0001, za0002 := range z.headers { + err = en.WriteString(za0001) + if err != nil { + err = msgp.WrapError(err, "headers") + return + } + err = en.WriteBytes(za0002) + if err != nil { + err = msgp.WrapError(err, "headers", za0001) + return + } + } + // write "body" + err = en.Append(0xa4, 0x62, 0x6f, 0x64, 0x79) + if err != nil { + return + } + err = en.WriteBytes(z.body) + if err != nil { + err = msgp.WrapError(err, "body") + return + } + // write "ctype" + err = en.Append(0xa5, 0x63, 0x74, 0x79, 0x70, 0x65) + if err != nil { + return + } + err = en.WriteBytes(z.ctype) + if err != nil { + err = msgp.WrapError(err, "ctype") + return + } + // write "cencoding" + err = en.Append(0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67) + if err != nil { + return + } + err = en.WriteBytes(z.cencoding) + if err != nil { + err = msgp.WrapError(err, "cencoding") + return + } + // write "status" + err = en.Append(0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.status) + if err != nil { + err = msgp.WrapError(err, "status") + return + } + // write "exp" + err = en.Append(0xa3, 0x65, 0x78, 0x70) + if err != nil { + return + } + err = en.WriteUint64(z.exp) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + // write "heapidx" + err = en.Append(0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78) + if err != nil { + return + } + err = en.WriteInt(z.heapidx) + if err != nil { + err = msgp.WrapError(err, "heapidx") + return + } + return +} + // MarshalMsg implements msgp.Marshaler func (z *item) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // map header, size 7 + // string "headers" + o = append(o, 0x87, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + o = msgp.AppendMapHeader(o, uint32(len(z.headers))) + for za0001, za0002 := range z.headers { + o = msgp.AppendString(o, za0001) + o = msgp.AppendBytes(o, za0002) + } // string "body" - o = append(o, 0x87, 0xa4, 0x62, 0x6f, 0x64, 0x79) + o = append(o, 0xa4, 0x62, 0x6f, 0x64, 0x79) o = msgp.AppendBytes(o, z.body) // string "ctype" o = append(o, 0xa5, 0x63, 0x74, 0x79, 0x70, 0x65) @@ -25,13 +215,6 @@ func (z *item) MarshalMsg(b []byte) (o []byte, err error) { // string "exp" o = append(o, 0xa3, 0x65, 0x78, 0x70) o = msgp.AppendUint64(o, z.exp) - // string "headers" - o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) - o = msgp.AppendMapHeader(o, uint32(len(z.headers))) - for za0001, za0002 := range z.headers { - o = msgp.AppendString(o, za0001) - o = msgp.AppendBytes(o, za0002) - } // string "heapidx" o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78) o = msgp.AppendInt(o, z.heapidx) @@ -56,36 +239,6 @@ func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { return } switch msgp.UnsafeString(field) { - case "body": - z.body, bts, err = msgp.ReadBytesBytes(bts, z.body) - if err != nil { - err = msgp.WrapError(err, "body") - return - } - case "ctype": - z.ctype, bts, err = msgp.ReadBytesBytes(bts, z.ctype) - if err != nil { - err = msgp.WrapError(err, "ctype") - return - } - case "cencoding": - z.cencoding, bts, err = msgp.ReadBytesBytes(bts, z.cencoding) - if err != nil { - err = msgp.WrapError(err, "cencoding") - return - } - case "status": - z.status, bts, err = msgp.ReadIntBytes(bts) - if err != nil { - err = msgp.WrapError(err, "status") - return - } - case "exp": - z.exp, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "exp") - return - } case "headers": var zb0002 uint32 zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) @@ -116,6 +269,36 @@ func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { } z.headers[za0001] = za0002 } + case "body": + z.body, bts, err = msgp.ReadBytesBytes(bts, z.body) + if err != nil { + err = msgp.WrapError(err, "body") + return + } + case "ctype": + z.ctype, bts, err = msgp.ReadBytesBytes(bts, z.ctype) + if err != nil { + err = msgp.WrapError(err, "ctype") + return + } + case "cencoding": + z.cencoding, bts, err = msgp.ReadBytesBytes(bts, z.cencoding) + if err != nil { + err = msgp.WrapError(err, "cencoding") + return + } + case "status": + z.status, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "status") + return + } + case "exp": + z.exp, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } case "heapidx": z.heapidx, bts, err = msgp.ReadIntBytes(bts) if err != nil { @@ -136,13 +319,13 @@ func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *item) Msgsize() (s int) { - s = 1 + 5 + msgp.BytesPrefixSize + len(z.body) + 6 + msgp.BytesPrefixSize + len(z.ctype) + 10 + msgp.BytesPrefixSize + len(z.cencoding) + 7 + msgp.IntSize + 4 + msgp.Uint64Size + 8 + msgp.MapHeaderSize + s = 1 + 8 + msgp.MapHeaderSize if z.headers != nil { for za0001, za0002 := range z.headers { _ = za0002 s += msgp.StringPrefixSize + len(za0001) + msgp.BytesPrefixSize + len(za0002) } } - s += 8 + msgp.IntSize + s += 5 + msgp.BytesPrefixSize + len(z.body) + 6 + msgp.BytesPrefixSize + len(z.ctype) + 10 + msgp.BytesPrefixSize + len(z.cencoding) + 7 + msgp.IntSize + 4 + msgp.Uint64Size + 8 + msgp.IntSize return } diff --git a/middleware/cache/manager_msgp_test.go b/middleware/cache/manager_msgp_test.go index ab4d912b..68693796 100644 --- a/middleware/cache/manager_msgp_test.go +++ b/middleware/cache/manager_msgp_test.go @@ -3,12 +3,13 @@ package cache // Code generated by github.com/tinylib/msgp DO NOT EDIT. import ( + "bytes" "testing" "github.com/tinylib/msgp/msgp" ) -func Test_MarshalUnmarshalitem(t *testing.T) { +func TestMarshalUnmarshalitem(t *testing.T) { v := item{} bts, err := v.MarshalMsg(nil) if err != nil { @@ -31,7 +32,7 @@ func Test_MarshalUnmarshalitem(t *testing.T) { } } -func Benchmark_MarshalMsgitem(b *testing.B) { +func BenchmarkMarshalMsgitem(b *testing.B) { v := item{} b.ReportAllocs() b.ResetTimer() @@ -40,7 +41,7 @@ func Benchmark_MarshalMsgitem(b *testing.B) { } } -func Benchmark_AppendMsgitem(b *testing.B) { +func BenchmarkAppendMsgitem(b *testing.B) { v := item{} bts := make([]byte, 0, v.Msgsize()) bts, _ = v.MarshalMsg(bts[0:0]) @@ -52,7 +53,7 @@ func Benchmark_AppendMsgitem(b *testing.B) { } } -func Benchmark_Unmarshalitem(b *testing.B) { +func BenchmarkUnmarshalitem(b *testing.B) { v := item{} bts, _ := v.MarshalMsg(nil) b.ReportAllocs() @@ -65,3 +66,58 @@ func Benchmark_Unmarshalitem(b *testing.B) { } } } + +func TestEncodeDecodeitem(t *testing.T) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecodeitem Msgsize() is inaccurate") + } + + vn := item{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/cache/manager_test.go b/middleware/cache/manager_test.go new file mode 100644 index 00000000..9aec5530 --- /dev/null +++ b/middleware/cache/manager_test.go @@ -0,0 +1,26 @@ +package cache + +import ( + "testing" + "time" + + "github.com/gofiber/utils/v2" + "github.com/stretchr/testify/assert" +) + +func Test_manager_get(t *testing.T) { + t.Parallel() + cacheManager := newManager(nil) + t.Run("Item not found in cache", func(t *testing.T) { + t.Parallel() + assert.Nil(t, cacheManager.get(utils.UUID())) + }) + t.Run("Item found in cache", func(t *testing.T) { + t.Parallel() + id := utils.UUID() + cacheItem := cacheManager.acquire() + cacheItem.body = []byte("test-body") + cacheManager.set(id, cacheItem, 10*time.Second) + assert.NotNil(t, cacheManager.get(id)) + }) +} diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go index 7d42c8bb..f258ba44 100644 --- a/middleware/compress/compress_test.go +++ b/middleware/compress/compress_test.go @@ -57,9 +57,7 @@ func Test_Compress_Different_Level(t *testing.T) { algorithms := []string{"gzip", "deflate", "br", "zstd"} for _, algo := range algorithms { - algo := algo for _, level := range levels { - level := level t.Run(fmt.Sprintf("%s_level %d", algo, level), func(t *testing.T) { t.Parallel() app := fiber.New() @@ -228,10 +226,10 @@ func Benchmark_Compress(b *testing.B) { name string acceptEncoding string }{ - {"Gzip", "gzip"}, - {"Deflate", "deflate"}, - {"Brotli", "br"}, - {"Zstd", "zstd"}, + {name: "Gzip", acceptEncoding: "gzip"}, + {name: "Deflate", acceptEncoding: "deflate"}, + {name: "Brotli", acceptEncoding: "br"}, + {name: "Zstd", acceptEncoding: "zstd"}, } for _, tt := range tests { @@ -268,20 +266,20 @@ func Benchmark_Compress_Levels(b *testing.B) { name string acceptEncoding string }{ - {"Gzip", "gzip"}, - {"Deflate", "deflate"}, - {"Brotli", "br"}, - {"Zstd", "zstd"}, + {name: "Gzip", acceptEncoding: "gzip"}, + {name: "Deflate", acceptEncoding: "deflate"}, + {name: "Brotli", acceptEncoding: "br"}, + {name: "Zstd", acceptEncoding: "zstd"}, } levels := []struct { name string level Level }{ - {"LevelDisabled", LevelDisabled}, - {"LevelDefault", LevelDefault}, - {"LevelBestSpeed", LevelBestSpeed}, - {"LevelBestCompression", LevelBestCompression}, + {name: "LevelDisabled", level: LevelDisabled}, + {name: "LevelDefault", level: LevelDefault}, + {name: "LevelBestSpeed", level: LevelBestSpeed}, + {name: "LevelBestCompression", level: LevelBestCompression}, } for _, tt := range tests { @@ -320,10 +318,10 @@ func Benchmark_Compress_Parallel(b *testing.B) { name string acceptEncoding string }{ - {"Gzip", "gzip"}, - {"Deflate", "deflate"}, - {"Brotli", "br"}, - {"Zstd", "zstd"}, + {name: "Gzip", acceptEncoding: "gzip"}, + {name: "Deflate", acceptEncoding: "deflate"}, + {name: "Brotli", acceptEncoding: "br"}, + {name: "Zstd", acceptEncoding: "zstd"}, } for _, tt := range tests { @@ -363,20 +361,20 @@ func Benchmark_Compress_Levels_Parallel(b *testing.B) { name string acceptEncoding string }{ - {"Gzip", "gzip"}, - {"Deflate", "deflate"}, - {"Brotli", "br"}, - {"Zstd", "zstd"}, + {name: "Gzip", acceptEncoding: "gzip"}, + {name: "Deflate", acceptEncoding: "deflate"}, + {name: "Brotli", acceptEncoding: "br"}, + {name: "Zstd", acceptEncoding: "zstd"}, } levels := []struct { name string level Level }{ - {"LevelDisabled", LevelDisabled}, - {"LevelDefault", LevelDefault}, - {"LevelBestSpeed", LevelBestSpeed}, - {"LevelBestCompression", LevelBestCompression}, + {name: "LevelDisabled", level: LevelDisabled}, + {name: "LevelDefault", level: LevelDefault}, + {name: "LevelBestSpeed", level: LevelBestSpeed}, + {name: "LevelBestCompression", level: LevelBestCompression}, } for _, tt := range tests { diff --git a/middleware/cors/config.go b/middleware/cors/config.go index 6e1d4697..2613bab9 100644 --- a/middleware/cors/config.go +++ b/middleware/cors/config.go @@ -41,15 +41,6 @@ type Config struct { // Optional. Default value []string{} 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. Note: If true, AllowOrigins - // cannot be set to true to prevent security vulnerabilities. - // - // Optional. Default value false. - AllowCredentials bool - // ExposeHeaders defines a whitelist headers that clients are allowed to // access. // @@ -65,6 +56,15 @@ type Config struct { // Optional. Default value 0. MaxAge int + // 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. Note: If true, AllowOrigins + // cannot be set to true to prevent security vulnerabilities. + // + // Optional. Default value false. + AllowCredentials bool + // AllowPrivateNetwork indicates whether the Access-Control-Allow-Private-Network // response header should be set to true, allowing requests from private networks. // diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go index e0fac917..7b17e143 100644 --- a/middleware/cors/cors.go +++ b/middleware/cors/cors.go @@ -6,6 +6,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" + "github.com/gofiber/utils/v2" ) // New creates a new middleware handler @@ -44,7 +45,7 @@ func New(config ...Config) fiber.Handler { break } if i := strings.Index(origin, "://*."); i != -1 { - trimmedOrigin := strings.TrimSpace(origin[:i+3] + origin[i+4:]) + trimmedOrigin := utils.Trim(origin[:i+3]+origin[i+4:], ' ') isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) @@ -52,7 +53,7 @@ func New(config ...Config) fiber.Handler { sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]} allowSOrigins = append(allowSOrigins, sd) } else { - trimmedOrigin := strings.TrimSpace(origin) + trimmedOrigin := utils.Trim(origin, ' ') isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go index d95c0f53..e255d5af 100644 --- a/middleware/cors/cors_test.go +++ b/middleware/cors/cors_test.go @@ -326,8 +326,8 @@ func Test_CORS_Subdomain(t *testing.T) { func Test_CORS_AllowOriginScheme(t *testing.T) { t.Parallel() tests := []struct { - pattern []string reqOrigin string + pattern []string shouldAllowOrigin bool }{ { @@ -682,9 +682,9 @@ func Test_CORS_AllowOriginsFunc(t *testing.T) { func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { testCases := []struct { Name string - Config Config RequestOrigin string ResponseOrigin string + Config Config }{ { Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginAllowed", @@ -829,10 +829,10 @@ func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { func Test_CORS_AllowCredentials(t *testing.T) { testCases := []struct { Name string - Config Config RequestOrigin string ResponseOrigin string ResponseCredentials string + Config Config }{ { Name: "AllowOriginsFuncDefined", diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go index 4e849577..84f217e5 100644 --- a/middleware/cors/utils_test.go +++ b/middleware/cors/utils_test.go @@ -10,33 +10,33 @@ import ( func Test_NormalizeOrigin(t *testing.T) { testCases := []struct { origin string - expectedValid bool expectedOrigin string + expectedValid bool }{ - {"http://example.com", true, "http://example.com"}, // Simple case should work. - {"http://example.com/", true, "http://example.com"}, // Trailing slash should be removed. - {"http://example.com:3000", true, "http://example.com:3000"}, // Port should be preserved. - {"http://example.com:3000/", true, "http://example.com:3000"}, // Trailing slash should be removed. - {"http://", false, ""}, // Invalid origin should not be accepted. - {"file:///etc/passwd", false, ""}, // File scheme should not be accepted. - {"https://*example.com", false, ""}, // Wildcard domain should not be accepted. - {"http://*.example.com", false, ""}, // Wildcard subdomain should not be accepted. - {"http://example.com/path", false, ""}, // Path should not be accepted. - {"http://example.com?query=123", false, ""}, // Query should not be accepted. - {"http://example.com#fragment", false, ""}, // Fragment should not be accepted. - {"http://localhost", true, "http://localhost"}, // Localhost should be accepted. - {"http://127.0.0.1", true, "http://127.0.0.1"}, // IPv4 address should be accepted. - {"http://[::1]", true, "http://[::1]"}, // IPv6 address should be accepted. - {"http://[::1]:8080", true, "http://[::1]:8080"}, // IPv6 address with port should be accepted. - {"http://[::1]:8080/", true, "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. - {"http://[::1]:8080/path", false, ""}, // IPv6 address with port and path should not be accepted. - {"http://[::1]:8080?query=123", false, ""}, // IPv6 address with port and query should not be accepted. - {"http://[::1]:8080#fragment", false, ""}, // IPv6 address with port and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment", false, ""}, // IPv6 address with port, path, query, and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/", false, ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/segment", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. + {origin: "http://example.com", expectedValid: true, expectedOrigin: "http://example.com"}, // Simple case should work. + {origin: "http://example.com/", expectedValid: true, expectedOrigin: "http://example.com"}, // Trailing slash should be removed. + {origin: "http://example.com:3000", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Port should be preserved. + {origin: "http://example.com:3000/", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Trailing slash should be removed. + {origin: "http://", expectedValid: false, expectedOrigin: ""}, // Invalid origin should not be accepted. + {origin: "file:///etc/passwd", expectedValid: false, expectedOrigin: ""}, // File scheme should not be accepted. + {origin: "https://*example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard domain should not be accepted. + {origin: "http://*.example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard subdomain should not be accepted. + {origin: "http://example.com/path", expectedValid: false, expectedOrigin: ""}, // Path should not be accepted. + {origin: "http://example.com?query=123", expectedValid: false, expectedOrigin: ""}, // Query should not be accepted. + {origin: "http://example.com#fragment", expectedValid: false, expectedOrigin: ""}, // Fragment should not be accepted. + {origin: "http://localhost", expectedValid: true, expectedOrigin: "http://localhost"}, // Localhost should be accepted. + {origin: "http://127.0.0.1", expectedValid: true, expectedOrigin: "http://127.0.0.1"}, // IPv4 address should be accepted. + {origin: "http://[::1]", expectedValid: true, expectedOrigin: "http://[::1]"}, // IPv6 address should be accepted. + {origin: "http://[::1]:8080", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port should be accepted. + {origin: "http://[::1]:8080/", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. + {origin: "http://[::1]:8080/path", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and path should not be accepted. + {origin: "http://[::1]:8080?query=123", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and query should not be accepted. + {origin: "http://[::1]:8080#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/segment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. } for _, tc := range testCases { @@ -59,16 +59,16 @@ func Test_MatchScheme(t *testing.T) { pattern string expected bool }{ - {"http://example.com", "http://example.com", true}, // Exact match should work. - {"https://example.com", "http://example.com", false}, // Scheme mismatch should matter. - {"http://example.com", "https://example.com", false}, // Scheme mismatch should matter. - {"http://example.com", "http://example.org", true}, // Different domains should not matter. - {"http://example.com", "http://example.com:8080", true}, // Port should not matter. - {"http://example.com:8080", "http://example.com", true}, // Port should not matter. - {"http://example.com:8080", "http://example.com:8081", true}, // Different ports should not matter. - {"http://localhost", "http://localhost", true}, // Localhost should match. - {"http://127.0.0.1", "http://127.0.0.1", true}, // IPv4 address should match. - {"http://[::1]", "http://[::1]", true}, // IPv6 address should match. + {domain: "http://example.com", pattern: "http://example.com", expected: true}, // Exact match should work. + {domain: "https://example.com", pattern: "http://example.com", expected: false}, // Scheme mismatch should matter. + {domain: "http://example.com", pattern: "https://example.com", expected: false}, // Scheme mismatch should matter. + {domain: "http://example.com", pattern: "http://example.org", expected: true}, // Different domains should not matter. + {domain: "http://example.com", pattern: "http://example.com:8080", expected: true}, // Port should not matter. + {domain: "http://example.com:8080", pattern: "http://example.com", expected: true}, // Port should not matter. + {domain: "http://example.com:8080", pattern: "http://example.com:8081", expected: true}, // Different ports should not matter. + {domain: "http://localhost", pattern: "http://localhost", expected: true}, // Localhost should match. + {domain: "http://127.0.0.1", pattern: "http://127.0.0.1", expected: true}, // IPv4 address should match. + {domain: "http://[::1]", pattern: "http://[::1]", expected: true}, // IPv6 address should match. } for _, tc := range testCases { @@ -86,20 +86,20 @@ func Test_NormalizeDomain(t *testing.T) { input string expectedOutput string }{ - {"http://example.com", "example.com"}, // Simple case with http scheme. - {"https://example.com", "example.com"}, // Simple case with https scheme. - {"http://example.com:3000", "example.com"}, // Case with port. - {"https://example.com:3000", "example.com"}, // Case with port and https scheme. - {"http://example.com/path", "example.com/path"}, // Case with path. - {"http://example.com?query=123", "example.com?query=123"}, // Case with query. - {"http://example.com#fragment", "example.com#fragment"}, // Case with fragment. - {"example.com", "example.com"}, // Case without scheme. - {"example.com:8080", "example.com"}, // Case without scheme but with port. - {"sub.example.com", "sub.example.com"}, // Case with subdomain. - {"sub.sub.example.com", "sub.sub.example.com"}, // Case with nested subdomain. - {"http://localhost", "localhost"}, // Case with localhost. - {"http://127.0.0.1", "127.0.0.1"}, // Case with IPv4 address. - {"http://[::1]", "[::1]"}, // Case with IPv6 address. + {input: "http://example.com", expectedOutput: "example.com"}, // Simple case with http scheme. + {input: "https://example.com", expectedOutput: "example.com"}, // Simple case with https scheme. + {input: "http://example.com:3000", expectedOutput: "example.com"}, // Case with port. + {input: "https://example.com:3000", expectedOutput: "example.com"}, // Case with port and https scheme. + {input: "http://example.com/path", expectedOutput: "example.com/path"}, // Case with path. + {input: "http://example.com?query=123", expectedOutput: "example.com?query=123"}, // Case with query. + {input: "http://example.com#fragment", expectedOutput: "example.com#fragment"}, // Case with fragment. + {input: "example.com", expectedOutput: "example.com"}, // Case without scheme. + {input: "example.com:8080", expectedOutput: "example.com"}, // Case without scheme but with port. + {input: "sub.example.com", expectedOutput: "sub.example.com"}, // Case with subdomain. + {input: "sub.sub.example.com", expectedOutput: "sub.sub.example.com"}, // Case with nested subdomain. + {input: "http://localhost", expectedOutput: "localhost"}, // Case with localhost. + {input: "http://127.0.0.1", expectedOutput: "127.0.0.1"}, // Case with IPv4 address. + {input: "http://[::1]", expectedOutput: "[::1]"}, // Case with IPv6 address. } for _, tc := range testCases { diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index 7f9f94d2..d37c33a5 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -13,11 +13,40 @@ import ( // Config defines the config for middleware. type Config struct { + // Store is used to store the state of the middleware + // + // Optional. Default: memory.New() + // Ignored if Session is set. + Storage fiber.Storage + // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool + // Session is used to store the state of the middleware + // + // Optional. Default: nil + // If set, the middleware will use the session store instead of the storage + Session *session.Store + + // KeyGenerator creates a new CSRF token + // + // Optional. Default: utils.UUID + KeyGenerator func() string + + // ErrorHandler is executed when an error is returned from fiber.Handler. + // + // Optional. Default: DefaultErrorHandler + ErrorHandler fiber.ErrorHandler + + // 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) + // KeyLookup is a string in the form of ":" that is used // to create an Extractor that extracts the token from the request. // Possible values: @@ -45,45 +74,10 @@ type Config struct { // 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 - // Value of SameSite cookie. // 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 - - // SingleUseToken indicates if the CSRF token be destroyed - // and a new one generated on each use. - // - // Optional. Default: false - SingleUseToken bool - - // Store is used to store the state of the middleware - // - // Optional. Default: memory.New() - // Ignored if Session is set. - Storage fiber.Storage - - // Session is used to store the state of the middleware - // - // Optional. Default: nil - // If set, the middleware will use the session store instead of the storage - Session *session.Store - // SessionKey is the key used to store the token in the session // // Default: "csrfToken" @@ -102,22 +96,28 @@ type Config struct { // Optional. Default: [] TrustedOrigins []string - // KeyGenerator creates a new CSRF token + // Expiration is the duration before csrf token will expire // - // Optional. Default: utils.UUID - KeyGenerator func() string + // Optional. Default: 1 * time.Hour + Expiration time.Duration - // ErrorHandler is executed when an error is returned from fiber.Handler. - // - // Optional. Default: DefaultErrorHandler - ErrorHandler fiber.ErrorHandler + // Indicates if CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool - // Extractor returns the csrf token + // Indicates if CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // Decides whether cookie should last for only the browser sesison. + // Ignores Expiration if set to true + CookieSessionOnly bool + + // SingleUseToken indicates if the CSRF token be destroyed + // and a new one generated on each use. // - // 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) + // Optional. Default: false + SingleUseToken bool } const HeaderName = "X-Csrf-Token" diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 0609b5d7..7186345c 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" ) var ( @@ -26,9 +27,9 @@ var ( // Handler for CSRF middleware type Handler struct { - config Config sessionManager *sessionManager storageManager *storageManager + config Config } // The contextKey type is unexported to prevent collisions with context keys defined in @@ -64,7 +65,7 @@ func New(config ...Config) fiber.Handler { for _, origin := range cfg.TrustedOrigins { if i := strings.Index(origin, "://*."); i != -1 { - trimmedOrigin := strings.TrimSpace(origin[:i+3] + origin[i+4:]) + trimmedOrigin := utils.Trim(origin[:i+3]+origin[i+4:], ' ') isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { panic("[CSRF] Invalid origin format in configuration:" + origin) @@ -72,7 +73,7 @@ func New(config ...Config) fiber.Handler { sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]} trustedSubOrigins = append(trustedSubOrigins, sd) } else { - trimmedOrigin := strings.TrimSpace(origin) + trimmedOrigin := utils.Trim(origin, ' ') isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) if !isValid { panic("[CSRF] Invalid origin format in configuration:" + origin) diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 2dafec97..34e6065a 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -140,7 +140,7 @@ func Test_CSRF_WithSession(t *testing.T) { h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) for _, header := range strings.Split(token, ";") { - if strings.Split(strings.TrimSpace(header), "=")[0] == ConfigDefault.CookieName { + if strings.Split(utils.Trim(header, ' '), "=")[0] == ConfigDefault.CookieName { token = strings.Split(header, "=")[1] break } @@ -249,7 +249,7 @@ func Test_CSRF_ExpiredToken_WithSession(t *testing.T) { h(ctx) token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) for _, header := range strings.Split(token, ";") { - if strings.Split(strings.TrimSpace(header), "=")[0] == ConfigDefault.CookieName { + if strings.Split(utils.Trim(header, ' '), "=")[0] == ConfigDefault.CookieName { token = strings.Split(header, "=")[1] break } @@ -901,13 +901,13 @@ func Test_CSRF_TrustedOrigins_InvalidOrigins(t *testing.T) { name string origin string }{ - {"No Scheme", "localhost"}, - {"Wildcard", "https://*"}, - {"Wildcard domain", "https://*example.com"}, - {"File Scheme", "file://example.com"}, - {"FTP Scheme", "ftp://example.com"}, - {"Port Wildcard", "http://example.com:*"}, - {"Multiple Wildcards", "https://*.*.com"}, + {name: "No Scheme", origin: "localhost"}, + {name: "Wildcard", origin: "https://*"}, + {name: "Wildcard domain", origin: "https://*example.com"}, + {name: "File Scheme", origin: "file://example.com"}, + {name: "FTP Scheme", origin: "ftp://example.com"}, + {name: "Port Wildcard", origin: "http://example.com:*"}, + {name: "Multiple Wildcards", origin: "https://*.*.com"}, } for _, tt := range tests { diff --git a/middleware/csrf/helpers_test.go b/middleware/csrf/helpers_test.go index bcb574e5..4540d72b 100644 --- a/middleware/csrf/helpers_test.go +++ b/middleware/csrf/helpers_test.go @@ -10,34 +10,34 @@ import ( func Test_normalizeOrigin(t *testing.T) { testCases := []struct { origin string - expectedValid bool expectedOrigin string + expectedValid bool }{ - {"http://example.com", true, "http://example.com"}, // Simple case should work. - {"HTTP://EXAMPLE.COM", true, "http://example.com"}, // Case should be normalized. - {"http://example.com/", true, "http://example.com"}, // Trailing slash should be removed. - {"http://example.com:3000", true, "http://example.com:3000"}, // Port should be preserved. - {"http://example.com:3000/", true, "http://example.com:3000"}, // Trailing slash should be removed. - {"http://", false, ""}, // Invalid origin should not be accepted. - {"file:///etc/passwd", false, ""}, // File scheme should not be accepted. - {"https://*example.com", false, ""}, // Wildcard domain should not be accepted. - {"http://*.example.com", false, ""}, // Wildcard subdomain should not be accepted. - {"http://example.com/path", false, ""}, // Path should not be accepted. - {"http://example.com?query=123", false, ""}, // Query should not be accepted. - {"http://example.com#fragment", false, ""}, // Fragment should not be accepted. - {"http://localhost", true, "http://localhost"}, // Localhost should be accepted. - {"http://127.0.0.1", true, "http://127.0.0.1"}, // IPv4 address should be accepted. - {"http://[::1]", true, "http://[::1]"}, // IPv6 address should be accepted. - {"http://[::1]:8080", true, "http://[::1]:8080"}, // IPv6 address with port should be accepted. - {"http://[::1]:8080/", true, "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. - {"http://[::1]:8080/path", false, ""}, // IPv6 address with port and path should not be accepted. - {"http://[::1]:8080?query=123", false, ""}, // IPv6 address with port and query should not be accepted. - {"http://[::1]:8080#fragment", false, ""}, // IPv6 address with port and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment", false, ""}, // IPv6 address with port, path, query, and fragment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/", false, ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. - {"http://[::1]:8080/path?query=123#fragment/invalid/segment", false, ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. + {origin: "http://example.com", expectedValid: true, expectedOrigin: "http://example.com"}, // Simple case should work. + {origin: "HTTP://EXAMPLE.COM", expectedValid: true, expectedOrigin: "http://example.com"}, // Case should be normalized. + {origin: "http://example.com/", expectedValid: true, expectedOrigin: "http://example.com"}, // Trailing slash should be removed. + {origin: "http://example.com:3000", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Port should be preserved. + {origin: "http://example.com:3000/", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Trailing slash should be removed. + {origin: "http://", expectedValid: false, expectedOrigin: ""}, // Invalid origin should not be accepted. + {origin: "file:///etc/passwd", expectedValid: false, expectedOrigin: ""}, // File scheme should not be accepted. + {origin: "https://*example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard domain should not be accepted. + {origin: "http://*.example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard subdomain should not be accepted. + {origin: "http://example.com/path", expectedValid: false, expectedOrigin: ""}, // Path should not be accepted. + {origin: "http://example.com?query=123", expectedValid: false, expectedOrigin: ""}, // Query should not be accepted. + {origin: "http://example.com#fragment", expectedValid: false, expectedOrigin: ""}, // Fragment should not be accepted. + {origin: "http://localhost", expectedValid: true, expectedOrigin: "http://localhost"}, // Localhost should be accepted. + {origin: "http://127.0.0.1", expectedValid: true, expectedOrigin: "http://127.0.0.1"}, // IPv4 address should be accepted. + {origin: "http://[::1]", expectedValid: true, expectedOrigin: "http://[::1]"}, // IPv6 address should be accepted. + {origin: "http://[::1]:8080", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port should be accepted. + {origin: "http://[::1]:8080/", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. + {origin: "http://[::1]:8080/path", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and path should not be accepted. + {origin: "http://[::1]:8080?query=123", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and query should not be accepted. + {origin: "http://[::1]:8080#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/segment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. } for _, tc := range testCases { diff --git a/middleware/csrf/manager_msgp_test.go b/middleware/csrf/manager_msgp_test.go deleted file mode 100644 index 4d2bd237..00000000 --- a/middleware/csrf/manager_msgp_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package csrf - -// Code generated by github.com/tinylib/msgp DO NOT EDIT. - -import ( - "testing" - - "github.com/tinylib/msgp/msgp" -) - -func Test_MarshalUnmarshalitem(t *testing.T) { - v := item{} - bts, err := v.MarshalMsg(nil) - if err != nil { - t.Fatal(err) - } - left, err := v.UnmarshalMsg(bts) - if err != nil { - t.Fatal(err) - } - if len(left) > 0 { - t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) - } - - left, err = msgp.Skip(bts) - if err != nil { - t.Fatal(err) - } - if len(left) > 0 { - t.Errorf("%d bytes left over after Skip(): %q", len(left), left) - } -} - -func Benchmark_MarshalMsgitem(b *testing.B) { - v := item{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - v.MarshalMsg(nil) - } -} - -func Benchmark_AppendMsgitem(b *testing.B) { - v := item{} - bts := make([]byte, 0, v.Msgsize()) - bts, _ = v.MarshalMsg(bts[0:0]) - b.SetBytes(int64(len(bts))) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - bts, _ = v.MarshalMsg(bts[0:0]) - } -} - -func Benchmark_Unmarshalitem(b *testing.B) { - v := item{} - bts, _ := v.MarshalMsg(nil) - b.ReportAllocs() - b.SetBytes(int64(len(bts))) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := v.UnmarshalMsg(bts) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/middleware/csrf/session_manager.go b/middleware/csrf/session_manager.go index 5d56121e..d927215a 100644 --- a/middleware/csrf/session_manager.go +++ b/middleware/csrf/session_manager.go @@ -10,8 +10,8 @@ import ( ) type sessionManager struct { - key string session *session.Store + key string } func newSessionManager(s *session.Store, k string) *sessionManager { @@ -55,7 +55,7 @@ func (m *sessionManager) setRaw(c fiber.Ctx, key string, raw []byte, exp time.Du return } // 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 - sess.Set(m.key, &Token{key, raw, time.Now().Add(exp)}) + sess.Set(m.key, &Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)}) if err := sess.Save(); err != nil { log.Warn("csrf: failed to save session: ", err) } diff --git a/middleware/csrf/storage_manager.go b/middleware/csrf/storage_manager.go index c3b1012f..76ad38cb 100644 --- a/middleware/csrf/storage_manager.go +++ b/middleware/csrf/storage_manager.go @@ -11,15 +11,16 @@ import ( "github.com/gofiber/utils/v2" ) -// go:generate msgp -// msgp -file="storage_manager.go" -o="storage_manager_msgp.go" -tests=false -unexported +// msgp -file="storage_manager.go" -o="storage_manager_msgp.go" -tests=true -unexported +// +//go:generate msgp -o=storage_manager_msgp.go -tests=true -unexported type item struct{} //msgp:ignore manager type storageManager struct { - pool sync.Pool - memory *memory.Storage - storage fiber.Storage + pool sync.Pool `msg:"-"` //nolint:revive // Ignore unexported type + memory *memory.Storage `msg:"-"` //nolint:revive // Ignore unexported type + storage fiber.Storage `msg:"-"` //nolint:revive // Ignore unexported type } func newStorageManager(storage fiber.Storage) *storageManager { diff --git a/middleware/csrf/storage_manager_msgp.go b/middleware/csrf/storage_manager_msgp.go index c382641a..f0ee8bd7 100644 --- a/middleware/csrf/storage_manager_msgp.go +++ b/middleware/csrf/storage_manager_msgp.go @@ -6,10 +6,51 @@ import ( "github.com/tinylib/msgp/msgp" ) +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 0 + _ = z + err = en.Append(0x80) + if err != nil { + return + } + return +} + // MarshalMsg implements msgp.Marshaler func (z item) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // map header, size 0 + _ = z o = append(o, 0x80) return } @@ -49,3 +90,88 @@ func (z item) Msgsize() (s int) { s = 1 return } + +// DecodeMsg implements msgp.Decodable +func (z *storageManager) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z storageManager) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 0 + _ = z + err = en.Append(0x80) + if err != nil { + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z storageManager) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 0 + _ = z + o = append(o, 0x80) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *storageManager) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z storageManager) Msgsize() (s int) { + s = 1 + return +} diff --git a/middleware/csrf/storage_manager_msgp_test.go b/middleware/csrf/storage_manager_msgp_test.go new file mode 100644 index 00000000..0aad0c2b --- /dev/null +++ b/middleware/csrf/storage_manager_msgp_test.go @@ -0,0 +1,236 @@ +package csrf + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "bytes" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestMarshalUnmarshalitem(t *testing.T) { + v := item{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgitem(b *testing.B) { + v := item{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgitem(b *testing.B) { + v := item{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalitem(b *testing.B) { + v := item{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecodeitem(t *testing.T) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecodeitem Msgsize() is inaccurate") + } + + vn := item{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalstorageManager(t *testing.T) { + v := storageManager{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgstorageManager(b *testing.B) { + v := storageManager{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgstorageManager(b *testing.B) { + v := storageManager{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalstorageManager(b *testing.B) { + v := storageManager{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecodestorageManager(t *testing.T) { + v := storageManager{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecodestorageManager Msgsize() is inaccurate") + } + + vn := storageManager{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodestorageManager(b *testing.B) { + v := storageManager{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodestorageManager(b *testing.B) { + v := storageManager{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/csrf/token.go b/middleware/csrf/token.go index ee88b9ae..b96b013a 100644 --- a/middleware/csrf/token.go +++ b/middleware/csrf/token.go @@ -5,7 +5,7 @@ import ( ) type Token struct { + Expiration time.Time `json:"expiration"` Key string `json:"key"` Raw []byte `json:"raw"` - Expiration time.Time `json:"expiration"` } diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go index 80db08d9..de9ba455 100644 --- a/middleware/encryptcookie/config.go +++ b/middleware/encryptcookie/config.go @@ -11,18 +11,6 @@ type Config struct { // Optional. Default: nil Next func(c fiber.Ctx) bool - // Array of cookie keys that should not be encrypted. - // - // Optional. Default: [] - Except []string - - // Base64 encoded unique key to encode & decode cookies. - // - // Required. Key length should be 16, 24, or 32 bytes when decoded - // if using the default EncryptCookie and DecryptCookie functions. - // You may use `encryptcookie.GenerateKey(length)` to generate a new key. - Key string - // Custom function to encrypt cookies. // // Optional. Default: EncryptCookie (using AES-GCM) @@ -32,6 +20,18 @@ type Config struct { // // Optional. Default: DecryptCookie (using AES-GCM) Decryptor func(encryptedString, key string) (string, error) + + // Base64 encoded unique key to encode & decode cookies. + // + // Required. Key length should be 16, 24, or 32 bytes when decoded + // if using the default EncryptCookie and DecryptCookie functions. + // You may use `encryptcookie.GenerateKey(length)` to generate a new key. + Key string + + // Array of cookie keys that should not be encrypted. + // + // Optional. Default: [] + Except []string } // ConfigDefault is the default config diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go index c2f7639b..1e7b2d38 100644 --- a/middleware/encryptcookie/encryptcookie_test.go +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -38,13 +38,12 @@ func Test_Middleware_InvalidKeys(t *testing.T) { tests := []struct { length int }{ - {11}, - {25}, - {60}, + {length: 11}, + {length: 25}, + {length: 60}, } for _, tt := range tests { - tt := tt t.Run(strconv.Itoa(tt.length)+"_length_encrypt", func(t *testing.T) { t.Parallel() key := make([]byte, tt.length) @@ -283,9 +282,9 @@ func Test_GenerateKey(t *testing.T) { tests := []struct { length int }{ - {16}, - {24}, - {32}, + {length: 16}, + {length: 24}, + {length: 32}, } decodeBase64 := func(t *testing.T, s string) []byte { @@ -296,7 +295,6 @@ func Test_GenerateKey(t *testing.T) { } for _, tt := range tests { - tt := tt t.Run(strconv.Itoa(tt.length)+"_length", func(t *testing.T) { t.Parallel() key := GenerateKey(tt.length) @@ -649,9 +647,9 @@ func Benchmark_GenerateKey(b *testing.B) { tests := []struct { length int }{ - {16}, - {24}, - {32}, + {length: 16}, + {length: 24}, + {length: 32}, } for _, tt := range tests { @@ -667,9 +665,9 @@ func Benchmark_GenerateKey_Parallel(b *testing.B) { tests := []struct { length int }{ - {16}, - {24}, - {32}, + {length: 16}, + {length: 24}, + {length: 32}, } for _, tt := range tests { diff --git a/middleware/envvar/envvar_test.go b/middleware/envvar/envvar_test.go index 197ca81e..e34969b1 100644 --- a/middleware/envvar/envvar_test.go +++ b/middleware/envvar/envvar_test.go @@ -34,7 +34,7 @@ func Test_EnvVarHandler(t *testing.T) { struct { Vars map[string]string `json:"vars"` }{ - map[string]string{"testKey": "testVal"}, + Vars: map[string]string{"testKey": "testVal"}, }) require.NoError(t, err) diff --git a/middleware/etag/config.go b/middleware/etag/config.go index 09512702..ccc47971 100644 --- a/middleware/etag/config.go +++ b/middleware/etag/config.go @@ -6,6 +6,10 @@ import ( // 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 @@ -15,11 +19,6 @@ type Config struct { // when byte range requests are used, but strong etags mean range // requests can still be cached. Weak bool - - // Next defines a function to skip this middleware when returned true. - // - // Optional. Default: nil - Next func(c fiber.Ctx) bool } // ConfigDefault is the default config diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go index 2a2c0ae1..aca57bc1 100644 --- a/middleware/etag/etag.go +++ b/middleware/etag/etag.go @@ -3,6 +3,7 @@ package etag import ( "bytes" "hash/crc32" + "math" "github.com/gofiber/fiber/v3" "github.com/valyala/bytebufferpool" @@ -56,8 +57,15 @@ func New(config ...Config) fiber.Handler { bb.Write(weakPrefix) } + // Write ETag bb.WriteByte('"') - bb.B = appendUint(bb.Bytes(), uint32(len(body))) + + bodyLength := len(body) + if bodyLength > math.MaxUint32 { + return c.SendStatus(fiber.StatusRequestEntityTooLarge) + } + + bb.B = appendUint(bb.Bytes(), uint32(bodyLength)) //nolint:gosec // Body length is validated above bb.WriteByte('-') bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q)) bb.WriteByte('"') diff --git a/middleware/favicon/favicon.go b/middleware/favicon/favicon.go index f1de00cb..e08b9b95 100644 --- a/middleware/favicon/favicon.go +++ b/middleware/favicon/favicon.go @@ -11,16 +11,17 @@ import ( // Config defines the config for middleware. type Config struct { + // FileSystem is an optional alternate filesystem to search for the favicon in. + // An example of this could be an embedded or network filesystem + // + // Optional. Default: nil + FileSystem fs.FS `json:"-"` + // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool - // Raw data of the favicon file - // - // Optional. Default: nil - Data []byte `json:"-"` - // File holds the path to an actual favicon that will be cached // // Optional. Default: "" @@ -31,16 +32,15 @@ type Config struct { // 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 - // - // Optional. Default: nil - FileSystem fs.FS `json:"-"` - // CacheControl defines how the Cache-Control header in the response should be set // // Optional. Default: "public, max-age=31536000" CacheControl string `json:"cache_control"` + + // Raw data of the favicon file + // + // Optional. Default: nil + Data []byte `json:"-"` } // ConfigDefault is the default config diff --git a/middleware/healthcheck/config.go b/middleware/healthcheck/config.go index 8b7b26a2..eba6d537 100644 --- a/middleware/healthcheck/config.go +++ b/middleware/healthcheck/config.go @@ -24,6 +24,7 @@ type Config struct { const ( DefaultLivenessEndpoint = "/livez" DefaultReadinessEndpoint = "/readyz" + DefaultStartupEndpoint = "/startupz" ) func defaultProbe(fiber.Ctx) bool { return true } diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go index e6c17487..07efa3de 100644 --- a/middleware/healthcheck/healthcheck_test.go +++ b/middleware/healthcheck/healthcheck_test.go @@ -34,30 +34,38 @@ func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { StrictRouting: true, }) - app.Get("/livez", NewHealthChecker()) - app.Get("/readyz", NewHealthChecker()) + app.Get(DefaultLivenessEndpoint, NewHealthChecker()) + app.Get(DefaultReadinessEndpoint, NewHealthChecker()) + app.Get(DefaultStartupEndpoint, NewHealthChecker()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") + shouldGiveOK(t, app, "/startupz") shouldGiveNotFound(t, app, "/readyz/") shouldGiveNotFound(t, app, "/livez/") + shouldGiveNotFound(t, app, "/startupz/") shouldGiveNotFound(t, app, "/notDefined/readyz") shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/startupz") } func Test_HealthCheck_Default(t *testing.T) { t.Parallel() app := fiber.New() - app.Get("/livez", NewHealthChecker()) - app.Get("/readyz", NewHealthChecker()) + app.Get(DefaultLivenessEndpoint, NewHealthChecker()) + app.Get(DefaultReadinessEndpoint, NewHealthChecker()) + app.Get(DefaultStartupEndpoint, NewHealthChecker()) shouldGiveOK(t, app, "/readyz") shouldGiveOK(t, app, "/livez") + shouldGiveOK(t, app, "/startupz") shouldGiveOK(t, app, "/readyz/") shouldGiveOK(t, app, "/livez/") + shouldGiveOK(t, app, "/startupz/") shouldGiveNotFound(t, app, "/notDefined/readyz") shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/startupz") } func Test_HealthCheck_Custom(t *testing.T) { @@ -80,6 +88,11 @@ func Test_HealthCheck_Custom(t *testing.T) { } }, })) + app.Get(DefaultStartupEndpoint, NewHealthChecker(Config{ + Probe: func(_ fiber.Ctx) bool { + return false + }, + })) // Setup custom liveness and readiness probes to simulate application health status // Live should return 200 with GET request @@ -97,6 +110,8 @@ func Test_HealthCheck_Custom(t *testing.T) { // Ready should return 503 with GET request before the channel is closed shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable) + shouldGiveStatus(t, app, "/startupz", fiber.StatusServiceUnavailable) + // Ready should return 200 with GET request after the channel is closed c1 <- struct{}{} shouldGiveOK(t, app, "/ready") @@ -155,13 +170,15 @@ func Test_HealthCheck_Next(t *testing.T) { }, }) - app.Get("/readyz", checker) - app.Get("/livez", checker) + app.Get(DefaultLivenessEndpoint, checker) + app.Get(DefaultReadinessEndpoint, checker) + app.Get(DefaultStartupEndpoint, checker) // This should give not found since there are no other handlers to execute // so it's like the route isn't defined at all shouldGiveNotFound(t, app, "/readyz") shouldGiveNotFound(t, app, "/livez") + shouldGiveNotFound(t, app, "/startupz") } func Benchmark_HealthCheck(b *testing.B) { @@ -169,6 +186,7 @@ func Benchmark_HealthCheck(b *testing.B) { app.Get(DefaultLivenessEndpoint, NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker()) + app.Get(DefaultStartupEndpoint, NewHealthChecker()) h := app.Handler() fctx := &fasthttp.RequestCtx{} @@ -190,6 +208,7 @@ func Benchmark_HealthCheck_Parallel(b *testing.B) { app.Get(DefaultLivenessEndpoint, NewHealthChecker()) app.Get(DefaultReadinessEndpoint, NewHealthChecker()) + app.Get(DefaultStartupEndpoint, NewHealthChecker()) h := app.Handler() diff --git a/middleware/helmet/README.md b/middleware/helmet/README.md deleted file mode 100644 index 7cd99d0a..00000000 --- a/middleware/helmet/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Helmet - -![Release](https://img.shields.io/github/release/gofiber/helmet.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/helmet/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/helmet/workflows/Security/badge.svg) -![Linter](https://github.com/gofiber/helmet/workflows/Linter/badge.svg) - -### Install -``` -go get -u github.com/gofiber/fiber/v3 -go get -u github.com/gofiber/helmet/v2 -``` -### Example -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/helmet/v2" -) - -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 -``` diff --git a/middleware/helmet/config.go b/middleware/helmet/config.go index dbbcd377..c7fa3cab 100644 --- a/middleware/helmet/config.go +++ b/middleware/helmet/config.go @@ -23,26 +23,10 @@ type Config struct { // 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 @@ -78,6 +62,22 @@ type Config struct { // X-Permitted-Cross-Domain-Policies // Optional. Default value "none". XPermittedCrossDomain string + + // HSTSMaxAge + // Optional. Default value 0. + HSTSMaxAge int + + // HSTSExcludeSubdomains + // Optional. Default value false. + HSTSExcludeSubdomains bool + + // CSPReportOnly + // Optional. Default value false. + CSPReportOnly bool + + // HSTSPreloadEnabled + // Optional. Default value false. + HSTSPreloadEnabled bool } // ConfigDefault is the default config diff --git a/middleware/idempotency/config.go b/middleware/idempotency/config.go index e002fcb1..414818a1 100644 --- a/middleware/idempotency/config.go +++ b/middleware/idempotency/config.go @@ -13,30 +13,6 @@ var ErrInvalidIdempotencyKey = errors.New("invalid idempotency key") // Config defines the config for middleware. 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. @@ -46,6 +22,30 @@ type Config struct { // // Optional. Default: an in-memory storage for this process only. Storage fiber.Storage + // 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 + + // 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 + + // KeyHeader is the name of the header that contains the idempotency key. + // + // Optional. Default: X-Idempotency-Key + KeyHeader string + + // KeepResponseHeaders is a list of headers that should be kept from the original response. + // + // Optional. Default: nil (to keep all headers) + KeepResponseHeaders []string + + // Lifetime is the maximum lifetime of an idempotency key. + // + // Optional. Default: 30 * time.Minute + Lifetime time.Duration } // ConfigDefault is the default config diff --git a/middleware/idempotency/locker.go b/middleware/idempotency/locker.go index bf8bf0e0..2c3348b8 100644 --- a/middleware/idempotency/locker.go +++ b/middleware/idempotency/locker.go @@ -11,9 +11,8 @@ type Locker interface { } type MemoryLock struct { - mu sync.Mutex - keys map[string]*sync.Mutex + mu sync.Mutex } func (l *MemoryLock) Lock(key string) error { diff --git a/middleware/idempotency/response.go b/middleware/idempotency/response.go index da09630f..aafbca60 100644 --- a/middleware/idempotency/response.go +++ b/middleware/idempotency/response.go @@ -3,11 +3,10 @@ package idempotency // response is a struct that represents the response of a request. // generation tool `go install github.com/tinylib/msgp@latest` // -//go:generate msgp -o=response_msgp.go -io=false -unexported +//go:generate msgp -o=response_msgp.go -tests=true -unexported type response struct { - StatusCode int `msg:"sc"` - Headers map[string][]string `msg:"hs"` - Body []byte `msg:"b"` + Body []byte `msg:"b"` + StatusCode int `msg:"sc"` } diff --git a/middleware/idempotency/response_msgp.go b/middleware/idempotency/response_msgp.go index 410d118c..c7873f4d 100644 --- a/middleware/idempotency/response_msgp.go +++ b/middleware/idempotency/response_msgp.go @@ -6,15 +6,151 @@ import ( "github.com/tinylib/msgp/msgp" ) +// DecodeMsg implements msgp.Decodable +func (z *response) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "hs": + var zb0002 uint32 + zb0002, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + if z.Headers == nil { + z.Headers = make(map[string][]string, zb0002) + } else if len(z.Headers) > 0 { + for key := range z.Headers { + delete(z.Headers, key) + } + } + for zb0002 > 0 { + zb0002-- + var za0001 string + var za0002 []string + za0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + var zb0003 uint32 + zb0003, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err, "Headers", za0001) + return + } + if cap(za0002) >= int(zb0003) { + za0002 = (za0002)[:zb0003] + } else { + za0002 = make([]string, zb0003) + } + for za0003 := range za0002 { + za0002[za0003], err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Headers", za0001, za0003) + return + } + } + z.Headers[za0001] = za0002 + } + case "b": + z.Body, err = dc.ReadBytes(z.Body) + if err != nil { + err = msgp.WrapError(err, "Body") + return + } + case "sc": + z.StatusCode, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "StatusCode") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *response) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "hs" + err = en.Append(0x83, 0xa2, 0x68, 0x73) + if err != nil { + return + } + err = en.WriteMapHeader(uint32(len(z.Headers))) + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + for za0001, za0002 := range z.Headers { + err = en.WriteString(za0001) + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + err = en.WriteArrayHeader(uint32(len(za0002))) + if err != nil { + err = msgp.WrapError(err, "Headers", za0001) + return + } + for za0003 := range za0002 { + err = en.WriteString(za0002[za0003]) + if err != nil { + err = msgp.WrapError(err, "Headers", za0001, za0003) + return + } + } + } + // write "b" + err = en.Append(0xa1, 0x62) + if err != nil { + return + } + err = en.WriteBytes(z.Body) + if err != nil { + err = msgp.WrapError(err, "Body") + return + } + // write "sc" + err = en.Append(0xa2, 0x73, 0x63) + if err != nil { + return + } + err = en.WriteInt(z.StatusCode) + if err != nil { + err = msgp.WrapError(err, "StatusCode") + return + } + return +} + // MarshalMsg implements msgp.Marshaler func (z *response) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // map header, size 3 - // string "sc" - o = append(o, 0x83, 0xa2, 0x73, 0x63) - o = msgp.AppendInt(o, z.StatusCode) // string "hs" - o = append(o, 0xa2, 0x68, 0x73) + o = append(o, 0x83, 0xa2, 0x68, 0x73) o = msgp.AppendMapHeader(o, uint32(len(z.Headers))) for za0001, za0002 := range z.Headers { o = msgp.AppendString(o, za0001) @@ -26,6 +162,9 @@ func (z *response) MarshalMsg(b []byte) (o []byte, err error) { // string "b" o = append(o, 0xa1, 0x62) o = msgp.AppendBytes(o, z.Body) + // string "sc" + o = append(o, 0xa2, 0x73, 0x63) + o = msgp.AppendInt(o, z.StatusCode) return } @@ -47,12 +186,6 @@ func (z *response) UnmarshalMsg(bts []byte) (o []byte, err error) { return } switch msgp.UnsafeString(field) { - case "sc": - z.StatusCode, bts, err = msgp.ReadIntBytes(bts) - if err != nil { - err = msgp.WrapError(err, "StatusCode") - return - } case "hs": var zb0002 uint32 zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) @@ -102,6 +235,12 @@ func (z *response) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "Body") return } + case "sc": + z.StatusCode, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "StatusCode") + return + } default: bts, err = msgp.Skip(bts) if err != nil { @@ -116,7 +255,7 @@ func (z *response) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *response) Msgsize() (s int) { - s = 1 + 3 + msgp.IntSize + 3 + msgp.MapHeaderSize + s = 1 + 3 + msgp.MapHeaderSize if z.Headers != nil { for za0001, za0002 := range z.Headers { _ = za0002 @@ -126,6 +265,6 @@ func (z *response) Msgsize() (s int) { } } } - s += 2 + msgp.BytesPrefixSize + len(z.Body) + s += 2 + msgp.BytesPrefixSize + len(z.Body) + 3 + msgp.IntSize return } diff --git a/middleware/idempotency/response_msgp_test.go b/middleware/idempotency/response_msgp_test.go index a1241f90..173c75ad 100644 --- a/middleware/idempotency/response_msgp_test.go +++ b/middleware/idempotency/response_msgp_test.go @@ -3,6 +3,7 @@ package idempotency // Code generated by github.com/tinylib/msgp DO NOT EDIT. import ( + "bytes" "testing" "github.com/tinylib/msgp/msgp" @@ -31,7 +32,7 @@ func TestMarshalUnmarshalresponse(t *testing.T) { } } -func Benchmark_MarshalMsgresponse(b *testing.B) { +func BenchmarkMarshalMsgresponse(b *testing.B) { v := response{} b.ReportAllocs() b.ResetTimer() @@ -40,7 +41,7 @@ func Benchmark_MarshalMsgresponse(b *testing.B) { } } -func Benchmark_AppendMsgresponse(b *testing.B) { +func BenchmarkAppendMsgresponse(b *testing.B) { v := response{} bts := make([]byte, 0, v.Msgsize()) bts, _ = v.MarshalMsg(bts[0:0]) @@ -52,7 +53,7 @@ func Benchmark_AppendMsgresponse(b *testing.B) { } } -func Benchmark_Unmarshalresponse(b *testing.B) { +func BenchmarkUnmarshalresponse(b *testing.B) { v := response{} bts, _ := v.MarshalMsg(nil) b.ReportAllocs() @@ -65,3 +66,58 @@ func Benchmark_Unmarshalresponse(b *testing.B) { } } } + +func TestEncodeDecoderesponse(t *testing.T) { + v := response{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecoderesponse Msgsize() is inaccurate") + } + + vn := response{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncoderesponse(b *testing.B) { + v := response{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecoderesponse(b *testing.B) { + v := response{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/keyauth/README.md b/middleware/keyauth/README.md deleted file mode 100644 index c4bd5814..00000000 --- a/middleware/keyauth/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Key Authentication - -![Release](https://img.shields.io/github/release/gofiber/keyauth.svg) -[![Discord](https://img.shields.io/badge/discord-join%20channel-7289DA)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/keyauth/workflows/Test/badge.svg) -![Security](https://github.com/gofiber/keyauth/workflows/Security/badge.svg) -![Linter](https://github.com/gofiber/keyauth/workflows/Linter/badge.svg) - -Special thanks to [József Sallai](https://github.com/jozsefsallai) & [Ray Mayemir](https://github.com/raymayemir) - -### Install -``` -go get -u github.com/gofiber/fiber/v3 -go get -u github.com/gofiber/keyauth/v2 -``` -### Example -```go -package main - -import ( - "github.com/gofiber/fiber/v3" - "github.com/gofiber/keyauth/v2" -) - -func main() { - app := fiber.New() - - app.Use(keyauth.New(keyauth.Config{ - KeyLookup: "cookie:access_token", - ContextKey: "my_token", - })) - - app.Get("/", func(c fiber.Ctx) error { - token := c.TokenFromContext(c) // "" is returned if not found - return c.SendString(token) - }) - - app.Listen(":3000") -} -``` -### Test -```curl -curl -v --cookie "access_token=hello_world" http://localhost:3000 -``` diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go index 8c41d60f..c7cb8172 100644 --- a/middleware/keyauth/config.go +++ b/middleware/keyauth/config.go @@ -23,6 +23,11 @@ type Config struct { // Optional. Default: 401 Invalid or expired key ErrorHandler fiber.ErrorHandler + CustomKeyLookup KeyLookupFunc + + // Validator is a function to validate key. + Validator func(fiber.Ctx, string) (bool, error) + // KeyLookup is a string in the form of ":" that is used // to extract key from the request. // Optional. Default value "header:Authorization". @@ -34,14 +39,9 @@ type Config struct { // - "cookie:" KeyLookup string - CustomKeyLookup KeyLookupFunc - // 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) } // ConfigDefault is the default config diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index e59e0936..9da675fe 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -23,8 +23,8 @@ func Test_AuthSources(t *testing.T) { authTokenName string description string APIKey string - expectedCode int expectedBody string + expectedCode int }{ { route: "/", @@ -282,8 +282,8 @@ func Test_MultipleKeyAuth(t *testing.T) { route string description string APIKey string - expectedCode int expectedBody string + expectedCode int }{ // No auth needed for / { diff --git a/middleware/limiter/config.go b/middleware/limiter/config.go index 3045282e..461d21a7 100644 --- a/middleware/limiter/config.go +++ b/middleware/limiter/config.go @@ -8,15 +8,26 @@ import ( // Config defines the config for middleware. type Config struct { + // Store is used to store the state of the middleware + // + // Default: an in memory store for this process only + Storage fiber.Storage + + // LimiterMiddleware is the struct that implements a limiter middleware. + // + // Default: a new Fixed Window Rate Limiter + LimiterMiddleware Handler // 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 `Expiration` seconds before sending a 429 response + // A function to dynamically calculate the max requests supported by the rate limiter middleware // - // Default: 5 - Max int + // Default: func(c fiber.Ctx) int { + // return c.Max + // } + MaxFunc func(c fiber.Ctx) int // KeyGenerator allows you to generate custom keys, by default c.IP() is used // @@ -25,11 +36,6 @@ type Config struct { // } 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 { @@ -37,6 +43,16 @@ type Config struct { // } LimitReached fiber.Handler + // Max number of recent connections during `Expiration` seconds before sending a 429 response + // + // Default: 5 + Max int + + // Expiration is the time on how long to keep records of requests in memory + // + // Default: 1 * time.Minute + Expiration time.Duration + // When set to true, requests with StatusCode >= 400 won't be counted. // // Default: false @@ -46,16 +62,6 @@ type Config struct { // // 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 - - // LimiterMiddleware is the struct that implements a limiter middleware. - // - // Default: a new Fixed Window Rate Limiter - LimiterMiddleware Handler } // ConfigDefault is the default config @@ -102,5 +108,10 @@ func configDefault(config ...Config) Config { if cfg.LimiterMiddleware == nil { cfg.LimiterMiddleware = ConfigDefault.LimiterMiddleware } + if cfg.MaxFunc == nil { + cfg.MaxFunc = func(_ fiber.Ctx) int { + return cfg.Max + } + } return cfg } diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go index 1e2a1aa0..010083f9 100644 --- a/middleware/limiter/limiter_fixed.go +++ b/middleware/limiter/limiter_fixed.go @@ -15,7 +15,6 @@ func (FixedWindow) New(cfg Config) fiber.Handler { var ( // Limiter variables mux = &sync.RWMutex{} - max = strconv.Itoa(cfg.Max) expiration = uint64(cfg.Expiration.Seconds()) ) @@ -27,8 +26,11 @@ func (FixedWindow) New(cfg Config) fiber.Handler { // Return new handler return func(c fiber.Ctx) error { - // Don't execute middleware if Next returns true - if cfg.Next != nil && cfg.Next(c) { + // Generate maxRequests from generator, if no generator was provided the default value returned is 5 + maxRequests := cfg.MaxFunc(c) + + // Don't execute middleware if Next returns true or if the max is 0 + if (cfg.Next != nil && cfg.Next(c)) || maxRequests == 0 { return c.Next() } @@ -60,7 +62,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { resetInSec := e.exp - ts // Set how many hits we have left - remaining := cfg.Max - e.currHits + remaining := maxRequests - e.currHits // Update storage manager.set(key, e, cfg.Expiration) @@ -68,7 +70,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { // Unlock entry mux.Unlock() - // Check if hits exceed the cfg.Max + // Check if hits exceed the max if remaining < 0 { // Return response with Retry-After header // https://tools.ietf.org/html/rfc6584 @@ -96,7 +98,7 @@ func (FixedWindow) New(cfg Config) fiber.Handler { } // We can continue, update RateLimit headers - c.Set(xRateLimitLimit, max) + c.Set(xRateLimitLimit, strconv.Itoa(maxRequests)) c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go index a9859347..de2f7d3b 100644 --- a/middleware/limiter/limiter_sliding.go +++ b/middleware/limiter/limiter_sliding.go @@ -16,7 +16,6 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { var ( // Limiter variables mux = &sync.RWMutex{} - max = strconv.Itoa(cfg.Max) expiration = uint64(cfg.Expiration.Seconds()) ) @@ -28,8 +27,11 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { // Return new handler return func(c fiber.Ctx) error { - // Don't execute middleware if Next returns true - if cfg.Next != nil && cfg.Next(c) { + // Generate maxRequests from generator, if no generator was provided the default value returned is 5 + maxRequests := cfg.MaxFunc(c) + + // Don't execute middleware if Next returns true or if the max is 0 + if (cfg.Next != nil && cfg.Next(c)) || maxRequests == 0 { return c.Next() } @@ -87,14 +89,14 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { // ^ ^ ^ ^ // ts e.exp End sample window End next window // <------------> - // resetInSec + // Reset In Sec // resetInSec = e.exp - ts - time until end of current window. // duration + expiration = end of next window. // Because we don't want to garbage collect in the middle of a window // we add the expiration to the duration. // Otherwise after the end of "sample window", attackers could launch // a new request with the full window length. - manager.set(key, e, time.Duration(resetInSec+expiration)*time.Second) + manager.set(key, e, time.Duration(resetInSec+expiration)*time.Second) //nolint:gosec // Not a concern // Unlock entry mux.Unlock() @@ -127,7 +129,7 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { } // We can continue, update RateLimit headers - c.Set(xRateLimitLimit, max) + c.Set(xRateLimitLimit, strconv.Itoa(maxRequests)) c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) diff --git a/middleware/limiter/limiter_test.go b/middleware/limiter/limiter_test.go index ed4470e9..ac79214f 100644 --- a/middleware/limiter/limiter_test.go +++ b/middleware/limiter/limiter_test.go @@ -14,6 +14,132 @@ import ( "github.com/valyala/fasthttp" ) +// go test -run Test_Limiter_With_Max_Func_With_Zero -race -v +func Test_Limiter_With_Max_Func_With_Zero_And_Limiter_Sliding(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + MaxFunc: func(_ fiber.Ctx) int { return 0 }, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c fiber.Ctx) error { + if c.Params("status") == "fail" { //nolint:goconst // test + 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, 200, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + 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_With_Max_Func_With_Zero -race -v +func Test_Limiter_With_Max_Func_With_Zero(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + MaxFunc: func(_ fiber.Ctx) int { + return 0 + }, + Expiration: 2 * time.Second, + Storage: memory.New(), + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + var wg sync.WaitGroup + + for i := 0; i <= 4; i++ { + wg.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "Hello tester!", string(body)) + }(&wg) + } + + wg.Wait() + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_With_Max_Func -race -v +func Test_Limiter_With_Max_Func(t *testing.T) { + t.Parallel() + app := fiber.New() + maxRequests := 10 + + app.Use(New(Config{ + MaxFunc: func(_ fiber.Ctx) int { + return maxRequests + }, + Expiration: 2 * time.Second, + Storage: memory.New(), + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + var wg sync.WaitGroup + + for i := 0; i <= maxRequests-1; i++ { + wg.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + assert.NoError(t, err) + assert.Equal(t, fiber.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "Hello tester!", string(body)) + }(&wg) + } + + wg.Wait() + + 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(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) +} + // go test -run Test_Limiter_Concurrency_Store -race -v func Test_Limiter_Concurrency_Store(t *testing.T) { t.Parallel() @@ -115,7 +241,7 @@ func Test_Limiter_Fixed_Window_No_Skip_Choices(t *testing.T) { })) app.Get("/:status", func(c fiber.Ctx) error { - if c.Params("status") == "fail" { //nolint:goconst // False positive + if c.Params("status") == "fail" { return c.SendStatus(400) } return c.SendStatus(200) diff --git a/middleware/limiter/manager.go b/middleware/limiter/manager.go index 4225cdd3..ecaee7f1 100644 --- a/middleware/limiter/manager.go +++ b/middleware/limiter/manager.go @@ -8,8 +8,9 @@ import ( "github.com/gofiber/fiber/v3/internal/memory" ) -// go:generate msgp // msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported +// +//go:generate msgp -o=manager_msgp.go -tests=false -unexported type item struct { currHits int prevHits int diff --git a/middleware/limiter/manager_msgp.go b/middleware/limiter/manager_msgp.go index 8437eb81..a0d81ec9 100644 --- a/middleware/limiter/manager_msgp.go +++ b/middleware/limiter/manager_msgp.go @@ -6,6 +6,89 @@ import ( "github.com/tinylib/msgp/msgp" ) +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "currHits": + z.currHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + case "prevHits": + z.prevHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + case "exp": + z.exp, err = dc.ReadUint64() + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "currHits" + err = en.Append(0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.currHits) + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + // write "prevHits" + err = en.Append(0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.prevHits) + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + // write "exp" + err = en.Append(0xa3, 0x65, 0x78, 0x70) + if err != nil { + return + } + err = en.WriteUint64(z.exp) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + return +} + // MarshalMsg implements msgp.Marshaler func (z item) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/middleware/limiter/manager_msgp_test.go b/middleware/limiter/manager_msgp_test.go index 39562df3..122b8a60 100644 --- a/middleware/limiter/manager_msgp_test.go +++ b/middleware/limiter/manager_msgp_test.go @@ -3,12 +3,13 @@ package limiter // Code generated by github.com/tinylib/msgp DO NOT EDIT. import ( + "bytes" "testing" "github.com/tinylib/msgp/msgp" ) -func Test_MarshalUnmarshalitem(t *testing.T) { +func TestMarshalUnmarshalitem(t *testing.T) { v := item{} bts, err := v.MarshalMsg(nil) if err != nil { @@ -31,7 +32,7 @@ func Test_MarshalUnmarshalitem(t *testing.T) { } } -func Benchmark_MarshalMsgitem(b *testing.B) { +func BenchmarkMarshalMsgitem(b *testing.B) { v := item{} b.ReportAllocs() b.ResetTimer() @@ -40,7 +41,7 @@ func Benchmark_MarshalMsgitem(b *testing.B) { } } -func Benchmark_AppendMsgitem(b *testing.B) { +func BenchmarkAppendMsgitem(b *testing.B) { v := item{} bts := make([]byte, 0, v.Msgsize()) bts, _ = v.MarshalMsg(bts[0:0]) @@ -52,7 +53,7 @@ func Benchmark_AppendMsgitem(b *testing.B) { } } -func Benchmark_Unmarshalitem(b *testing.B) { +func BenchmarkUnmarshalitem(b *testing.B) { v := item{} bts, _ := v.MarshalMsg(nil) b.ReportAllocs() @@ -65,3 +66,58 @@ func Benchmark_Unmarshalitem(b *testing.B) { } } } + +func TestEncodeDecodeitem(t *testing.T) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecodeitem Msgsize() is inaccurate") + } + + vn := item{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodeitem(b *testing.B) { + v := item{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/logger/config.go b/middleware/logger/config.go index dcf4d452..4826151e 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -10,6 +10,11 @@ import ( // Config defines the config for middleware. type Config struct { + // Output is a writer where logs are written + // + // Default: os.Stdout + Output io.Writer + // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil @@ -26,6 +31,20 @@ type Config struct { // Optional. Default: map[string]LogFunc CustomTags map[string]LogFunc + // 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 default logger of Fiber. + // + // Optional. Default: defaultLogger + LoggerFunc func(c fiber.Ctx, data *Data, cfg Config) error + + timeZoneLocation *time.Location + // Format defines the logging tags // // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error} @@ -46,31 +65,13 @@ type Config struct { // 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 default logger of Fiber. - // - // 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 + enableColors bool + enableLatency bool } const ( diff --git a/middleware/logger/data.go b/middleware/logger/data.go index 2d5955dc..8191bfea 100644 --- a/middleware/logger/data.go +++ b/middleware/logger/data.go @@ -7,12 +7,12 @@ import ( // Data is a struct to define some variables to use in custom logger function. type Data struct { - Pid string - ErrPaddingStr string - ChainErr error - TemplateChain [][]byte - LogFuncChain []LogFunc Start time.Time Stop time.Time + ChainErr error Timestamp atomic.Value + Pid string + ErrPaddingStr string + TemplateChain [][]byte + LogFuncChain []LogFunc } diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 6d9c6295..e359bd4a 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -11,7 +11,6 @@ import ( "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "github.com/valyala/bytebufferpool" - "github.com/valyala/fasthttp" ) // default logger for fiber @@ -151,7 +150,7 @@ func beforeHandlerFunc(cfg Config) { func appendInt(output Buffer, v int) (int, error) { old := output.Len() - output.Set(fasthttp.AppendUint(output.Bytes(), v)) + output.Set(strconv.AppendInt(output.Bytes(), int64(v), 10)) return output.Len() - old, nil } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index bbbec561..0bc06531 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -256,17 +256,17 @@ func getLatencyTimeUnits() []struct { unit string div time.Duration }{ - {"ms", time.Millisecond}, - {"s", time.Second}, + {unit: "ms", div: time.Millisecond}, + {unit: "s", div: time.Second}, } } return []struct { unit string div time.Duration }{ - {"µs", time.Microsecond}, - {"ms", time.Millisecond}, - {"s", time.Second}, + {unit: "µs", div: time.Microsecond}, + {unit: "ms", div: time.Millisecond}, + {unit: "s", div: time.Second}, } } @@ -407,14 +407,40 @@ func Test_Response_Body(t *testing.T) { require.Equal(t, expectedGetResponse, buf.String()) buf.Reset() // Reset buffer to test POST - _, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) - require.NoError(t, err) expectedPostResponse := "Post in test" + require.NoError(t, err) require.Equal(t, expectedPostResponse, buf.String()) } +// go test -run Test_Request_Body +func Test_Request_Body(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + app := fiber.New() + + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status}", + Output: buf, + })) + + app.Post("/", func(c fiber.Ctx) error { + c.Response().Header.SetContentLength(5) + return c.SendString("World") + }) + + // Create a POST request with a body + body := []byte("Hello") + req := httptest.NewRequest(fiber.MethodPost, "/", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/octet-stream") + + _, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, "5 5 200", buf.String()) +} + // go test -run Test_Logger_AppendUint func Test_Logger_AppendUint(t *testing.T) { t.Parallel() @@ -432,10 +458,21 @@ func Test_Logger_AppendUint(t *testing.T) { return c.SendString("hello") }) + app.Get("/content", func(c fiber.Ctx) error { + c.Response().Header.SetContentLength(5) + return c.SendString("hello") + }) + 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()) + require.Equal(t, "-2 0 200", buf.String()) + + buf.Reset() + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/content", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Equal(t, "-2 5 200", buf.String()) } // go test -run Test_Logger_Data_Race -race @@ -618,7 +655,9 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { 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()) + + // -2 means identity, -1 means chunked, 200 status + require.Equal(t, "-2 -1 200", buf.String()) } type fakeOutput int diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 8d147fd0..8baacfdc 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -87,13 +87,10 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.Write(c.Body()) }, TagBytesReceived: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { - return appendInt(output, len(c.Request().Body())) + return appendInt(output, c.Request().Header.ContentLength()) }, TagBytesSent: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { - if c.Response().Header.ContentLength() < 0 { - return appendInt(output, 0) - } - return appendInt(output, len(c.Response().Body())) + return appendInt(output, c.Response().Header.ContentLength()) }, TagRoute: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { return output.WriteString(c.Route().Path) diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index bd6a58e7..13b01b53 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp/fasthttpadaptor" ) @@ -71,7 +72,7 @@ func New(config ...Config) fiber.Handler { default: // pprof index only works with trailing slash if strings.HasSuffix(path, "/") { - path = strings.TrimRight(path, "/") + path = utils.TrimRight(path, '/') } else { path = prefix + "/" } diff --git a/middleware/pprof/pprof_test.go b/middleware/pprof/pprof_test.go index fd450657..7a279d48 100644 --- a/middleware/pprof/pprof_test.go +++ b/middleware/pprof/pprof_test.go @@ -100,7 +100,6 @@ func Test_Pprof_Subs(t *testing.T) { } for _, sub := range subs { - sub := sub t.Run(sub, func(t *testing.T) { target := "/debug/pprof/" + sub if sub == "profile" { @@ -128,7 +127,6 @@ func Test_Pprof_Subs_WithPrefix(t *testing.T) { } for _, sub := range subs { - sub := sub t.Run(sub, func(t *testing.T) { target := "/federated-fiber/debug/pprof/" + sub if sub == "profile" { diff --git a/middleware/proxy/config.go b/middleware/proxy/config.go index 51ccc8dd..5edee17f 100644 --- a/middleware/proxy/config.go +++ b/middleware/proxy/config.go @@ -15,14 +15,6 @@ type Config struct { // 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 @@ -33,6 +25,22 @@ type Config struct { // Optional. Default: nil ModifyResponse fiber.Handler + // tls config for the http client. + 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, TlsConfig + // and DialDualStack will not be used if the client are set. + Client *fasthttp.LBClient + + // 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 + // Timeout is the request timeout used when calling the proxy client // // Optional. Default: 1 second @@ -47,14 +55,6 @@ type Config struct { // Per-connection buffer size for responses' writing. WriteBufferSize int - // tls config for the http client. - 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, TlsConfig - // and DialDualStack will not be used if the client are set. - Client *fasthttp.LBClient - // Attempt to connect to both ipv4 and ipv6 host addresses if set to true. // // By default client connects only to ipv4 addresses, since unfortunately ipv6 diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go index 9dad5d55..2ac1e2cb 100644 --- a/middleware/proxy/proxy.go +++ b/middleware/proxy/proxy.go @@ -214,10 +214,10 @@ func DomainForward(hostname, addr string, clients ...*fasthttp.Client) fiber.Han } type roundrobin struct { - sync.Mutex + pool []string current int - pool []string + sync.Mutex } // this method will return a string of addr server from list server. diff --git a/middleware/recover/config.go b/middleware/recover/config.go index a857ae5b..1641c528 100644 --- a/middleware/recover/config.go +++ b/middleware/recover/config.go @@ -11,15 +11,15 @@ type Config struct { // 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) + + // EnableStackTrace enables handling stack trace + // + // Optional. Default: false + EnableStackTrace bool } // ConfigDefault is the default config diff --git a/middleware/redirect/config.go b/middleware/redirect/config.go index bebc0c02..ebd9d067 100644 --- a/middleware/redirect/config.go +++ b/middleware/redirect/config.go @@ -21,12 +21,12 @@ type Config struct { // "/users/*/orders/*": "/user/$1/order/$2", Rules map[string]string + rulesRegex map[*regexp.Regexp]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 diff --git a/middleware/requestid/config.go b/middleware/requestid/config.go index 16eddbdc..2e47f3b9 100644 --- a/middleware/requestid/config.go +++ b/middleware/requestid/config.go @@ -12,15 +12,15 @@ type Config struct { // 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 + + // Header is the header key where to get/set the unique request ID + // + // Optional. Default: "X-Request-ID" + Header string } // ConfigDefault is the default config diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go index 4cd1c4d0..314a8214 100644 --- a/middleware/rewrite/rewrite_test.go +++ b/middleware/rewrite/rewrite_test.go @@ -9,6 +9,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" ) func Test_New(t *testing.T) { @@ -170,3 +171,209 @@ func Test_Rewrite(t *testing.T) { require.NoError(t, err) require.Equal(t, fiber.StatusNotFound, resp.StatusCode) } + +func Benchmark_Rewrite(b *testing.B) { + // Helper function to create a new Fiber app with rewrite middleware + createApp := func(config Config) *fiber.App { + app := fiber.New() + app.Use(New(config)) + return app + } + + // Benchmark: Rewrite with Next function always returns true + b.Run("Next always true", func(b *testing.B) { + app := createApp(Config{ + Next: func(fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/old") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + app.Handler()(reqCtx) + } + }) + + // Benchmark: Rewrite with Next function always returns false + b.Run("Next always false", func(b *testing.B) { + app := createApp(Config{ + Next: func(fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/old") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + app.Handler()(reqCtx) + } + }) + + // Benchmark: Rewrite with tokens + b.Run("Rewrite with tokens", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/users/123/orders/456") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + app.Handler()(reqCtx) + } + }) + + // Benchmark: Non-matching request, handled by default route + b.Run("NonMatch with default", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/not-matching-any-rule") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + app.Handler()(reqCtx) + } + }) + + // Benchmark: Non-matching request, with no default route + b.Run("NonMatch without default", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/not-matching-any-rule") + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + app.Handler()(reqCtx) + } + }) +} + +func Benchmark_Rewrite_Parallel(b *testing.B) { + // Helper function to create a new Fiber app with rewrite middleware + createApp := func(config Config) *fiber.App { + app := fiber.New() + app.Use(New(config)) + return app + } + + // Parallel Benchmark: Rewrite with Next function always returns true + b.Run("Next always true", func(b *testing.B) { + app := createApp(Config{ + Next: func(fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + b.RunParallel(func(pb *testing.PB) { + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/old") + for pb.Next() { + app.Handler()(reqCtx) + } + }) + }) + + // Parallel Benchmark: Rewrite with Next function always returns false + b.Run("Next always false", func(b *testing.B) { + app := createApp(Config{ + Next: func(fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + b.RunParallel(func(pb *testing.PB) { + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/old") + for pb.Next() { + app.Handler()(reqCtx) + } + }) + }) + + // Parallel Benchmark: Rewrite with tokens + b.Run("Rewrite with tokens", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + + b.RunParallel(func(pb *testing.PB) { + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/users/123/orders/456") + for pb.Next() { + app.Handler()(reqCtx) + } + }) + }) + + // Parallel Benchmark: Non-matching request, handled by default route + b.Run("NonMatch with default", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + app.Use(func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + b.RunParallel(func(pb *testing.PB) { + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/not-matching-any-rule") + for pb.Next() { + app.Handler()(reqCtx) + } + }) + }) + + // Parallel Benchmark: Non-matching request, with no default route + b.Run("NonMatch without default", func(b *testing.B) { + app := createApp(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + }) + + b.RunParallel(func(pb *testing.PB) { + reqCtx := &fasthttp.RequestCtx{} + reqCtx.Request.SetRequestURI("/not-matching-any-rule") + for pb.Next() { + app.Handler()(reqCtx) + } + }) + }) +} diff --git a/middleware/session/config.go b/middleware/session/config.go index b98eeb25..1eabc05b 100644 --- a/middleware/session/config.go +++ b/middleware/session/config.go @@ -10,14 +10,14 @@ import ( // 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 + // KeyGenerator generates the session key. + // Optional. Default value utils.UUIDv4 + KeyGenerator func() string + // KeyLookup is a string in the form of ":" that is used // to extract session id from the request. // Possible values: "header:", "query:" or "cookie:" @@ -32,6 +32,19 @@ type Config struct { // Optional. Default value "". CookiePath string + // Value of SameSite cookie. + // Optional. Default value "Lax". + CookieSameSite string + + // Source defines where to obtain the session id + source Source + + // The session name + sessionName string + // Allowed session duration + // Optional. Default value 24 * time.Hour + Expiration time.Duration + // Indicates if cookie is secure. // Optional. Default value false. CookieSecure bool @@ -40,24 +53,10 @@ type Config struct { // Optional. Default value false. CookieHTTPOnly bool - // Value of SameSite cookie. - // 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 - - // Source defines where to obtain the session id - source Source - - // The session name - sessionName string } type Source string diff --git a/middleware/session/data.go b/middleware/session/data.go index d80c767e..08cb833f 100644 --- a/middleware/session/data.go +++ b/middleware/session/data.go @@ -4,11 +4,12 @@ import ( "sync" ) -// go:generate msgp -// msgp -file="data.go" -o="data_msgp.go" -tests=false -unexported +// msgp -file="data.go" -o="data_msgp.go" -tests=true -unexported +// +//go:generate msgp -o=data_msgp.go -tests=true -unexported type data struct { - sync.RWMutex - Data map[string]any + Data map[string]any + sync.RWMutex `msg:"-"` } var dataPool = sync.Pool{ diff --git a/middleware/session/data_msgp.go b/middleware/session/data_msgp.go new file mode 100644 index 00000000..a93ffcfb --- /dev/null +++ b/middleware/session/data_msgp.go @@ -0,0 +1,184 @@ +package session + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *data) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "Data": + var zb0002 uint32 + zb0002, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + if z.Data == nil { + z.Data = make(map[string]interface{}, zb0002) + } else if len(z.Data) > 0 { + for key := range z.Data { + delete(z.Data, key) + } + } + for zb0002 > 0 { + zb0002-- + var za0001 string + var za0002 interface{} + za0001, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + za0002, err = dc.ReadIntf() + if err != nil { + err = msgp.WrapError(err, "Data", za0001) + return + } + z.Data[za0001] = za0002 + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *data) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 1 + // write "Data" + err = en.Append(0x81, 0xa4, 0x44, 0x61, 0x74, 0x61) + if err != nil { + return + } + err = en.WriteMapHeader(uint32(len(z.Data))) + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + for za0001, za0002 := range z.Data { + err = en.WriteString(za0001) + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + err = en.WriteIntf(za0002) + if err != nil { + err = msgp.WrapError(err, "Data", za0001) + return + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *data) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 1 + // string "Data" + o = append(o, 0x81, 0xa4, 0x44, 0x61, 0x74, 0x61) + o = msgp.AppendMapHeader(o, uint32(len(z.Data))) + for za0001, za0002 := range z.Data { + o = msgp.AppendString(o, za0001) + o, err = msgp.AppendIntf(o, za0002) + if err != nil { + err = msgp.WrapError(err, "Data", za0001) + return + } + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *data) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "Data": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + if z.Data == nil { + z.Data = make(map[string]interface{}, zb0002) + } else if len(z.Data) > 0 { + for key := range z.Data { + delete(z.Data, key) + } + } + for zb0002 > 0 { + var za0001 string + var za0002 interface{} + zb0002-- + za0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Data") + return + } + za0002, bts, err = msgp.ReadIntfBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Data", za0001) + return + } + z.Data[za0001] = za0002 + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *data) Msgsize() (s int) { + s = 1 + 5 + msgp.MapHeaderSize + if z.Data != nil { + for za0001, za0002 := range z.Data { + _ = za0002 + s += msgp.StringPrefixSize + len(za0001) + msgp.GuessSize(za0002) + } + } + return +} diff --git a/middleware/session/data_msgp_test.go b/middleware/session/data_msgp_test.go new file mode 100644 index 00000000..7532b6af --- /dev/null +++ b/middleware/session/data_msgp_test.go @@ -0,0 +1,123 @@ +package session + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "bytes" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestMarshalUnmarshaldata(t *testing.T) { + v := data{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgdata(b *testing.B) { + v := data{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgdata(b *testing.B) { + v := data{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshaldata(b *testing.B) { + v := data{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecodedata(t *testing.T) { + v := data{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecodedata Msgsize() is inaccurate") + } + + vn := data{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodedata(b *testing.B) { + v := data{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodedata(b *testing.B) { + v := data{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/session/session.go b/middleware/session/session.go index 7272ba18..8a165900 100644 --- a/middleware/session/session.go +++ b/middleware/session/session.go @@ -13,14 +13,14 @@ import ( ) type Session struct { - mu sync.RWMutex // Mutex to protect non-data fields - id string // session id - fresh bool // if new session ctx fiber.Ctx // fiber context config *Store // store configuration data *data // key value data byteBuffer *bytes.Buffer // byte buffer for the en- and decode + id string // session id exp time.Duration // expiration of this session + mu sync.RWMutex // Mutex to protect non-data fields + fresh bool // if new session } var sessionPool = sync.Pool{ diff --git a/middleware/session/store.go b/middleware/session/store.go index 05fba8e2..01b4548c 100644 --- a/middleware/session/store.go +++ b/middleware/session/store.go @@ -35,7 +35,7 @@ func New(config ...Config) *Store { } return &Store{ - cfg, + Config: cfg, } } diff --git a/middleware/static/config.go b/middleware/static/config.go index cc7a9357..7a6489ed 100644 --- a/middleware/static/config.go +++ b/middleware/static/config.go @@ -9,17 +9,44 @@ import ( // Config defines the config for middleware. type Config struct { - // Next defines a function to skip this middleware when returned true. - // - // Optional. Default: nil - Next func(c fiber.Ctx) bool - // FS is the file system to serve the static files from. // You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc. // // Optional. Default: nil FS fs.FS + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c fiber.Ctx) bool + + // ModifyResponse defines a function that allows you to alter the response. + // + // Optional. Default: nil + ModifyResponse fiber.Handler + + // NotFoundHandler defines a function to handle when the path is not found. + // + // Optional. Default: nil + NotFoundHandler fiber.Handler + + // The names of the index files for serving a directory. + // + // Optional. Default: []string{"index.html"}. + IndexNames []string `json:"index"` + + // Expiration duration for inactive file handlers. + // Use a negative time.Duration to disable it. + // + // Optional. Default: 10 * time.Second. + CacheDuration time.Duration `json:"cache_duration"` + + // The value for the Cache-Control HTTP-header + // that is set on the file response. MaxAge is defined in seconds. + // + // Optional. Default: 0. + MaxAge int `json:"max_age"` + // When set to true, the server tries minimizing CPU usage by caching compressed files. // This works differently than the github.com/gofiber/compression middleware. // @@ -40,33 +67,6 @@ type Config struct { // // Optional. Default: false. Download bool `json:"download"` - - // The names of the index files for serving a directory. - // - // Optional. Default: []string{"index.html"}. - IndexNames []string `json:"index"` - - // Expiration duration for inactive file handlers. - // Use a negative time.Duration to disable it. - // - // Optional. Default: 10 * time.Second. - CacheDuration time.Duration `json:"cache_duration"` - - // The value for the Cache-Control HTTP-header - // that is set on the file response. MaxAge is defined in seconds. - // - // Optional. Default: 0. - MaxAge int `json:"max_age"` - - // ModifyResponse defines a function that allows you to alter the response. - // - // Optional. Default: nil - ModifyResponse fiber.Handler - - // NotFoundHandler defines a function to handle when the path is not found. - // - // Optional. Default: nil - NotFoundHandler fiber.Handler } // ConfigDefault is the default config @@ -86,7 +86,7 @@ func configDefault(config ...Config) Config { cfg := config[0] // Set default values - if cfg.IndexNames == nil || len(cfg.IndexNames) == 0 { + if len(cfg.IndexNames) == 0 { cfg.IndexNames = ConfigDefault.IndexNames } diff --git a/middleware/static/static_test.go b/middleware/static/static_test.go index c4cc519d..4d736b0c 100644 --- a/middleware/static/static_test.go +++ b/middleware/static/static_test.go @@ -650,11 +650,11 @@ func Test_isFile(t *testing.T) { t.Parallel() cases := []struct { + filesystem fs.FS + gotError error name string path string - filesystem fs.FS expected bool - gotError error }{ { name: "file", @@ -723,7 +723,7 @@ func Test_isFile(t *testing.T) { func Test_Static_Compress(t *testing.T) { t.Parallel() - dir := "../../.github/testdata/fs" + dir := "../../.github/testdata/fs" //nolint:goconst // test app := fiber.New() app.Get("/*", New(dir, Config{ Compress: true, @@ -733,7 +733,6 @@ func Test_Static_Compress(t *testing.T) { algorithms := []string{"zstd", "gzip", "br"} for _, algo := range algorithms { - algo := algo t.Run(algo+"_compression", func(t *testing.T) { t.Parallel() // request non-compressable file (less than 200 bytes), Content Lengh will remain the same diff --git a/mount.go b/mount.go index 9eab8aca..d97b226d 100644 --- a/mount.go +++ b/mount.go @@ -6,23 +6,24 @@ package fiber import ( "sort" - "strings" "sync" "sync/atomic" + + "github.com/gofiber/utils/v2" ) // Put fields related to mounting. type mountFields struct { // Mounted and main apps appList map[string]*App + // Prefix of app if it was mounted + mountPath string // Ordered keys of apps (sorted by key length for Render) 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 } // Create empty mountFields instance @@ -39,7 +40,7 @@ func newMountFields(app *App) *mountFields { // 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, subApp *App) Router { - prefix = strings.TrimRight(prefix, "/") + prefix = utils.TrimRight(prefix, '/') if prefix == "" { prefix = "/" } @@ -69,7 +70,7 @@ func (app *App) mount(prefix string, subApp *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, "/") + groupPath = utils.TrimRight(groupPath, '/') if groupPath == "" { groupPath = "/" } @@ -187,7 +188,7 @@ func (app *App) processSubAppsRoutes() { // If not, update the route's position and continue route.pos = routePos if !route.use || (route.use && m == 0) { - handlersCount += uint32(len(route.Handlers)) + handlersCount += uint32(len(route.Handlers)) //nolint:gosec // Not a concern } continue } @@ -218,7 +219,7 @@ func (app *App) processSubAppsRoutes() { 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))) + atomic.AddUint32(&app.routesCount, uint32(len(subRoutes))) //nolint:gosec // Not a concern // Mark the parent app's routes as refreshed app.routesRefreshed = true diff --git a/mount_test.go b/mount_test.go index cd021b24..bb57fbbc 100644 --- a/mount_test.go +++ b/mount_test.go @@ -347,7 +347,7 @@ func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { app := New() tsf := func(c Ctx, _ error) error { - return c.Status(200).SendString("hi, i'm a custom sub sub fiber error") + return c.Status(200).SendString("hi, i'm a custom sub fiber error 2") } tripleSubFiber := New(Config{ ErrorHandler: tsf, @@ -394,7 +394,7 @@ func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { b, err = io.ReadAll(resp2.Body) require.NoError(t, err, "iotuil.ReadAll()") - require.Equal(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body") + require.Equal(t, "hi, i'm a custom sub fiber error 2", string(b), "Third fiber Response body") } // go test -run Test_Mount_Route_Names diff --git a/path.go b/path.go index 0b0118a3..e4d32c9b 100644 --- a/path.go +++ b/path.go @@ -28,20 +28,20 @@ type routeParser struct { // routeSegment holds the segment metadata type routeSegment struct { // const information - Const string // constant part of the route - // parameter information - IsParam bool // Truth value that indicates whether it is a parameter or a constant part - ParamName string // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added - ComparePart string // search part to find the end of the parameter - PartCount int // how often is the search part contained in the non-param segments? -> necessary for greedy search - IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus - IsOptional bool // indicates whether the parameter is optional or not - // common information - IsLast bool // shows if the segment is the last one for the route - HasOptionalSlash bool // segment has the possibility of an optional slash - Constraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default - Length int // length of the parameter for segment, when its 0 then the length is undetermined + Const string // constant part of the route + ParamName string // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added + ComparePart string // search part to find the end of the parameter + Constraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default + PartCount int // how often is the search part contained in the non-param segments? -> necessary for greedy search + Length int // length of the parameter for segment, when its 0 then the length is undetermined // future TODO: add support for optional groups "/abc(/def)?" + // parameter information + IsParam bool // Truth value that indicates whether it is a parameter or a constant part + IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus + IsOptional bool // indicates whether the parameter is optional or not + // common information + IsLast bool // shows if the segment is the last one for the route + HasOptionalSlash bool // segment has the possibility of an optional slash } // different special routing signs @@ -51,7 +51,7 @@ const ( optionalParam byte = '?' // concludes a parameter by name and makes it optional paramStarterChar byte = ':' // start character for a parameter with name slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional - slashDelimiterStr = "/" // separator for the route, unlike the other delimiters this character at the end can be optional + slashDelimiterStr byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional escapeChar byte = '\\' // escape character paramConstraintStart byte = '<' // start of type constraint for a parameter paramConstraintEnd byte = '>' // end of type constraint for a parameter @@ -65,11 +65,11 @@ const ( type TypeConstraint int16 type Constraint struct { - ID TypeConstraint RegexCompiler *regexp.Regexp - Data []string Name string + Data []string customConstraints []CustomConstraint + ID TypeConstraint } // CustomConstraint is an interface for custom constraints @@ -161,7 +161,7 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { } // Strict routing, remove trailing slashes if !config.StrictRouting && len(patternPretty) > 1 { - patternPretty = strings.TrimRight(patternPretty, "/") + patternPretty = utils.TrimRight(patternPretty, '/') } parser := parseRoute(patternPretty) @@ -233,7 +233,7 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { } else { comparePart = segs[i].Const if len(comparePart) > 1 { - comparePart = strings.TrimRight(comparePart, slashDelimiterStr) + comparePart = utils.TrimRight(comparePart, slashDelimiterStr) } } } diff --git a/path_test.go b/path_test.go index 071f7e9e..14eda46c 100644 --- a/path_test.go +++ b/path_test.go @@ -5,7 +5,6 @@ package fiber import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -142,9 +141,9 @@ func Test_Path_matchParams(t *testing.T) { 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'", testCollection.pattern, c.url)) + require.Equal(t, c.match, match, "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'", testCollection.pattern, c.url)) + require.Equal(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], "route: '%s', url: '%s'", testCollection.pattern, c.url) } } } @@ -163,7 +162,7 @@ func Test_RoutePatternMatch(t *testing.T) { continue } match := RoutePatternMatch(c.url, pattern) - require.Equal(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", pattern, c.url)) + require.Equal(t, c.match, match, "route: '%s', url: '%s'", pattern, c.url) } } for _, testCase := range routeTestCases { @@ -224,9 +223,9 @@ func Benchmark_Path_matchParams(t *testing.B) { matchRes = true } } - require.Equal(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + require.Equal(t, c.match, matchRes, "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'", testCollection.pattern, c.url)) + require.Equal(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], "route: '%s', url: '%s'", testCollection.pattern, c.url) } }) } @@ -257,7 +256,7 @@ func Benchmark_RoutePatternMatch(t *testing.B) { matchRes = true } } - require.Equal(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + require.Equal(t, c.match, matchRes, "route: '%s', url: '%s'", testCollection.pattern, c.url) }) } } diff --git a/path_testcases_test.go b/path_testcases_test.go index 26ec5b74..d4c8dc8d 100644 --- a/path_testcases_test.go +++ b/path_testcases_test.go @@ -10,8 +10,8 @@ import ( type routeTestCase struct { url string - match bool params []string + match bool partialCheck bool } diff --git a/prefork.go b/prefork.go index 966b8bc1..745ed306 100644 --- a/prefork.go +++ b/prefork.go @@ -72,13 +72,13 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) er // 👮 master process 👮 type child struct { - pid int err error + pid int } // create variables - max := runtime.GOMAXPROCS(0) + maxProcs := runtime.GOMAXPROCS(0) childs := make(map[int]*exec.Cmd) - channel := make(chan child, max) + channel := make(chan child, maxProcs) // kill child procs when master exits defer func() { @@ -95,7 +95,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) er var pids []string // launch child procs - for i := 0; i < max; i++ { + for i := 0; i < maxProcs; i++ { 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, @@ -131,7 +131,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg ListenConfig) er // notify master if child crashes go func() { - channel <- child{pid, cmd.Wait()} + channel <- child{pid: pid, err: cmd.Wait()} }() } diff --git a/prefork_test.go b/prefork_test.go index 63cd635a..1cfdcdb0 100644 --- a/prefork_test.go +++ b/prefork_test.go @@ -95,11 +95,11 @@ func Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) { func setupIsChild(t *testing.T) { t.Helper() - require.NoError(t, os.Setenv(envPreforkChildKey, envPreforkChildVal)) + require.NoError(t, os.Setenv(envPreforkChildKey, envPreforkChildVal)) //nolint:tenv // Ignore error } func teardownIsChild(t *testing.T) { t.Helper() - require.NoError(t, os.Setenv(envPreforkChildKey, "")) + require.NoError(t, os.Setenv(envPreforkChildKey, "")) //nolint:tenv // Ignore error } diff --git a/redirect.go b/redirect.go index 741a2e12..ebbcb499 100644 --- a/redirect.go +++ b/redirect.go @@ -6,7 +6,6 @@ package fiber import ( "errors" - "strings" "sync" "github.com/gofiber/fiber/v3/binder" @@ -19,7 +18,7 @@ var redirectPool = sync.Pool{ New: func() any { return &Redirect{ status: StatusFound, - oldInput: make(map[string]string, 0), + messages: make(redirectionMsgs, 0), } }, } @@ -32,13 +31,37 @@ const ( CookieDataAssigner = ":" ) +// redirectionMsgs is a struct that used to store flash messages and old input data in cookie using MSGP. +// msgp -file="redirect.go" -o="redirect_msgp.go" -unexported +// +//msgp:ignore Redirect RedirectConfig OldInputData FlashMessage +type redirectionMsg struct { + key string + value string + level uint8 + isOldInput bool +} + +type redirectionMsgs []redirectionMsg + +// OldInputData is a struct that holds the old input data. +type OldInputData struct { + Key string + Value string +} + +// FlashMessage is a struct that holds the flash message data. +type FlashMessage struct { + Key string + Value string + Level uint8 +} + // Redirect is a struct that holds the redirect data. type Redirect struct { - c *DefaultCtx // Embed ctx - status int // Status code of redirection. Default: StatusFound - - messages []string // Flash messages - oldInput map[string]string // Old input data + c *DefaultCtx // Embed ctx + messages redirectionMsgs // Flash messages and old input data + status int // Status code of redirection. Default: StatusFound } // RedirectConfig A config to use with Redirect().Route() @@ -71,10 +94,6 @@ func ReleaseRedirect(r *Redirect) { func (r *Redirect) release() { r.status = 302 r.messages = r.messages[:0] - // reset map - for k := range r.oldInput { - delete(r.oldInput, k) - } r.c = nil } @@ -90,8 +109,28 @@ 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, value string) *Redirect { - r.messages = append(r.messages, key+CookieDataAssigner+value) +func (r *Redirect) With(key, value string, level ...uint8) *Redirect { + // Get level + var msgLevel uint8 + if len(level) > 0 { + msgLevel = level[0] + } + + // Override old message if exists + for i, msg := range r.messages { + if msg.key == key && !msg.isOldInput { + r.messages[i].value = value + r.messages[i].level = msgLevel + + return r + } + } + + r.messages = append(r.messages, redirectionMsg{ + key: key, + value: value, + level: msgLevel, + }) return r } @@ -105,28 +144,39 @@ func (r *Redirect) WithInput() *Redirect { ctype := utils.ToLower(utils.UnsafeString(r.c.Context().Request.Header.ContentType())) ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) + oldInput := make(map[string]string) switch ctype { case MIMEApplicationForm: - _ = r.c.Bind().Form(r.oldInput) //nolint:errcheck // not needed + _ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed case MIMEMultipartForm: - _ = r.c.Bind().MultipartForm(r.oldInput) //nolint:errcheck // not needed + _ = r.c.Bind().MultipartForm(oldInput) //nolint:errcheck // not needed default: - _ = r.c.Bind().Query(r.oldInput) //nolint:errcheck // not needed + _ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed + } + + // Add old input data + for k, v := range oldInput { + r.messages = append(r.messages, redirectionMsg{ + key: k, + value: v, + isOldInput: true, + }) } return r } // Messages Get flash messages. -func (r *Redirect) Messages() map[string]string { - msgs := r.c.redirectionMessages - flashMessages := make(map[string]string, len(msgs)) +func (r *Redirect) Messages() []FlashMessage { + flashMessages := make([]FlashMessage, 0) - for _, msg := range msgs { - k, v := parseMessage(msg) - - if !strings.HasPrefix(k, OldInputDataPrefix) { - flashMessages[k] = v + for _, msg := range r.c.flashMessages { + if !msg.isOldInput { + flashMessages = append(flashMessages, FlashMessage{ + Key: msg.key, + Value: msg.value, + Level: msg.level, + }) } } @@ -134,47 +184,52 @@ func (r *Redirect) Messages() map[string]string { } // Message Get flash message by key. -func (r *Redirect) Message(key string) string { - msgs := r.c.redirectionMessages +func (r *Redirect) Message(key string) FlashMessage { + msgs := r.c.flashMessages for _, msg := range msgs { - k, v := parseMessage(msg) - - if !strings.HasPrefix(k, OldInputDataPrefix) && k == key { - return v + if msg.key == key && !msg.isOldInput { + return FlashMessage{ + Key: msg.key, + Value: msg.value, + Level: msg.level, + } } } - return "" + + return FlashMessage{} } // OldInputs Get old input data. -func (r *Redirect) OldInputs() map[string]string { - msgs := r.c.redirectionMessages - oldInputs := make(map[string]string, len(msgs)) +func (r *Redirect) OldInputs() []OldInputData { + inputs := make([]OldInputData, 0) - for _, msg := range msgs { - k, v := parseMessage(msg) - - if strings.HasPrefix(k, OldInputDataPrefix) { - // remove "old_input_data_" part from key - oldInputs[k[len(OldInputDataPrefix):]] = v + for _, msg := range r.c.flashMessages { + if msg.isOldInput { + inputs = append(inputs, OldInputData{ + Key: msg.key, + Value: msg.value, + }) } } - return oldInputs + + return inputs } // OldInput Get old input data by key. -func (r *Redirect) OldInput(key string) string { - msgs := r.c.redirectionMessages +func (r *Redirect) OldInput(key string) OldInputData { + msgs := r.c.flashMessages for _, msg := range msgs { - k, v := parseMessage(msg) - - if strings.HasPrefix(k, OldInputDataPrefix) && k[len(OldInputDataPrefix):] == key { - return v + if msg.key == key && msg.isOldInput { + return OldInputData{ + Key: msg.key, + Value: msg.value, + } } } - return "" + + return OldInputData{} } // To redirect to the URL derived from the specified path, with specified status. @@ -240,66 +295,32 @@ func (r *Redirect) Back(fallback ...string) error { return r.To(location) } -// parseAndClearFlashMessages is a method to get flash messages before removing them +// parseAndClearFlashMessages is a method to get flash messages before they are getting removed func (r *Redirect) parseAndClearFlashMessages() { // parse flash messages cookieValue := r.c.Cookies(FlashCookieName) - var commaPos int - for { - commaPos = findNextNonEscapedCharsetPosition(cookieValue, []byte(CookieDataSeparator)) - if commaPos == -1 { - r.c.redirectionMessages = append(r.c.redirectionMessages, strings.Trim(cookieValue, " ")) - break - } - r.c.redirectionMessages = append(r.c.redirectionMessages, strings.Trim(cookieValue[:commaPos], " ")) - cookieValue = cookieValue[commaPos+1:] + _, err := r.c.flashMessages.UnmarshalMsg(r.c.app.getBytes(cookieValue)) + if err != nil { + return } - - r.c.ClearCookie(FlashCookieName) } // processFlashMessages is a helper function to process flash messages and old input data // and set them as cookies func (r *Redirect) processFlashMessages() { - // Flash messages - if len(r.messages) > 0 || len(r.oldInput) > 0 { - messageText := bytebufferpool.Get() - defer bytebufferpool.Put(messageText) - - // flash messages - for i, message := range r.messages { - messageText.WriteString(message) - // 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) - } - } - r.messages = r.messages[:0] - - // old input data - i := 1 - for k, v := range r.oldInput { - messageText.WriteString(OldInputDataPrefix + k + CookieDataAssigner + v) - if len(r.oldInput) != i { - messageText.WriteString(CookieDataSeparator) - } - i++ - } - - r.c.Cookie(&Cookie{ - Name: FlashCookieName, - Value: r.c.app.getString(messageText.Bytes()), - SessionOnly: true, - }) - } -} - -// parseMessage is a helper function to parse flash messages and old input data -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:]) + if len(r.messages) == 0 { + return } - return RemoveEscapeChar(raw), "" + val, err := r.messages.MarshalMsg(nil) + if err != nil { + return + } + + r.c.Cookie(&Cookie{ + Name: FlashCookieName, + Value: r.c.app.getString(val), + SessionOnly: true, + }) } diff --git a/redirect_msgp.go b/redirect_msgp.go new file mode 100644 index 00000000..da11533d --- /dev/null +++ b/redirect_msgp.go @@ -0,0 +1,272 @@ +package fiber + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *redirectionMsg) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "key": + z.key, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "key") + return + } + case "value": + z.value, err = dc.ReadString() + if err != nil { + err = msgp.WrapError(err, "value") + return + } + case "level": + z.level, err = dc.ReadUint8() + if err != nil { + err = msgp.WrapError(err, "level") + return + } + case "isOldInput": + z.isOldInput, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "isOldInput") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *redirectionMsg) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 4 + // write "key" + err = en.Append(0x84, 0xa3, 0x6b, 0x65, 0x79) + if err != nil { + return + } + err = en.WriteString(z.key) + if err != nil { + err = msgp.WrapError(err, "key") + return + } + // write "value" + err = en.Append(0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65) + if err != nil { + return + } + err = en.WriteString(z.value) + if err != nil { + err = msgp.WrapError(err, "value") + return + } + // write "level" + err = en.Append(0xa5, 0x6c, 0x65, 0x76, 0x65, 0x6c) + if err != nil { + return + } + err = en.WriteUint8(z.level) + if err != nil { + err = msgp.WrapError(err, "level") + return + } + // write "isOldInput" + err = en.Append(0xaa, 0x69, 0x73, 0x4f, 0x6c, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74) + if err != nil { + return + } + err = en.WriteBool(z.isOldInput) + if err != nil { + err = msgp.WrapError(err, "isOldInput") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *redirectionMsg) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 4 + // string "key" + o = append(o, 0x84, 0xa3, 0x6b, 0x65, 0x79) + o = msgp.AppendString(o, z.key) + // string "value" + o = append(o, 0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65) + o = msgp.AppendString(o, z.value) + // string "level" + o = append(o, 0xa5, 0x6c, 0x65, 0x76, 0x65, 0x6c) + o = msgp.AppendUint8(o, z.level) + // string "isOldInput" + o = append(o, 0xaa, 0x69, 0x73, 0x4f, 0x6c, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74) + o = msgp.AppendBool(o, z.isOldInput) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *redirectionMsg) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "key": + z.key, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "key") + return + } + case "value": + z.value, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "value") + return + } + case "level": + z.level, bts, err = msgp.ReadUint8Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "level") + return + } + case "isOldInput": + z.isOldInput, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "isOldInput") + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *redirectionMsg) Msgsize() (s int) { + s = 1 + 4 + msgp.StringPrefixSize + len(z.key) + 6 + msgp.StringPrefixSize + len(z.value) + 6 + msgp.Uint8Size + 11 + msgp.BoolSize + return +} + +// DecodeMsg implements msgp.Decodable +func (z *redirectionMsgs) DecodeMsg(dc *msgp.Reader) (err error) { + var zb0002 uint32 + zb0002, err = dc.ReadArrayHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + if cap((*z)) >= int(zb0002) { + (*z) = (*z)[:zb0002] + } else { + (*z) = make(redirectionMsgs, zb0002) + } + for zb0001 := range *z { + err = (*z)[zb0001].DecodeMsg(dc) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z redirectionMsgs) EncodeMsg(en *msgp.Writer) (err error) { + err = en.WriteArrayHeader(uint32(len(z))) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0003 := range z { + err = z[zb0003].EncodeMsg(en) + if err != nil { + err = msgp.WrapError(err, zb0003) + return + } + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z redirectionMsgs) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendArrayHeader(o, uint32(len(z))) + for zb0003 := range z { + o, err = z[zb0003].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, zb0003) + return + } + } + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *redirectionMsgs) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0002 uint32 + zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if cap((*z)) >= int(zb0002) { + (*z) = (*z)[:zb0002] + } else { + (*z) = make(redirectionMsgs, zb0002) + } + for zb0001 := range *z { + bts, err = (*z)[zb0001].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z redirectionMsgs) Msgsize() (s int) { + s = msgp.ArrayHeaderSize + for zb0003 := range z { + s += z[zb0003].Msgsize() + } + return +} diff --git a/redirect_msgp_test.go b/redirect_msgp_test.go new file mode 100644 index 00000000..c03d6ffc --- /dev/null +++ b/redirect_msgp_test.go @@ -0,0 +1,236 @@ +package fiber + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "bytes" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestMarshalUnmarshalredirectionMsg(t *testing.T) { + v := redirectionMsg{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgredirectionMsg(b *testing.B) { + v := redirectionMsg{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgredirectionMsg(b *testing.B) { + v := redirectionMsg{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalredirectionMsg(b *testing.B) { + v := redirectionMsg{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecoderedirectionMsg(t *testing.T) { + v := redirectionMsg{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecoderedirectionMsg Msgsize() is inaccurate") + } + + vn := redirectionMsg{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncoderedirectionMsg(b *testing.B) { + v := redirectionMsg{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecoderedirectionMsg(b *testing.B) { + v := redirectionMsg{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalredirectionMsgs(t *testing.T) { + v := redirectionMsgs{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgredirectionMsgs(b *testing.B) { + v := redirectionMsgs{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgredirectionMsgs(b *testing.B) { + v := redirectionMsgs{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalredirectionMsgs(b *testing.B) { + v := redirectionMsgs{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecoderedirectionMsgs(t *testing.T) { + v := redirectionMsgs{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Log("WARNING: TestEncodeDecoderedirectionMsgs Msgsize() is inaccurate") + } + + vn := redirectionMsgs{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncoderedirectionMsgs(b *testing.B) { + v := redirectionMsgs{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecoderedirectionMsgs(b *testing.B) { + v := redirectionMsgs{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/redirect_test.go b/redirect_test.go index 70f583ea..7544aec0 100644 --- a/redirect_test.go +++ b/redirect_test.go @@ -5,16 +5,13 @@ package fiber import ( - "context" - "net" + "bytes" + "mime/multipart" "net/url" "testing" - "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/fasthttputil" ) // go test -run Test_Redirect_To @@ -40,16 +37,20 @@ func Test_Redirect_To_WithFlashMessages(t *testing.T) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - err := c.Redirect().With("success", "1").With("message", "test").To("http://example.com") + err := c.Redirect().With("success", "2").With("success", "1").With("message", "test", 2).To("http://example.com") require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) - equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" - require.True(t, equal) + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing - c.Redirect().parseAndClearFlashMessages() - require.Equal(t, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 2) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 2, isOldInput: false}) } // go test -run Test_Redirect_Route_WithParams @@ -88,6 +89,7 @@ func Test_Redirect_Route_WithParams_WithQueries(t *testing.T) { }) 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))) require.NoError(t, err, "url.Parse(location)") @@ -183,11 +185,15 @@ func Test_Redirect_Back_WithFlashMessages(t *testing.T) { require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/", string(c.Response().Header.Peek(HeaderLocation))) - equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" - require.True(t, equal) + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing - c.Redirect().parseAndClearFlashMessages() - require.Equal(t, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 2) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) } // go test -run Test_Redirect_Back_WithReferer @@ -222,43 +228,143 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed err := c.Redirect().With("success", "1").With("message", "test").Route("user") + + require.Contains(t, c.redirect.messages, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.NoError(t, err) require.Equal(t, 302, c.Response().StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) - equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" - require.True(t, equal) + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing - c.Redirect().parseAndClearFlashMessages() - require.Equal(t, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 2) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) } // go test -run Test_Redirect_Route_WithOldInput func Test_Redirect_Route_WithOldInput(t *testing.T) { t.Parallel() - app := New() - app.Get("/user", func(c Ctx) error { - return c.SendString("user") - }).Name("user") + t.Run("Query", func(t *testing.T) { + t.Parallel() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + app := New() + app.Get("/user", func(c Ctx) error { + return c.SendString("user") + }).Name("user") - c.Request().URI().SetQueryString("id=1&name=tom") - 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))) + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed - require.Contains(t, c.GetRespHeader(HeaderSetCookie), "fiber_flash=") - require.Contains(t, c.GetRespHeader(HeaderSetCookie), "success:1") - require.Contains(t, c.GetRespHeader(HeaderSetCookie), "message:test") + c.Request().URI().SetQueryString("id=1&name=tom") + err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") - require.Contains(t, c.GetRespHeader(HeaderSetCookie), ",old_input_data_id:1") - require.Contains(t, c.GetRespHeader(HeaderSetCookie), ",old_input_data_name:tom") + require.Contains(t, c.redirect.messages, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "id", value: "1", isOldInput: true}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "name", value: "tom", isOldInput: true}) - c.Redirect().parseAndClearFlashMessages() - require.Equal(t, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + require.NoError(t, err) + require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) + + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 4) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "id", value: "1", level: 0, isOldInput: true}) + require.Contains(t, msgs, redirectionMsg{key: "name", value: "tom", level: 0, isOldInput: true}) + }) + + t.Run("Form", func(t *testing.T) { + t.Parallel() + + app := New() + app.Post("/user", func(c Ctx) error { + return c.SendString("user") + }).Name("user") + + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + c.Request().Header.Set(HeaderContentType, MIMEApplicationForm) + c.Request().SetBodyString("id=1&name=tom") + err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") + + require.Contains(t, c.redirect.messages, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "id", value: "1", isOldInput: true}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "name", value: "tom", isOldInput: true}) + + require.NoError(t, err) + require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) + + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 4) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "id", value: "1", level: 0, isOldInput: true}) + require.Contains(t, msgs, redirectionMsg{key: "name", value: "tom", level: 0, isOldInput: true}) + }) + + t.Run("MultipartForm", func(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/user", func(c Ctx) error { + return c.SendString("user") + }).Name("user") + + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + require.NoError(t, writer.WriteField("id", "1")) + require.NoError(t, writer.WriteField("name", "tom")) + require.NoError(t, writer.Close()) + + c.Request().SetBody(body.Bytes()) + c.Request().Header.Set(HeaderContentType, writer.FormDataContentType()) + + err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") + + require.Contains(t, c.redirect.messages, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "id", value: "1", isOldInput: true}) + require.Contains(t, c.redirect.messages, redirectionMsg{key: "name", value: "tom", isOldInput: true}) + + require.NoError(t, err) + require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) + + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(t, err) + + require.Len(t, msgs, 4) + require.Contains(t, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) + require.Contains(t, msgs, redirectionMsg{key: "id", value: "1", level: 0, isOldInput: true}) + require.Contains(t, msgs, redirectionMsg{key: "name", value: "tom", level: 0, isOldInput: true}) + }) } // go test -run Test_Redirect_parseAndClearFlashMessages @@ -272,105 +378,83 @@ func Test_Redirect_parseAndClearFlashMessages(t *testing.T) { c := app.AcquireCtx(&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") + msgs := redirectionMsgs{ + { + key: "success", + value: "1", + }, + { + key: "message", + value: "test", + }, + { + key: "name", + value: "tom", + isOldInput: true, + }, + { + key: "id", + value: "1", + isOldInput: true, + }, + } + + val, err := msgs.MarshalMsg(nil) + require.NoError(t, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) c.Redirect().parseAndClearFlashMessages() - require.Equal(t, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + require.Equal(t, FlashMessage{ + Key: "success", + Value: "1", + Level: 0, + }, c.Redirect().Message("success")) - require.Equal(t, "1", c.Redirect().Message("success")) - require.Equal(t, "test", c.Redirect().Message("message")) - require.Equal(t, map[string]string{"success": "1", "message": "test"}, c.Redirect().Messages()) + require.Equal(t, FlashMessage{ + Key: "message", + Value: "test", + Level: 0, + }, c.Redirect().Message("message")) - require.Equal(t, "1", c.Redirect().OldInput("id")) - require.Equal(t, "tom", c.Redirect().OldInput("name")) - require.Equal(t, map[string]string{"id": "1", "name": "tom"}, c.Redirect().OldInputs()) -} + require.Equal(t, FlashMessage{}, c.Redirect().Message("not_message")) -// go test -run Test_Redirect_Request -func Test_Redirect_Request(t *testing.T) { - t.Parallel() - app := New() - - app.Get("/", func(c Ctx) error { - return c.Redirect().With("key", "value").With("key2", "value2").With("co\\:m\\,ma", "Fi\\:ber\\, v3").Route("name") - }) - - app.Get("/with-inputs", func(c Ctx) error { - return c.Redirect().WithInput().With("key", "value").With("key2", "value2").Route("name") - }) - - app.Get("/just-inputs", func(c Ctx) error { - return c.Redirect().WithInput().Route("name") - }) - - app.Get("/redirected", func(c Ctx) error { - return c.JSON(Map{ - "messages": c.Redirect().Messages(), - "inputs": c.Redirect().OldInputs(), - }) - }).Name("name") - - // Start test server - ln := fasthttputil.NewInmemoryListener() - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) - defer cancel() - - err := app.Listener(ln, ListenConfig{ - DisableStartupMessage: true, - GracefulContext: ctx, - }) - - assert.NoError(t, err) - }() - - // Test cases - testCases := []struct { - URL string - CookieValue string - ExpectedBody string - ExpectedStatusCode int - ExpectedErr error - }{ + require.Equal(t, []FlashMessage{ { - URL: "/", - CookieValue: "key:value,key2:value2,co\\:m\\,ma:Fi\\:ber\\, v3", - ExpectedBody: `{"inputs":{},"messages":{"co:m,ma":"Fi:ber, v3","key":"value","key2":"value2"}}`, - ExpectedStatusCode: StatusOK, - ExpectedErr: nil, + Key: "success", + Value: "1", + Level: 0, }, { - URL: "/with-inputs?name=john&surname=doe", - CookieValue: "key:value,key2:value2,key:value,key2:value2,old_input_data_name:john,old_input_data_surname:doe", - ExpectedBody: `{"inputs":{"name":"john","surname":"doe"},"messages":{"key":"value","key2":"value2"}}`, - ExpectedStatusCode: StatusOK, - ExpectedErr: nil, + Key: "message", + Value: "test", + Level: 0, + }, + }, c.Redirect().Messages()) + + require.Equal(t, OldInputData{ + Key: "id", + Value: "1", + }, c.Redirect().OldInput("id")) + + require.Equal(t, OldInputData{ + Key: "name", + Value: "tom", + }, c.Redirect().OldInput("name")) + + require.Equal(t, OldInputData{}, c.Redirect().OldInput("not_name")) + + require.Equal(t, []OldInputData{ + { + Key: "name", + Value: "tom", }, { - URL: "/just-inputs?name=john&surname=doe", - CookieValue: "old_input_data_name:john,old_input_data_surname:doe", - ExpectedBody: `{"inputs":{"name":"john","surname":"doe"},"messages":{}}`, - ExpectedStatusCode: StatusOK, - ExpectedErr: nil, + Key: "id", + Value: "1", }, - } - - for _, tc := range testCases { - client := &fasthttp.HostClient{ - Dial: func(_ string) (net.Conn, error) { - return ln.Dial() - }, - } - req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse() - req.SetRequestURI("http://example.com" + tc.URL) - req.Header.SetCookie(FlashCookieName, tc.CookieValue) - err := client.DoRedirects(req, resp, 1) - - require.NoError(t, err) - require.Equal(t, tc.ExpectedBody, string(resp.Body())) - require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) - } + }, c.Redirect().OldInputs()) } // go test -v -run=^$ -bench=Benchmark_Redirect_Route -benchmem -count=4 @@ -454,11 +538,35 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) { require.Equal(b, 302, c.Response().StatusCode()) require.Equal(b, "/user", string(c.Response().Header.Peek(HeaderLocation))) - equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" - require.True(b, equal) + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing - c.Redirect().parseAndClearFlashMessages() - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + var msgs redirectionMsgs + _, err = msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(b, err) + + require.Contains(b, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(b, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) +} + +var testredirectionMsgs = redirectionMsgs{ + { + key: "success", + value: "1", + }, + { + key: "message", + value: "test", + }, + { + key: "name", + value: "tom", + isOldInput: true, + }, + { + key: "id", + value: "1", + isOldInput: true, + }, } // go test -v -run=^$ -bench=Benchmark_Redirect_parseAndClearFlashMessages -benchmem -count=4 @@ -470,7 +578,10 @@ func Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) { c := app.AcquireCtx(&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") + val, err := testredirectionMsgs.MarshalMsg(nil) + require.NoError(b, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) b.ReportAllocs() b.ResetTimer() @@ -479,15 +590,25 @@ func Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) { c.Redirect().parseAndClearFlashMessages() } - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) + require.Equal(b, FlashMessage{ + Key: "success", + Value: "1", + }, c.Redirect().Message("success")) - require.Equal(b, "1", c.Redirect().Message("success")) - require.Equal(b, "test", c.Redirect().Message("message")) - require.Equal(b, map[string]string{"success": "1", "message": "test"}, c.Redirect().Messages()) + require.Equal(b, FlashMessage{ + Key: "message", + Value: "test", + }, c.Redirect().Message("message")) - require.Equal(b, "1", c.Redirect().OldInput("id")) - require.Equal(b, "tom", c.Redirect().OldInput("name")) - require.Equal(b, map[string]string{"id": "1", "name": "tom"}, c.Redirect().OldInputs()) + require.Equal(b, OldInputData{ + Key: "id", + Value: "1", + }, c.Redirect().OldInput("id")) + + require.Equal(b, OldInputData{ + Key: "name", + Value: "tom", + }, c.Redirect().OldInput("name")) } // go test -v -run=^$ -bench=Benchmark_Redirect_processFlashMessages -benchmem -count=4 @@ -508,7 +629,15 @@ func Benchmark_Redirect_processFlashMessages(b *testing.B) { c.Redirect().processFlashMessages() } - require.Equal(b, "fiber_flash=success:1,message:test; path=/; SameSite=Lax", c.GetRespHeader(HeaderSetCookie)) + c.Context().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing + + var msgs redirectionMsgs + _, err := msgs.UnmarshalMsg([]byte(c.Cookies(FlashCookieName))) + require.NoError(b, err) + + require.Len(b, msgs, 2) + require.Contains(b, msgs, redirectionMsg{key: "success", value: "1", level: 0, isOldInput: false}) + require.Contains(b, msgs, redirectionMsg{key: "message", value: "test", level: 0, isOldInput: false}) } // go test -v -run=^$ -bench=Benchmark_Redirect_Messages -benchmem -count=4 @@ -520,10 +649,13 @@ func Benchmark_Redirect_Messages(b *testing.B) { c := app.AcquireCtx(&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") + val, err := testredirectionMsgs.MarshalMsg(nil) + require.NoError(b, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) c.Redirect().parseAndClearFlashMessages() - var msgs map[string]string + var msgs []FlashMessage b.ReportAllocs() b.ResetTimer() @@ -532,8 +664,17 @@ func Benchmark_Redirect_Messages(b *testing.B) { msgs = c.Redirect().Messages() } - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) - require.Equal(b, map[string]string{"success": "1", "message": "test"}, msgs) + require.Contains(b, msgs, FlashMessage{ + Key: "success", + Value: "1", + Level: 0, + }) + + require.Contains(b, msgs, FlashMessage{ + Key: "message", + Value: "test", + Level: 0, + }) } // go test -v -run=^$ -bench=Benchmark_Redirect_OldInputs -benchmem -count=4 @@ -545,10 +686,13 @@ func Benchmark_Redirect_OldInputs(b *testing.B) { c := app.AcquireCtx(&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") + val, err := testredirectionMsgs.MarshalMsg(nil) + require.NoError(b, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) c.Redirect().parseAndClearFlashMessages() - var oldInputs map[string]string + var oldInputs []OldInputData b.ReportAllocs() b.ResetTimer() @@ -557,8 +701,15 @@ func Benchmark_Redirect_OldInputs(b *testing.B) { oldInputs = c.Redirect().OldInputs() } - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) - require.Equal(b, map[string]string{"id": "1", "name": "tom"}, oldInputs) + require.Contains(b, oldInputs, OldInputData{ + Key: "name", + Value: "tom", + }) + + require.Contains(b, oldInputs, OldInputData{ + Key: "id", + Value: "1", + }) } // go test -v -run=^$ -bench=Benchmark_Redirect_Message -benchmem -count=4 @@ -570,10 +721,13 @@ func Benchmark_Redirect_Message(b *testing.B) { c := app.AcquireCtx(&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") + val, err := testredirectionMsgs.MarshalMsg(nil) + require.NoError(b, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) c.Redirect().parseAndClearFlashMessages() - var msg string + var msg FlashMessage b.ReportAllocs() b.ResetTimer() @@ -582,8 +736,11 @@ func Benchmark_Redirect_Message(b *testing.B) { msg = c.Redirect().Message("message") } - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) - require.Equal(b, "test", msg) + require.Equal(b, FlashMessage{ + Key: "message", + Value: "test", + Level: 0, + }, msg) } // go test -v -run=^$ -bench=Benchmark_Redirect_OldInput -benchmem -count=4 @@ -595,10 +752,13 @@ func Benchmark_Redirect_OldInput(b *testing.B) { c := app.AcquireCtx(&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") + val, err := testredirectionMsgs.MarshalMsg(nil) + require.NoError(b, err) + + c.Request().Header.Set(HeaderCookie, "fiber_flash="+string(val)) c.Redirect().parseAndClearFlashMessages() - var input string + var input OldInputData b.ReportAllocs() b.ResetTimer() @@ -607,6 +767,8 @@ func Benchmark_Redirect_OldInput(b *testing.B) { input = c.Redirect().OldInput("name") } - require.Equal(b, "fiber_flash=; expires=Tue, 10 Nov 2009 23:00:00 GMT", c.GetRespHeader(HeaderSetCookie)) - require.Equal(b, "tom", input) + require.Equal(b, OldInputData{ + Key: "name", + Value: "tom", + }, input) } diff --git a/router.go b/router.go index eae9adef..d2429c19 100644 --- a/router.go +++ b/router.go @@ -43,23 +43,24 @@ type Router interface { // Route is a struct that holds all metadata for each registered handler. type Route struct { // ### important: 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 - routeParser routeParser // Parameter parser - group *Group // Group instance. used for routes in groups + group *Group // Group instance. used for routes in groups + + path string // Prettified path // Public fields 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 + Path string `json:"path"` // Original registered route path + Params []string `json:"params"` // Case sensitive param keys + Handlers []Handler `json:"-"` // Ctx handlers + routeParser routeParser // Parameter parser + // 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 '/' } func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { @@ -252,7 +253,7 @@ func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { } // Strict routing, remove trailing slashes if !app.config.StrictRouting && len(prettyPath) > 1 { - prettyPath = strings.TrimRight(prettyPath, "/") + prettyPath = utils.TrimRight(prettyPath, '/') } route.Path = prefixedPath @@ -323,7 +324,7 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler } // Strict routing, remove trailing slashes if !app.config.StrictRouting && len(pathPretty) > 1 { - pathPretty = strings.TrimRight(pathPretty, "/") + pathPretty = utils.TrimRight(pathPretty, '/') } // Is layer a middleware? isUse := method == methodUse @@ -357,7 +358,7 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler Handlers: handlers, } // Increment global handler count - atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) + atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) //nolint:gosec // Not a concern // Middleware route matches all HTTP methods if isUse { @@ -375,6 +376,9 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler } func (app *App) addRoute(method string, route *Route, isMounted ...bool) { + app.mutex.Lock() + defer app.mutex.Unlock() + // Check mounted routes var mounted bool if len(isMounted) > 0 { @@ -400,15 +404,28 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { // Execute onRoute hooks & change latestRoute if not adding mounted route if !mounted { - app.mutex.Lock() app.latestRoute = route if err := app.hooks.executeOnRouteHooks(*route); err != nil { panic(err) } - app.mutex.Unlock() } } +// BuildTree rebuilds the prefix tree from the previously registered routes. +// This method is useful when you want to register routes dynamically after the app has started. +// It is not recommended to use this method on production environments because rebuilding +// the tree is performance-intensive and not thread-safe in runtime. Since building the tree +// is only done in the startupProcess of the app, this method does not makes sure that the +// routeTree is being safely changed, as it would add a great deal of overhead in the request. +// Latest benchmark results showed a degradation from 82.79 ns/op to 94.48 ns/op and can be found in: +// https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283 +func (app *App) RebuildTree() *App { + app.mutex.Lock() + defer app.mutex.Unlock() + + return app.buildTree() +} + // buildTree build the prefix tree from the previously registered routes func (app *App) buildTree() *App { if !app.routesRefreshed { diff --git a/router_test.go b/router_test.go index 57ce9209..5509039c 100644 --- a/router_test.go +++ b/router_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net/http" "net/http/httptest" "os" "testing" @@ -368,6 +369,33 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) { require.Equal(t, "Cannot DELETE /does/not/exist<script>alert('foo');</script>", string(c.Response.Body())) } +func Test_App_Rebuild_Tree(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/test", func(c Ctx) error { + app.Get("/dynamically-defined", func(c Ctx) error { + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() + + return c.SendStatus(http.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusNotFound, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") +} + ////////////////////////////////////////////// ///////////////// BENCHMARKS ///////////////// ////////////////////////////////////////////// @@ -542,6 +570,27 @@ func Benchmark_Router_Next(b *testing.B) { require.Equal(b, 4, c.indexRoute) } +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default -benchmem -count=4 +func Benchmark_Router_Next_Default(b *testing.B) { + app := New() + app.Get("/", func(_ Ctx) error { + return nil + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} + // go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4 func Benchmark_Route_Match(b *testing.B) { var match bool