[
  {
    "path": ".cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"language\": \"en, en-gb, en-us\",\n  \"useGitignore\": true,\n  \"caseSensitive\": false,\n  \"import\": [\n    \"@cspell/dict-en_us/cspell-ext.json\",\n    \"@cspell/dict-en-gb/cspell-ext.json\",\n    \"@cspell/dict-software-terms/cspell-ext.json\",\n    \"@cspell/dict-golang/cspell-ext.json\",\n    \"@cspell/dict-fullstack/cspell-ext.json\",\n    \"@cspell/dict-docker/cspell-ext.json\",\n    \"@cspell/dict-k8s/cspell-ext.json\",\n    \"@cspell/dict-node/cspell-ext.json\",\n    \"@cspell/dict-npm/cspell-ext.json\",\n    \"@cspell/dict-typescript/cspell-ext.json\",\n    \"@cspell/dict-html/cspell-ext.json\",\n    \"@cspell/dict-css/cspell-ext.json\",\n    \"@cspell/dict-shell/cspell-ext.json\",\n    \"@cspell/dict-python/cspell-ext.json\",\n    \"@cspell/dict-redis/cspell-ext.json\",\n    \"@cspell/dict-sql/cspell-ext.json\",\n    \"@cspell/dict-filetypes/cspell-ext.json\",\n    \"@cspell/dict-companies/cspell-ext.json\",\n    \"@cspell/dict-markdown/cspell-ext.json\",\n    \"@cspell/dict-en-common-misspellings/cspell-ext.json\",\n    \"@cspell/dict-people-names/cspell-ext.json\"\n  ],\n  \"dictionaries\": [\n    \"en_us\",\n    \"en-gb\",\n    \"softwareTerms\",\n    \"web-services\",\n    \"networking-terms\",\n    \"software-term-suggestions\",\n    \"software-services\",\n    \"software-terms\",\n    \"software-tools\",\n    \"coding-compound-terms\",\n    \"golang\",\n    \"fullstack\",\n    \"docker\",\n    \"k8s\",\n    \"node\",\n    \"npm\",\n    \"typescript\",\n    \"html\",\n    \"css\",\n    \"shell\",\n    \"python\",\n    \"redis\",\n    \"sql\",\n    \"filetypes\",\n    \"companies\",\n    \"markdown\",\n    \"en-common-misspellings\",\n    \"people-names\",\n    \"data-science\",\n    \"data-science-models\",\n    \"data-science-tools\"\n  ],\n  \"ignorePaths\": [\n    \".git\",\n    \"node_modules\",\n    \"vendor\",\n    \"internal\",\n    \".github\",\n    \"**/*.svg\",\n    \"**/*.png\",\n    \"**/*.jpg\",\n    \"**/*.jpeg\",\n    \"**/*.gif\",\n    \"**/*.ico\",\n    \"**/*.lock\",\n    \"**/*_gen.go\",\n    \"**/*_msgp.go\",\n    \"**/*_msgp_test.go\",\n    \"go.mod\",\n    \"go.sum\",\n    \".golangci.yml\",\n    \".markdownlint.yml\",\n    \"AGENTS.md\"\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at http://editorconfig.org\n; This style originates from https://github.com/fewagency/best-practices\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.go]\nindent_style = tab\nindent_size = 4\ntab_width = 4\n\n[Makefile]\nindent_style = tab\n\n[*.{yml,yaml,json,md}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Handle line endings automatically for files detected as text\n# and leave all files detected as binary untouched.\n* text=auto eol=lf\n\n# Force batch scripts to always use CRLF line endings so that if a repo is accessed\n# in Windows via a file share from Linux, the scripts will work.\n*.{cmd,[cC][mM][dD]} text eol=crlf\n*.{bat,[bB][aA][tT]} text eol=crlf\n\n# Force bash scripts to always use LF line endings so that if a repo is accessed\n# in Unix via a file share from Windows, the scripts will work.\n*.sh text eol=lf\n"
  },
  {
    "path": ".github/.editorconfig",
    "content": "; https://editorconfig.org/\n\nroot = true\n\n[*]\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nindent_style = space\nindent_size = 2\n\n[{Makefile,go.mod,go.sum,*.go,.gitmodules}]\nindent_style = tab\nindent_size = 8\n\n[*.md]\nindent_size = 4\ntrim_trailing_whitespace = false\n\neclint_indent_style = unset\n\n[Dockerfile]\nindent_size = 4"
  },
  {
    "path": ".github/.hound.yml",
    "content": "golint:\n  enabled: false"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @gofiber/maintainers\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord](https://gofiber.io/discord). All complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,\navailable 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).\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\n[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).\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\nBefore making any changes to this repository, we kindly request you to initiate discussions for proposed changes that do not yet have an associated [issue](https://github.com/gofiber/fiber/issues). Please use our [Discord](https://gofiber.io/discord) server to initiate these discussions. For [issue](https://github.com/gofiber/fiber/issues) that already exist, you may proceed with discussions using our [issue](https://github.com/gofiber/fiber/issues) tracker or any other suitable method, in consultation with the repository owners. Your collaboration is greatly appreciated.\n\nPlease note: we have a [code of conduct](https://github.com/gofiber/fiber/blob/main/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `Fiber` project.\n\n## Pull Requests or Commits\n\nTitles always we must use prefix according to below:\n\n> 🔥 Feature, ♻️ Refactor, 🩹 Fix, 🚨 Test, 📚 Doc, 🎨 Style\n\n- 🔥 Feature: Add flow to add person\n- ♻️ Refactor: Rename file X to Y\n- 🩹 Fix: Improve flow\n- 🚨 Test: Validate to add a new person\n- 📚 Doc: Translate to Portuguese middleware redirect\n- 🎨 Style: Respected pattern Golint\n\nAll 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.\n\n## 👍 Contribute\n\nIf you want to say **thank you** and/or support the active development of `Fiber`:\n\n1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project.\n2. Tweet about the project [on your 𝕏 (Twitter)](https://x.com/intent/tweet?text=%F0%9F%9A%80%20Fiber%20%E2%80%94%20is%20an%20Express.js%20inspired%20web%20framework%20build%20on%20Fasthttp%20for%20%23Go%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber).\n3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog.\n4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny).\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [gofiber] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncustom: https://github.com/sponsors/gofiber\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yaml",
    "content": "name: \"\\U0001F41B Bug Report\"\ntitle: \"\\U0001F41B [Bug]: \"\ndescription: Create a bug report to help us fix it.\nlabels: [\"☢️ Bug\"]\n\nbody:\n  - type: markdown\n    id: notice\n    attributes:\n      value: |\n        ### Notice\n        **This repository is not related to external or third-part Fiber modules. If you have a problem with them, open an issue under their repos. If you think the problem is related to Fiber, open the issue here.**\n        - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord).\n        - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template.\n        - Write your issue with clear and understandable English.\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Bug Description\"\n      description: \"A clear and detailed description of what the bug is.\"\n      placeholder: \"Explain your problem clearly and in detail.\"\n    validations:\n      required: true\n  - type: textarea\n    id: how-to-reproduce\n    attributes:\n      label: How to Reproduce\n      description: \"Steps to reproduce the behavior and what should be observed in the end.\"\n      placeholder: \"Tell us step by step how we can replicate your problem and what we should see in the end.\"\n      value: |\n          Steps to reproduce the behavior:\n          1. Go to '....'\n          2. Click on '....'\n          3. Do '....'\n          4. See '....'\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: \"A clear and detailed description of what you think should happen.\"\n      placeholder: \"Tell us what Fiber should normally do.\"\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: \"Fiber Version\"\n      description: \"Some bugs may be fixed in future Fiber releases, so we have to know your Fiber version.\"\n      placeholder: \"Write your Fiber version. (v2.33.0, v2.34.1...)\"\n    validations:\n      required: true\n  - type: textarea\n    id: snippet\n    attributes:\n      label: \"Code Snippet (optional)\"\n      description: \"For some issues, we need to know some parts of your code.\"\n      placeholder: \"Share a code snippet that you think is related to the issue.\"\n      render: go\n      value: |\n        package main\n\n        import \"github.com/gofiber/fiber/v3\"\n        import \"log\"\n\n        func main() {\n          app := fiber.New()\n\n          // Steps to reproduce\n\n          log.Fatal(app.Listen(\":3000\"))\n        }\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: \"Checklist:\"\n      description: \"By submitting this issue, you confirm that:\"\n      options:\n        - label: \"I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/main/.github/CODE_OF_CONDUCT.md).\"\n          required: true\n        - label: \"I have checked for existing issues that describe my problem prior to opening this one.\"\n          required: true\n        - label: \"I understand that improperly formatted bug reports may be closed without explanation.\"\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yaml",
    "content": "name: \"📝 Feature Proposal\"\ntitle: \"📝 [Proposal]: \"\ndescription: Propose a feature or improvement for Fiber.\nlabels: [\"📝 Proposal\", \"✏️ Feature\", \"v3\"]\n\nbody:\n  - type: markdown\n    id: notice\n    attributes:\n      value: |\n        ### Notice\n        - For questions, join our [Discord server](https://gofiber.io/discord).\n        - Please write in clear, understandable English.\n        - Ensure your proposal aligns with Express design principles and HTTP RFC standards.\n        - Describe features expected to remain stable and not require changes in the foreseeable future.\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Feature Proposal Description\"\n      description: \"A clear and detailed description of the feature you are proposing for Fiber v3. How should it work, and what API endpoints and methods would it involve?\"\n      placeholder: \"Describe your feature proposal clearly and in detail, including API endpoints and methods.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: express-alignment\n    attributes:\n      label: \"Alignment with Express API\"\n      description: \"Explain how your proposal aligns with the design and API of Express.js. Provide comparative examples if possible.\"\n      placeholder: \"Outline how the feature aligns with Express.js design principles and API standards.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: standards-compliance\n    attributes:\n      label: \"HTTP RFC Standards Compliance\"\n      description: \"Confirm that the feature complies with HTTP RFC standards, and describe any relevant aspects.\"\n      placeholder: \"Detail how the feature adheres to HTTP RFC standards.\"\n    validations:\n      required: true\n  - type: textarea\n    id: stability\n    attributes:\n      label: \"API Stability\"\n      description: \"Discuss the expected stability of the feature and its API. How do you ensure that it will not require changes or deprecations in the near future?\"\n      placeholder: \"Describe measures taken to ensure the feature's API stability over time.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: examples\n    attributes:\n      label: \"Feature Examples\"\n      description: \"Provide concrete examples and code snippets to illustrate how the proposed feature should function.\"\n      placeholder: \"Share code snippets that exemplify the proposed feature and its usage.\"\n      render: go\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: \"Checklist:\"\n      description: \"By submitting this issue, you confirm that:\"\n      options:\n        - label: \"I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/main/.github/CODE_OF_CONDUCT.md).\"\n          required: true\n        - label: \"I have searched for existing issues that describe my proposal before opening this one.\"\n          required: true\n        - label: \"I understand that a proposal that does not meet these guidelines may be closed without explanation.\"\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/maintenance-task.yaml",
    "content": "name: \"🧹 Maintenance Task\"\ntitle: \"🧹 [Maintenance]: \"\ndescription: Describe a maintenance task for the v3 of the Fiber project.\nlabels: [\"🧹 Updates\", \"v3\"]\n\nbody:\n  - type: markdown\n    id: notice\n    attributes:\n      value: |\n        ### Notice\n        - Before submitting a maintenance task, please check if a similar task has already been filed.\n        - Clearly outline the purpose of the maintenance task and its impact on the project.\n        - Use clear and understandable English.\n  - type: textarea\n    id: task-description\n    attributes:\n      label: \"Maintenance Task Description\"\n      description: \"Provide a detailed description of the maintenance task. Include any specific areas of the codebase that require attention, and the desired outcomes of this task.\"\n      placeholder: \"Detail the maintenance task, specifying what needs to be done and why it is necessary.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: impact\n    attributes:\n      label: \"Impact on the Project\"\n      description: \"Explain the impact this maintenance will have on the project. Include benefits and potential risks if applicable.\"\n      placeholder: \"Describe how completing this task will benefit the project, or the risks of not addressing it.\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: \"Additional Context (optional)\"\n      description: \"Any additional information or context regarding the maintenance task that might be helpful.\"\n      placeholder: \"Provide any additional information that may be relevant to the task at hand.\"\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: \"Checklist:\"\n      description: \"Please confirm the following:\"\n      options:\n        - label: \"I have confirmed that this maintenance task is currently not being addressed.\"\n          required: true\n        - label: \"I understand that this task will be evaluated by the maintainers and prioritized accordingly.\"\n          required: true\n        - label: \"I am available to provide further information if needed.\"\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yaml",
    "content": "name: \"🤔 Question\"\ntitle: \"\\U0001F917 [Question]: \"\ndescription: Ask a question so we can help you easily.\nlabels: [\"🤔 Question\"]\n\nbody:\n  - type: markdown\n    id: notice\n    attributes:\n      value: |\n        ### Notice\n        - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord).\n        - If you think this is just a bug, open the issue with the **☢️ Bug Report** template.\n        - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template.\n        - Write your issue with clear and understandable English.\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Question Description\"\n      description: \"A clear and detailed description of the question.\"\n      placeholder: \"Explain your question clearly, and in detail.\"\n    validations:\n      required: true\n  - type: textarea\n    id: snippet\n    attributes:\n      label: \"Code Snippet (optional)\"\n      description: \"Code snippet may be really helpful to describe some features.\"\n      placeholder: \"Share a code snippet to explain the feature better.\"\n      render: go\n      value: |\n        package main\n\n        import \"github.com/gofiber/fiber/v3\"\n        import \"log\"\n\n        func main() {\n          app := fiber.New()\n\n          // An example to describe the question\n\n          log.Fatal(app.Listen(\":3000\"))\n        }\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: \"Checklist:\"\n      description: \"By submitting this issue, you confirm that:\"\n      options:\n        - label: \"I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/main/.github/CODE_OF_CONDUCT.md).\"\n          required: true\n        - label: \"I have checked for existing issues that describe my questions prior to opening this one.\"\n          required: true\n        - label: \"I understand that improperly formatted questions may be closed without explanation.\"\n          required: true\n"
  },
  {
    "path": ".github/README.md",
    "content": "<h1 align=\"center\">\n  <a href=\"https://gofiber.io\">\n    <picture>\n      <source height=\"125\" media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/gofiber/docs/master/static/img/v3/logo-dark.svg\">\n      <img height=\"125\" alt=\"Fiber\" src=\"https://raw.githubusercontent.com/gofiber/docs/master/static/img/v3/logo.svg\">\n    </picture>\n  </a>\n  <br>\n  <a href=\"https://pkg.go.dev/github.com/gofiber/fiber/v3#pkg-overview\">\n    <img src=\"https://img.shields.io/badge/%F0%9F%93%9A%20godoc-pkg-00ACD7.svg?color=00ACD7&style=flat-square\">\n  </a>\n  <a href=\"https://goreportcard.com/report/github.com/gofiber/fiber/v3\">\n    <img src=\"https://img.shields.io/badge/%F0%9F%93%9D%20goreport-A%2B-75C46B?style=flat-square\">\n  </a>\n  <a href=\"https://codecov.io/gh/gofiber/fiber\" >\n   <img alt=\"Codecov\" src=\"https://img.shields.io/codecov/c/github/gofiber/fiber?token=3Cr92CwaPQ&style=flat-square&logo=codecov&label=codecov\">\n </a>\n  <a href=\"https://github.com/gofiber/fiber/actions?query=workflow%3ATest\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/fiber/test.yml?branch=main&label=%F0%9F%A7%AA%20tests&style=flat-square&color=75C46B\">\n  </a>\n    <a href=\"https://docs.gofiber.io\">\n    <img src=\"https://img.shields.io/badge/%F0%9F%92%A1%20fiber-docs-00ACD7.svg?style=flat-square\">\n  </a>\n  <a href=\"https://gofiber.io/discord\">\n    <img src=\"https://img.shields.io/discord/704680098577514527?style=flat-square&label=%F0%9F%92%AC%20discord&color=00ACD7\">\n  </a>\n</h1>\n<p align=\"center\">\n  <em><b>Fiber</b> is an <a href=\"https://github.com/expressjs/express\">Express</a> inspired <b>web framework</b> built on top of <a href=\"https://github.com/valyala/fasthttp\">Fasthttp</a>, the <b>fastest</b> HTTP engine for <a href=\"https://go.dev/doc/\">Go</a>. Designed to <b>ease</b> things up for <b>fast</b> development with <a href=\"https://docs.gofiber.io/#zero-allocation\"><b>zero memory allocation</b></a> and <b>performance</b> in mind.</em>\n</p>\n\n---\n\n## ⚙️ Installation\n\nFiber requires **Go version `1.25` 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:\n\n```bash\ngo mod init github.com/your/repo\n```\n\nTo learn more about Go modules and how they work, you can check out the [Using Go Modules](https://go.dev/blog/using-go-modules) blog post.\n\nAfter setting up your project, you can install Fiber with the `go get` command:\n\n```bash\ngo get -u github.com/gofiber/fiber/v3\n```\n\nThis command fetches the Fiber package and adds it to your project's dependencies, allowing you to start building your web applications with Fiber.\n\n## ⚡️ Quickstart\n\nGetting 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.\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    // Initialize a new Fiber app\n    app := fiber.New()\n\n    // Define a route for the GET method on the root path '/'\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        // Send a string response to the client\n        return c.SendString(\"Hello, World 👋!\")\n    })\n\n    // Start the server on port 3000\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nThis simple server is easy to set up and run. It introduces the core concepts of Fiber: app initialization, route definition, and starting the server. Just run this Go program, and visit `http://localhost:3000` in your browser to see the message.\n\n## Zero Allocation\n\nFiber is optimized for **high-performance**, meaning values returned from **fiber.Ctx** are **not** immutable by default and **will** be re-used across requests. As a rule of thumb, you **must** only use context values within the handler and **must not** keep any references. Once you return from the handler, any values obtained from the context will be re-used in future requests. Visit our [documentation](https://docs.gofiber.io/#zero-allocation) to learn more.\n\n## 🤖 Benchmarks\n\nThese tests are performed by [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext). If you want to see all the results, please visit our [Wiki](https://docs.gofiber.io/extra/benchmarks).\n\n<p float=\"left\" align=\"middle\">\n  <img src=\"https://raw.githubusercontent.com/gofiber/docs/master/static/img/v3/plaintext.png\" width=\"49%\">\n  <img src=\"https://raw.githubusercontent.com/gofiber/docs/master/static/img/v3/json.png\" width=\"49%\">\n</p>\n\n## 🎯 Features\n\n- Robust [Routing](https://docs.gofiber.io/guide/routing)\n- Serve [Static Files](https://docs.gofiber.io/api/app#static)\n- Extreme [Performance](https://docs.gofiber.io/extra/benchmarks)\n- [Low Memory](https://docs.gofiber.io/extra/benchmarks) footprint\n- [API Endpoints](https://docs.gofiber.io/api/ctx)\n- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) support\n- [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming\n- [Template Engines](https://github.com/gofiber/template)\n- [WebSocket Support](https://github.com/gofiber/contrib/tree/main/websocket)\n- [Socket.io Support](https://github.com/gofiber/contrib/tree/main/socketio)\n- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse)\n- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter)\n- And much more, [explore Fiber](https://docs.gofiber.io/)\n\n## 💡 Philosophy\n\nNew gophers that make the switch from [Node.js](https://nodejs.org/en/about/) to [Go](https://go.dev/doc/) are dealing with a learning curve before they can start building their web applications or microservices. Fiber, as a **web framework**, was created with the idea of **minimalism** and follows the **UNIX way**, so that new gophers can quickly enter the world of Go with a warm and trusted welcome.\n\nFiber is **inspired** by Express, the most popular web framework on the Internet. We combined the **ease** of Express and **raw performance** of Go. If you have ever implemented a web application in Node.js (_using Express or similar_), then many methods and principles will seem **very common** to you.\n\nWe **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord) _and all over the Internet_ to create a **fast**, **flexible** and **friendly** Go web framework for **any** task, **deadline** and developer **skill**! Just like Express does in the JavaScript world.\n\n## ⚠️ Limitations\n\n- 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 version 1.25 or higher.\n- Fiber automatically adapts common `net/http` handler shapes when you register them on the router, and you can still use the [adaptor middleware](https://docs.gofiber.io/next/middleware/adaptor/) when you need to bridge entire apps or `net/http` middleware.\n\n### net/http compatibility\n\nFiber can run side by side with the standard library. The router accepts existing `net/http` handlers directly and even works with native `fasthttp.RequestHandler` callbacks, so you can plug in legacy endpoints without wrapping them manually:\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"net/http\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    httpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        if _, err := w.Write([]byte(\"served by net/http\")); err != nil {\n            panic(err)\n        }\n    })\n\n    app := fiber.New()\n    app.Get(\"/\", httpHandler)\n\n    // Start the server on port 3000\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nWhen you need to convert entire applications or re-use `net/http` middleware chains, rely on the [adaptor middleware](https://docs.gofiber.io/next/middleware/adaptor/). It converts handlers and middlewares in both directions and even lets you mount a Fiber app in a `net/http` server.\n\n### Express-style handlers\n\nFiber also adapts Express-style callbacks that operate on the lightweight `fiber.Req` and `fiber.Res` helper interfaces. This lets you port middleware and route handlers from Express-inspired codebases while keeping Fiber's router features:\n\n```go\n// Request/response handlers (2-argument)\napp.Get(\"/\", func(req fiber.Req, res fiber.Res) error {\n    return res.SendString(\"Hello from Express-style handlers!\")\n})\n\n// Middleware with an error-returning next callback (3-argument)\napp.Use(func(req fiber.Req, res fiber.Res, next func() error) error {\n    if req.IP() == \"192.168.1.254\" {\n        return res.SendStatus(fiber.StatusForbidden)\n    }\n    return next()\n})\n\n// Middleware with a no-arg next callback (3-argument)\napp.Use(func(req fiber.Req, res fiber.Res, next func()) {\n    if req.Get(\"X-Skip\") == \"true\" {\n        return // stop the chain without calling next\n    }\n    next()\n})\n```\n\n> **Note:** Adapted `net/http` handlers continue to operate with the standard-library semantics. They don't get access to `fiber.Ctx` features and incur the overhead of the compatibility layer, so native `fiber.Handler` callbacks still provide the best performance.\n\n## 👀 Examples\n\nListed 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).\n\n### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // GET /api/register\n    app.Get(\"/api/*\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"✋ %s\", c.Params(\"*\"))\n        return c.SendString(msg) // => ✋ register\n    })\n\n    // GET /flights/LAX-SFO\n    app.Get(\"/flights/:from-:to\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"💸 From: %s, To: %s\", c.Params(\"from\"), c.Params(\"to\"))\n        return c.SendString(msg) // => 💸 From: LAX, To: SFO\n    })\n\n    // GET /dictionary.txt\n    app.Get(\"/:file.:ext\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"📃 %s.%s\", c.Params(\"file\"), c.Params(\"ext\"))\n        return c.SendString(msg) // => 📃 dictionary.txt\n    })\n\n    // GET /john/75\n    app.Get(\"/:name/:age/:gender?\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"👴 %s is %s years old\", c.Params(\"name\"), c.Params(\"age\"))\n        return c.SendString(msg) // => 👴 john is 75 years old\n    })\n\n    // GET /john\n    app.Get(\"/:name\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"Hello, %s 👋!\", c.Params(\"name\"))\n        return c.SendString(msg) // => Hello john 👋!\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/api/*\", func(c fiber.Ctx) error {\n        msg := fmt.Sprintf(\"✋ %s\", c.Params(\"*\"))\n        return c.SendString(msg) // => ✋ register\n    }).Name(\"api\")\n\n    route := app.GetRoute(\"api\")\n\n    data, _ := json.MarshalIndent(route, \"\", \"  \")\n    fmt.Println(string(data))\n    // Prints:\n    // {\n    //    \"method\": \"GET\",\n    //    \"name\": \"api\",\n    //    \"path\": \"/api/*\",\n    //    \"params\": [\n    //      \"*1\"\n    //    ]\n    // }\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/static\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Serve static files from the \"./public\" directory\n    app.Get(\"/*\", static.New(\"./public\"))\n    // => http://localhost:3000/js/script.js\n    // => http://localhost:3000/css/style.css\n\n    app.Get(\"/prefix*\", static.New(\"./public\"))\n    // => http://localhost:3000/prefix/js/script.js\n    // => http://localhost:3000/prefix/css/style.css\n\n    // Serve a single file for any unmatched routes\n    app.Get(\"*\", static.New(\"./public/index.html\"))\n    // => http://localhost:3000/any/path/shows/index.html\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Middleware that matches any route\n    app.Use(func(c fiber.Ctx) error {\n        fmt.Println(\"🥇 First handler\")\n        return c.Next()\n    })\n\n    // Middleware that matches all routes starting with /api\n    app.Use(\"/api\", func(c fiber.Ctx) error {\n        fmt.Println(\"🥈 Second handler\")\n        return c.Next()\n    })\n\n    // GET /api/list\n    app.Get(\"/api/list\", func(c fiber.Ctx) error {\n        fmt.Println(\"🥉 Last handler\")\n        return c.SendString(\"Hello, World 👋!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n<details>\n  <summary>📚 Show more code examples</summary>\n\n### Views Engines\n\n📖 [Config](https://docs.gofiber.io/api/fiber#config)\n📖 [Engines](https://github.com/gofiber/template)\n📖 [Render](https://docs.gofiber.io/api/ctx#render)\n\nFiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set.\n\nIf you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), or [pug](https://github.com/Joker/jade), etc., check out our [Template](https://github.com/gofiber/template) package that supports multiple view engines.\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/template/pug\"\n)\n\nfunc main() {\n    // Initialize a new Fiber app with Pug template engine\n    app := fiber.New(fiber.Config{\n        Views: pug.New(\"./views\", \".pug\"),\n    })\n\n    // Define a route that renders the \"home.pug\" template\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.Render(\"home\", fiber.Map{\n            \"title\": \"Homepage\",\n            \"year\":  1999,\n        })\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Grouping Routes into Chains\n\n📖 [Group](https://docs.gofiber.io/api/app#group)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc middleware(c fiber.Ctx) error {\n    log.Println(\"Middleware executed\")\n    return c.Next()\n}\n\nfunc handler(c fiber.Ctx) error {\n    return c.SendString(\"Handler response\")\n}\n\nfunc main() {\n    app := fiber.New()\n\n    // Root API group with middleware\n    api := app.Group(\"/api\", middleware) // /api\n\n    // API v1 routes\n    v1 := api.Group(\"/v1\", middleware) // /api/v1\n    v1.Get(\"/list\", handler)           // /api/v1/list\n    v1.Get(\"/user\", handler)           // /api/v1/user\n\n    // API v2 routes\n    v2 := api.Group(\"/v2\", middleware) // /api/v2\n    v2.Get(\"/list\", handler)           // /api/v2/list\n    v2.Get(\"/user\", handler)           // /api/v2/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Middleware Logger\n\n📖 [Logger](https://docs.gofiber.io/api/middleware/logger)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Use Logger middleware\n    app.Use(logger.New())\n\n    // Define routes\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, Logger!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Cross-Origin Resource Sharing (CORS)\n\n📖 [CORS](https://docs.gofiber.io/api/middleware/cors)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/cors\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Use CORS middleware with default settings\n    app.Use(cors.New())\n\n    // Define routes\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"CORS enabled!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nCheck CORS by passing any domain in `Origin` header:\n\n```bash\ncurl -H \"Origin: http://example.com\" --verbose http://localhost:3000\n```\n\n### Custom 404 Response\n\n📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Define routes\n    app.Get(\"/\", static.New(\"./public\"))\n\n    app.Get(\"/demo\", func(c fiber.Ctx) error {\n        return c.SendString(\"This is a demo page!\")\n    })\n\n    app.Post(\"/register\", func(c fiber.Ctx) error {\n        return c.SendString(\"Registration successful!\")\n    })\n\n    // Middleware to handle 404 Not Found\n    app.Use(func(c fiber.Ctx) error {\n        return c.SendStatus(fiber.StatusNotFound) // => 404 \"Not Found\"\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### JSON Response\n\n📖 [JSON](https://docs.gofiber.io/api/ctx#json)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\ntype User struct {\n    Name string `json:\"name\"`\n    Age  int    `json:\"age\"`\n}\n\nfunc main() {\n    app := fiber.New()\n\n    // Route that returns a JSON object\n    app.Get(\"/user\", func(c fiber.Ctx) error {\n        return c.JSON(&User{\"John\", 20})\n        // => {\"name\":\"John\", \"age\":20}\n    })\n\n    // Route that returns a JSON map\n    app.Get(\"/json\", func(c fiber.Ctx) error {\n        return c.JSON(fiber.Map{\n            \"success\": true,\n            \"message\": \"Hi John!\",\n        })\n        // => {\"success\":true, \"message\":\"Hi John!\"}\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### WebSocket Upgrade\n\n📖 [Websocket](https://github.com/gofiber/websocket)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/websocket\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // WebSocket route\n    app.Get(\"/ws\", websocket.New(func(c *websocket.Conn) {\n        defer c.Close()\n        for {\n            // Read message from client\n            mt, msg, err := c.ReadMessage()\n            if err != nil {\n                log.Println(\"read:\", err)\n                break\n            }\n            log.Printf(\"recv: %s\", msg)\n\n            // Write message back to client\n            err = c.WriteMessage(mt, msg)\n            if err != nil {\n                log.Println(\"write:\", err)\n                break\n            }\n        }\n    }))\n\n    log.Fatal(app.Listen(\":3000\"))\n    // Connect via WebSocket at ws://localhost:3000/ws\n}\n```\n\n### Server-Sent Events\n\n📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"bufio\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/valyala/fasthttp\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Server-Sent Events route\n    app.Get(\"/sse\", func(c fiber.Ctx) error {\n        c.Set(\"Content-Type\", \"text/event-stream\")\n        c.Set(\"Cache-Control\", \"no-cache\")\n        c.Set(\"Connection\", \"keep-alive\")\n        c.Set(\"Transfer-Encoding\", \"chunked\")\n\n        c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {\n            var i int\n            for {\n                i++\n                msg := fmt.Sprintf(\"%d - the time is %v\", i, time.Now())\n                fmt.Fprintf(w, \"data: Message: %s\\n\\n\", msg)\n                fmt.Println(msg)\n\n                w.Flush()\n                time.Sleep(5 * time.Second)\n            }\n        })\n\n        return nil\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Recover Middleware\n\n📖 [Recover](https://docs.gofiber.io/api/middleware/recover)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/recover\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Use Recover middleware to handle panics gracefully\n    app.Use(recover.New())\n\n    // Route that intentionally panics\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        panic(\"normally this would crash your app\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Using Trusted Proxy\n\n📖 [Config](https://docs.gofiber.io/api/fiber#config)\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        // Configure trusted proxies - WARNING: Only trust proxies you control\n        // Using TrustProxy: true with unrestricted IPs can lead to IP spoofing\n        TrustProxy: true,\n        TrustProxyConfig: fiber.TrustProxyConfig{\n            Proxies: []string{\"10.0.0.0/8\", \"172.16.0.0/12\"}, // Example: Internal network ranges only\n        },\n        ProxyHeader: fiber.HeaderXForwardedFor,\n    })\n\n    // Define routes\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Trusted Proxy Configured!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n</details>\n\n## 🧬 Internal Middleware\n\nHere is a list of middleware that are included within the Fiber framework.\n\n| Middleware                                                                           | Description                                                                                                                                                             |\n|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| [adaptor](https://github.com/gofiber/fiber/tree/main/middleware/adaptor)             | Converter for net/http handlers to/from Fiber request handlers.                                                                                                         |\n| [basicauth](https://github.com/gofiber/fiber/tree/main/middleware/basicauth)         | Provides HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials.                            |\n| [cache](https://github.com/gofiber/fiber/tree/main/middleware/cache)                 | Intercept and cache HTTP responses.                                                                                                                                     |\n| [compress](https://github.com/gofiber/fiber/tree/main/middleware/compress)           | Compression middleware for Fiber, with support for `deflate`, `gzip`, `brotli` and `zstd`.                                                                             |\n| [cors](https://github.com/gofiber/fiber/tree/main/middleware/cors)                   | Enable cross-origin resource sharing (CORS) with various options.                                                                                                       |\n| [csrf](https://github.com/gofiber/fiber/tree/main/middleware/csrf)                   | Protect from CSRF exploits.                                                                                                                                             |\n| [earlydata](https://github.com/gofiber/fiber/tree/main/middleware/earlydata)         | Adds support for TLS 1.3's early data (\"0-RTT\") feature.                                                                                                                |\n| [encryptcookie](https://github.com/gofiber/fiber/tree/main/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values.                                                                                                                        |\n| [envvar](https://github.com/gofiber/fiber/tree/main/middleware/envvar)               | Expose environment variables with providing an optional config.                                                                                                         |\n| [etag](https://github.com/gofiber/fiber/tree/main/middleware/etag)                   | Allows for caches to be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed.                      |\n| [expvar](https://github.com/gofiber/fiber/tree/main/middleware/expvar)               | Serves via its HTTP server runtime exposed variables in the JSON format.                                                                                                 |\n| [favicon](https://github.com/gofiber/fiber/tree/main/middleware/favicon)             | Ignore favicon from logs or serve from memory if a file path is provided.                                                                                               |\n| [healthcheck](https://github.com/gofiber/fiber/tree/main/middleware/healthcheck)     | Liveness and Readiness probes for Fiber.                                                                                                                                |\n| [helmet](https://github.com/gofiber/fiber/tree/main/middleware/helmet)               | Helps secure your apps by setting various HTTP headers.                                                                                                                 |\n| [idempotency](https://github.com/gofiber/fiber/tree/main/middleware/idempotency)     | Allows for fault-tolerant APIs where duplicate requests do not erroneously cause the same action performed multiple times on the server-side.                           |\n| [keyauth](https://github.com/gofiber/fiber/tree/main/middleware/keyauth)             | Adds support for key based authentication.                                                                                                                              |\n| [limiter](https://github.com/gofiber/fiber/tree/main/middleware/limiter)             | Adds Rate-limiting support to Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset.                                             |\n| [logger](https://github.com/gofiber/fiber/tree/main/middleware/logger)               | HTTP request/response logger.                                                                                                                                           |\n| [paginate](https://github.com/gofiber/fiber/tree/main/middleware/paginate)           | Extracts pagination parameters from query strings. Supports page-based, offset-based, and cursor-based pagination with multi-field sorting.                             |\n| [pprof](https://github.com/gofiber/fiber/tree/main/middleware/pprof)                 | Serves runtime profiling data in pprof format.                                                                                                                          |\n| [proxy](https://github.com/gofiber/fiber/tree/main/middleware/proxy)                 | Allows you to proxy requests to multiple servers.                                                                                                                       |\n| [recover](https://github.com/gofiber/fiber/tree/main/middleware/recover)             | Recovers from panics anywhere in the stack chain and handles the control to the centralized ErrorHandler.                                                               |\n| [redirect](https://github.com/gofiber/fiber/tree/main/middleware/redirect)           | Redirect middleware.                                                                                                                                                    |\n| [requestid](https://github.com/gofiber/fiber/tree/main/middleware/requestid)         | Adds a request ID to every request.                                                                                                                                     |\n| [responsetime](https://github.com/gofiber/fiber/tree/main/middleware/responsetime)   | Measures request handling duration and writes it to a configurable response header.                          |\n| [rewrite](https://github.com/gofiber/fiber/tree/main/middleware/rewrite)             | Rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links.                        |\n| [session](https://github.com/gofiber/fiber/tree/main/middleware/session)             | Session middleware. NOTE: This middleware uses our Storage package.                                                                                                     |\n| [skip](https://github.com/gofiber/fiber/tree/main/middleware/skip)                   | Skip middleware that skips a wrapped handler if a predicate is true.                                                                                                    |\n| [static](https://github.com/gofiber/fiber/tree/main/middleware/static)               | Static middleware for Fiber that serves static files such as **images**, **CSS**, and **JavaScript**.                                                                    |\n| [timeout](https://github.com/gofiber/fiber/tree/main/middleware/timeout)             | Adds a max time for a request and forwards to ErrorHandler if it is exceeded.                                                                                           |\n\n## 🧬 External Middleware\n\nList of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people).\n\n| Middleware                                        | Description                                                                                                           |\n| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- |\n| [contrib](https://github.com/gofiber/contrib)   | Third-party middlewares                                                                                               |\n| [storage](https://github.com/gofiber/storage)   | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares.     |\n| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber.      |\n\n## 🕶️ Awesome List\n\nFor more articles, middlewares, examples, or tools, check our [awesome list](https://github.com/gofiber/awesome-fiber).\n\n## 👍 Contribute\n\nIf you want to say **Thank You** and/or support the active development of `Fiber`:\n\n1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project.\n2. Tweet about the project [on your 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber).\n3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or your personal blog.\n4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny).\n\n## 💻 Development\n\nTo ensure your contributions are ready for a Pull Request, please use the following `Makefile` commands. These tools help maintain code quality and consistency.\n\n- **make help**: Display available commands.\n- **make audit**: Conduct quality checks.\n- **make benchmark**: Benchmark code performance.\n- **make coverage**: Generate test coverage report.\n- **make format**: Automatically format code.\n- **make lint**: Run lint checks.\n- **make test**: Execute all tests.\n- **make tidy**: Tidy dependencies.\n\nRun these commands to ensure your code adheres to project standards and best practices.\n\n## ☕ Supporters\n\nFiber is an open-source project that runs on donations to pay the bills, e.g., our domain name, GitBook, Netlify, and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny).\n\n|                                                            | User                                             | Donation |\n| ---------------------------------------------------------- | ------------------------------------------------ | -------- |\n| ![](https://avatars.githubusercontent.com/u/204341?s=25)   | [@destari](https://github.com/destari)           | ☕ x 10   |\n| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5    |\n| <img src=\"https://avatars.githubusercontent.com/u/56607882?s=25\" alt=\"thomasvvugt\" style=\"width: 25px; height: 25px;\"> | [@thomasvvugt](https://github.com/thomasvvugt)   | ☕ x 5    |\n| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy)   | ☕ x 5    |\n| ![](https://avatars.githubusercontent.com/u/1094221?s=25)  | [@ekaputra07](https://github.com/ekaputra07)     | ☕ x 5    |\n| ![](https://avatars.githubusercontent.com/u/194590?s=25)   | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5    |\n| ![](https://avatars.githubusercontent.com/u/186637?s=25)   | [@candidosales](https://github.com/candidosales) | ☕ x 5    |\n| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax)               | ☕ x 3    |\n| ![](https://avatars.githubusercontent.com/u/635852?s=25)   | [@bihe](https://github.com/bihe)                 | ☕ x 3    |\n| ![](https://avatars.githubusercontent.com/u/307334?s=25)   | [@justdave](https://github.com/justdave)         | ☕ x 3    |\n| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr)               | ☕ x 1    |\n| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar)       | ☕ x 1    |\n| ![](https://avatars.githubusercontent.com/u/2978730?s=25)  | [@diegowifi](https://github.com/diegowifi)       | ☕ x 1   |\n| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0)             | ☕ x 1   |\n| ![](https://avatars.githubusercontent.com/u/5638101?s=25)  | [@raymayemir](https://github.com/raymayemir)     | ☕ x 1   |\n| ![](https://avatars.githubusercontent.com/u/619996?s=25)   | [@melkorm](https://github.com/melkorm)           | ☕ x 1   |\n| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1   |\n| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy)             | ☕ x 1   |\n\n## 💻 Code Contributors\n\n<img src=\"https://opencollective.com/fiber/contributors.svg?width=890&button=false\" alt=\"Code Contributors\" style=\"max-width:100%;\">\n\n## ⭐️ Stargazers\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=gofiber/fiber&type=Date&theme=dark\" />\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=gofiber/fiber&type=Date\" />\n  <img src=\"https://api.star-history.com/svg?repos=gofiber/fiber&type=Date\" />\n</picture>\n\n## 🧾 License\n\nCopyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/main/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International).\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n1. [Supported Versions](#versions)\n2. [Reporting security problems to Fiber](#reporting)\n3. [Security Points of Contact](#contact)\n4. [Incident Response Process](#process)\n\n<a name=\"versions\"></a>\n\n## Supported Versions\n\nThe table below shows the supported versions for Fiber which include security updates.\n\n| Version   | Supported          |\n| --------- | ------------------ |\n| >= 1.12.6 | :white_check_mark: |\n| < 1.12.6  | :x:                |\n\n<a name=\"reporting\"></a>\n\n## Reporting security problems to Fiber\n\n**DO NOT CREATE AN ISSUE** to report a security problem. Instead, please\nsend us an e-mail at `team@gofiber.io` or join our discord server via\n[this invite link](https://gofiber.io/discord) and send a private message\nto any of the maintainers.\n\n<a name=\"contact\"></a>\n\n## Security Points of Contact\n\nFor security-related matters, please contact any of the\n[@maintainers](https://github.com/orgs/gofiber/teams/maintainers).\n\nThe maintainers are the only persons with administrative access to Fiber's source code\nand respond to security incident reports as fast as possible, within one business day\nat the latest.\n\n<a name=\"process\"></a>\n\n## Incident Response Process\n\nIn case an incident is discovered or reported, we will follow the following\nprocess to contain, respond and remediate:\n\n### 1. Containment\n\nThe first step is to find out the root cause, nature and scope of the incident.\n\n- Is still ongoing? If yes, first priority is to stop it.\n- Is the incident outside of our influence? If yes, first priority is to contain it.\n- Find out knows about the incident and who is affected.\n- Find out what data was potentially exposed.\n\n### 2. Response\n\nAfter the initial assessment and containment to our best abilities, we will\ndocument all actions taken in a response plan.\n\nWe will create a comment in the official `#announcements` channel to inform users about\nthe incident and what actions we took to contain it.\n\n### 3. Remediation\n\nOnce the incident is confirmed to be resolved, we will summarize the lessons\nlearned from the incident and create a list of actions we will take to prevent\nit from happening again.\n\n### Secure accounts with access\n\nThe [Fiber Organization](https://github.com/gofiber) requires 2FA authorization\nfor all of it's members.\n\n### Critical Updates And Security Notices\n\nWe learn about critical software updates and security threats from these sources\n\n1. GitHub Security Alerts\n2. GitHub: [https://status.github.com/](https://status.github.com/) & [@githubstatus](https://twitter.com/githubstatus)\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "coverage:\r\n  status:\r\n    project:\r\n      default:\r\n        target: auto\r\n        threshold: 0.5%\r\n        base: auto\r\n    patch:\r\n      default:\r\n        target: auto\r\n        threshold: 0.5%\r\n        base: auto\r\nignore:\r\n  # Ignore generated root files\r\n  - \"*_msgp.go\"\r\n  - \"*_msgp_test.go\"\r\n  - \"*_gen.go\"\r\n  # Ignore generated files below root\r\n  - \"**/*_msgp.go\"\r\n  - \"**/*_msgp_test.go\"\r\n  - \"**/*_gen.go\"\r\n  # Ignore internal and docs\r\n  - \"internal/**\"\r\n  - \"docs/**\"\r\n"
  },
  {
    "path": ".github/config.yml",
    "content": "# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome\n# Comment to be posted to on first time issues\nnewIssueWelcomeComment: >\n  Thanks for opening your first issue here! 🎉 Be sure to follow the issue template!\n  If you need help or want to chat with us, join us on Discord https://gofiber.io/discord\n\n# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome\n# Comment to be posted to on PRs from first time contributors in your repository\nnewPRWelcomeComment: >\n  Thanks for opening this pull request! 🎉 Please check out our contributing guidelines.\n  If you need help or want to chat with us, join us on Discord https://gofiber.io/discord\n\n# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge\n# Comment to be posted to on pull requests merged by a first time user\nfirstPRMergeComment: >\n  Congrats on merging your first pull request! 🎉 We here at Fiber are proud of you!\n  If you need help or want to chat with us, join us on Discord https://gofiber.io/discord\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Copilot Usage\n\nWhen modifying code, always perform these steps:\n\n1. **Ensure code quality**\n   - `make format` to format the project.\n   - `make lint` for static analysis.\n   - `make test` to run the test suite.\n\n2. **Maintain documentation**\n   Review and update the contents of the `docs` folder if necessary.\n\n3. **Check Markdown**\n   - Finish by running `make markdown` to lint all Markdown files.\n"
  },
  {
    "path": ".github/copilot-setup-steps.yml",
    "content": "steps:\n  - run: |\n      if [ -d vendor ] || go list -m -mod=readonly all; then\n        echo \"Dependencies already present\"\n      else\n        go mod tidy && go mod download && go mod vendor\n      fi\n  - run: |\n      go install gotest.tools/gotestsum@latest\n      go install golang.org/x/vuln/cmd/govulncheck@latest\n      go install mvdan.cc/gofumpt@latest\n      go install github.com/tinylib/msgp@latest\n      go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c\n      go install github.com/dkorunic/betteralign/cmd/betteralign@latest\n  - run: go mod tidy\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\" # Location of package manifests\n    labels:\n      - \"🤖 Dependencies\"\n    schedule:\n      interval: \"daily\"\n    allow:\n      # Allow both direct and indirect updates for all packages.\n      - dependency-type: \"all\"\n    groups:\n      fasthttp-modules:\n          patterns:\n              - \"github.com/valyala/fasthttp\"\n              - \"github.com/valyala/fasthttp/**\"\n      golang-modules:\n          patterns:\n              - \"golang.org/x/**\"\n      valyala-utils-modules:\n          patterns:\n              - \"github.com/valyala/bytebufferpool\"\n              - \"github.com/valyala/tcplisten\"\n      google-modules:\n          patterns:\n              - \"github.com/google/**\"\n              - \"google.golang.org/**\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule: \n      interval: daily\n    labels: \n     - \"🤖 Dependencies\"\n"
  },
  {
    "path": ".github/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Test file</title>\n</head>\n\n<body>\n  Hello, World!\n</body>\n\n</html>"
  },
  {
    "path": ".github/labeler.yml",
    "content": "_extends: .github\r\nlabels:\r\n  - label: 'v3'\r\n    matcher:\r\n      baseBranch: 'main'\r\n      title: '/(v3)/i'\r\n      body: '/(v3)/i'\r\n  - label: 'v2'\r\n    matcher:\r\n      baseBranch: 'v2'\r\n      title: '/(v2)/i'\r\n\r\n\r\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "# Description\n\nPlease 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.\n\nFixes # (issue)\n\n## Changes introduced\n\nList the new features or adjustments introduced in this pull request. Provide details on benchmarks, documentation updates, changelog entries, and if applicable, the migration guide.\n\n- [ ] Benchmarks: Describe any performance benchmarks and improvements related to the changes.\n- [ ] Documentation Update: Detail the updates made to the documentation and links to the changed files.\n- [ ] Changelog/What's New: Include a summary of the additions for the upcoming release notes.\n- [ ] Migration Guide: If necessary, provide a guide or steps for users to migrate their existing code to accommodate these changes.\n- [ ] API Alignment with Express: Explain how the changes align with the Express API.\n- [ ] API Longevity: Discuss the steps taken to ensure that the new or updated APIs are consistent and not prone to breaking changes.\n- [ ] Examples: Provide examples demonstrating the new features or changes in action.\n\n## Type of change\n\nPlease delete options that are not relevant.\n\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Enhancement (improvement to existing features and functionality)\n- [ ] Documentation update (changes to documentation)\n- [ ] Performance improvement (non-breaking change which improves efficiency)\n- [ ] Code consistency (non-breaking change which improves code reliability and robustness)\n\n## Checklist\n\nBefore you submit your pull request, please make sure you meet these requirements:\n\n- [ ] Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage.\n- [ ] Conducted a self-review of the code and provided comments for complex or critical parts.\n- [ ] Updated the documentation in the `/docs/` directory for [Fiber's documentation](https://docs.gofiber.io/).\n- [ ] Added or updated unit tests to validate the effectiveness of the changes or new features.\n- [ ] Ensured that new and existing unit tests pass locally with the changes.\n- [ ] Verified that any new dependencies are essential and have been agreed upon by the maintainers/community.\n- [ ] Aimed for optimal performance with minimal allocations in the new code.\n- [ ] Provided benchmarks for the new code to analyze and improve upon.\n\n## Commit formatting\n\nPlease use emojis in commit messages for an easy way to identify the purpose or intention of a commit. Check out the emoji cheatsheet here: [CONTRIBUTING.md](https://github.com/gofiber/fiber/blob/main/.github/CONTRIBUTING.md#pull-requests-or-commits)\n"
  },
  {
    "path": ".github/release-drafter.yml",
    "content": "name-template: 'v$RESOLVED_VERSION'\ntag-template: 'v$RESOLVED_VERSION'\ncommitish: main\nfilter-by-commitish: true\ninclude-labels:\n  - 'v3'\nexclude-labels:\n  - 'v2'\ncategories:\n  - title: '❗ Breaking Changes'\n    labels:\n      - '❗ BreakingChange'\n  - title: '🚀 New'\n    labels:\n      - '✏️ Feature'\n      - '📝 Proposal'\n  - title: '🧹 Updates'\n    labels:\n      - '🧹 Updates'\n      - '⚡️ Performance'\n  - title: '🐛 Fixes'\n    labels:\n      - '☢️ Bug'\n  - title: '🛠️ Maintenance'\n    labels:\n      - '🤖 Dependencies'\n  - title: '📚 Documentation'\n    labels:\n      - '📒 Documentation'\nchange-template: '- $TITLE (#$NUMBER)'\nchange-title-escapes: '\\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.\nexclude-contributors:\n  - dependabot\n  - dependabot[bot]\nversion-resolver:\n  major:\n    labels:\n      - '❗ BreakingChange'\n  minor:\n    labels:\n      - '✏️ Feature'\n  patch:\n    labels:\n      - '📒 Documentation'\n      - '☢️ Bug'\n      - '🤖 Dependencies'\n      - '🧹 Updates'\n      - '⚡️ Performance'\n  default: patch\ntemplate: |\n    $CHANGES\n\n    **📒 Documentation**: https://docs.gofiber.io/next/\n\n    **💬 Discord**: https://gofiber.io/discord\n\n    **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION\n\n    Thank you $CONTRIBUTORS for making this release possible.\n"
  },
  {
    "path": ".github/release.yml",
    "content": "# .github/release.yml\n\nchangelog:\n  categories:\n    - title: '❗ Breaking Changes'\n      labels:\n        - '❗ BreakingChange'\n    - title: '🚀 New Features'\n      labels:\n        - '✏️ Feature'\n        - '📝 Proposal'\n    - title: '🧹 Updates'\n      labels:\n        - '🧹 Updates'\n        - '⚡️ Performance'\n    - title: '🐛 Bug Fixes'\n      labels:\n        - '☢️ Bug'\n    - title: '🛠️ Maintenance'\n      labels:\n        - '🤖 Dependencies'\n    - title: '📚 Documentation'\n      labels:\n        - '📒 Documentation'\n    - title: 'Other Changes'\n      labels:\n        - '*'\n"
  },
  {
    "path": ".github/scripts/sync_docs.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\n# Some env variables\nBRANCH=\"main\"\nREPO_URL=\"github.com/gofiber/docs.git\"\nAUTHOR_EMAIL=\"github-actions[bot]@users.noreply.github.com\"\nAUTHOR_USERNAME=\"github-actions[bot]\"\nVERSION_FILE=\"versions.json\"\nREPO_DIR=\"core\"\nCOMMIT_URL=\"https://github.com/gofiber/fiber\"\nDOCUSAURUS_COMMAND=\"npm run docusaurus -- docs:version\"\n\n# Set commit author\ngit config --global user.email \"${AUTHOR_EMAIL}\"\ngit config --global user.name \"${AUTHOR_USERNAME}\"\n\ngit clone https://${TOKEN}@${REPO_URL} fiber-docs\n\n# Handle push event\nif [ \"$EVENT\" == \"push\" ]; then\n  latest_commit=$(git rev-parse --short HEAD)\n  #log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/)\n  #if [[ $log_output != \"\" ]]; then\n    cp -a docs/* fiber-docs/docs/${REPO_DIR}\n  #fi\n\n# Handle release event\nelif [ \"$EVENT\" == \"release\" ]; then\n  major_version=\"${TAG_NAME%%.*}\"\n  echo \"Major version: $major_version\"\n\n  # Form new version name\n  new_version=\"${major_version}.x\"\n  echo \"New version: $new_version\"\n\n  cd fiber-docs/ || true\n  npm ci\n\n  # Check if contrib_versions.json exists and modify it if required\n  if [[ -f $VERSION_FILE ]]; then\n    echo \"Modifying version file: $VERSION_FILE\"\n    jq --arg new_version \"$new_version\" 'del(.[] | select(. == $new_version))' $VERSION_FILE > temp.json && mv temp.json $VERSION_FILE\n  fi\n\n  # Run docusaurus versioning command\n  $DOCUSAURUS_COMMAND \"${new_version}\"\n\n  if [[ -f $VERSION_FILE ]]; then\n    echo \"Sorting version file: $VERSION_FILE\"\n    jq 'sort | reverse' ${VERSION_FILE} > temp.json && mv temp.json ${VERSION_FILE}\n  fi\nfi\n\n# Push changes\ncd fiber-docs/ || true\ngit add .\nif [[ $EVENT == \"push\" ]]; then\n  git commit -m \"Add docs from ${COMMIT_URL}/commit/${latest_commit}\"\nelif [[ $EVENT == \"release\" ]]; then\n  git commit -m \"Sync docs for release ${COMMIT_URL}/releases/tag/${TAG_NAME}\"\nfi\n\nMAX_RETRIES=5\nDELAY=5\nretry=0\n\nwhile ((retry < MAX_RETRIES)); do\n  git push https://${TOKEN}@${REPO_URL} && break\n  retry=$((retry + 1))\n  git pull --rebase\n  sleep $DELAY\ndone\n\nif ((retry == MAX_RETRIES)); then\n  echo \"Failed to push after $MAX_RETRIES attempts. Exiting with 1.\"\n  exit 1\nfi\n"
  },
  {
    "path": ".github/testdata/ca-chain.cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFeTCCA2GgAwIBAgIDEAISMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlVT\nMQ8wDQYDVQQIDAZEZW5pYWwxFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQK\nDANEaXMxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMjAyMDgyMDA3MjlaFw0zMjAy\nMDYyMDA3MjlaMEAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZEZW5pYWwxDDAKBgNV\nBAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOC\nAg8AMIICCgKCAgEA5Cho0kbBDi1cy8bURStc95hK2RzjBQMd2hN5gFxZdF5knBfC\nLSiPMxtAn9zJYzYc9+Cq7hIOK19cgG4yKk9GFZaUe+mU4yWxRg1ViSu/jzQ04sVc\nJRSbSklXY1RNyxpUtGelxnluUvdvuXXlCPmKob4IsUtI1FTcumG1mzIO+cAzBd1J\nKQtNTUO9XfSHYusV/FQO2hIbaXcFgSAg50JJfYZaUw51J07j3vdb6lb1x4rRmIaq\n8txrdHo0Y2tXHsq6jry1QrOZfoz4WbYcoID3JU1MC5f1HyR5uYiCA1RJVGnQ3iSX\n3yM+gRy3SFPeaASs2useSzGkMr/pDlbcSVmsbXsasBxZq85T1FE8vuY6K4XlU2sN\nPyiPrNjDgVkQ8Lbj1B9oKEYKkmSieBx9YwRLarfru1kt+g3kdXuel7DyHpm+j/13\nvqjyF9DAyx4wAEZC+DzeqBsbuiDdRkzwFMcKPxYpgSTLawnCjlFapPvE5kGN+O/j\nTo2qWbWUU/upzBvHu4tnICSapJJ0VqA+7M7yaBAsIWK/yjNTzpHfx0oHudl8wBOG\nySfOE52uouFsp2vs06YpEg2nGn/7Iu0Rbbwt4iFcSZlEnSk0cQlyMZxdj3M2fMKa\n/nrRQm7guPbVmBJOFHZuTTiilNSduSsDwCjJkGdJkSVYbj3+eJzKwYstnT8CAwEA\nAaNmMGQwHQYDVR0OBBYEFHwli1hTCVJLHPTHWW8O8BCaHci9MB8GA1UdIwQYMBaA\nFDlb/7rpDA2ZzsLZmqbW/krUtmGOMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P\nAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBtMxa2/w6kGF9cmqpTdQ1La8nY\nR4Zoewnn+SCmcSOwCyBC32g0Ry6nKKUpJBpJEid5lBzWveIw4K7pdWvmuqmeMuWI\nilvlCLzqYPigmnEIW96hc6XiQvl9NC5j+SAZSC+4uCNhEUx5pEbE1FU1gIX+szdJ\ntLdPwwg63Ce/us6QZ7Tx8qLIr+XU+DrCgjIheQFShtoNYDw0GxEtjeo8vHynj8EZ\n+p0OZgqoNlnRbQbllruDFPXDJVI23DVhNpJhT86iQDMtMV53ypMu62LXmdQIKa7l\nITnEMGO626RKqw2kDHt7yinBlt1nHskaeeLya6K/08uJkqRCjzOshJgsjQ3e62vQ\nMht9QvGBCAoY09fIGxRihtTWCFDe7MEnbh1PPYB7cZTOMnL3wxRPzLLYhclX+pt0\nbBf7Dn84b3tdC5BFXBJeZMs5QSCvn4yrTew+NvvX3oL6Ny1JDZYaG5PhKf00J6iy\nTkXzK2n9U9RX+krPk8fU9Ae1nayD0vrmGaVcBdRQPn4XUuS3LhdlkHfr28z1nF9m\nffd0WBrJlNX9SoKtsMj8VJFZ/nJ0EcCcY1mG3k/IGAY1HUeo4A+C5E/UO3h4+tOL\nuqUa8rkl9HoE4fIWdQVxtQjEdATSuJusaK7CFpWH8A0w9VchDx74saiwwGhVyXYk\nyBwSA5U88ymkQ7qNJg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIFnTCCA4WgAwIBAgIUQnfvDIm6z+973AYRTLorZHEQA30wDQYJKoZIhvcNAQEL\nBQAwVjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By\naW5nZmllbGQxDDAKBgNVBAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy\nMDIwODIwMDcyOVoXDTQyMDIwMzIwMDcyOVowVjELMAkGA1UEBhMCVVMxDzANBgNV\nBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0RpczES\nMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC\nAgEA+Cf/fKPvI5+Nh81wpxghLrpAjM1MyhdHUDXc8bu7NTNYZ+6ArMqDeKSszTWT\ngLV9EeJs57KwjwXIoYZDTcLvpjanrZ2s7JDEqsGl7S6Xr67qzYghlF/GaB3f3lAi\nGsvhmDgC4jkdCvkVBKOB4tE0dy6fnNCmIKhhJDje51p90LWFuX5sIKO2trgte6b/\nP1PW8rOjedPd5Z+QCG4Mi8JnbJicX1YaOySGMcXHm/eM2wy5I3pEdUreZDFbjB7l\nCKa1kFAnDXBiQAoErggMlcXe6C+avB13wYCfi+R+9m2X0svmerSz+oHDCOhvnD52\nEE7fBv89VS6pR8mykz1eHiBKkVT1qdmONThUQ8mqwxlo06bZyoykKSxG6ffJnGbM\nGkTWlaNEdZuY0pITQLxEX2SwLJuZKfTPFheno83bLCqTOTuWo7h1mLe0ogJMxl//\nNHzyoMJ0b1bbrRgsLcMaDn1MsI+gVdRY89+cZ2uedEgrr7fl3KFWiF2S57bxX1fJ\nP8HC0bzMny4jMtIf6YUqDtpGDPjZq3PGqcrcO0dVYkuaC96H3xLcvQxgkMDK0sm0\npUbWlECzAag/lxeeC22VnedqgCpiq/9z1b4j884ZkhIJyht0HR7L4I0gO/R/mWUY\n8bO9XCMYmP0CjO7u93IlzQ7aSIpWprTHxjpiPelmcO001jkCAwEAAaNjMGEwHQYD\nVR0OBBYEFDlb/7rpDA2ZzsLZmqbW/krUtmGOMB8GA1UdIwQYMBaAFDlb/7rpDA2Z\nzsLZmqbW/krUtmGOMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0G\nCSqGSIb3DQEBCwUAA4ICAQDk02Tu0ypqnS897WBx98l2nYIrEhcrJg8ZMmSwEa9J\n7TANofzsP9931YoQMh6BI6hB3OkyL6FYTUDykpGMMasojtL/F2iXEsjema2ilZ/7\nhNAZ+j5mBemMwXfkfmRguXvnl7EWaZETgEoxhcOTYoYUYqDcyzuwK63fOs+YA5ke\nO8E3F1aLHzLpqVpiG7t740L7LdibNPko9JOd31Gqcq3nhXMf6/rOdL8VSj/F+4BG\nopgJBruJV9NxWRI/b0G6eImvaYL/Ljfd8wzwNpmYkNkHbhAiaHeXJQ05mebmr2Dr\nwne9QeSJkXCs/K5A/8+0CYNN4homt8xNNN02SnJ5e6nv1A1ntMW9n6n2KYo87tz9\nVmqWXg7Y1BqXj287WRaWPJsBa2RBP1W2d3BQfHKJfu15blyXaczTi87WayEsBnQq\nTXy+1QP0IwQerSTOxdW25UoJmH18SRbLEIQs9Htvcpz2AncTYjeiLFa15FO2r5hP\nLYc9QOKn6yIZP9lYztleEqOLTmHnRnFcupDol+/x88d+kVLqmXDiKmWbVIz7C735\nxgImsyrCPPYYiEA7/yaP5G1o5XU93kRPrtg/7jjyF+uBZ70fcbED3prpuiJYrL0O\ngvQUgmGUU30mPHjAKkEACeRXtoqucRDxvIkBb5zUvZG8RmSFae5siAWwLD7D7VJa\nIA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": ".github/testdata/fs/css/style.css",
    "content": "h1 {\n    color: red;\n    text-align: center;\n}"
  },
  {
    "path": ".github/testdata/fs/css/test/style2.css",
    "content": "h1 {\n  color: black;\n}\n"
  },
  {
    "path": ".github/testdata/fs/index.html",
    "content": "<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link rel=\"stylesheet\" href=\"/css/style.css\">\n    <title>Document</title>\n</head>\n<body>\n    <img src=\"/img/fiber.png\" alt=\"\">\n    <h1>Hello, World!</h1>\n</body>\n</html>"
  },
  {
    "path": ".github/testdata/hello_world.tmpl",
    "content": "<h1>Hello {{ .Name }}!</h1>"
  },
  {
    "path": ".github/testdata/index.html",
    "content": "<p>Hello, Fiber!</p>"
  },
  {
    "path": ".github/testdata/index.tmpl",
    "content": "<h1>{{.Title}}</h1>"
  },
  {
    "path": ".github/testdata/main.tmpl",
    "content": "<h1>I'm main</h1>"
  },
  {
    "path": ".github/testdata/ssl.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG\n3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U\nwq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0\nFlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf\nIJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg\nGeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF\nsh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2\nsNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D\nuGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb\nK2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3\nYqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+\nDVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk\nB0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV\nBggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x\nIzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY\nwqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj\nwZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D\nFICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m\ntiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX\nfQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU\nILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk\nK/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT\n6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt\n9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN\nCj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV\nc257YgaWmjK9uB0Y2r2VxS0G\n-----END PRIVATE KEY-----"
  },
  {
    "path": ".github/testdata/ssl.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV\nBAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV\nMRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D\nK2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te\n+z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij\nL5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1\nxRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY\n6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98\nL3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2\n45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li\nK91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6\nX+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI\nwhdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd\n-----END CERTIFICATE-----"
  },
  {
    "path": ".github/testdata/template-invalid.html",
    "content": "<h1>{{.Title}</h1>\n"
  },
  {
    "path": ".github/testdata/template.tmpl",
    "content": "<h1>{{.Title}} {{.Summary}}</h1>"
  },
  {
    "path": ".github/testdata/testRoutes.json",
    "content": "{\n  \"test_routes\": [{\n      \"method\": \"GET\",\n      \"path\": \"/authorizations\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/authorizations/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/authorizations\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/authorizations/clients/inf1nd873nf8912g9t\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/authorizations/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/authorizations/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/applications/2nds981mng6azl127y/tokens\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/networks/fenny/fiber/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/received_events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/received_events/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/events/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/events/orgs/gofiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/feeds\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/notifications\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/notifications\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/notifications\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications/threads/1337\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/notifications/threads/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications/threads/1337/subscription\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/notifications/threads/1337/subscription\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/notifications/threads/1337/subscription\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stargazers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/starred/fenny/fiber\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/starred/fenny/fiber\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/starred/fenny/fiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/subscribers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/subscriptions\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/subscriptions\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/subscription\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/subscription\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/subscription\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/subscriptions/fenny/fiber\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/subscriptions/fenny/fiber\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/subscriptions/fenny/fiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/gists\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/gists\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/gists/1337\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/gists/1337/star\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/gists/1337/star\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/1337/star\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/gists/1337/forks\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/gists/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/blobs/v948b24g98ubngw9082bn02giub\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/git/blobs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/commits/v948b24g98ubngw9082bn02giub\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/git/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/refs/im/a/wildcard\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/refs\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/git/refs\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/git/refs/im/a/wildcard\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/git/refs/im/a/wildcard\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/tags/v948b24g98ubngw9082bn02giub\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/git/tags\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/git/trees/v948b24g98ubngw9082bn02giub\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/git/trees\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/1000\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/issues\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/issues/1000\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/assignees\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/assignees/nic\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/comments/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/comments\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/issues/comments/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/issues/comments/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/events/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/labels/john\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/labels\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/labels/john\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/labels/john\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/labels\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/labels\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/labels/john\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/labels\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/issues/1000/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/milestones/1000/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/milestones\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/milestones/1000\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/milestones\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/milestones/1000\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/milestones/1000\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/emojis\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gitignore/templates\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gitignore/templates/john\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/markdown\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/markdown/raw\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/meta\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/rate_limit\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/orgs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/orgs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/orgs/gofiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/members/fenny\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/orgs/gofiber/members/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/public_members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/public_members/fenny\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/orgs/gofiber/public_members/fenny\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/orgs/gofiber/public_members/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/orgs/gofiber/teams\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/teams/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/1337/members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/1337/members/fenny\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/teams/1337/members/fenny\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/1337/members/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/1337/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/1337/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/teams/1337/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/1337/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/pulls\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/files\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/merge\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/merge\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/pulls/comments/1000\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/pulls/1000/comments\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/pulls/comments/1000\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/pulls/comments/1000\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/gofiber/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repositories\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/repos\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/orgs/gofiber/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/contributors\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/languages\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/tags\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/branches\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/branches/master\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/collaborators\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/collaborators/fenny\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/collaborators/fenny\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/collaborators/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/comments/1337\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/comments/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/comments/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/readme\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/contents/im/a/wildcard\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/fenny/fiber/contents/im/a/wildcard\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/contents/im/a/wildcard\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/gzip/google\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/keys/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/keys\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/keys/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/keys/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/downloads\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/downloads/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/downloads/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/forks\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/forks\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/hooks\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/hooks/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/hooks\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/hooks/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/hooks/1337/tests\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/hooks/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/merges\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/releases\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/releases/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/releases\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/fenny/fiber/releases/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/fenny/fiber/releases/1337\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/releases/1337/assets\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stats/contributors\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stats/commit_activity\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stats/code_frequency\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stats/participation\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/stats/punch_card\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/fenny/fiber/statuses/google\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/fenny/fiber/statuses/google\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/repositories\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/code\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/users\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/issues/search/fenny/fibersitory/locked/finish\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/repos/search/finish\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/user/search/finish\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/user/email/info@gofiber.io\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/followers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/followers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/following\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/following\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/following/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/following/renan\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/following/fenny\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/following/fenny\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/fenny/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/keys/1337\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/keys\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/user/keys/1337\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/keys/1337\"\n    }\n  ],\n  \"github_api\": [{\n      \"method\": \"GET\",\n      \"path\": \"/authorizations\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/authorizations/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/authorizations\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/authorizations/clients/:client_id\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/authorizations/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/authorizations/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/applications/:client_id/tokens/:access_token\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/applications/:client_id/tokens\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/applications/:client_id/tokens/:access_token\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/networks/:owner/:repo/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/received_events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/received_events/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/events/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/events/orgs/:org\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/feeds\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/notifications\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/notifications\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/notifications\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications/threads/:id\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/notifications/threads/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/notifications/threads/:id/subscription\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/notifications/threads/:id/subscription\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/notifications/threads/:id/subscription\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stargazers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/starred/:owner/:repo\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/starred/:owner/:repo\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/starred/:owner/:repo\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/subscribers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/subscriptions\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/subscriptions\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/subscription\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/subscription\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/subscription\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/subscriptions/:owner/:repo\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/subscriptions/:owner/:repo\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/subscriptions/:owner/:repo\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/gists\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/public\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/starred\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/gists\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/gists/:id\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/gists/:id/star\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/gists/:id/star\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gists/:id/star\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/gists/:id/forks\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/gists/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/blobs/:sha\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/git/blobs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/commits/:sha\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/git/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/refs/*\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/refs\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/git/refs\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/git/refs/*\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/git/refs/*\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/tags/:sha\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/git/tags\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/git/trees/:sha\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/git/trees\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/:number\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/issues\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/issues/:number\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/assignees\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/assignees/:assignee\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/comments/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/comments\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/issues/comments/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/issues/comments/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/events\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/events/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/labels/:name\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/labels\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/labels/:name\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/labels/:name\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/labels\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/labels\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/labels/:name\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/labels\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/issues/:number/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/milestones/:number/labels\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/milestones\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/milestones/:number\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/milestones\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/milestones/:number\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/milestones/:number\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/emojis\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gitignore/templates\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/gitignore/templates/:name\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/markdown\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/markdown/raw\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/meta\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/rate_limit\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/orgs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/orgs\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/orgs/:org\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/members/:user\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/orgs/:org/members/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/public_members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/public_members/:user\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/orgs/:org/public_members/:user\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/orgs/:org/public_members/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/orgs/:org/teams\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/teams/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/:id/members\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/:id/members/:user\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/teams/:id/members/:user\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/:id/members/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/:id/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/teams/:id/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/teams/:id/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/teams/:id/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/pulls\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/files\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/merge\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/merge\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/pulls/comments/:number\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/pulls/:number/comments\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/pulls/comments/:number\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/pulls/comments/:number\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/orgs/:org/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repositories\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/repos\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/orgs/:org/repos\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/contributors\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/languages\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/teams\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/tags\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/branches\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/branches/:branch\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/collaborators\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/collaborators/:user\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/collaborators/:user\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/collaborators/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/commits/:sha/comments\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/commits/:sha/comments\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/comments/:id\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/comments/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/comments/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/commits\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/commits/:sha\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/readme\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/contents/*\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/repos/:owner/:repo/contents/*\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/contents/*\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/:archive_format/:ref\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/keys/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/keys\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/keys/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/keys/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/downloads\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/downloads/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/downloads/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/forks\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/forks\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/hooks\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/hooks/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/hooks\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/hooks/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/hooks/:id/tests\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/hooks/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/merges\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/releases\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/releases/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/releases\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/repos/:owner/:repo/releases/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/repos/:owner/:repo/releases/:id\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/releases/:id/assets\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stats/contributors\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stats/commit_activity\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stats/code_frequency\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stats/participation\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/stats/punch_card\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/repos/:owner/:repo/statuses/:ref\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/repos/:owner/:repo/statuses/:ref\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/repositories\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/code\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/issues\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/search/users\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/issues/search/:owner/:repository/:state/:keyword\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/repos/search/:keyword\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/user/search/:keyword\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/legacy/user/email/:email\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/emails\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/followers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/followers\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/following\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/following\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/following/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/following/:target_user\"\n    },\n    {\n      \"method\": \"PUT\",\n      \"path\": \"/user/following/:user\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/following/:user\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/users/:user/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/keys\"\n    },\n    {\n      \"method\": \"GET\",\n      \"path\": \"/user/keys/:id\"\n    },\n    {\n      \"method\": \"POST\",\n      \"path\": \"/user/keys\"\n    },\n    {\n      \"method\": \"PATCH\",\n      \"path\": \"/user/keys/:id\"\n    },\n    {\n      \"method\": \"DELETE\",\n      \"path\": \"/user/keys/:id\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/testdata2/bruh.tmpl",
    "content": "<h1>I'm Bruh</h1>"
  },
  {
    "path": ".github/testdata3/hello_world.tmpl",
    "content": "<h1>Hello {{ .Name }}!</h1>"
  },
  {
    "path": ".github/workflows/auto-labeler.yml",
    "content": "name: auto-labeler\n\non:\n  issues:\n    types: [opened, edited, milestoned]\n  pull_request_target:\n    types: [opened, edited, reopened, synchronize]\n\njobs:\n  auto-labeler:\n    uses: gofiber/.github/.github/workflows/auto-labeler.yml@main\n    secrets:\n      github-token: ${{ secrets.ISSUE_PR_TOKEN }}\n    with:\n      config-path: .github/labeler.yml\n      config-repository: gofiber/fiber\n"
  },
  {
    "path": ".github/workflows/benchmark.yml",
    "content": "on:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n\npermissions:\n  # deployments permission to deploy GitHub pages website\n  deployments: write\n  # contents permission to update benchmark contents in gh-pages branch\n  contents: write\n  # allow posting comments to pull request\n  pull-requests: write\n\nname: Benchmark\njobs:\n  Compare:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0 # to be able to retrieve the last commit in main\n\n      - name: Install Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          # NOTE: Keep this in sync with the version from go.mod\n          go-version: \"1.25.x\"\n\n      - name: Run Benchmark\n        run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt\n\n      - name: Remove _Parallel Benchmarks\n        run: |\n          awk '!/^Benchmark.*_Parallel/' output.txt > output_filtered.txt\n          mv output_filtered.txt output.txt\n\n      # NOTE: Benchmarks could change with different CPU types\n      - name: Get GitHub Runner System Information\n        uses: kenchan0130/actions-system-info@59699597e84e80085a750998045983daa49274c4 # v1.4.0\n        id: system-info\n\n      - name: Get Main branch SHA\n        id: get-main-branch-sha\n        run: |\n          SHA=$(git rev-parse origin/main)\n          echo \"sha=$SHA\" >> $GITHUB_OUTPUT\n\n      - name: Get Benchmark Results from main branch\n        id: cache\n        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        with:\n          path: ./cache\n          key: ${{ steps.get-main-branch-sha.outputs.sha }}-${{ runner.os }}-${{ steps.system-info.outputs.cpu-model }}-benchmark\n\n      # This will only run if we have Benchmark Results from main branch\n      - name: Compare PR Benchmark Results with main branch\n        uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0\n        if: steps.cache.outputs.cache-hit == 'true'\n        with:\n          tool: 'go'\n          output-file-path: output.txt\n          external-data-json-path: ./cache/benchmark-data.json\n          # Do not save the data (This allows comparing benchmarks)\n          save-data-file: false\n          fail-on-alert: true\n          # Comment on the PR if the branch is not a fork\n          comment-on-alert: ${{ github.event.pull_request.head.repo.fork == false }}\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          summary-always: true\n          alert-threshold: \"150%\"\n          go-force-package-suffix: true\n\n      - name: Store Benchmark Results for main branch\n        uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0\n        if: ${{ github.ref_name == 'main' }}\n        with:\n          tool: 'go'\n          output-file-path: output.txt\n          external-data-json-path: ./cache/benchmark-data.json\n          # Save the data to external file (cache)\n          save-data-file: true\n          fail-on-alert: false\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          summary-always: true\n          alert-threshold: \"150%\"\n          go-force-package-suffix: true\n\n      - name: Publish Benchmark Results to GitHub Pages\n        uses: benchmark-action/github-action-benchmark@a7bc2366eda11037936ea57d811a43b3418d3073 # v1.21.0\n        if: ${{ github.ref_name == 'main' }}\n        with:\n          tool: 'go'\n          output-file-path: output.txt\n          benchmark-data-dir-path: \"benchmarks\"\n          fail-on-alert: false\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          comment-on-alert: true\n          summary-always: true\n          # Save the data to external file (GitHub Pages)\n          save-data-file: true\n          alert-threshold: \"150%\"\n          auto-push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}\n          go-force-package-suffix: true\n\n      - name: Update Benchmark Results cache\n        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        if: ${{ github.ref_name == 'main' }}\n        with:\n          path: ./cache\n          key: ${{ steps.get-main-branch-sha.outputs.sha }}-${{ runner.os }}-${{ steps.system-info.outputs.cpu-model }}-benchmark\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n  schedule:\n    - cron: \"0 3 * * 6\"\n\njobs:\n  analyse:\n    name: Analyse\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          # We must fetch at least the immediate parents so that if this is\n          # a pull request then we can checkout the head.\n          fetch-depth: 2\n\n      # If this run was triggered by a pull request event, then checkout\n      # the head of the pull request instead of the merge commit.\n      - run: git checkout HEAD^2\n        if: ${{ github.event_name == 'pull_request' }}\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0\n        # Override language selection by uncommenting this and choosing your languages\n        with:\n          languages: go\n\n      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n      # If this step fails, then you should remove it and run the build manually (see below)\n      - name: Autobuild\n        uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0\n\n      # ℹ️ Command-line programs to run using the OS shell.\n      # 📚 https://git.io/JvXDl\n\n      # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines\n      #    and modify them (or add more) to build your code if your project\n      #    uses a compiled language\n\n      #- run: |\n      #   make bootstrap\n      #   make release\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0\n"
  },
  {
    "path": ".github/workflows/dependabot_automerge.yml",
    "content": "name: Dependabot auto-merge\non:\n  workflow_dispatch:\n  pull_request_target:\npermissions:\n  contents: write\n  pull-requests: write\njobs:\n  wait_for_checks:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      - name: Wait for check is finished\n        uses: lewagon/wait-on-check-action@v1.5.0\n        id: wait_for_checks\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n          running-workflow-name: wait_for_checks\n          check-regexp: unit\n          repo-token: ${{ secrets.PR_TOKEN }}\n          wait-interval: 10\n  dependabot:\n    needs: [wait_for_checks]\n    name: Dependabot auto-merge\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' }}\n    steps:\n      - name: Dependabot metadata\n        id: metadata\n        uses: dependabot/fetch-metadata@v2.5.0\n        with:\n          github-token: \"${{ secrets.PR_TOKEN }}\"\n      - name: Enable auto-merge for Dependabot PRs\n        if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}\n        run: |\n          gh pr review --approve \"$PR_URL\"\n          gh pr merge --auto --merge \"$PR_URL\"\n        env:\n          PR_URL: ${{github.event.pull_request.html_url}}\n          GITHUB_TOKEN: ${{secrets.PR_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/linter.yml",
    "content": "name: golangci-lint\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n\npermissions:\n  # Required: allow read access to the content for analysis.\n  contents: read\n  # Optional: allow read access to pull request. Use with `only-new-issues` option.\n  pull-requests: read\n  # Optional: Allow write access to checks to allow the action to annotate code in the PR.\n  checks: write\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          # NOTE: Keep this in sync with the version from go.mod\n          go-version: \"1.25.x\"\n          cache: false\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0\n        with:\n          # NOTE: Keep this in sync with the version from .golangci.yml\n          version: v2.5.0\n          install-mode: goinstall\n"
  },
  {
    "path": ".github/workflows/manual-dependabot.yml",
    "content": "# https://github.com/dependabot/dependabot-script/blob/main/manual-github-actions.yaml\n# https://github.com/dependabot/dependabot-script?tab=readme-ov-file#github-actions-standalone\nname: ManualDependabot\n\non:\n  workflow_dispatch:\n    inputs:\n      package-manager:\n        description: 'The package manager to use'\n        required: true\n        default: 'gomod'\n      directory:\n        description: 'The directory to scan'\n        required: true\n        default: '/'\n\npermissions:\n  contents: read\n\njobs:\n  dependabot:\n    permissions:\n      contents: write  # for Git to git push\n      pull-requests: write  # for repo-sync/pull-request to create pull requests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Checkout dependabot\n        run: |\n          cd /tmp/\n          git clone https://github.com/dependabot/dependabot-script\n\n      - name: Build image\n        run: |\n          cd /tmp/dependabot-script\n          docker build -t \"dependabot/dependabot-script\" -f Dockerfile .\n\n      - name: Run dependabot\n        env:\n          PACKAGE_MANAGER: ${{ github.event.inputs.package-manager }}\n          DIRECTORY: ${{ github.event.inputs.directory }}\n          GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          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\n"
  },
  {
    "path": ".github/workflows/markdown.yml",
    "content": "name: markdownlint\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths:\n      - \"**/*.md\"\n  pull_request:\n    paths:\n      - \"**/*.md\"\n\njobs:\n  markdownlint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Run markdownlint-cli2\n        uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0\n        with:\n          globs: |\n            **/*.md\n            #vendor\n"
  },
  {
    "path": ".github/workflows/modernize.yml",
    "content": "name: Modernize Lint\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*_msgp*.go\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*_msgp*.go\"\n\npermissions:\n  contents: read\n  pull-requests: write\n  checks: write\n\njobs:\n  modernize:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          # NOTE: Keep this in sync with the version from go.mod\n          go-version: \"1.25.x\"\n          cache: false\n\n      - name: modernize\n        run: go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test=false ./...\n"
  },
  {
    "path": ".github/workflows/move-closed-milestone-items.yml",
    "content": "name: Move closed milestone items\n\non:\n  workflow_dispatch:\n    inputs:\n      source_milestone:\n        description: Milestone that currently owns the closed items\n        required: true\n        type: string\n      target_milestone:\n        description: Milestone that should receive the closed items\n        required: true\n        type: string\n\npermissions:\n  contents: read\n  issues: write\n  pull-requests: write\n\njobs:\n  move-closed-items:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Move closed items to target milestone\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{ secrets.ISSUE_PR_TOKEN }}\n          script: |\n            const dispatchInputs = context.payload.inputs ?? {};\n            const sourceTitle = (dispatchInputs.source_milestone ?? '').trim();\n            const targetTitle = (dispatchInputs.target_milestone ?? '').trim();\n\n            if (sourceTitle.length === 0 || targetTitle.length === 0) {\n              throw new Error('Both source_milestone and target_milestone must be non-empty.');\n            }\n\n            if (sourceTitle === targetTitle) {\n              throw new Error('source_milestone and target_milestone must be different.');\n            }\n\n            const owner = context.repo.owner;\n            const repo = context.repo.repo;\n\n            async function listMilestones() {\n              return github.paginate(github.rest.issues.listMilestones, {\n                owner,\n                repo,\n                state: 'all',\n                per_page: 100,\n              });\n            }\n\n            function findMilestoneByTitle(milestones, title) {\n              return milestones.find((milestone) => milestone.title === title);\n            }\n\n            let milestones = await listMilestones();\n\n            const sourceMilestone = findMilestoneByTitle(milestones, sourceTitle);\n            if (!sourceMilestone) {\n              throw new Error(`Source milestone \"${sourceTitle}\" was not found.`);\n            }\n\n            let targetMilestone = findMilestoneByTitle(milestones, targetTitle);\n            if (!targetMilestone) {\n              const createdMilestone = await github.rest.issues.createMilestone({\n                owner,\n                repo,\n                title: targetTitle,\n              });\n              targetMilestone = createdMilestone.data;\n              core.info(`Created target milestone \"${targetTitle}\" (#${targetMilestone.number}).`);\n            } else if (targetMilestone.state !== 'open') {\n              const reopenedMilestone = await github.rest.issues.updateMilestone({\n                owner,\n                repo,\n                milestone_number: targetMilestone.number,\n                state: 'open',\n              });\n              targetMilestone = reopenedMilestone.data;\n              core.info(`Reopened target milestone \"${targetTitle}\" (#${targetMilestone.number}).`);\n            }\n\n            const closedItems = await github.paginate(github.rest.issues.listForRepo, {\n              owner,\n              repo,\n              state: 'closed',\n              milestone: String(sourceMilestone.number),\n              per_page: 100,\n            });\n\n            if (closedItems.length === 0) {\n              core.notice(`No closed items were found in milestone \"${sourceTitle}\".`);\n              return;\n            }\n\n            for (const item of closedItems) {\n              await github.rest.issues.update({\n                owner,\n                repo,\n                issue_number: item.number,\n                milestone: targetMilestone.number,\n              });\n\n              const itemType = item.pull_request ? 'pull request' : 'issue';\n              core.info(`Moved ${itemType} #${item.number} to milestone \"${targetTitle}\".`);\n            }\n\n            core.notice(\n              `Moved ${closedItems.length} closed item(s) from \"${sourceTitle}\" to \"${targetTitle}\".`,\n            );\n"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "content": "name: Release Drafter\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  update_release_draft:\n    permissions:\n      # write permission is required to create a github release\n      contents: write\n      # write permission is required for autolabeler\n      # otherwise, read permission is required at least\n      pull-requests: read\n    runs-on: ubuntu-latest\n    steps:\n      - uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1\n        with:\n          disable-autolabeler: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/spell-check.yml",
    "content": "name: Spell check\n\non:\n  workflow_dispatch:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - reopened\n      - ready_for_review\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n  pull-requests: read\n\njobs:\n  cspell:\n    name: cspell\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Node.js\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: \"20.x\"\n\n      - name: Install cspell dictionaries\n        run: |\n          npm install --no-save \\\n            @cspell/dict-en_us \\\n            @cspell/dict-en-gb \\\n            @cspell/dict-software-terms \\\n            @cspell/dict-golang \\\n            @cspell/dict-fullstack \\\n            @cspell/dict-docker \\\n            @cspell/dict-k8s \\\n            @cspell/dict-node \\\n            @cspell/dict-npm \\\n            @cspell/dict-typescript \\\n            @cspell/dict-html \\\n            @cspell/dict-css \\\n            @cspell/dict-shell \\\n            @cspell/dict-python \\\n            @cspell/dict-redis \\\n            @cspell/dict-sql \\\n            @cspell/dict-filetypes \\\n            @cspell/dict-companies \\\n            @cspell/dict-markdown \\\n            @cspell/dict-en-common-misspellings \\\n            @cspell/dict-people-names \\\n            @cspell/dict-data-science\n\n      - name: Run cspell\n        uses: streetsidesoftware/cspell-action@9cd41bb518a24fefdafd9880cbab8f0ceba04d28 # v8.3.0\n        with:\n          incremental_files_only: false\n          check_dot_files: explicit\n          report: typos\n          verbose: true\n\n      - name: Run codespell\n        uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2\n        with:\n          skip: ./.git,./node_modules,./**/*.go,./*.go,./.github/workflows/spell-check.yml\n          ignore_words_list: TE,te\n"
  },
  {
    "path": ".github/workflows/sync-docs.yml",
    "content": "name: \"Sync docs\"\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths:\n      - \"docs/**\"\n  release:\n    types: [published]\n\njobs:\n  sync-docs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          fetch-depth: 2\n\n      - name: Setup Node.js environment\n        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: \"22.x\"\n\n      - name: Sync docs\n        run: ./.github/scripts/sync_docs.sh\n        env:\n          EVENT: ${{ github.event_name }}\n          TAG_NAME: ${{ github.ref_name }}\n          TOKEN: ${{ secrets.DOC_SYNC_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n\njobs:\n  unit:\n    strategy:\n      matrix:\n        go-version: [1.25.x, 1.26.x]\n        platform: [ubuntu-latest, windows-latest, macos-latest]\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Test\n        run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -shuffle=on\n\n      - name: Upload coverage reports to Codecov\n        if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.25.x' }}\n        uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          flags: unittests\n          slug: gofiber/fiber\n          verbose: true\n\n  repeated:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: stable\n\n      - name: Test\n        run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on\n"
  },
  {
    "path": ".github/workflows/v3-label-automation.yml",
    "content": "name: Assign v3 project and milestone\n\non:\n  issues:\n    types:\n      - labeled\n  pull_request_target:\n    types:\n      - labeled\n\npermissions:\n  contents: read\n  issues: write\n  pull-requests: write\n\njobs:\n  assign-v3:\n    if: ${{ github.event.label && github.event.label.name == 'v3' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Add item to v3 project\n        uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2\n        with:\n          project-url: https://github.com/orgs/gofiber/projects/1\n          github-token: ${{ secrets.ISSUE_PR_TOKEN }}\n      - name: Assign v3 milestone\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{ secrets.ISSUE_PR_TOKEN }}\n          script: |\n            const payload = context.eventName === 'issues' ? context.payload.issue : context.payload.pull_request;\n            const issueNumber = payload.number;\n            const milestones = await github.paginate(github.rest.issues.listMilestones, {\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'open',\n              per_page: 100,\n            });\n            const milestone = milestones.find((item) => item.title === 'v3');\n            if (!milestone) {\n              throw new Error('Milestone \"v3\" was not found.');\n            }\n            await github.rest.issues.update({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: issueNumber,\n              milestone: milestone.number,\n            });\n"
  },
  {
    "path": ".github/workflows/vulncheck.yml",
    "content": "name: Run govulncheck\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n\njobs:\n  govulncheck-check:\n    runs-on: ubuntu-latest\n    env:\n      GO111MODULE: on\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Install Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"stable\"\n          check-latest: true\n          cache: false\n\n      - name: Install Govulncheck\n        run: go install golang.org/x/vuln/cmd/govulncheck@latest\n\n      - name: Run Govulncheck\n        run: govulncheck ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n*.tmp\n\n# Output of the go coverage tool\n**/*.out\n\n.cache\n.gocache\n\n# IDE files\n.vscode\n.DS_Store\n.idea\n.claude\n\n# Misc\n*.fiber.gz\n*.fiber.zst\n*.fiber.br\n*.fasthttp.gz\n*.fasthttp.zst\n*.fasthttp.br\n*.test.gz\n*.test.zst\n*.test.br\n*.pprof\n*.workspace\n\n# Dependencies\n/vendor/\nvendor/\nvendor\n/Godeps/\n\n# Local tools\nbin/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  modules-download-mode: readonly\n  allow-serial-runners: true\nlinters:\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - containedctx\n    - contextcheck\n    - copyloopvar\n    - decorder\n    - depguard\n    - dogsled\n    - dupword\n    - durationcheck\n    - err113\n    - errchkjson\n    - errname\n    - errorlint\n    - exhaustive\n    - forbidigo\n    - forcetypeassert\n    - ginkgolinter\n    - gochecksumtype\n    - goconst\n    - gocritic\n    - gomoddirectives\n    - goprintffuncname\n    - gosec\n    - grouper\n    - loggercheck\n    - makezero\n    - mirror\n    - misspell\n    - musttag\n    - nakedret\n    - nilerr\n    - nilnil\n    # - noctx # TODO: enable this once the codebase is migrated to context aware APIs\n    - nolintlint\n    - nonamedreturns\n    - nosprintfhostport\n    - perfsprint\n    - predeclared\n    - promlinter\n    - protogetter\n    - reassign\n    - revive\n    - rowserrcheck\n    - sloglint\n    - spancheck\n    - sqlclosecheck\n    - staticcheck\n    - tagliatelle\n    - testableexamples\n    - testifylint\n    - thelper\n    - tparallel\n    - unconvert\n    - unparam\n    - usestdlibvars\n    - whitespace\n    - wrapcheck\n    - zerologlint\n  settings:\n    depguard:\n      rules:\n        all:\n          list-mode: lax\n          deny:\n            - pkg: flag\n              desc: \"`flag` package is only allowed in main.go\"\n            - pkg: log\n              desc: logging is provided by `pkg/log`\n            - pkg: io/ioutil\n              desc: \"`io/ioutil` package is deprecated, use the `io` and `os` package instead\"\n    errcheck:\n      disable-default-exclusions: true\n      check-type-assertions: true\n      check-blank: true\n      exclude-functions:\n        - (*bytes.Buffer).Write\n        - (*github.com/valyala/bytebufferpool.ByteBuffer).Write\n        - (*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte\n        - (*github.com/valyala/bytebufferpool.ByteBuffer).WriteString\n    errchkjson:\n      report-no-exported: true\n    exhaustive:\n      default-signifies-exhaustive: true\n    forbidigo:\n      forbid:\n        - pattern: ^print(ln)?$\n        - pattern: ^fmt\\.Print(f|ln)?$\n        - pattern: ^http\\.Default(Client|ServeMux|Transport)$\n      analyze-types: true\n    goconst:\n      numbers: true\n    gocritic:\n      enabled-tags:\n        - diagnostic\n        - style\n        - performance\n      settings:\n        captLocal:\n          paramsOnly: false\n        elseif:\n          skipBalanced: false\n        underef:\n          skipRecvDeref: false\n    gosec:\n      excludes:\n        - G104\n      config:\n        global:\n          audit: true\n    govet:\n      enable-all: true\n    grouper:\n      import-require-single-import: true\n      import-require-grouping: true\n    loggercheck:\n      require-string-key: true\n      no-printf-like: true\n    misspell:\n      locale: US\n    nolintlint:\n      require-explanation: true\n      require-specific: true\n    nonamedreturns:\n      report-error-in-defer: true\n    perfsprint:\n      err-error: true\n    predeclared:\n      qualified-name: true\n    promlinter:\n      strict: true\n    revive:\n      enable-all-rules: true\n      rules:\n        - name: add-constant\n          disabled: true\n        - name: argument-limit\n          disabled: true\n        - name: banned-characters\n          disabled: true\n        - name: cognitive-complexity\n          disabled: true\n        - name: confusing-results\n          disabled: true\n        - name: comment-spacings\n          arguments:\n            - nolint\n          disabled: true\n        - name: cyclomatic\n          disabled: true\n        - name: enforce-slice-style\n          arguments:\n            - make\n          disabled: true\n        - name: exported\n          disabled: true\n        - name: file-header\n          disabled: true\n        - name: function-result-limit\n          arguments:\n            - 3\n        - name: function-length\n          disabled: true\n        - name: line-length-limit\n          disabled: true\n        - name: max-public-structs\n          disabled: true\n        - name: modifies-parameter\n          disabled: true\n        - name: nested-structs\n          disabled: true\n        - name: package-comments\n          disabled: true\n        - name: optimize-operands-order\n          disabled: true\n        - name: unchecked-type-assertion\n          disabled: true\n        - name: unhandled-error\n          disabled: true\n    staticcheck:\n      checks:\n        - all\n        - -ST1000\n        - -ST1020\n        - -ST1021\n        - -ST1022\n    tagalign:\n      strict: true\n    tagliatelle:\n      case:\n        rules:\n          json: snake\n    testifylint:\n      enable-all: true\n    testpackage:\n      skip-regexp: ^$\n    unparam:\n      check-exported: false\n    unused:\n      field-writes-are-uses: true\n      exported-fields-are-used: true\n    usestdlibvars:\n      http-method: true\n      http-status-code: true\n      time-weekday: false\n      time-month: false\n      time-layout: false\n      crypto-hash: true\n      default-rpc-path: true\n      sql-isolation-level: true\n      tls-signature-scheme: true\n      constant-kind: true\n    wrapcheck:\n      ignore-package-globs:\n        - github.com/gofiber/fiber/*\n        - github.com/valyala/fasthttp\n  exclusions:\n    generated: lax\n    rules:\n      - text: (?i)do not define dynamic errors, use wrapped static errors instead*\n        linters:\n          - err113\n      - path: log/.*\\.go\n        linters:\n          - depguard\n      - path: _test\\.go\n        linters:\n          - bodyclose\n          - err113\n          - goconst # disabling goconst in test files only\n      - source: (?i)fmt.Fprintf?\n        linters:\n          - errcheck\n          - revive\n    paths:\n      - _msgp\\.go\n      - _msgp_test\\.go\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofmt\n    - gofumpt\n    - goimports\n  settings:\n    gci:\n      sections:\n        - standard\n        - prefix(github.com/gofiber/fiber)\n        - default\n        - blank\n        - dot\n      custom-order: true\n    gofumpt:\n      module-path: github.com/gofiber/fiber\n      extra-rules: true\n  exclusions:\n    generated: lax\n    paths:\n      - _msgp\\.go\n      - _msgp_test\\.go\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".markdownlint.yml",
    "content": "# Example markdownlint configuration with all properties set to their default value\n\n# Default state for all rules\ndefault: true\n\n# Path to configuration file to extend\nextends: null\n\n# 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\n# NOTE: The docs intentionally jump heading levels for anchor stability, so skip this rule globally.\nMD001: false\n\n# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md003.md\nMD003:\n  # Heading style\n  style: \"consistent\"\n\n# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md004.md\nMD004:\n  # List style\n  style: \"consistent\"\n\n# MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md005.md\nMD005: true\n\n# MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md007.md\nMD007:\n  # Spaces for indent\n  indent: \n  # Whether to indent the first level of the list\n  start_indented: false\n  # Spaces for first level indent (when start_indented is set)\n  start_indent: 2\n\n# MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md009.md\nMD009:\n  # Spaces for line break\n  br_spaces: 2\n  # Allow spaces for empty lines in list items\n  list_item_empty_lines: false\n  # Include unnecessary breaks\n  strict: true\n\n# MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md010.md\nMD010:\n  # Include code blocks\n  code_blocks: true\n  # Fenced code languages to ignore\n  ignore_code_languages: []\n  # Number of spaces for each hard tab\n  spaces_per_tab: 4\n\n# MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md011.md\nMD011: true\n\n# MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md\nMD012:\n  # Consecutive blank lines\n  maximum: 1\n\n# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md\nMD013: false\n\n# MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md014.md\nMD014: true\n\n# 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\nMD018: true\n\n# 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\nMD019: true\n\n# 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\nMD020: true\n\n# 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\nMD021: true\n\n# MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md022.md\nMD022:\n  # Blank lines above heading\n  lines_above: 1\n  # Blank lines below heading\n  lines_below: 1\n\n# 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\nMD023: true\n\n# MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md\nMD024: false\n\n# 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\nMD025:\n  # Heading level\n  level: 1\n  # RegExp for matching title in front matter\n  front_matter_title: \"^\\\\s*title\\\\s*[:=]\"\n\n# MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md026.md\nMD026:\n  # Punctuation characters\n  punctuation: \".,;:!。，；：！\"\n\n# MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md027.md\nMD027: true\n\n# MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md\nMD028: true\n\n# MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md\nMD029:\n  # List style\n  style: \"one_or_ordered\"\n\n# MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md030.md\nMD030:\n  # Spaces for single-line unordered list items\n  ul_single: 1\n  # Spaces for single-line ordered list items\n  ol_single: 1\n  # Spaces for multi-line unordered list items\n  ul_multi: 1\n  # Spaces for multi-line ordered list items\n  ol_multi: 1\n\n# 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\nMD031:\n  # Include list items\n  list_items: true\n\n# MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md032.md\nMD032: true\n\n# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md033.md\nMD033: false\n\n# MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md\nMD034: true\n\n# MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md035.md\nMD035:\n  # Horizontal rule style\n  style: \"consistent\"\n\n# MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md036.md\nMD036:\n  # Punctuation characters\n  punctuation: \".,;:!?。，；：！？\"\n\n# MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md037.md\nMD037: true\n\n# MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md038.md\nMD038: true\n\n# MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md039.md\nMD039: true\n\n# MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md040.md\nMD040:\n  # List of languages\n  allowed_languages: []\n  # Require language only\n  language_only: false\n\n# 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\nMD041:\n  # Heading level\n  level: 1\n  # RegExp for matching title in front matter\n  front_matter_title: \"^\\\\s*title\\\\s*[:=]\"\n\n# MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md042.md\nMD042: true\n\n# MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md043.md\nMD043: false\n\n# MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md044.md\nMD044:\n  # List of proper names\n  names: []\n  # Include code blocks\n  code_blocks: true\n  # Include HTML elements\n  html_elements: true\n\n# MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md045.md\nMD045: false\n\n# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md046.md\nMD046:\n  # Block style\n  style: \"fenced\"\n\n# MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md047.md\nMD047: true\n\n# MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md048.md\nMD048:\n  # Code fence style\n  style: \"backtick\"\n\n# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md049.md\nMD049:\n  # Emphasis style\n  style: \"consistent\"\n\n# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md050.md\nMD050:\n  # Strong style\n  style: \"consistent\"\n\n# MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md\nMD051: true\n\n# 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\nMD052:\n  # Include shortcut syntax\n  shortcut_syntax: false\n\n# 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\nMD053:\n  # Ignored definitions\n  ignored_definitions:\n    - \"//\"\n\n# MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md054.md\nMD054:\n  # Allow autolinks\n  autolink: false\n  # Allow inline links and images\n  inline: true\n  # Allow full reference links and images\n  full: true\n  # Allow collapsed reference links and images\n  collapsed: true\n  # Allow shortcut reference links and images\n  shortcut: true\n  # Allow URLs as inline links\n  url_inline: true\n\n# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md055.md\nMD055:\n  # Table pipe style\n  style: \"consistent\"\n\n# MD060/table-column-style : Table column style : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md060.md\n# NOTE: Legacy docs rely on hand-crafted alignment, so disable this rule to avoid noisy warnings.\nMD060: false\n\n# MD056/table-column-count : Table column count : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md056.md\nMD056: true\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\n## Agent Instructions\n\n> **Note for the AI agent:** Before running the startup script, verify that all dependencies and CLI tools are already available. If a `vendor/` directory exists **or** `go list -m -mod=readonly all` succeeds (indicating modules are cached), you may skip the entire startup script—including all `go mod` commands and `go install` steps. Otherwise, run the script as listed below.\n\n### General coding practices\n\n- When adding Go tests, always invoke `t.Parallel()` at the start of each test and subtest to maximize concurrency.\n- Prefer `github.com/gofiber/utils/v2` helpers (for example, `utils.Trim`) when performing common operations such as string manipulation, whenever it is practical and appropriate for the surrounding code.\n- Keep all protocol behavior RFC-compliant (e.g., HTTP/1.1 requirements) and document any intentional deviations.\n- Protect hot paths from regressions: profile changes.\n- Apply secure-by-default choices (validation, timeouts, sanitization) and ensure new code hardens attack surfaces.\n\n---\n\n## Startup script (reference only – do not run)\n\n- Fetch dependencies:\n\n  ```bash\n  go mod tidy && go mod download && go mod vendor\n  ```\n\n- Install CLI tools referenced in Makefile:\n\n  ```bash\n  go install gotest.tools/gotestsum@latest                 # test runner\n  go install golang.org/x/vuln/cmd/govulncheck@latest      # vulnerability scanner\n  go install mvdan.cc/gofumpt@latest                       # code formatter\n  go install github.com/tinylib/msgp@latest                # msgp codegen\n  go install github.com/vburenin/ifacemaker@f30b6f9bdbed4b5c4804ec9ba4a04a999525c202  # interface impls\n  go install github.com/dkorunic/betteralign/cmd/betteralign@latest  # struct alignment\n  go install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest\n  go mod tidy                                              # clean up go.mod & go.sum\n  ```\n\n## Makefile commands\n\nUse `make help` to list all available commands. Common targets include:\n\n- **audit**: run `go mod verify`, `go vet`, and `govulncheck` for quality checks.\n- **benchmark**: run benchmarks with `go test`.\n- **coverage**: generate a coverage report.\n- **format**: apply formatting using `gofumpt`.\n- **lint**: execute `golangci-lint`.\n- **test**: run the test suite with `gotestsum`.\n- **longtest**: run the test suite 15 times with shuffling enabled.\n- **tidy**: clean and tidy dependencies.\n- **betteralign**: optimize struct field alignment.\n- **generate**: run `go generate` after installing msgp and ifacemaker.\n- **modernize**: run golps modernize\n\nThese targets can be invoked via `make <target>` as needed during development and testing.\n\n## Pull request guidelines\n\n- PR titles must start with a category prefix describing the change: `🐛 bug:`, `🔥 feat:`, `📒 docs:`, or `🧹 chore:`.\n- Generated PR titles and bodies must summarize the *entire* set of changes on the branch (for example, based on `git log --oneline <base>..HEAD` or the full diff), **not** just the latest commit. The Summary section should reflect all modifications that will be merged.\n\n## Programmatic checks\n\nBefore presenting final changes or submitting a pull request, run each of the\nfollowing commands and ensure they succeed. Include the command outputs in your\nfinal response to confirm they were executed:\n\n```bash\nmake audit\nmake generate\nmake betteralign\nmake modernize\nmake format\nmake lint\nmake test\n```\n\nAll checks must pass before the generated code can be merged.\n\nAfter completing the programmatic checks above, confirm that any relevant\ndocumentation has been updated to reflect the changes made, including PR\ninstructions when applicable.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\r\n\r\nCopyright (c) 2019-present Fenny and Contributors\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy\r\nof this software and associated documentation files (the \"Software\"), to deal\r\nin the Software without restriction, including without limitation the rights\r\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\ncopies of the Software, and to permit persons to whom the Software is\r\nfurnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all\r\ncopies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\nSOFTWARE.\r\n"
  },
  {
    "path": "Makefile",
    "content": "GOVERSION ?= $(shell go env GOVERSION)\n\n## help: 💡 Display available commands\n.PHONY: help\nhelp:\n\t@echo '⚡️ GoFiber/Fiber Development:'\n\t@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' |  sed -e 's/^/ /'\n\n## audit: 🚀 Conduct quality checks\n.PHONY: audit\naudit:\n\tgo mod verify\n\tgo vet ./...\n\tGOTOOLCHAIN=$(GOVERSION) go run golang.org/x/vuln/cmd/govulncheck@latest ./...\n\n## benchmark: 📈 Benchmark code performance\n.PHONY: benchmark\nbenchmark:\n\tgo test ./... -benchmem -bench=. -run=^Benchmark_$\n\n## coverage: ☂️  Generate coverage report\n.PHONY: coverage\ncoverage:\n\tGOTOOLCHAIN=$(GOVERSION) go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=/tmp/coverage.out -covermode=atomic\n\tgo tool cover -html=/tmp/coverage.out\n\n## format: 🎨 Fix code format issues\n.PHONY: format\nformat:\n\tGOTOOLCHAIN=$(GOVERSION) go run mvdan.cc/gofumpt@latest -w -l .\n\n## markdown: 🎨 Find markdown format issues (Requires markdownlint-cli2)\n.PHONY: markdown\nmarkdown:\n\t@which markdownlint-cli2 > /dev/null || npm install -g markdownlint-cli2\n\tmarkdownlint-cli2 \"**/*.md\" \"#vendor\"\n\n## lint: 🚨 Run lint checks\n.PHONY: lint\nlint:\n\tGOTOOLCHAIN=$(GOVERSION) go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 run ./...\n\n## modernize: 🛠 Run gopls modernize\n.PHONY: modernize\nmodernize:\n\tGOTOOLCHAIN=$(GOVERSION) go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test=false ./...\n\n## test: 🚦 Execute all tests\n.PHONY: test\ntest:\n\tGOTOOLCHAIN=$(GOVERSION) go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on\n\n## longtest: 🚦 Execute all tests 10x\n.PHONY: longtest\nlongtest:\n\tGOTOOLCHAIN=$(GOVERSION) go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on\n\n## tidy: 📌 Clean and tidy dependencies\n.PHONY: tidy\ntidy:\n\tgo mod tidy -v\n\n## betteralign: 📐 Optimize alignment of fields in structs\n.PHONY: betteralign\nbetteralign:\n\tGOTOOLCHAIN=$(GOVERSION) go run github.com/dkorunic/betteralign/cmd/betteralign@v0.8.0 -test_files -generated_files -apply ./...\n\n## generate: ⚡️ Generate msgp && interface implementations\n.PHONY: generate\ngenerate:\n\tgo install github.com/tinylib/msgp@latest\n\tgo install github.com/vburenin/ifacemaker@f30b6f9bdbed4b5c4804ec9ba4a04a999525c202\n\tgo generate ./...\n\n# actionspin: 🤖 Bulk replace GitHub actions references from version tags to commit hashes\n.PHONY: actionspin\nactionspin:\n\tGOTOOLCHAIN=$(GOVERSION) go run github.com/mashiike/actionspin/cmd/actionspin@latest\n"
  },
  {
    "path": "adapter.go",
    "content": "package fiber\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttpadaptor\"\n)\n\n// toFiberHandler converts a supported handler type to a Fiber handler.\nfunc toFiberHandler(handler any) (Handler, bool) {\n\tif handler == nil {\n\t\treturn nil, false\n\t}\n\n\tswitch handler.(type) {\n\tcase Handler, func(Ctx): // (1)-(2) Fiber handlers\n\t\treturn adaptFiberHandler(handler)\n\tcase func(Req, Res) error, func(Req, Res), func(Req, Res, func() error) error, func(Req, Res, func() error), func(Req, Res, func()) error, func(Req, Res, func()), func(Req, Res, func(error)), func(Req, Res, func(error)) error, func(Req, Res, func(error) error), func(Req, Res, func(error) error) error: // (3)-(12) Express-style request handlers\n\t\treturn adaptExpressHandler(handler)\n\tcase http.HandlerFunc, http.Handler, func(http.ResponseWriter, *http.Request): // (13)-(15) net/http handlers\n\t\treturn adaptHTTPHandler(handler)\n\tcase fasthttp.RequestHandler, func(*fasthttp.RequestCtx) error: // (16)-(17) fasthttp handlers\n\t\treturn adaptFastHTTPHandler(handler)\n\tdefault: // (18) unsupported handler type\n\t\treturn nil, false\n\t}\n}\n\nfunc adaptFiberHandler(handler any) (Handler, bool) {\n\tswitch h := handler.(type) {\n\tcase Handler: // (1) direct Fiber handler\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn h, true\n\tcase func(Ctx): // (2) Fiber handler without error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\th(c)\n\t\t\treturn nil\n\t\t}, true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n\nfunc adaptExpressHandler(handler any) (Handler, bool) {\n\tswitch h := handler.(type) {\n\tcase func(Req, Res) error: // (3) Express-style handler with error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\treturn h(c.Req(), c.Res())\n\t\t}, true\n\tcase func(Req, Res): // (4) Express-style handler without error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\th(c.Req(), c.Res())\n\t\t\treturn nil\n\t\t}, true\n\tcase func(Req, Res, func() error) error: // (5) Express-style handler with error-returning next callback and error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\treturn h(c.Req(), c.Res(), func() error {\n\t\t\t\treturn c.Next()\n\t\t\t})\n\t\t}, true\n\tcase func(Req, Res, func() error): // (6) Express-style handler with error-returning next callback\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\th(c.Req(), c.Res(), func() error {\n\t\t\t\tnextErr = c.Next()\n\t\t\t\treturn nextErr\n\t\t\t})\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func()) error: // (7) Express-style handler with no-arg next callback and error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\terr := h(c.Req(), c.Res(), func() {\n\t\t\t\tnextErr = c.Next()\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func()): // (8) Express-style handler with no-arg next callback\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\th(c.Req(), c.Res(), func() {\n\t\t\t\tnextErr = c.Next()\n\t\t\t})\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func(error)): // (9) Express-style handler with error-accepting next callback\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\th(c.Req(), c.Res(), func(err error) {\n\t\t\t\tif err != nil {\n\t\t\t\t\tnextErr = err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tnextErr = c.Next()\n\t\t\t})\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func(error)) error: // (10) Express-style handler with error-accepting next callback and error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\terr := h(c.Req(), c.Res(), func(nextErrArg error) {\n\t\t\t\tif nextErrArg != nil {\n\t\t\t\t\tnextErr = nextErrArg\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tnextErr = c.Next()\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func(error) error): // (11) Express-style handler with error-accepting next callback that returns an error\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\th(c.Req(), c.Res(), func(nextErrArg error) error {\n\t\t\t\tif nextErrArg != nil {\n\t\t\t\t\tnextErr = nextErrArg\n\t\t\t\t\treturn nextErrArg\n\t\t\t\t}\n\t\t\t\tnextErr = c.Next()\n\t\t\t\treturn nextErr\n\t\t\t})\n\t\t\treturn nextErr\n\t\t}, true\n\tcase func(Req, Res, func(error) error) error: // (12) Express-style handler with error-accepting next callback that returns an error and error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\tvar nextErr error\n\t\t\terr := h(c.Req(), c.Res(), func(nextErrArg error) error {\n\t\t\t\tif nextErrArg != nil {\n\t\t\t\t\tnextErr = nextErrArg\n\t\t\t\t\treturn nextErrArg\n\t\t\t\t}\n\t\t\t\tnextErr = c.Next()\n\t\t\t\treturn nextErr\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nextErr\n\t\t}, true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n\nfunc adaptHTTPHandler(handler any) (Handler, bool) {\n\tswitch h := handler.(type) {\n\tcase http.HandlerFunc: // (13) net/http HandlerFunc\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn wrapHTTPHandler(h), true\n\tcase http.Handler: // (14) net/http Handler implementation\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\thv := reflect.ValueOf(h)\n\t\tif isNilableKind(hv.Kind()) && hv.IsNil() {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn wrapHTTPHandler(h), true\n\tcase func(http.ResponseWriter, *http.Request): // (15) net/http function handler\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn wrapHTTPHandler(http.HandlerFunc(h)), true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n\nfunc isNilableKind(kind reflect.Kind) bool {\n\tswitch kind {\n\tcase reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice, reflect.UnsafePointer:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc adaptFastHTTPHandler(handler any) (Handler, bool) {\n\tswitch h := handler.(type) {\n\tcase fasthttp.RequestHandler: // (16) fasthttp handler\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\th(c.RequestCtx())\n\t\t\treturn nil\n\t\t}, true\n\tcase func(*fasthttp.RequestCtx) error: // (17) fasthttp handler with error return\n\t\tif h == nil {\n\t\t\treturn nil, false\n\t\t}\n\t\treturn func(c Ctx) error {\n\t\t\treturn h(c.RequestCtx())\n\t\t}, true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n\n// wrapHTTPHandler adapts a net/http handler to a Fiber handler.\nfunc wrapHTTPHandler(handler http.Handler) Handler {\n\tif handler == nil {\n\t\treturn nil\n\t}\n\n\tadapted := fasthttpadaptor.NewFastHTTPHandler(handler)\n\n\treturn func(c Ctx) error {\n\t\tadapted(c.RequestCtx())\n\t\treturn nil\n\t}\n}\n\n// collectHandlers converts a slice of handler arguments to Fiber handlers.\n// The context string is used to provide informative panic messages when an\n// unsupported handler type is encountered.\nfunc collectHandlers(context string, args ...any) []Handler {\n\thandlers := make([]Handler, 0, len(args))\n\n\tfor i, arg := range args {\n\t\thandler, ok := toFiberHandler(arg)\n\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"%s: invalid handler #%d (%T)\\n\", context, i, arg))\n\t\t}\n\t\thandlers = append(handlers, handler)\n\t}\n\n\treturn handlers\n}\n"
  },
  {
    "path": "adapter_test.go",
    "content": "package fiber\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc TestToFiberHandler_Nil(t *testing.T) {\n\tt.Parallel()\n\n\tvar handler Handler\n\tconverted, ok := toFiberHandler(handler)\n\trequire.False(t, ok)\n\trequire.Nil(t, converted)\n}\n\nfunc TestToFiberHandler_FiberHandler(t *testing.T) {\n\tt.Parallel()\n\n\tfiberHandler := func(c Ctx) error { return c.SendStatus(http.StatusAccepted) }\n\n\tconverted, ok := toFiberHandler(fiberHandler)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\trequire.Equal(t, reflect.ValueOf(fiberHandler).Pointer(), reflect.ValueOf(converted).Pointer())\n}\n\nfunc TestToFiberHandler_FiberHandlerNoErrorReturn(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(c Ctx) {\n\t\trequire.Equal(t, app, c.App())\n\t\tc.Set(\"X-Handler\", \"ok\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\n\trequire.NoError(t, converted(ctx))\n\trequire.Equal(t, \"ok\", string(ctx.Response().Header.Peek(\"X-Handler\")))\n}\n\nfunc TestNewTestCtx_ReturnsDefaultCtx(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\trequire.NotNil(t, app)\n\trequire.NotNil(t, ctx)\n\trequire.Equal(t, app, ctx.App())\n}\n\nfunc newTestCtx(t *testing.T) (*App, *DefaultCtx) {\n\tt.Helper()\n\n\tapp := New()\n\tfasthttpCtx := &fasthttp.RequestCtx{}\n\tcustomCtx := app.AcquireCtx(fasthttpCtx)\n\tctx, ok := customCtx.(*DefaultCtx)\n\trequire.True(t, ok)\n\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(customCtx)\n\t})\n\n\treturn app, ctx\n}\n\nfunc withRouteHandlers(t *testing.T, ctx *DefaultCtx, handlers ...Handler) {\n\tt.Helper()\n\n\tctx.route = &Route{Handlers: handlers}\n\tctx.indexHandler = 0\n\tt.Cleanup(func() {\n\t\tctx.route = nil\n\t\tctx.indexHandler = 0\n\t})\n}\n\nfunc TestToFiberHandler_ExpressTwoParamsWithError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\treturn res.SendString(\"express\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\trequire.NoError(t, converted(ctx))\n\trequire.Equal(t, \"express\", string(ctx.Response().Body()))\n}\n\nfunc TestToFiberHandler_ExpressTwoParamsWithoutError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res) {\n\t\tassert.Equal(t, app, req.App())\n\t\trequire.NoError(t, res.SendStatus(http.StatusCreated))\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\trequire.NoError(t, converted(ctx))\n\trequire.Equal(t, http.StatusCreated, ctx.Response().StatusCode())\n}\n\nfunc TestToFiberHandler_ExpressThreeParamsWithError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func() error) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\treturn next()\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextErr := errors.New(\"next\")\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nextErr\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.ErrorIs(t, err, nextErr)\n\trequire.True(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressThreeParamsWithoutError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, _ Res, next func() error) {\n\t\tassert.Equal(t, app, req.App())\n\t\terr := next()\n\t\trequire.Error(t, err)\n\t\tassert.EqualError(t, err, \"next without error\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextHandler := func(_ Ctx) error {\n\t\treturn errors.New(\"next without error\")\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"next without error\")\n}\n\nfunc TestToFiberHandler_ExpressNextNoArgWithErrorReturn(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func()) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\tnext()\n\t\treturn nil\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextErr := errors.New(\"next without return value\")\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nextErr\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.ErrorIs(t, err, nextErr)\n\trequire.True(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorContinuesOnNil(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error)) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\tnext(nil)\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorShortCircuitsOnError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error)) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\tnext(errors.New(\"next error\"))\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"next error\")\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturn_ShortCircuitsOnNextError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error)) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\tnext(errors.New(\"next error\"))\n\t\treturn nil\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"next error\")\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturnCallback_PropagatesNextError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error) error) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\trequire.EqualError(t, next(nil), \"next error\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextErr := errors.New(\"next error\")\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nextErr\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.ErrorIs(t, err, nextErr)\n\trequire.True(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturnCallback_ShortCircuitsOnNextError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error) error) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\trequire.EqualError(t, next(errors.New(\"next error\")), \"next error\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"next error\")\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturn_PrefersHandlerErrorOverNextError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error) error) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\trequire.EqualError(t, next(errors.New(\"next error\")), \"next error\")\n\t\treturn errors.New(\"handler error\")\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"handler error\")\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturn_PropagatesNextErrorWhenNoReturnError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func(error) error) error {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\treturn next(errors.New(\"next error\"))\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nil\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"next error\")\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextWithErrorReturnCallback_StopsChainWithoutNextCall(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, _ func(error) error) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\t// Intentionally do not call next.\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn errors.New(\"should not be called\")\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.False(t, nextCalled)\n}\n\nfunc TestAdapter_MixedHandlerIntegration(t *testing.T) {\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\tc.Set(\"X-Middleware\", \"fiber\")\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(func(_ Req, res Res, next func() error) error {\n\t\tres.Set(\"X-Express\", \"middleware\")\n\t\treturn next()\n\t})\n\n\tapp.Get(\"/fiber\", func(c Ctx) error {\n\t\tc.Set(\"X-Route\", \"fiber\")\n\t\treturn c.SendString(\"fiber handler\")\n\t})\n\n\tapp.Post(\"/express\", func(_ Req, res Res) error {\n\t\tres.Set(\"X-Route\", \"express\")\n\t\treturn res.SendString(\"express handler\")\n\t})\n\n\tvar httpHandlerWriteErr error\n\tapp.Put(\"/http\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"X-Route\", \"http\")\n\t\tw.WriteHeader(http.StatusAccepted)\n\t\t_, httpHandlerWriteErr = w.Write([]byte(\"http handler\"))\n\t})\n\n\tapp.Delete(\"/fasthttp\", func(ctx *fasthttp.RequestCtx) error {\n\t\tctx.Response.Header.Set(\"X-Route\", \"fasthttp\")\n\t\tctx.SetStatusCode(http.StatusCreated)\n\t\tctx.SetBodyString(\"fasthttp handler\")\n\t\treturn nil\n\t})\n\n\trun := func(name string, buildRequest func() *http.Request, expectStatus int, expectBody, expectRoute string) {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := buildRequest()\n\n\t\t\tresp, err := app.Test(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t\t})\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, expectStatus, resp.StatusCode)\n\t\t\trequire.Equal(t, expectBody, string(body))\n\t\t\trequire.Equal(t, \"fiber\", resp.Header.Get(\"X-Middleware\"))\n\t\t\trequire.Equal(t, \"middleware\", resp.Header.Get(\"X-Express\"))\n\t\t\trequire.Equal(t, expectRoute, resp.Header.Get(\"X-Route\"))\n\t\t})\n\t}\n\n\trun(\"fiber\", func() *http.Request {\n\t\treturn httptest.NewRequest(http.MethodGet, \"/fiber\", http.NoBody)\n\t}, http.StatusOK, \"fiber handler\", \"fiber\")\n\n\trun(\"express\", func() *http.Request {\n\t\treturn httptest.NewRequest(http.MethodPost, \"/express\", http.NoBody)\n\t}, http.StatusOK, \"express handler\", \"express\")\n\n\trun(\"net/http\", func() *http.Request {\n\t\treturn httptest.NewRequest(http.MethodPut, \"/http\", http.NoBody)\n\t}, http.StatusAccepted, \"http handler\", \"http\")\n\n\trequire.NoError(t, httpHandlerWriteErr)\n\n\trun(\"fasthttp\", func() *http.Request {\n\t\treturn httptest.NewRequest(http.MethodDelete, \"/fasthttp\", http.NoBody)\n\t}, http.StatusCreated, \"fasthttp handler\", \"fasthttp\")\n}\n\nfunc TestToFiberHandler_ExpressNextNoArgPropagatesError(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, next func()) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\tnext()\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextErr := errors.New(\"next without return value\")\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn nextErr\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.ErrorIs(t, err, nextErr)\n\trequire.True(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextNoArgStopsChain(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ctx := newTestCtx(t)\n\n\thandler := func(req Req, res Res, _ func()) {\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t\t// Intentionally do not call next().\n\t}\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\n\tnextCalled := false\n\tnextHandler := func(_ Ctx) error {\n\t\tnextCalled = true\n\t\treturn errors.New(\"should not be called\")\n\t}\n\n\twithRouteHandlers(t, ctx, converted, nextHandler)\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.False(t, nextCalled)\n}\n\nfunc TestToFiberHandler_ExpressNextNoArgMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\tcallOrder := make([]string, 0, 2)\n\n\tapp.Use(func(req Req, res Res, next func()) {\n\t\tcallOrder = append(callOrder, \"middleware\")\n\t\tnext()\n\t\tassert.Equal(t, app, req.App())\n\t\tassert.Equal(t, app, res.App())\n\t})\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tcallOrder = append(callOrder, \"handler\")\n\t\treturn c.SendStatus(http.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\trequire.NoError(t, resp.Body.Close())\n\n\trequire.Equal(t, []string{\"middleware\", \"handler\"}, callOrder)\n}\n\nfunc TestCollectHandlers_HTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\tvar writeErr error\n\thttpHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"X-HTTP\", \"ok\")\n\t\tw.WriteHeader(http.StatusTeapot)\n\t\t_, writeErr = w.Write([]byte(\"http\"))\n\t})\n\n\thandlers := collectHandlers(\"test\", httpHandler)\n\trequire.Len(t, handlers, 1)\n\tconverted := handlers[0]\n\trequire.NotNil(t, converted)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusTeapot, ctx.Response().StatusCode())\n\trequire.Equal(t, \"ok\", string(ctx.Response().Header.Peek(\"X-HTTP\")))\n\trequire.Equal(t, \"http\", string(ctx.Response().Body()))\n\trequire.NoError(t, writeErr)\n}\n\nfunc TestToFiberHandler_HTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\tvar writeErr error\n\tvar handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"X-HTTP\", \"handler\")\n\t\t_, writeErr = w.Write([]byte(\"through\"))\n\t})\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"handler\", string(ctx.Response().Header.Peek(\"X-HTTP\")))\n\trequire.Equal(t, \"through\", string(ctx.Response().Body()))\n\trequire.NoError(t, writeErr)\n}\n\nfunc TestToFiberHandler_FasthttpHandlerWithError(t *testing.T) {\n\tt.Parallel()\n\n\t_, ctx := newTestCtx(t)\n\n\tfasthttpHandler := func(fctx *fasthttp.RequestCtx) error {\n\t\tfctx.Response.Header.Set(\"X-FASTHTTP\", \"error\")\n\t\treturn errors.New(\"fasthttp error\")\n\t}\n\n\tconverted, ok := toFiberHandler(fasthttpHandler)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\n\terr := converted(ctx)\n\trequire.EqualError(t, err, \"fasthttp error\")\n\trequire.Equal(t, \"error\", string(ctx.Response().Header.Peek(\"X-FASTHTTP\")))\n}\n\nfunc TestToFiberHandler_HTTPHandler_Flush(t *testing.T) {\n\tt.Parallel()\n\n\tvar handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"X-HTTP\", \"handler\")\n\t\t_, err := w.Write([]byte(\"through\"))\n\t\tflusher, ok := w.(http.Flusher)\n\t\tassert.True(t, ok, \"w does not implement http.Flusher\")\n\t\tflusher.Flush()\n\t\tassert.NoError(t, err)\n\t})\n\n\tconverted, ok := toFiberHandler(handler)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"handler\", string(ctx.Response().Header.Peek(\"X-HTTP\")))\n\trequire.Equal(t, \"through\", string(ctx.Response().Body()))\n}\n\nfunc TestWrapHTTPHandler_Flush_App_Test(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\tt.Fatal(\"w does not implement http.Flusher\")\n\t\t}\n\t\tw.WriteHeader(StatusOK)\n\t\tfmt.Fprintf(w, \"Hello \")\n\t\tflusher.Flush()\n\t\tfmt.Fprintf(w, \"World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // not needed\n\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello World!\", string(body))\n}\n\nfunc Test_HTTPHandler_App_Test_Interrupted(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\tt.Fatal(\"w does not implement http.Flusher\")\n\t\t}\n\t\tw.WriteHeader(StatusOK)\n\t\tfmt.Fprintf(w, \"Hello \")\n\t\tflusher.Flush()\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tfmt.Fprintf(w, \"World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout:       200 * time.Millisecond,\n\t\tFailOnTimeout: true, // Changed to true to test interrupted behavior\n\t})\n\t// With FailOnTimeout: true, we should get a timeout error\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n\trequire.Nil(t, resp)\n}\n\nfunc TestToFiberHandler_HTTPHandlerFunc(t *testing.T) {\n\tt.Parallel()\n\n\thttpFunc := func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}\n\n\tconverted, ok := toFiberHandler(httpFunc)\n\trequire.True(t, ok)\n\trequire.NotNil(t, converted)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\terr := converted(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusNoContent, ctx.Response().StatusCode())\n}\n\nfunc TestWrapHTTPHandler_Nil(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Nil(t, wrapHTTPHandler(nil))\n}\n\nfunc TestCollectHandlers_InvalidType(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithValue(t, \"context: invalid handler #0 (int)\\n\", func() {\n\t\tcollectHandlers(\"context\", 42)\n\t})\n}\n\nfunc TestCollectHandlers_TypedNilHTTPHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tvar handlerFunc http.HandlerFunc\n\tvar handler http.Handler\n\tvar raw func(http.ResponseWriter, *http.Request)\n\n\ttests := []struct {\n\t\thandler any\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname:    \"HandlerFunc\",\n\t\t\thandler: handlerFunc,\n\t\t},\n\t\t{\n\t\t\tname:    \"Handler\",\n\t\t\thandler: handler,\n\t\t},\n\t\t{\n\t\t\tname:    \"Function\",\n\t\t\thandler: raw,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\texpected := fmt.Sprintf(\"context: invalid handler #0 (%T)\\n\", tt.handler)\n\n\t\t\trequire.PanicsWithValue(t, expected, func() {\n\t\t\t\tcollectHandlers(\"context\", tt.handler)\n\t\t\t})\n\t\t})\n\t}\n}\n\ntype dummyHandler struct{}\n\nfunc (dummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}\n\ntype dummyFuncHandler func(http.ResponseWriter, *http.Request)\n\nfunc (handler dummyFuncHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif handler == nil {\n\t\treturn\n\t}\n\thandler(w, r)\n}\n\nfunc TestCollectHandlers_TypedNilPointerHTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\tvar handler http.Handler = (*dummyHandler)(nil)\n\n\trequire.PanicsWithValue(t, \"context: invalid handler #0 (*fiber.dummyHandler)\\n\", func() {\n\t\tcollectHandlers(\"context\", handler)\n\t})\n}\n\nfunc TestCollectHandlers_TypedNilFuncHTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\tvar handler http.Handler = dummyFuncHandler(nil)\n\texpected := fmt.Sprintf(\"context: invalid handler #0 (%T)\\n\", handler)\n\n\trequire.PanicsWithValue(t, expected, func() {\n\t\tcollectHandlers(\"context\", handler)\n\t})\n}\n\nfunc TestCollectHandlers_TypedNilFasthttpHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tvar requestHandler fasthttp.RequestHandler\n\tvar requestHandlerWithError func(*fasthttp.RequestCtx) error\n\n\ttests := []struct {\n\t\thandler any\n\t\tname    string\n\t}{\n\t\t{\n\t\t\tname:    \"RequestHandler\",\n\t\t\thandler: requestHandler,\n\t\t},\n\t\t{\n\t\t\tname:    \"RequestHandlerWithError\",\n\t\t\thandler: requestHandlerWithError,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\texpected := fmt.Sprintf(\"context: invalid handler #0 (%T)\\n\", tt.handler)\n\n\t\t\trequire.PanicsWithValue(t, expected, func() {\n\t\t\t\tcollectHandlers(\"context\", tt.handler)\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestCollectHandlers_FasthttpHandler(t *testing.T) {\n\tt.Parallel()\n\n\tbefore := func(c Ctx) error {\n\t\tc.Set(\"X-Before\", \"fiber\")\n\t\treturn nil\n\t}\n\n\tfasthttpHandler := fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {\n\t\tctx.Response.Header.Set(\"X-FASTHTTP\", \"ok\")\n\t\tctx.SetBody([]byte(\"done\"))\n\t})\n\n\thandlers := collectHandlers(\"fasthttp\", before, fasthttpHandler)\n\trequire.Len(t, handlers, 2)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\tfor _, handler := range handlers {\n\t\trequire.NoError(t, handler(ctx))\n\t}\n\n\trequire.Equal(t, \"fiber\", string(ctx.Response().Header.Peek(\"X-Before\")))\n\trequire.Equal(t, \"ok\", string(ctx.Response().Header.Peek(\"X-FASTHTTP\")))\n\trequire.Equal(t, \"done\", string(ctx.Response().Body()))\n}\n\nfunc TestCollectHandlers_FiberHandlerNoErrorReturn(t *testing.T) {\n\tt.Parallel()\n\n\tnoError := func(c Ctx) {\n\t\tc.Set(\"X-Handler\", \"fiber\")\n\t}\n\n\thandlers := collectHandlers(\"ctx\", noError)\n\trequire.Len(t, handlers, 1)\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\trequire.NoError(t, handlers[0](ctx))\n\trequire.Equal(t, \"fiber\", string(ctx.Response().Header.Peek(\"X-Handler\")))\n}\n\nfunc TestCollectHandlers_MixedHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tbefore := func(c Ctx) error {\n\t\tc.Set(\"X-Before\", \"fiber\")\n\t\treturn nil\n\t}\n\tvar writeErr error\n\thttpHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t_, writeErr = w.Write([]byte(\"done\"))\n\t})\n\n\thandlers := collectHandlers(\"test\", before, httpHandler)\n\trequire.Len(t, handlers, 2)\n\trequire.Equal(t, reflect.ValueOf(before).Pointer(), reflect.ValueOf(handlers[0]).Pointer())\n\trequire.NotNil(t, handlers[1])\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\terr := handlers[0](ctx)\n\trequire.NoError(t, err)\n\n\terr = handlers[1](ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"done\", string(ctx.Response().Body()))\n\trequire.Equal(t, \"fiber\", string(ctx.Response().Header.Peek(\"X-Before\")))\n\trequire.NoError(t, writeErr)\n}\n\nfunc TestCollectHandlers_Nil(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithValue(t, \"nil: invalid handler #0 (<nil>)\\n\", func() {\n\t\tcollectHandlers(\"nil\", nil)\n\t})\n}\n"
  },
  {
    "path": "addon/retry/README.md",
    "content": "# Retry Addon\n\nRetry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network\noperations. This addon uses an exponential backoff algorithm with jitter. It calls the function multiple times and tries\nto make it successful. If all calls are failed, then, it returns an error. It adds a jitter at each retry step because adding\na jitter is a way to break synchronization across the client and avoid collision.\n\n## Table of Contents\n\n- [Retry Addon](#retry-addon)\n- [Table of Contents](#table-of-contents)\n- [Signatures](#signatures)\n- [Examples](#examples)\n- [Default Config](#default-config)\n- [Custom Config](#custom-config)\n- [Config](#config)\n- [Default Config Example](#default-config-example)\n\n## Signatures\n\n```go\nfunc NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff\n```\n\n## Examples\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3/addon/retry\"\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    expBackoff := retry.NewExponentialBackoff(retry.Config{})\n\n    // Local variables that will be used inside of Retry\n    var resp *client.Response\n    var err error\n\n    // Retry a network request and return an error to signify to try again\n    err = expBackoff.Retry(func() error {\n        client := client.New()\n        resp, err = client.Get(\"https://gofiber.io\")\n        if err != nil {\n            return fmt.Errorf(\"GET gofiber.io failed: %w\", err)\n        }\n        if resp.StatusCode() != 200 {\n            return fmt.Errorf(\"GET gofiber.io did not return OK 200\")\n        }\n        return nil\n    })\n\n    // If all retries failed, panic\n    if err != nil {\n        panic(err)\n    }\n    fmt.Printf(\"GET gofiber.io succeeded with status code %d\\n\", resp.StatusCode())\n}\n```\n\n## Default Config\n\n```go\nretry.NewExponentialBackoff()\n```\n\n## Custom Config\n\n```go\nretry.NewExponentialBackoff(retry.Config{\n    InitialInterval: 2 * time.Second,\n    MaxBackoffTime:  64 * time.Second,\n    Multiplier:      2.0,\n    MaxRetryCount:   15,\n})\n```\n\n## Config\n\n```go\n// Config defines the config for addon.\ntype Config struct {\n    // InitialInterval defines the initial time interval for backoff algorithm.\n    //\n    // Optional. Default: 1 * time.Second\n    InitialInterval time.Duration\n\n    // MaxBackoffTime defines maximum time duration for backoff algorithm. When\n    // the algorithm is reached this time, rest of the retries will be maximum\n    // 32 seconds.\n    //\n    // Optional. Default: 32 * time.Second\n    MaxBackoffTime time.Duration\n\n    // Multiplier defines multiplier number of the backoff algorithm.\n    //\n    // Optional. Default: 2.0\n    Multiplier float64\n\n    // MaxRetryCount defines maximum retry count for the backoff algorithm.\n    //\n    // Optional. Default: 10\n    MaxRetryCount int\n}\n```\n\n## Default Config Example\n\n```go\n// DefaultConfig is the default config for retry.\nvar DefaultConfig = Config{\n    InitialInterval: 1 * time.Second,\n    MaxBackoffTime:  32 * time.Second,\n    Multiplier:      2.0,\n    MaxRetryCount:   10,\n    currentInterval: 1 * time.Second,\n}\n```\n"
  },
  {
    "path": "addon/retry/config.go",
    "content": "package retry\n\nimport (\n\t\"time\"\n)\n\n// Config defines the config for addon.\ntype Config struct {\n\t// InitialInterval defines the initial time interval for backoff algorithm.\n\t//\n\t// Optional. Default: 1 * time.Second\n\tInitialInterval time.Duration\n\n\t// MaxBackoffTime defines maximum time duration for backoff algorithm. When\n\t// the algorithm is reached this time, rest of the retries will be maximum\n\t// 32 seconds.\n\t//\n\t// Optional. Default: 32 * time.Second\n\tMaxBackoffTime time.Duration\n\n\t// Multiplier defines multiplier number of the backoff algorithm.\n\t//\n\t// Optional. Default: 2.0\n\tMultiplier float64\n\n\t// MaxRetryCount defines maximum retry count for the backoff algorithm.\n\t//\n\t// Optional. Default: 10\n\tMaxRetryCount int\n\n\t// currentInterval tracks the current waiting time.\n\t//\n\t// Optional. Default: 1 * time.Second\n\tcurrentInterval time.Duration\n}\n\n// DefaultConfig is the default config for retry.\nvar DefaultConfig = Config{\n\tInitialInterval: 1 * time.Second,\n\tMaxBackoffTime:  32 * time.Second,\n\tMultiplier:      2.0,\n\tMaxRetryCount:   10,\n\tcurrentInterval: 1 * time.Second,\n}\n\n// configDefault sets the config values if they are not set.\nfunc configDefault(config ...Config) Config {\n\tif len(config) == 0 {\n\t\treturn DefaultConfig\n\t}\n\tcfg := config[0]\n\tif cfg.InitialInterval == 0 {\n\t\tcfg.InitialInterval = DefaultConfig.InitialInterval\n\t}\n\tif cfg.MaxBackoffTime == 0 {\n\t\tcfg.MaxBackoffTime = DefaultConfig.MaxBackoffTime\n\t}\n\tif cfg.Multiplier <= 0 {\n\t\tcfg.Multiplier = DefaultConfig.Multiplier\n\t}\n\tif cfg.MaxRetryCount <= 0 {\n\t\tcfg.MaxRetryCount = DefaultConfig.MaxRetryCount\n\t}\n\tif cfg.currentInterval == 0 {\n\t\tcfg.currentInterval = cfg.InitialInterval\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "addon/retry/config_test.go",
    "content": "package retry\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigDefault_NoConfig(t *testing.T) {\n\tt.Parallel()\n\tcfg := configDefault()\n\trequire.Equal(t, DefaultConfig, cfg)\n}\n\nfunc TestConfigDefault_Custom(t *testing.T) {\n\tt.Parallel()\n\tcustom := Config{\n\t\tInitialInterval: 2 * time.Second,\n\t\tMaxBackoffTime:  64 * time.Second,\n\t\tMultiplier:      3.0,\n\t\tMaxRetryCount:   5,\n\t\tcurrentInterval: 2 * time.Second,\n\t}\n\tcfg := configDefault(custom)\n\trequire.Equal(t, custom, cfg)\n}\n\nfunc TestConfigDefault_PartialAndNegative(t *testing.T) {\n\tt.Parallel()\n\tcfg := configDefault(Config{Multiplier: -1, MaxRetryCount: 0})\n\trequire.Equal(t, DefaultConfig, cfg)\n}\n\nfunc TestConfigDefault_CustomInitialInterval(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{InitialInterval: 5 * time.Second})\n\n\trequire.Equal(t, 5*time.Second, cfg.currentInterval)\n\trequire.Equal(t, 5*time.Second, cfg.InitialInterval)\n}\n\nfunc TestConfigDefault_CustomCurrentInterval(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{currentInterval: 3 * time.Second})\n\n\trequire.Equal(t, 3*time.Second, cfg.currentInterval)\n\trequire.Equal(t, DefaultConfig.InitialInterval, cfg.InitialInterval)\n}\n\nfunc TestConfigDefault_CurrentIntervalAndInitialDiffer(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{InitialInterval: 5 * time.Second, currentInterval: 3 * time.Second})\n\n\trequire.Equal(t, 5*time.Second, cfg.InitialInterval)\n\trequire.Equal(t, 3*time.Second, cfg.currentInterval)\n}\n\nfunc TestNewExponentialBackoff_Config(t *testing.T) {\n\tt.Parallel()\n\n\tbackoff := NewExponentialBackoff(Config{InitialInterval: 4 * time.Second})\n\n\trequire.Equal(t, 4*time.Second, backoff.InitialInterval)\n\trequire.Equal(t, 4*time.Second, backoff.currentInterval)\n}\n"
  },
  {
    "path": "addon/retry/exponential_backoff.go",
    "content": "package retry\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n\t\"time\"\n)\n\n// ExponentialBackoff is a retry mechanism for retrying some calls.\ntype ExponentialBackoff struct {\n\t// InitialInterval is the initial time interval for backoff algorithm.\n\tInitialInterval time.Duration\n\n\t// MaxBackoffTime is the maximum time duration for backoff algorithm. It limits\n\t// the maximum sleep time.\n\tMaxBackoffTime time.Duration\n\n\t// Multiplier is a multiplier number of the backoff algorithm.\n\tMultiplier float64\n\n\t// MaxRetryCount is the maximum number of retry count.\n\tMaxRetryCount int\n\n\t// currentInterval tracks the current sleep time.\n\tcurrentInterval time.Duration\n}\n\n// NewExponentialBackoff creates a ExponentialBackoff with default values.\nfunc NewExponentialBackoff(config ...Config) *ExponentialBackoff {\n\tcfg := configDefault(config...)\n\treturn &ExponentialBackoff{\n\t\tInitialInterval: cfg.InitialInterval,\n\t\tMaxBackoffTime:  cfg.MaxBackoffTime,\n\t\tMultiplier:      cfg.Multiplier,\n\t\tMaxRetryCount:   cfg.MaxRetryCount,\n\t\tcurrentInterval: cfg.currentInterval,\n\t}\n}\n\n// Retry is the core logic of the retry mechanism. If the calling function returns\n// nil as an error, then the Retry method is terminated with returning nil. Otherwise,\n// if all function calls are returned error, then the method returns this error.\nfunc (e *ExponentialBackoff) Retry(f func() error) error {\n\tif e.currentInterval <= 0 {\n\t\te.currentInterval = e.InitialInterval\n\t}\n\tvar err error\n\tfor i := 0; i < e.MaxRetryCount; i++ {\n\t\terr = f()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tif i < e.MaxRetryCount-1 {\n\t\t\tnext := e.next()\n\t\t\ttime.Sleep(next)\n\t\t}\n\t}\n\treturn err\n}\n\n// next calculates the next sleeping time interval.\nfunc (e *ExponentialBackoff) next() time.Duration {\n\t// generate a random value between [0, 1000)\n\tn, err := rand.Int(rand.Reader, big.NewInt(1000))\n\tif err != nil {\n\t\treturn e.MaxBackoffTime\n\t}\n\tt := e.currentInterval + (time.Duration(n.Int64()) * time.Millisecond)\n\te.currentInterval = time.Duration(float64(e.currentInterval) * e.Multiplier)\n\tif t >= e.MaxBackoffTime {\n\t\te.currentInterval = e.MaxBackoffTime\n\t\treturn e.MaxBackoffTime\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "addon/retry/exponential_backoff_test.go",
    "content": "package retry\n\nimport (\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_ExponentialBackoff_Retry(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\texpErr     error\n\t\texpBackoff *ExponentialBackoff\n\t\tf          func() error\n\t\tname       string\n\t}{\n\t\t{\n\t\t\tname:       \"With default values - successful\",\n\t\t\texpBackoff: NewExponentialBackoff(),\n\t\t\tf: func() error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Successful function\",\n\t\t\texpBackoff: &ExponentialBackoff{\n\t\t\t\tInitialInterval: 1 * time.Millisecond,\n\t\t\t\tMaxBackoffTime:  100 * time.Millisecond,\n\t\t\t\tMultiplier:      2.0,\n\t\t\t\tMaxRetryCount:   5,\n\t\t\t},\n\t\t\tf: func() error {\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Unsuccessful function\",\n\t\t\texpBackoff: &ExponentialBackoff{\n\t\t\t\tInitialInterval: 2 * time.Millisecond,\n\t\t\t\tMaxBackoffTime:  100 * time.Millisecond,\n\t\t\t\tMultiplier:      2.0,\n\t\t\t\tMaxRetryCount:   5,\n\t\t\t},\n\t\t\tf: func() error {\n\t\t\t\treturn errors.New(\"failed function\")\n\t\t\t},\n\t\t\texpErr: errors.New(\"failed function\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := tt.expBackoff.Retry(tt.f)\n\t\t\trequire.Equal(t, tt.expErr, err)\n\t\t})\n\t}\n}\n\nfunc Test_ExponentialBackoff_Retry_NoSleepAfterLastAttempt(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tlargeInterval = 5 * time.Second // would be used for sleep if bug existed\n\t\tmaxAcceptable = 2 * time.Second // Retry must return well before largeInterval\n\t)\n\n\teb := &ExponentialBackoff{\n\t\tInitialInterval: largeInterval,\n\t\tMaxBackoffTime:  largeInterval * 2,\n\t\tMultiplier:      2.0,\n\t\tMaxRetryCount:   1,\n\t}\n\n\tstart := time.Now()\n\terr := eb.Retry(func() error { return errors.New(\"only attempt\") })\n\telapsed := time.Since(start)\n\n\trequire.Error(t, err)\n\trequire.Equal(t, \"only attempt\", err.Error())\n\trequire.Less(t, elapsed, maxAcceptable,\n\t\t\"Retry must not sleep after the last failed attempt; took %v (expected < %v)\", elapsed, maxAcceptable)\n}\n\nfunc Test_ExponentialBackoff_Next(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname                 string\n\t\texpBackoff           *ExponentialBackoff\n\t\texpNextTimeIntervals []time.Duration\n\t}{\n\t\t{\n\t\t\tname:       \"With default values\",\n\t\t\texpBackoff: NewExponentialBackoff(),\n\t\t\texpNextTimeIntervals: []time.Duration{\n\t\t\t\t1 * time.Second,\n\t\t\t\t2 * time.Second,\n\t\t\t\t4 * time.Second,\n\t\t\t\t8 * time.Second,\n\t\t\t\t16 * time.Second,\n\t\t\t\t32 * time.Second,\n\t\t\t\t32 * time.Second,\n\t\t\t\t32 * time.Second,\n\t\t\t\t32 * time.Second,\n\t\t\t\t32 * time.Second,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Custom values\",\n\t\t\texpBackoff: &ExponentialBackoff{\n\t\t\t\tInitialInterval: 2.0 * time.Second,\n\t\t\t\tMaxBackoffTime:  64 * time.Second,\n\t\t\t\tMultiplier:      3.0,\n\t\t\t\tMaxRetryCount:   8,\n\t\t\t\tcurrentInterval: 2.0 * time.Second,\n\t\t\t},\n\t\t\texpNextTimeIntervals: []time.Duration{\n\t\t\t\t2 * time.Second,\n\t\t\t\t6 * time.Second,\n\t\t\t\t18 * time.Second,\n\t\t\t\t54 * time.Second,\n\t\t\t\t64 * time.Second,\n\t\t\t\t64 * time.Second,\n\t\t\t\t64 * time.Second,\n\t\t\t\t64 * time.Second,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tfor i := range tt.expBackoff.MaxRetryCount {\n\t\t\t\tnext := tt.expBackoff.next()\n\t\t\t\tif next < tt.expNextTimeIntervals[i] || next > tt.expNextTimeIntervals[i]+1*time.Second {\n\t\t\t\t\tt.Errorf(\"wrong next time:\\n\"+\n\t\t\t\t\t\t\"actual:%v\\n\"+\n\t\t\t\t\t\t\"expected range:%v-%v\\n\",\n\t\t\t\t\t\tnext, tt.expNextTimeIntervals[i], tt.expNextTimeIntervals[i]+1*time.Second)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ExponentialBackoff_NextRandFailure(t *testing.T) {\n\t// Backup original reader and restore at the end\n\toriginal := rand.Reader\n\tdefer func() { rand.Reader = original }()\n\trand.Reader = failingReader{}\n\n\texpBackoff := &ExponentialBackoff{\n\t\tInitialInterval: 1 * time.Second,\n\t\tMaxBackoffTime:  10 * time.Second,\n\t\tMultiplier:      2,\n\t\tMaxRetryCount:   3,\n\t\tcurrentInterval: 1 * time.Second,\n\t}\n\tnext := expBackoff.next()\n\trequire.Equal(t, expBackoff.MaxBackoffTime, next)\n\t// currentInterval should not change when random fails\n\trequire.Equal(t, 1*time.Second, expBackoff.currentInterval)\n}\n\ntype failingReader struct{}\n\nfunc (failingReader) Read(_ []byte) (int, error) { return 0, errors.New(\"fail\") }\n"
  },
  {
    "path": "app.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\n// Package fiber is an Express inspired web framework built on top of Fasthttp,\n// the fastest HTTP engine for Go. Designed to ease things up for fast\n// development with zero memory allocation and performance in mind.\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3/binder\"\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\n// Version of current fiber package\nconst Version = \"3.1.0\"\n\n// Handler defines a function to serve HTTP requests.\ntype Handler = func(Ctx) error\n\n// Map is a shortcut for map[string]any, useful for JSON returns\ntype Map map[string]any\n\n// ErrorHandler defines a function that will process all errors\n// returned from any handlers in the stack\n//\n//\tcfg := fiber.Config{}\n//\tcfg.ErrorHandler = func(c Ctx, err error) error {\n//\t code := StatusInternalServerError\n//\t var e *fiber.Error\n//\t if errors.As(err, &e) {\n//\t   code = e.Code\n//\t }\n//\t c.Set(HeaderContentType, MIMETextPlainCharsetUTF8)\n//\t return c.Status(code).SendString(err.Error())\n//\t}\n//\tapp := fiber.New(cfg)\ntype ErrorHandler = func(Ctx, error) error\n\n// Error represents an error that occurred while handling a request.\ntype Error struct {\n\tMessage string `json:\"message\"`\n\tCode    int    `json:\"code\"`\n}\n\n// App denotes the Fiber application.\ntype App struct {\n\t// App config\n\tconfig Config\n\t// Indicates if the value was explicitly configured\n\tconfigured Config\n\t// Ctx pool\n\tpool sync.Pool\n\t// Fasthttp server\n\tserver *fasthttp.Server\n\t// Converts string to a byte slice\n\ttoBytes func(s string) (b []byte)\n\t// Converts byte slice to a string\n\ttoString func(b []byte) string\n\t// Hooks\n\thooks *Hooks\n\t// Latest route & group\n\tlatestRoute *Route\n\t// newCtxFunc\n\tnewCtxFunc func(app *App) CustomCtx\n\t// TLS handler\n\ttlsHandler *TLSHandler\n\t// Mount fields\n\tmountFields *mountFields\n\t// state management\n\tstate *State\n\t// Route stack divided by HTTP methods\n\tstack [][]*Route\n\t// customConstraints is a list of external constraints\n\tcustomConstraints []CustomConstraint\n\t// sendfiles stores configurations for handling ctx.SendFile operations\n\tsendfiles []*sendFileStore\n\t// custom binders\n\tcustomBinders []CustomBinder\n\t// Route stack divided by HTTP methods and route prefixes\n\ttreeStack []map[int][]*Route\n\t// sendfilesMutex is a mutex used for sendfile operations\n\tsendfilesMutex sync.RWMutex\n\tmutex          sync.Mutex\n\t// Amount of registered handlers\n\thandlersCount uint32\n\t// contains the information if the route stack has been changed to build the optimized tree\n\troutesRefreshed bool\n\t// hasCustomCtx tracks whether app uses a custom context implementation\n\thasCustomCtx bool\n}\n\n// Config is a struct holding the server settings.\ntype Config struct { //nolint:govet // Aligning the struct fields is not necessary. betteralign:ignore\n\t// Enables the \"Server: value\" HTTP header.\n\t//\n\t// Default: \"\"\n\tServerHeader string `json:\"server_header\"`\n\n\t// When set to true, the router treats \"/foo\" and \"/foo/\" as different.\n\t// By default this is disabled and both \"/foo\" and \"/foo/\" will execute the same handler.\n\t//\n\t// Default: false\n\tStrictRouting bool `json:\"strict_routing\"`\n\n\t// When set to true, enables case-sensitive routing.\n\t// E.g. \"/FoO\" and \"/foo\" are treated as different routes.\n\t// By default this is disabled and both \"/FoO\" and \"/foo\" will execute the same handler.\n\t//\n\t// Default: false\n\tCaseSensitive bool `json:\"case_sensitive\"`\n\n\t// When set to true, disables automatic registration of HEAD routes for\n\t// every GET route.\n\t//\n\t// Default: false\n\tDisableHeadAutoRegister bool `json:\"disable_head_auto_register\"`\n\n\t// When set to true, this relinquishes the 0-allocation promise in certain\n\t// cases in order to access the handler values (e.g. request bodies) in an\n\t// immutable fashion so that these values are available even if you return\n\t// from handler.\n\t//\n\t// Default: false\n\tImmutable bool `json:\"immutable\"`\n\n\t// When set to true, converts all encoded characters in the route back\n\t// before setting the path for the context, so that the routing,\n\t// the returning of the current url from the context `ctx.Path()`\n\t// and the parameters `ctx.Params(%key%)` with decoded characters will work\n\t//\n\t// Default: false\n\tUnescapePath bool `json:\"unescape_path\"`\n\n\t// Max body size that the server accepts.\n\t// Zero or negative values fall back to the default limit.\n\t//\n\t// Default: 4 * 1024 * 1024\n\tBodyLimit int `json:\"body_limit\"`\n\n\t// MaxRanges sets the maximum number of ranges parsed from a Range header.\n\t// Zero or negative values fall back to the default limit.\n\t//\n\t// Default: 16\n\tMaxRanges int `json:\"max_ranges\"`\n\n\t// Maximum number of concurrent connections.\n\t//\n\t// Default: 256 * 1024\n\tConcurrency int `json:\"concurrency\"`\n\n\t// Views is the interface that wraps the Render function.\n\t//\n\t// Default: nil\n\tViews Views `json:\"-\"`\n\n\t// Views Layout is the global layout for all template render until override on Render function.\n\t//\n\t// Default: \"\"\n\tViewsLayout string `json:\"views_layout\"`\n\n\t// PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine\n\t//\n\t// Default: false\n\tPassLocalsToViews bool `json:\"pass_locals_to_views\"`\n\n\t// PassLocalsToContext controls whether StoreInContext also propagates values to\n\t// the request context.Context for Fiber-backed contexts.\n\t//\n\t// ValueFromContext for Fiber-backed contexts always reads from c.Locals().\n\t//\n\t// Default: false\n\tPassLocalsToContext bool `json:\"pass_locals_to_context\"`\n\n\t// The amount of time allowed to read the full request including body.\n\t// It is reset after the request handler has returned.\n\t// The connection's read deadline is reset when the connection opens.\n\t//\n\t// Default: unlimited\n\tReadTimeout time.Duration `json:\"read_timeout\"`\n\n\t// The maximum duration before timing out writes of the response.\n\t// It is reset after the request handler has returned.\n\t//\n\t// Default: unlimited\n\tWriteTimeout time.Duration `json:\"write_timeout\"`\n\n\t// The maximum amount of time to wait for the next request when keep-alive is enabled.\n\t// If IdleTimeout is zero, the value of ReadTimeout is used.\n\t//\n\t// Default: unlimited\n\tIdleTimeout time.Duration `json:\"idle_timeout\"`\n\n\t// Per-connection buffer size for requests' reading.\n\t// This also limits the maximum header size.\n\t// Increase this buffer if your clients send multi-KB RequestURIs\n\t// and/or multi-KB headers (for example, BIG cookies).\n\t//\n\t// Default: 4096\n\tReadBufferSize int `json:\"read_buffer_size\"`\n\n\t// Per-connection buffer size for responses' writing.\n\t//\n\t// Default: 4096\n\tWriteBufferSize int `json:\"write_buffer_size\"`\n\n\t// CompressedFileSuffixes adds suffix to the original file name and\n\t// tries saving the resulting compressed file under the new file name.\n\t//\n\t// Default: map[string]string{\"gzip\": \".fiber.gz\", \"br\": \".fiber.br\", \"zstd\": \".fiber.zst\"}\n\tCompressedFileSuffixes map[string]string `json:\"compressed_file_suffixes\"`\n\n\t// ProxyHeader will enable c.IP() to return the value of the given header key\n\t// By default c.IP() will return the Remote IP from the TCP connection\n\t// This property can be useful if you are behind a load balancer: X-Forwarded-*\n\t// NOTE: headers are easily spoofed and the detected IP addresses are unreliable.\n\t//\n\t// Default: \"\"\n\tProxyHeader string `json:\"proxy_header\"`\n\n\t// GETOnly rejects all non-GET requests if set to true.\n\t// This option is useful as anti-DoS protection for servers\n\t// accepting only GET requests. The request size is limited\n\t// by ReadBufferSize if GETOnly is set.\n\t//\n\t// Default: false\n\tGETOnly bool `json:\"get_only\"`\n\n\t// ErrorHandler is executed when an error is returned from fiber.Handler.\n\t//\n\t// Default: DefaultErrorHandler\n\tErrorHandler ErrorHandler `json:\"-\"`\n\n\t// When set to true, disables keep-alive connections.\n\t// The server will close incoming connections after sending the first response to client.\n\t//\n\t// Default: false\n\tDisableKeepalive bool `json:\"disable_keepalive\"`\n\n\t// When set to true, causes the default date header to be excluded from the response.\n\t//\n\t// Default: false\n\tDisableDefaultDate bool `json:\"disable_default_date\"`\n\n\t// When set to true, causes the default Content-Type header to be excluded from the response.\n\t//\n\t// Default: false\n\tDisableDefaultContentType bool `json:\"disable_default_content_type\"`\n\n\t// When set to true, disables header normalization.\n\t// By default all header names are normalized: conteNT-tYPE -> Content-Type.\n\t//\n\t// Default: false\n\tDisableHeaderNormalizing bool `json:\"disable_header_normalizing\"`\n\n\t// This function allows to setup app name for the app\n\t//\n\t// Default: nil\n\tAppName string `json:\"app_name\"`\n\n\t// StreamRequestBody enables request body streaming,\n\t// and calls the handler sooner when given body is\n\t// larger than the current limit.\n\t//\n\t// Default: false\n\tStreamRequestBody bool\n\n\t// Will not pre parse Multipart Form data if set to true.\n\t//\n\t// This option is useful for servers that desire to treat\n\t// multipart form data as a binary blob, or choose when to parse the data.\n\t//\n\t// Server pre parses multipart form data by default.\n\t//\n\t// Default: false\n\tDisablePreParseMultipartForm bool\n\n\t// Aggressively reduces memory usage at the cost of higher CPU usage\n\t// if set to true.\n\t//\n\t// Try enabling this option only if the server consumes too much memory\n\t// serving mostly idle keep-alive connections. This may reduce memory\n\t// usage by more than 50%.\n\t//\n\t// Default: false\n\tReduceMemoryUsage bool `json:\"reduce_memory_usage\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// JSONMarshal\n\t//\n\t// Allowing for flexibility in using another json library for encoding\n\t// Default: json.Marshal\n\tJSONEncoder utils.JSONMarshal `json:\"-\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// JSONUnmarshal\n\t//\n\t// Allowing for flexibility in using another json library for decoding\n\t// Default: json.Unmarshal\n\tJSONDecoder utils.JSONUnmarshal `json:\"-\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// MsgPackMarshal\n\t//\n\t// Allowing for flexibility in using another msgpack library for encoding\n\t// Default: binder.UnimplementedMsgpackMarshal\n\tMsgPackEncoder utils.MsgPackMarshal `json:\"-\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// MsgPackUnmarshal\n\t//\n\t// Allowing for flexibility in using another msgpack library for decoding\n\t// Default: binder.UnimplementedMsgpackUnmarshal\n\tMsgPackDecoder utils.MsgPackUnmarshal `json:\"-\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// CBORMarshal\n\t//\n\t// Allowing for flexibility in using another cbor library for encoding\n\t// Default: binder.UnimplementedCborMarshal\n\tCBOREncoder utils.CBORMarshal `json:\"-\"`\n\n\t// When set by an external client of Fiber it will use the provided implementation of a\n\t// CBORUnmarshal\n\t//\n\t// Allowing for flexibility in using another cbor library for decoding\n\t// Default: binder.UnimplementedCborUnmarshal\n\tCBORDecoder utils.CBORUnmarshal `json:\"-\"`\n\n\t// XMLEncoder set by an external client of Fiber it will use the provided implementation of a\n\t// XMLMarshal\n\t//\n\t// Allowing for flexibility in using another XML library for encoding\n\t// Default: xml.Marshal\n\tXMLEncoder utils.XMLMarshal `json:\"-\"`\n\n\t// XMLDecoder set by an external client of Fiber it will use the provided implementation of a\n\t// XMLUnmarshal\n\t//\n\t// Allowing for flexibility in using another XML library for decoding\n\t// Default: xml.Unmarshal\n\tXMLDecoder utils.XMLUnmarshal `json:\"-\"`\n\n\t// If you find yourself behind some sort of proxy, like a load balancer,\n\t// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.\n\t// For example, the Host HTTP header is usually used to return the requested host.\n\t// But when you’re behind a proxy, the actual host may be stored in an X-Forwarded-Host header.\n\t//\n\t// If you are behind a proxy, you should enable TrustProxy to prevent header spoofing.\n\t// If you enable TrustProxy and do not provide a TrustProxyConfig, Fiber will skip\n\t// all headers that could be spoofed.\n\t// If the request IP is in the TrustProxyConfig.Proxies allowlist, then:\n\t//   1. c.Scheme() get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header\n\t//   2. c.IP() get value from ProxyHeader header.\n\t//   3. c.Host() and c.Hostname() get value from X-Forwarded-Host header\n\t// But if the request IP is NOT in the TrustProxyConfig.Proxies allowlist, then:\n\t//   1. c.Scheme() WON'T get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header,\n\t//    will return https when a TLS connection is handled by the app, or http otherwise.\n\t//   2. c.IP() WON'T get value from ProxyHeader header, will return RemoteIP() from fasthttp context\n\t//   3. c.Host() and c.Hostname() WON'T get value from X-Forwarded-Host header, fasthttp.Request.URI().Host()\n\t//    will be used to get the hostname.\n\t//\n\t// To automatically trust all loopback, link-local, or private IP addresses,\n\t// without manually adding them to the TrustProxyConfig.Proxies allowlist,\n\t// you can set TrustProxyConfig.Loopback, TrustProxyConfig.LinkLocal, or TrustProxyConfig.Private to true.\n\t//\n\t// Default: false\n\tTrustProxy bool `json:\"trust_proxy\"`\n\n\t// Read TrustProxy doc.\n\t//\n\t// Default: DefaultTrustProxyConfig\n\tTrustProxyConfig TrustProxyConfig `json:\"trust_proxy_config\"`\n\n\t// If set to true, c.IP() and c.IPs() will validate IP addresses before returning them.\n\t// Also, c.IP() will return only the first valid IP rather than just the raw header\n\t// WARNING: this has a performance cost associated with it.\n\t//\n\t// Default: false\n\tEnableIPValidation bool `json:\"enable_ip_validation\"`\n\n\t// You can define custom color scheme. They'll be used for startup message, route list and some middlewares.\n\t//\n\t// Optional. Default: DefaultColors\n\tColorScheme Colors `json:\"color_scheme\"`\n\n\t// If you want to validate header/form/query... automatically when to bind, you can define struct validator.\n\t// Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator.\n\t//\n\t// Default: nil\n\tStructValidator StructValidator\n\n\t// RequestMethods provides customizability for HTTP methods. You can add/remove methods as you wish.\n\t//\n\t// Optional. Default: DefaultMethods\n\tRequestMethods []string\n\n\t// EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.\n\t// For example, you can use it to parse multiple values from a query parameter like this:\n\t//   /api?foo=bar,baz == foo[]=bar&foo[]=baz\n\t//\n\t// Optional. Default: false\n\tEnableSplittingOnParsers bool `json:\"enable_splitting_on_parsers\"`\n\n\t// Services is a list of services that are used by the app (e.g. databases, caches, etc.)\n\t//\n\t// Optional. Default: a zero value slice\n\tServices []Service\n\n\t// ServicesStartupContextProvider is a context provider for the startup of the services.\n\t//\n\t// Optional. Default: a provider that returns context.Background()\n\tServicesStartupContextProvider func() context.Context\n\n\t// ServicesShutdownContextProvider is a context provider for the shutdown of the services.\n\t//\n\t// Optional. Default: a provider that returns context.Background()\n\tServicesShutdownContextProvider func() context.Context\n}\n\n// Default TrustProxyConfig\nvar DefaultTrustProxyConfig = TrustProxyConfig{}\n\n// TrustProxyConfig is a struct for configuring trusted proxies if Config.TrustProxy is true.\ntype TrustProxyConfig struct {\n\tips map[string]struct{}\n\n\t// Proxies is a list of trusted proxy IP addresses or CIDR ranges.\n\t//\n\t// Default: []string\n\tProxies []string `json:\"proxies\"`\n\n\tranges []*net.IPNet\n\n\t// LinkLocal enables trusting all link-local IP ranges (e.g., 169.254.0.0/16, fe80::/10).\n\t//\n\t// Default: false\n\tLinkLocal bool `json:\"link_local\"`\n\n\t// Loopback enables trusting all loopback IP ranges (e.g., 127.0.0.0/8, ::1/128).\n\t//\n\t// Default: false\n\tLoopback bool `json:\"loopback\"`\n\n\t// Private enables trusting all private IP ranges (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7).\n\t//\n\t// Default: false\n\tPrivate bool `json:\"private\"`\n\n\t// UnixSocket enables trusting Unix domain socket connections.\n\t// When enabled, requests from Unix sockets are treated as trusted proxies.\n\t//\n\t// Default: false\n\tUnixSocket bool `json:\"unix_socket\"`\n}\n\n// RouteMessage is some message need to be print when server starts\ntype RouteMessage struct {\n\tname     string\n\tmethod   string\n\tpath     string\n\thandlers string\n}\n\n// Default Config values\nconst (\n\tDefaultBodyLimit       = 4 * 1024 * 1024\n\tDefaultMaxRanges       = 16\n\tDefaultConcurrency     = 256 * 1024\n\tDefaultReadBufferSize  = 4096\n\tDefaultWriteBufferSize = 4096\n)\n\nconst (\n\tmethodGet = iota\n\tmethodHead\n\tmethodPost\n\tmethodPut\n\tmethodDelete\n\tmethodConnect\n\tmethodOptions\n\tmethodTrace\n\tmethodPatch\n)\n\n// HTTP methods enabled by default\nvar DefaultMethods = []string{\n\tmethodGet:     MethodGet,\n\tmethodHead:    MethodHead,\n\tmethodPost:    MethodPost,\n\tmethodPut:     MethodPut,\n\tmethodDelete:  MethodDelete,\n\tmethodConnect: MethodConnect,\n\tmethodOptions: MethodOptions,\n\tmethodTrace:   MethodTrace,\n\tmethodPatch:   MethodPatch,\n}\n\n// httpReadResponse - Used for test mocking http.ReadResponse\nvar httpReadResponse = http.ReadResponse\n\n// DefaultErrorHandler that process return errors from handlers\nfunc DefaultErrorHandler(c Ctx, err error) error {\n\tcode := StatusInternalServerError\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\tcode = e.Code\n\t}\n\tc.Set(HeaderContentType, MIMETextPlainCharsetUTF8)\n\treturn c.Status(code).SendString(err.Error())\n}\n\n// New creates a new Fiber named instance.\n//\n//\tapp := fiber.New()\n//\n// You can pass optional configuration options by passing a Config struct:\n//\n//\tapp := fiber.New(fiber.Config{\n//\t    ServerHeader: \"Fiber\",\n//\t})\nfunc New(config ...Config) *App {\n\t// Create a new app\n\tapp := &App{\n\t\t// Create config\n\t\tconfig:        Config{},\n\t\ttoBytes:       utils.UnsafeBytes,\n\t\ttoString:      utils.UnsafeString,\n\t\tlatestRoute:   &Route{},\n\t\tcustomBinders: []CustomBinder{},\n\t\tsendfiles:     []*sendFileStore{},\n\t}\n\n\t// Create Ctx pool\n\tapp.pool = sync.Pool{\n\t\tNew: func() any {\n\t\t\tif app.newCtxFunc != nil {\n\t\t\t\treturn app.newCtxFunc(app)\n\t\t\t}\n\t\t\treturn NewDefaultCtx(app)\n\t\t},\n\t}\n\n\t// Define hooks\n\tapp.hooks = newHooks(app)\n\n\t// Define mountFields\n\tapp.mountFields = newMountFields(app)\n\n\t// Define state\n\tapp.state = newState()\n\n\t// Override config if provided\n\tif len(config) > 0 {\n\t\tapp.config = config[0]\n\t}\n\n\t// Initialize configured before defaults are set\n\tapp.configured = app.config\n\tif err := app.validateConfiguredServices(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Override default values\n\tif app.config.BodyLimit <= 0 {\n\t\tapp.config.BodyLimit = DefaultBodyLimit\n\t}\n\tif app.config.MaxRanges <= 0 {\n\t\tapp.config.MaxRanges = DefaultMaxRanges\n\t}\n\tif app.config.Concurrency <= 0 {\n\t\tapp.config.Concurrency = DefaultConcurrency\n\t}\n\tif app.config.ReadBufferSize <= 0 {\n\t\tapp.config.ReadBufferSize = DefaultReadBufferSize\n\t}\n\tif app.config.WriteBufferSize <= 0 {\n\t\tapp.config.WriteBufferSize = DefaultWriteBufferSize\n\t}\n\tif app.config.CompressedFileSuffixes == nil {\n\t\tapp.config.CompressedFileSuffixes = map[string]string{\n\t\t\t\"gzip\": \".fiber.gz\",\n\t\t\t\"br\":   \".fiber.br\",\n\t\t\t\"zstd\": \".fiber.zst\",\n\t\t}\n\t}\n\n\tif app.config.Immutable {\n\t\tapp.toBytes, app.toString = toBytesImmutable, toStringImmutable\n\t}\n\n\tif app.config.ErrorHandler == nil {\n\t\tapp.config.ErrorHandler = DefaultErrorHandler\n\t}\n\n\tif app.config.JSONEncoder == nil {\n\t\tapp.config.JSONEncoder = json.Marshal\n\t}\n\tif app.config.JSONDecoder == nil {\n\t\tapp.config.JSONDecoder = json.Unmarshal\n\t}\n\tif app.config.MsgPackEncoder == nil {\n\t\tapp.config.MsgPackEncoder = binder.UnimplementedMsgpackMarshal\n\t}\n\tif app.config.MsgPackDecoder == nil {\n\t\tapp.config.MsgPackDecoder = binder.UnimplementedMsgpackUnmarshal\n\t}\n\tif app.config.CBOREncoder == nil {\n\t\tapp.config.CBOREncoder = binder.UnimplementedCborMarshal\n\t}\n\tif app.config.CBORDecoder == nil {\n\t\tapp.config.CBORDecoder = binder.UnimplementedCborUnmarshal\n\t}\n\tif app.config.XMLEncoder == nil {\n\t\tapp.config.XMLEncoder = xml.Marshal\n\t}\n\tif app.config.XMLDecoder == nil {\n\t\tapp.config.XMLDecoder = xml.Unmarshal\n\t}\n\tif len(app.config.RequestMethods) == 0 {\n\t\tapp.config.RequestMethods = DefaultMethods\n\t}\n\n\tapp.config.TrustProxyConfig.ips = make(map[string]struct{}, len(app.config.TrustProxyConfig.Proxies))\n\tfor _, ipAddress := range app.config.TrustProxyConfig.Proxies {\n\t\tapp.handleTrustedProxy(ipAddress)\n\t}\n\n\t// Create router stack\n\tapp.stack = make([][]*Route, len(app.config.RequestMethods))\n\tapp.treeStack = make([]map[int][]*Route, len(app.config.RequestMethods))\n\n\t// Override colors\n\tapp.config.ColorScheme = defaultColors(&app.config.ColorScheme)\n\n\t// Init app\n\tapp.init()\n\n\t// Return app\n\treturn app\n}\n\n// NewWithCustomCtx creates a new Fiber instance and applies the\n// provided function to generate a custom context type. It mirrors the behavior\n// of calling `New()` followed by `app.setCtxFunc(fn)`.\nfunc NewWithCustomCtx(newCtxFunc func(app *App) CustomCtx, config ...Config) *App {\n\tapp := New(config...)\n\tapp.setCtxFunc(newCtxFunc)\n\treturn app\n}\n\n// GetString returns s unchanged when Immutable is off or s is read-only (rodata).\n// Otherwise, it returns a detached copy (strings.Clone).\nfunc (app *App) GetString(s string) string {\n\tif !app.config.Immutable || s == \"\" {\n\t\treturn s\n\t}\n\tif isReadOnly(unsafe.Pointer(unsafe.StringData(s))) { //nolint:gosec // pointer check avoids unnecessary copy\n\t\treturn s // literal / rodata → safe to return as-is\n\t}\n\treturn strings.Clone(s) // heap-backed / aliased → detach\n}\n\n// GetBytes returns b unchanged when Immutable is off or b is read-only (rodata).\n// Otherwise, it returns a detached copy.\nfunc (app *App) GetBytes(b []byte) []byte {\n\tif !app.config.Immutable || len(b) == 0 {\n\t\treturn b\n\t}\n\tif isReadOnly(unsafe.Pointer(unsafe.SliceData(b))) { //nolint:gosec // pointer check avoids unnecessary copy\n\t\treturn b // rodata → safe to return as-is\n\t}\n\treturn utils.CopyBytes(b) // detach when backed by request/response memory\n}\n\n// Adds an ip address to TrustProxyConfig.ranges or TrustProxyConfig.ips based on whether it is an IP range or not\nfunc (app *App) handleTrustedProxy(ipAddress string) {\n\tif strings.IndexByte(ipAddress, '/') >= 0 {\n\t\t_, ipNet, err := net.ParseCIDR(ipAddress)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"IP range %q could not be parsed: %v\", ipAddress, err)\n\t\t} else {\n\t\t\tapp.config.TrustProxyConfig.ranges = append(app.config.TrustProxyConfig.ranges, ipNet)\n\t\t}\n\t} else {\n\t\tip := net.ParseIP(ipAddress)\n\t\tif ip == nil {\n\t\t\tlog.Warnf(\"IP address %q could not be parsed\", ipAddress)\n\t\t} else {\n\t\t\tapp.config.TrustProxyConfig.ips[ipAddress] = struct{}{}\n\t\t}\n\t}\n}\n\n// setCtxFunc applies the given context factory to the app.\n// It is used internally by NewWithCustomCtx. It doesn't allow adding new methods,\n// only customizing existing ones.\nfunc (app *App) setCtxFunc(function func(app *App) CustomCtx) {\n\tapp.newCtxFunc = function\n\tapp.hasCustomCtx = function != nil\n\n\tif app.server != nil {\n\t\tapp.server.Handler = app.requestHandler\n\t}\n}\n\n// RegisterCustomConstraint allows to register custom constraint.\nfunc (app *App) RegisterCustomConstraint(constraint CustomConstraint) {\n\tapp.customConstraints = append(app.customConstraints, constraint)\n}\n\n// RegisterCustomBinder Allows to register custom binders to use as Bind().Custom(\"name\").\n// They should be compatible with CustomBinder interface.\nfunc (app *App) RegisterCustomBinder(customBinder CustomBinder) {\n\tapp.customBinders = append(app.customBinders, customBinder)\n}\n\n// ReloadViews reloads the configured view engine by invoking its Load method.\n// It returns an error if no view engine is configured or if reloading fails.\nfunc (app *App) ReloadViews() error {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tapps := map[string]*App{\"\": app}\n\tif app.mountFields != nil {\n\t\tapps = app.mountFields.appList\n\t}\n\n\tvar reloaded bool\n\tfor _, targetApp := range apps {\n\t\tif targetApp == nil || targetApp.config.Views == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif viewValue := reflect.ValueOf(targetApp.config.Views); viewValue.Kind() == reflect.Pointer && viewValue.IsNil() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := targetApp.config.Views.Load(); err != nil {\n\t\t\treturn fmt.Errorf(\"fiber: failed to reload views: %w\", err)\n\t\t}\n\n\t\treloaded = true\n\t}\n\n\tif !reloaded {\n\t\treturn ErrNoViewEngineConfigured\n\t}\n\n\treturn nil\n}\n\n// SetTLSHandler Can be used to set ClientHelloInfo when using TLS with Listener.\nfunc (app *App) SetTLSHandler(tlsHandler *TLSHandler) {\n\t// Attach the tlsHandler to the config\n\tapp.mutex.Lock()\n\tapp.tlsHandler = tlsHandler\n\tapp.mutex.Unlock()\n}\n\n// Name Assign name to specific route.\nfunc (app *App) Name(name string) Router {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tfor _, routes := range app.stack {\n\t\tfor _, route := range routes {\n\t\t\tisMethodValid := route.Method == app.latestRoute.Method || app.latestRoute.use ||\n\t\t\t\t(app.latestRoute.Method == MethodGet && route.Method == MethodHead)\n\n\t\t\tif route.Path == app.latestRoute.Path && isMethodValid {\n\t\t\t\troute.Name = name\n\t\t\t\tif route.group != nil {\n\t\t\t\t\troute.Name = route.group.name + route.Name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := app.hooks.executeOnNameHooks(app.latestRoute); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn app\n}\n\n// GetRoute Get route by name\nfunc (app *App) GetRoute(name string) Route {\n\tfor _, routes := range app.stack {\n\t\tfor _, route := range routes {\n\t\t\tif route.Name == name {\n\t\t\t\treturn *route\n\t\t\t}\n\t\t}\n\t}\n\n\treturn Route{}\n}\n\n// GetRoutes Get all routes. When filterUseOption equal to true, it will filter the routes registered by the middleware.\nfunc (app *App) GetRoutes(filterUseOption ...bool) []Route {\n\tvar rs []Route\n\tvar filterUse bool\n\tif len(filterUseOption) != 0 {\n\t\tfilterUse = filterUseOption[0]\n\t}\n\tfor _, routes := range app.stack {\n\t\tfor _, route := range routes {\n\t\t\tif filterUse && route.use {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trs = append(rs, *route)\n\t\t}\n\t}\n\treturn rs\n}\n\n// Use registers a middleware route that will match requests\n// with the provided prefix (which is optional and defaults to \"/\").\n// Also, you can pass another app instance as a sub-router along a routing path.\n// It's very useful to split up a large API as many independent routers and\n// compose them as a single service using Use. The fiber's error handler and\n// any of the fiber's sub apps are added to the application's error handlers\n// to be invoked on errors that happen within the prefix route.\n//\n//\t\tapp.Use(func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t\tapp.Use(\"/api\", func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t\tapp.Use(\"/api\", handler, func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t \tsubApp := fiber.New()\n//\t\tapp.Use(\"/mounted-path\", subApp)\n//\n// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...\nfunc (app *App) Use(args ...any) Router {\n\tvar prefix string\n\tvar subApp *App\n\tvar prefixes []string\n\tvar handlers []Handler\n\n\tfor i := range args {\n\t\tswitch arg := args[i].(type) {\n\t\tcase string:\n\t\t\tprefix = arg\n\t\tcase *App:\n\t\t\tsubApp = arg\n\t\tcase []string:\n\t\t\tprefixes = arg\n\t\tdefault:\n\t\t\thandler, ok := toFiberHandler(arg)\n\t\t\tif !ok {\n\t\t\t\tpanic(fmt.Sprintf(\"use: invalid handler %v\\n\", reflect.TypeOf(arg)))\n\t\t\t}\n\t\t\thandlers = append(handlers, handler)\n\t\t}\n\t}\n\n\tif len(prefixes) == 0 {\n\t\tprefixes = append(prefixes, prefix)\n\t}\n\n\tfor _, prefix := range prefixes {\n\t\tif subApp != nil {\n\t\t\treturn app.mount(prefix, subApp)\n\t\t}\n\n\t\tapp.register([]string{methodUse}, prefix, nil, handlers...)\n\t}\n\n\treturn app\n}\n\n// Get registers a route for GET methods that requests a representation\n// of the specified resource. Requests using GET should only retrieve data.\nfunc (app *App) Get(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodGet}, path, handler, handlers...)\n}\n\n// Head registers a route for HEAD methods that asks for a response identical\n// to that of a GET request, but without the response body.\nfunc (app *App) Head(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodHead}, path, handler, handlers...)\n}\n\n// Post registers a route for POST methods that is used to submit an entity to the\n// specified resource, often causing a change in state or side effects on the server.\nfunc (app *App) Post(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodPost}, path, handler, handlers...)\n}\n\n// Put registers a route for PUT methods that replaces all current representations\n// of the target resource with the request payload.\nfunc (app *App) Put(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodPut}, path, handler, handlers...)\n}\n\n// Delete registers a route for DELETE methods that deletes the specified resource.\nfunc (app *App) Delete(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodDelete}, path, handler, handlers...)\n}\n\n// Connect registers a route for CONNECT methods that establishes a tunnel to the\n// server identified by the target resource.\nfunc (app *App) Connect(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodConnect}, path, handler, handlers...)\n}\n\n// Options registers a route for OPTIONS methods that is used to describe the\n// communication options for the target resource.\nfunc (app *App) Options(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodOptions}, path, handler, handlers...)\n}\n\n// Trace registers a route for TRACE methods that performs a message loop-back\n// test along the path to the target resource.\nfunc (app *App) Trace(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodTrace}, path, handler, handlers...)\n}\n\n// Patch registers a route for PATCH methods that is used to apply partial\n// modifications to a resource.\nfunc (app *App) Patch(path string, handler any, handlers ...any) Router {\n\treturn app.Add([]string{MethodPatch}, path, handler, handlers...)\n}\n\n// Add allows you to specify multiple HTTP methods to register a route.\n// The provided handlers are executed in order, starting with `handler` and then the variadic `handlers`.\nfunc (app *App) Add(methods []string, path string, handler any, handlers ...any) Router {\n\tconverted := collectHandlers(\"add\", append([]any{handler}, handlers...)...)\n\tapp.register(methods, path, nil, converted...)\n\n\treturn app\n}\n\n// All will register the handler on all HTTP methods\nfunc (app *App) All(path string, handler any, handlers ...any) Router {\n\treturn app.Add(app.config.RequestMethods, path, handler, handlers...)\n}\n\n// Group is used for Routes with common prefix to define a new sub-router with optional middleware.\n//\n//\tapi := app.Group(\"/api\")\n//\tapi.Get(\"/users\", handler)\nfunc (app *App) Group(prefix string, handlers ...any) Router {\n\tgrp := &Group{Prefix: prefix, app: app}\n\tif len(handlers) > 0 {\n\t\tconverted := collectHandlers(\"group\", handlers...)\n\t\tapp.register([]string{methodUse}, prefix, grp, converted...)\n\t}\n\tif err := app.hooks.executeOnGroupHooks(*grp); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn grp\n}\n\n// RouteChain creates a Registering instance that lets you declare a stack of\n// handlers for the same route. Handlers defined via the returned Register are\n// scoped to the provided path.\nfunc (app *App) RouteChain(path string) Register {\n\t// Create new route\n\troute := &Registering{app: app, path: path}\n\n\treturn route\n}\n\n// Route is used to define routes with a common prefix inside the supplied\n// function. It mirrors the legacy helper and reuses the Group method to create\n// a sub-router.\nfunc (app *App) Route(prefix string, fn func(router Router), name ...string) Router {\n\tif fn == nil {\n\t\tpanic(\"route handler 'fn' cannot be nil\")\n\t}\n\t// Create new group\n\tgroup := app.Group(prefix)\n\tif len(name) > 0 {\n\t\tgroup.Name(name[0])\n\t}\n\n\t// Define routes\n\tfn(group)\n\n\treturn group\n}\n\n// Error makes it compatible with the `error` interface.\nfunc (e *Error) Error() string {\n\treturn e.Message\n}\n\n// NewError creates a new Error instance with an optional message\nfunc NewError(code int, message ...string) *Error {\n\terr := &Error{\n\t\tCode:    code,\n\t\tMessage: utils.StatusMessage(code),\n\t}\n\tif len(message) > 0 {\n\t\terr.Message = message[0]\n\t}\n\treturn err\n}\n\n// NewErrorf creates a new Error instance with an optional message.\n// Additional arguments are formatted using fmt.Sprintf when provided.\n// If the first argument in the message slice is not a string, the function\n// falls back to using fmt.Sprint on the first element to generate the message.\nfunc NewErrorf(code int, message ...any) *Error {\n\tvar msg string\n\n\tswitch len(message) {\n\tcase 0:\n\t\t// nothing to override\n\t\tmsg = utils.StatusMessage(code)\n\n\tcase 1:\n\t\t// One argument → treat it like fmt.Sprint(arg)\n\t\tif s, ok := message[0].(string); ok {\n\t\t\tmsg = s\n\t\t} else {\n\t\t\tmsg = fmt.Sprint(message[0])\n\t\t}\n\n\tdefault:\n\t\t// Two or more → first must be a format string.\n\t\tif format, ok := message[0].(string); ok {\n\t\t\tmsg = fmt.Sprintf(format, message[1:]...)\n\t\t} else {\n\t\t\t// If the first arg isn’t a string, fall back.\n\t\t\tmsg = fmt.Sprint(message[0])\n\t\t}\n\t}\n\n\treturn &Error{Code: code, Message: msg}\n}\n\n// Config returns the app config as value ( read-only ).\nfunc (app *App) Config() Config {\n\treturn app.config\n}\n\n// Handler returns the server handler.\nfunc (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476\n\t// prepare the server for the start\n\tapp.startupProcess()\n\treturn app.requestHandler\n}\n\n// Stack returns the raw router stack.\nfunc (app *App) Stack() [][]*Route {\n\treturn app.stack\n}\n\n// HandlersCount returns the amount of registered handlers.\nfunc (app *App) HandlersCount() uint32 {\n\treturn app.handlersCount\n}\n\n// Shutdown gracefully shuts down the server without interrupting any active connections.\n// Shutdown works by first closing all open listeners and then waiting indefinitely for all connections to return to idle before shutting down.\n//\n// Make sure the program doesn't exit and waits instead for Shutdown to return.\n//\n// Important: app.Listen() must be called in a separate goroutine; otherwise, shutdown hooks will not work\n// as Listen() is a blocking operation. Example:\n//\n//\tgo app.Listen(\":3000\")\n//\t// ...\n//\tapp.Shutdown()\n//\n// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.\nfunc (app *App) Shutdown() error {\n\treturn app.ShutdownWithContext(context.Background())\n}\n\n// ShutdownWithTimeout gracefully shuts down the server without interrupting any active connections. However, if the timeout is exceeded,\n// ShutdownWithTimeout will forcefully close any active connections.\n// ShutdownWithTimeout works by first closing all open listeners and then waiting for all connections to return to idle before shutting down.\n//\n// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return.\n//\n// ShutdownWithTimeout does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.\nfunc (app *App) ShutdownWithTimeout(timeout time.Duration) error {\n\tctx, cancelFunc := context.WithTimeout(context.Background(), timeout)\n\tdefer cancelFunc()\n\treturn app.ShutdownWithContext(ctx)\n}\n\n// ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.\n//\n// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return.\n//\n// ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.\nfunc (app *App) ShutdownWithContext(ctx context.Context) error {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tvar err error\n\n\tif app.server == nil {\n\t\treturn ErrNotRunning\n\t}\n\n\t// Execute the Shutdown hook\n\tapp.hooks.executeOnPreShutdownHooks()\n\tdefer app.hooks.executeOnPostShutdownHooks(err)\n\n\terr = app.server.ShutdownWithContext(ctx)\n\treturn err\n}\n\n// Server returns the underlying fasthttp server\nfunc (app *App) Server() *fasthttp.Server {\n\treturn app.server\n}\n\n// Hooks returns the hook struct to register hooks.\nfunc (app *App) Hooks() *Hooks {\n\treturn app.hooks\n}\n\n// State returns the state struct to store global data in order to share it between handlers.\nfunc (app *App) State() *State {\n\treturn app.state\n}\n\nvar ErrTestGotEmptyResponse = errors.New(\"test: got empty response\")\n\n// TestConfig is a struct holding Test settings\ntype TestConfig struct {\n\t// Timeout defines the maximum duration a\n\t// test can run before timing out.\n\t// Default: time.Second\n\tTimeout time.Duration\n\n\t// FailOnTimeout specifies whether the test\n\t// should return a timeout error if the HTTP response\n\t// exceeds the Timeout duration.\n\t// Default: true\n\tFailOnTimeout bool\n}\n\n// Test is used for internal debugging by passing a *http.Request.\n// Config is optional and defaults to a 1s error on timeout,\n// 0 timeout will disable it completely.\nfunc (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error) {\n\t// Default config\n\tcfg := TestConfig{\n\t\tTimeout:       time.Second,\n\t\tFailOnTimeout: true,\n\t}\n\n\t// Override config if provided\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\n\t// Add Content-Length if not provided with body\n\tif req.Body != http.NoBody && req.Header.Get(HeaderContentLength) == \"\" {\n\t\treq.Header.Add(HeaderContentLength, strconv.FormatInt(req.ContentLength, 10))\n\t}\n\n\t// Dump raw http request\n\tdump, err := httputil.DumpRequest(req, true)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to dump request: %w\", err)\n\t}\n\n\t// Create test connection\n\tconn := new(testConn)\n\n\t// Write raw http request\n\tif _, err = conn.r.Write(dump); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write: %w\", err)\n\t}\n\t// prepare the server for the start\n\tapp.startupProcess()\n\n\t// Serve conn to server\n\tchannel := make(chan error, 1)\n\tgo func() {\n\t\tvar returned bool\n\t\tdefer func() {\n\t\t\tif !returned {\n\t\t\t\tchannel <- ErrHandlerExited\n\t\t\t}\n\t\t}()\n\n\t\tchannel <- app.server.ServeConn(conn)\n\t\treturned = true\n\t}()\n\n\t// Wait for callback\n\tif cfg.Timeout > 0 {\n\t\t// With timeout\n\t\tselect {\n\t\tcase err = <-channel:\n\t\tcase <-time.After(cfg.Timeout):\n\t\t\tif cfg.FailOnTimeout {\n\t\t\t\tconn.Close() //nolint:errcheck // It is fine to ignore the error here\n\t\t\t\treturn nil, os.ErrDeadlineExceeded\n\t\t\t}\n\t\t\t// When FailOnTimeout is false, wait up to 1 additional second for the handler\n\t\t\t// to complete and write a response. This prevents indefinite blocking while\n\t\t\t// allowing slow handlers to finish.\n\t\t\tselect {\n\t\t\tcase err = <-channel:\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\t// Handler took too long even with extra time\n\t\t\t\tconn.Close() //nolint:errcheck // It is fine to ignore the error here\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Without timeout\n\t\terr = <-channel\n\t}\n\n\t// Check for errors\n\tif err != nil && !errors.Is(err, fasthttp.ErrGetOnly) && !errors.Is(err, errTestConnClosed) {\n\t\treturn nil, err\n\t}\n\n\t// Read response(s)\n\tbuffer := bufio.NewReader(&conn.w)\n\n\tvar res *http.Response\n\tfor {\n\t\t// Convert raw http response to *http.Response\n\t\tres, err = httpReadResponse(buffer, req)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\treturn nil, ErrTestGotEmptyResponse\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"failed to read response: %w\", err)\n\t\t}\n\n\t\t// Break if this response is non-1xx or there are no more responses\n\t\tif res.StatusCode >= http.StatusOK || buffer.Buffered() == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Discard interim response body before reading the next one\n\t\tif res.Body != nil {\n\t\t\tif _, errCopy := io.Copy(io.Discard, res.Body); errCopy != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to discard interim response body: %w\", errCopy)\n\t\t\t}\n\t\t\tif errClose := res.Body.Close(); errClose != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to close interim response body: %w\", errClose)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\ntype disableLogger struct{}\n\n// Printf implements the fasthttp Logger interface and discards log output.\nfunc (*disableLogger) Printf(string, ...any) {\n}\n\nfunc (app *App) init() *App {\n\t// lock application\n\tapp.mutex.Lock()\n\n\t// Initialize Services when needed,\n\t// panics if there is an error starting them.\n\tapp.initServices()\n\n\t// Only load templates if a view engine is specified\n\tif app.config.Views != nil {\n\t\tif err := app.config.Views.Load(); err != nil {\n\t\t\tlog.Warnf(\"failed to load views: %v\", err)\n\t\t}\n\t}\n\n\t// create fasthttp server\n\tapp.server = &fasthttp.Server{\n\t\tLogger:       &disableLogger{},\n\t\tLogAllErrors: false,\n\t\tErrorHandler: app.serverErrorHandler,\n\t}\n\n\t// fasthttp server settings\n\tapp.server.Handler = app.requestHandler\n\tapp.server.Name = app.config.ServerHeader\n\tapp.server.Concurrency = app.config.Concurrency\n\tapp.server.NoDefaultDate = app.config.DisableDefaultDate\n\tapp.server.NoDefaultContentType = app.config.DisableDefaultContentType\n\tapp.server.DisableHeaderNamesNormalizing = app.config.DisableHeaderNormalizing\n\tapp.server.DisableKeepalive = app.config.DisableKeepalive\n\tapp.server.MaxRequestBodySize = app.config.BodyLimit\n\tapp.server.NoDefaultServerHeader = app.config.ServerHeader == \"\"\n\tapp.server.ReadTimeout = app.config.ReadTimeout\n\tapp.server.WriteTimeout = app.config.WriteTimeout\n\tapp.server.IdleTimeout = app.config.IdleTimeout\n\tapp.server.ReadBufferSize = app.config.ReadBufferSize\n\tapp.server.WriteBufferSize = app.config.WriteBufferSize\n\tapp.server.GetOnly = app.config.GETOnly\n\tapp.server.ReduceMemoryUsage = app.config.ReduceMemoryUsage\n\tapp.server.StreamRequestBody = app.config.StreamRequestBody\n\tapp.server.DisablePreParseMultipartForm = app.config.DisablePreParseMultipartForm\n\n\t// unlock application\n\tapp.mutex.Unlock()\n\n\t// Register the Services shutdown handler once the app is initialized and unlocked.\n\tapp.Hooks().OnPostShutdown(func(_ error) error {\n\t\tif err := app.shutdownServices(app.servicesShutdownCtx()); err != nil {\n\t\t\tlog.Errorf(\"failed to shutdown services: %v\", err)\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn app\n}\n\n// ErrorHandler is the application's method in charge of finding the\n// appropriate handler for the given request. It searches any mounted\n// sub fibers by their prefixes and if it finds a match, it uses that\n// error handler. Otherwise, it uses the configured error handler for\n// the app, which if not set is the DefaultErrorHandler.\nfunc (app *App) ErrorHandler(ctx Ctx, err error) error {\n\tvar (\n\t\tmountedErrHandler  ErrorHandler\n\t\tmountedPrefixParts int\n\t)\n\n\tnormalizedPath := utils.AddTrailingSlashString(ctx.Path())\n\n\tfor _, prefix := range app.mountFields.appListKeys {\n\t\tsubApp := app.mountFields.appList[prefix]\n\t\tnormalizedPrefix := utils.AddTrailingSlashString(prefix)\n\n\t\tif prefix != \"\" && strings.HasPrefix(normalizedPath, normalizedPrefix) {\n\t\t\t// Count slashes instead of splitting - more efficient\n\t\t\tparts := strings.Count(prefix, \"/\") + 1\n\t\t\tif mountedPrefixParts <= parts {\n\t\t\t\tif subApp.configured.ErrorHandler != nil {\n\t\t\t\t\tmountedErrHandler = subApp.config.ErrorHandler\n\t\t\t\t}\n\n\t\t\t\tmountedPrefixParts = parts\n\t\t\t}\n\t\t}\n\t}\n\n\tif mountedErrHandler != nil {\n\t\treturn mountedErrHandler(ctx, err)\n\t}\n\n\treturn app.config.ErrorHandler(ctx, err)\n}\n\n// serverErrorHandler is a wrapper around the application's error handler method\n// user for the fasthttp server configuration. It maps a set of fasthttp errors to fiber\n// errors before calling the application's error handler method.\nfunc (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) {\n\t// Acquire Ctx with fasthttp request from pool\n\tc := app.AcquireCtx(fctx)\n\tdefer app.ReleaseCtx(c)\n\n\tvar (\n\t\terrNetOP *net.OpError\n\t\tnetErr   net.Error\n\t)\n\n\tswitch {\n\tcase errors.As(err, new(*fasthttp.ErrSmallBuffer)):\n\t\terr = ErrRequestHeaderFieldsTooLarge\n\tcase errors.As(err, &errNetOP) && errNetOP.Timeout():\n\t\terr = ErrRequestTimeout\n\tcase errors.As(err, &netErr):\n\t\terr = ErrBadGateway\n\tcase errors.Is(err, fasthttp.ErrBodyTooLarge):\n\t\terr = ErrRequestEntityTooLarge\n\tcase errors.Is(err, fasthttp.ErrGetOnly):\n\t\terr = ErrMethodNotAllowed\n\tcase strings.Contains(err.Error(), \"unsupported http request method\"):\n\t\terr = ErrNotImplemented\n\tcase strings.Contains(err.Error(), \"timeout\"):\n\t\terr = ErrRequestTimeout\n\tdefault:\n\t\terr = NewError(StatusBadRequest, err.Error())\n\t}\n\n\tif c.getMethodInt() != -1 {\n\t\tc.setSkipNonUseRoutes(true)\n\t\tdefer c.setSkipNonUseRoutes(false)\n\n\t\tvar nextErr error\n\t\tif d, isDefault := c.(*DefaultCtx); isDefault {\n\t\t\t_, nextErr = app.next(d)\n\t\t} else {\n\t\t\t_, nextErr = app.nextCustom(c)\n\t\t}\n\n\t\tif nextErr != nil && !errors.Is(nextErr, ErrNotFound) && !errors.Is(nextErr, ErrMethodNotAllowed) {\n\t\t\tlog.Errorf(\"serverErrorHandler: middleware traversal failed: %v\", nextErr)\n\t\t}\n\t}\n\n\tif catch := app.ErrorHandler(c, err); catch != nil {\n\t\tlog.Errorf(\"serverErrorHandler: failed to call ErrorHandler: %v\", catch)\n\t\t_ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here\n\t\treturn\n\t}\n}\n\n// startupProcess Is the method which executes all the necessary processes just before the start of the server.\nfunc (app *App) startupProcess() {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tapp.ensureAutoHeadRoutesLocked()\n\tfor prefix, subApp := range app.mountFields.appList {\n\t\tif prefix == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tsubApp.ensureAutoHeadRoutes()\n\t}\n\tapp.mountStartupProcess()\n\n\t// build route tree stack\n\tapp.buildTree()\n}\n\n// Run onListen hooks. If they return an error, panic.\nfunc (app *App) runOnListenHooks(listenData *ListenData) {\n\tif err := app.hooks.executeOnListenHooks(listenData); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "app_integration_test.go",
    "content": "package fiber_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/basicauth\"\n\t\"github.com/gofiber/fiber/v3/middleware/cache\"\n\t\"github.com/gofiber/fiber/v3/middleware/compress\"\n\t\"github.com/gofiber/fiber/v3/middleware/cors\"\n\t\"github.com/gofiber/fiber/v3/middleware/csrf\"\n\t\"github.com/gofiber/fiber/v3/middleware/encryptcookie\"\n\t\"github.com/gofiber/fiber/v3/middleware/envvar\"\n\t\"github.com/gofiber/fiber/v3/middleware/helmet\"\n\t\"github.com/gofiber/fiber/v3/middleware/keyauth\"\n\t\"github.com/gofiber/fiber/v3/middleware/limiter\"\n\t\"github.com/gofiber/fiber/v3/middleware/recover\"\n\t\"github.com/gofiber/fiber/v3/middleware/requestid\"\n\t\"github.com/gofiber/fiber/v3/middleware/session\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n)\n\ntype integrationCustomCtx struct {\n\t*fiber.DefaultCtx\n}\n\nfunc newIntegrationCustomCtx(app *fiber.App) fiber.CustomCtx {\n\treturn &integrationCustomCtx{DefaultCtx: fiber.NewDefaultCtx(app)}\n}\n\nfunc performOversizedRequest(t *testing.T, app *fiber.App, configure func(req *fasthttp.Request)) *fasthttp.Response {\n\tt.Helper()\n\n\tln := fasthttputil.NewInmemoryListener()\n\terrCh := make(chan error, 1)\n\n\tgo func() {\n\t\terrCh <- app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})\n\t}()\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, app.Shutdown())\n\t\tif err := <-errCh; err != nil && !errors.Is(err, net.ErrClosed) {\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t})\n\n\trequire.Eventually(t, func() bool {\n\t\tconn, err := ln.Dial()\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tif err := conn.Close(); err != nil {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}, time.Second, 10*time.Millisecond)\n\n\treq := fasthttp.AcquireRequest()\n\tresp := fasthttp.AcquireResponse()\n\n\treq.SetRequestURI(\"http://example.com/\")\n\treq.Header.SetMethod(fiber.MethodPost)\n\treq.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\treq.SetBody(bytes.Repeat([]byte{'a'}, 32))\n\tif configure != nil {\n\t\tconfigure(req)\n\t}\n\n\tclient := fasthttp.Client{\n\t\tDial: func(string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t},\n\t}\n\n\trequire.NoError(t, client.Do(req, resp))\n\n\trespCopy := fasthttp.AcquireResponse()\n\tresp.CopyTo(respCopy)\n\n\tfasthttp.ReleaseRequest(req)\n\tfasthttp.ReleaseResponse(resp)\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseResponse(respCopy)\n\t})\n\n\treturn respCopy\n}\n\nvar integrationEncryptCookieKey = encryptcookie.GenerateKey(32)\n\n// middlewareCombinationTestCase describes a middleware stack that should keep its\n// headers intact even when the default error handler runs. Keeping it as a named\n// type (instead of an inline struct) makes the massive table below easier to\n// scan and extend.\n//\n//nolint:govet // field alignment is secondary to readability for this test table\ntype middlewareCombinationTestCase struct { // betteralign:ignore - readability takes priority in tests\n\tname             string\n\tsetup            func(app *fiber.App)\n\tconfigureRequest func(req *fasthttp.Request)\n\thandler          func(c fiber.Ctx) error\n\tassertions       func(t *testing.T, resp *fasthttp.Response)\n\texpectedStatus   int\n}\n\nfunc (tc middlewareCombinationTestCase) statusOrDefault() int {\n\tif tc.expectedStatus == 0 {\n\t\treturn fiber.StatusInternalServerError\n\t}\n\treturn tc.expectedStatus\n}\n\nfunc (tc middlewareCombinationTestCase) handlerOrDefault() func(fiber.Ctx) error {\n\tif tc.handler != nil {\n\t\treturn tc.handler\n\t}\n\n\treturn func(fiber.Ctx) error {\n\t\treturn fiber.NewError(fiber.StatusInternalServerError, \"middleware combination failure\")\n\t}\n}\n\nfunc Test_Integration_RequestID_ContextPropagationFlag(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"disabled by default\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Use(requestid.New(requestid.Config{Generator: func() string { return \"rid-disabled\" }}))\n\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\trequire.Equal(t, \"rid-disabled\", requestid.FromContext(c))\n\t\t\trequire.Empty(t, requestid.FromContext(c.Context()))\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"enabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\t\tapp.Use(requestid.New(requestid.Config{Generator: func() string { return \"rid-enabled\" }}))\n\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\trequire.Equal(t, \"rid-enabled\", requestid.FromContext(c))\n\t\t\trequire.Equal(t, \"rid-enabled\", requestid.FromContext(c.Context()))\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t})\n}\n\nfunc Test_Integration_App_ServerErrorHandler_MiddlewareCombinationHeaders(t *testing.T) {\n\tt.Parallel()\n\n\t// This integration suite exercises representative middleware stacks to ensure their\n\t// response headers survive after Fiber's default error handler emits a failure.\n\n\tconst (\n\t\t// Origins used by the CORS stacks in this suite.\n\t\tcorsHelmetOrigin    = \"https://cors-and-helmet.example\"\n\t\tcorsRequestIDOrigin = \"https://cors-and-requestid.example\"\n\t\tcorsCSRForigin      = \"https://cors-and-csrf.example\"\n\t\tcorsCacheOrigin     = \"https://cors-and-cache.example\"\n\t\tcorsSessionOrigin   = \"https://cors-and-session.example\"\n\t\tcorsHelmetRequestID = \"https://cors-helmet-requestid.example\"\n\n\t\tcsrfCookieName      = \"combo-csrf\"\n\t\tgeneratedRequestID  = \"generated-combo-request-id\"\n\t\thelmetLimiterMax    = 7\n\t\thelmetLimiterReset  = 60\n\t\trequestIDHeader     = \"combo-request-id\"\n\t\tcsrfTokenValue      = \"csrf-token\"\n\t\tencryptedCookieName = \"combo-encrypted\"\n\t\tencryptedCookieVal  = \"unencrypted\"\n\t\tenvvarAllowHeader   = fiber.MethodGet + \", \" + fiber.MethodHead\n\t\tbasicRealm          = \"combo-basic\"\n\t\tkeyAuthRealm        = \"combo-key\"\n\t\tkeyAuthErrorDesc    = \"missing-key\"\n\t)\n\n\t// Each entry wires up a different middleware stack so we can ensure response mutations\n\t// survive the hop through the default error handler.\n\ttestCases := []middlewareCombinationTestCase{\n\t\t// --- CORS-focused stacks keep cross-origin metadata on error responses.\n\t\t{\n\t\t\tname: \"cors+helmet\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{AllowOrigins: []string{corsHelmetOrigin}}))\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsHelmetOrigin)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsHelmetOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Equal(t, \"same-origin\", string(resp.Header.Peek(\"Cross-Origin-Opener-Policy\")))\n\t\t\t\trequire.Equal(t, \"same-origin\", string(resp.Header.Peek(\"Cross-Origin-Resource-Policy\")))\n\t\t\t\trequire.Equal(t, \"require-corp\", string(resp.Header.Peek(\"Cross-Origin-Embedder-Policy\")))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cors+requestid\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{AllowOrigins: []string{corsRequestIDOrigin}}))\n\t\t\t\tapp.Use(requestid.New())\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsRequestIDOrigin)\n\t\t\t\treq.Header.Set(\"X-Request-ID\", requestIDHeader)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsRequestIDOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, requestIDHeader, string(resp.Header.Peek(\"X-Request-ID\")))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cors+helmet+requestid\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{AllowOrigins: []string{corsHelmetRequestID}}))\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(requestid.New(requestid.Config{\n\t\t\t\t\tGenerator: func() string {\n\t\t\t\t\t\treturn generatedRequestID\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsHelmetRequestID)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsHelmetRequestID, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, generatedRequestID, string(resp.Header.Peek(\"X-Request-ID\")))\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cors+cache\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{AllowOrigins: []string{corsCacheOrigin}}))\n\t\t\t\tapp.Use(cache.New())\n\t\t\t\t// Cache needs the default error handler to execute so it can emit X-Cache on failures.\n\t\t\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\t\t\tif err := c.Next(); err != nil {\n\t\t\t\t\t\tif handlerErr := app.Config().ErrorHandler(c, err); handlerErr != nil {\n\t\t\t\t\t\t\treturn handlerErr\n\t\t\t\t\t\t}\n\t\t\t\t\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsCacheOrigin)\n\t\t\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsCacheOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, \"unreachable\", string(resp.Header.Peek(\"X-Cache\")))\n\t\t\t\trequire.Equal(t, \"no-store\", string(resp.Header.Peek(fiber.HeaderCacheControl)))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cors+session\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{\n\t\t\t\t\tAllowOrigins:     []string{corsSessionOrigin},\n\t\t\t\t\tAllowCredentials: true,\n\t\t\t\t}))\n\t\t\t\tapp.Use(session.New())\n\t\t\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\t\t\tif sm := session.FromContext(c); sm != nil {\n\t\t\t\t\t\tsm.Set(\"cors-session\", \"enabled\")\n\t\t\t\t\t}\n\t\t\t\t\treturn c.Next()\n\t\t\t\t})\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsSessionOrigin)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsSessionOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, \"true\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\t\t\t\trequire.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), \"session_id=\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+encryptcookie\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(encryptcookie.New(encryptcookie.Config{Key: integrationEncryptCookieKey}))\n\t\t\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\t\t\tc.Cookie(&fiber.Cookie{Name: encryptedCookieName, Value: encryptedCookieVal})\n\t\t\t\t\treturn c.Next()\n\t\t\t\t})\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\tcookieHeader := string(resp.Header.Peek(fiber.HeaderSetCookie))\n\t\t\t\trequire.Contains(t, cookieHeader, encryptedCookieName+\"=\")\n\t\t\t\trequire.NotContains(t, cookieHeader, encryptedCookieVal)\n\t\t\t},\n\t\t},\n\t\t// --- Helmet anchored stacks validate security headers across other middleware.\n\t\t{\n\t\t\tname: \"helmet+limiter\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(limiter.New(limiter.Config{\n\t\t\t\t\tMax:        helmetLimiterMax,\n\t\t\t\t\tExpiration: time.Duration(helmetLimiterReset) * time.Second,\n\t\t\t\t\tKeyGenerator: func(fiber.Ctx) string {\n\t\t\t\t\t\treturn \"helmet+limiter\"\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Equal(t, strconv.Itoa(helmetLimiterMax), string(resp.Header.Peek(\"X-RateLimit-Limit\")))\n\t\t\t\trequire.Equal(t, strconv.Itoa(helmetLimiterMax-1), string(resp.Header.Peek(\"X-RateLimit-Remaining\")))\n\t\t\t\trequire.Equal(t, strconv.Itoa(helmetLimiterReset), string(resp.Header.Peek(\"X-RateLimit-Reset\")))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"cors+csrf\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(cors.New(cors.Config{\n\t\t\t\t\tAllowOrigins:     []string{corsCSRForigin},\n\t\t\t\t\tAllowCredentials: true,\n\t\t\t\t}))\n\t\t\t\tapp.Use(csrf.New(csrf.Config{\n\t\t\t\t\tCookieName:   csrfCookieName,\n\t\t\t\t\tKeyGenerator: func() string { return csrfTokenValue },\n\t\t\t\t}))\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\t\t\treq.Header.Set(fiber.HeaderOrigin, corsCSRForigin)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, corsCSRForigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t\trequire.Equal(t, \"true\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\t\t\t\trequire.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), csrfCookieName+\"=\"+csrfTokenValue)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+session\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(session.New())\n\t\t\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\t\t\tif sm := session.FromContext(c); sm != nil {\n\t\t\t\t\t\tsm.Set(\"combo-session\", \"enabled\")\n\t\t\t\t\t}\n\t\t\t\t\treturn c.Next()\n\t\t\t\t})\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), \"session_id=\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+csrf\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(csrf.New(csrf.Config{\n\t\t\t\t\tCookieName:   csrfCookieName,\n\t\t\t\t\tKeyGenerator: func() string { return csrfTokenValue },\n\t\t\t\t}))\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), csrfCookieName+\"=\"+csrfTokenValue)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+envvar\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(envvar.New(envvar.Config{ExportVars: map[string]string{\"COMBO_ENV\": \"configured\"}}))\n\t\t\t},\n\t\t\texpectedStatus: fiber.StatusMethodNotAllowed,\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Equal(t, envvarAllowHeader, string(resp.Header.Peek(fiber.HeaderAllow)))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+basicauth\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(basicauth.New(basicauth.Config{\n\t\t\t\t\tRealm: basicRealm,\n\t\t\t\t\tUnauthorized: func(c fiber.Ctx) error {\n\t\t\t\t\t\tc.Set(fiber.HeaderWWWAuthenticate, \"Basic realm=\\\"\"+basicRealm+\"\\\", charset=\\\"UTF-8\\\"\")\n\t\t\t\t\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\t\t\t\t\tc.Set(fiber.HeaderVary, fiber.HeaderAuthorization)\n\t\t\t\t\t\tc.Status(fiber.StatusUnauthorized)\n\t\t\t\t\t\treturn fiber.ErrUnauthorized\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t},\n\t\t\texpectedStatus: fiber.StatusUnauthorized,\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Equal(t, \"Basic realm=\\\"\"+basicRealm+\"\\\", charset=\\\"UTF-8\\\"\", string(resp.Header.Peek(fiber.HeaderWWWAuthenticate)))\n\t\t\t\trequire.Equal(t, \"no-store\", string(resp.Header.Peek(fiber.HeaderCacheControl)))\n\t\t\t\trequire.Equal(t, fiber.HeaderAuthorization, string(resp.Header.Peek(fiber.HeaderVary)))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+keyauth\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(keyauth.New(keyauth.Config{\n\t\t\t\t\tRealm:            keyAuthRealm,\n\t\t\t\t\tError:            keyauth.ErrorInvalidToken,\n\t\t\t\t\tErrorDescription: keyAuthErrorDesc,\n\t\t\t\t\tValidator: func(fiber.Ctx, string) (bool, error) {\n\t\t\t\t\t\treturn false, nil\n\t\t\t\t\t},\n\t\t\t\t\tErrorHandler: func(c fiber.Ctx, _ error) error {\n\t\t\t\t\t\tc.Status(fiber.StatusUnauthorized)\n\t\t\t\t\t\treturn fiber.ErrUnauthorized\n\t\t\t\t\t},\n\t\t\t\t}))\n\t\t\t},\n\t\t\texpectedStatus: fiber.StatusUnauthorized,\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\tauthenticate := string(resp.Header.Peek(fiber.HeaderWWWAuthenticate))\n\t\t\t\trequire.Contains(t, authenticate, \"Bearer realm=\\\"\"+keyAuthRealm+\"\\\"\")\n\t\t\t\trequire.Contains(t, authenticate, \"error=\\\"\"+keyauth.ErrorInvalidToken+\"\\\"\")\n\t\t\t\trequire.Contains(t, authenticate, \"error_description=\\\"\"+keyAuthErrorDesc+\"\\\"\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+compress\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(compress.New())\n\t\t\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\t\t\tif err := c.Next(); err != nil {\n\t\t\t\t\t\tif handlerErr := app.Config().ErrorHandler(c, err); handlerErr != nil {\n\t\t\t\t\t\t\treturn handlerErr\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Inflate the error body so the compress middleware has something to work with.\n\t\t\t\t\t\tif body := c.Response().Body(); len(body) > 0 {\n\t\t\t\t\t\t\tc.Response().SetBodyString(strings.Repeat(string(body), 32))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t},\n\t\t\tconfigureRequest: func(req *fasthttp.Request) {\n\t\t\t\treq.Header.Set(fiber.HeaderAcceptEncoding, \"gzip\")\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\trequire.Equal(t, \"gzip\", string(resp.Header.Peek(fiber.HeaderContentEncoding)))\n\t\t\t\trequire.Equal(t, fiber.HeaderAcceptEncoding, string(resp.Header.Peek(fiber.HeaderVary)))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"helmet+recover\",\n\t\t\tsetup: func(app *fiber.App) {\n\t\t\t\tapp.Use(helmet.New())\n\t\t\t\tapp.Use(recover.New())\n\t\t\t},\n\t\t\thandler: func(fiber.Ctx) error {\n\t\t\t\tpanic(\"panic for recover middleware\")\n\t\t\t},\n\t\t\tassertions: func(t *testing.T, resp *fasthttp.Response) {\n\t\t\t\tt.Helper()\n\t\t\t\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\t\t\t\t// Recover writes a plain-text body; ensure we still return content to clients while\n\t\t\t\t// keeping Helmet's security headers intact.\n\t\t\t\trequire.Contains(t, string(resp.Body()), \"panic for recover middleware\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\ttc.setup(app)\n\t\t\t// Every stack shares the same route that always hits the default error handler so we\n\t\t\t// can verify which headers survive the error response. A few cases override the\n\t\t\t// handler to exercise panic recovery or other routes that still flow through the\n\t\t\t// default error path.\n\t\t\tapp.All(\"/\", tc.handlerOrDefault())\n\n\t\t\tresp := performOversizedRequest(t, app, tc.configureRequest)\n\n\t\t\trequire.Equal(t, tc.statusOrDefault(), resp.StatusCode())\n\t\t\ttc.assertions(t, resp)\n\t\t})\n\t}\n}\n\nfunc Test_Integration_App_ServerErrorHandler_PreservesCORSHeadersOnBodyLimit(t *testing.T) {\n\tapp := fiber.New(fiber.Config{BodyLimit: 16})\n\tapp.Use(cors.New(cors.Config{\n\t\tAllowOrigins:     []string{\"https://example.com\"},\n\t\tAllowCredentials: true,\n\t\tExposeHeaders:    []string{\"X-Request-ID\"},\n\t}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, nil)\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, \"https://example.com\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\trequire.Equal(t, \"true\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"X-Request-ID\", string(resp.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))\n\trequire.Equal(t, \"Origin\", string(resp.Header.Peek(fiber.HeaderVary)))\n}\n\nfunc Test_Integration_App_ServerErrorHandler_PreservesHelmetHeadersOnBodyLimit(t *testing.T) {\n\tapp := fiber.New(fiber.Config{BodyLimit: 16})\n\tapp.Use(helmet.New())\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, nil)\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\trequire.Equal(t, \"same-origin\", string(resp.Header.Peek(\"Cross-Origin-Opener-Policy\")))\n\trequire.Equal(t, \"same-origin\", string(resp.Header.Peek(\"Cross-Origin-Resource-Policy\")))\n\trequire.Equal(t, \"require-corp\", string(resp.Header.Peek(\"Cross-Origin-Embedder-Policy\")))\n}\n\nfunc Test_Integration_App_ServerErrorHandler_PreservesRequestID(t *testing.T) {\n\tconst expectedRequestID = \"integration-request-id\"\n\n\tapp := fiber.New(fiber.Config{BodyLimit: 16})\n\tapp.Use(requestid.New())\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, func(req *fasthttp.Request) {\n\t\treq.Header.Set(\"X-Request-ID\", expectedRequestID)\n\t})\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, expectedRequestID, string(resp.Header.Peek(\"X-Request-ID\")))\n}\n\nfunc Test_Integration_App_ServerErrorHandler_GroupMiddlewareChain(t *testing.T) {\n\tapp := fiber.New(fiber.Config{BodyLimit: 16})\n\tapp.Use(helmet.New())\n\n\tapi := app.Group(\"/api\")\n\tapi.Use(requestid.New())\n\tapi.Use(func(c fiber.Ctx) error {\n\t\tc.Set(\"X-Group-Middleware\", \"active\")\n\t\treturn c.Next()\n\t})\n\tapi.Post(\"/resource\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, func(req *fasthttp.Request) {\n\t\treq.SetRequestURI(\"http://example.com/api/resource\")\n\t})\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, \"nosniff\", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))\n\trequire.NotEmpty(t, resp.Header.Peek(\"X-Request-ID\"))\n\trequire.Equal(t, \"active\", string(resp.Header.Peek(\"X-Group-Middleware\")))\n}\n\nfunc Test_Integration_App_ServerErrorHandler_RetainsHeadersFromSubsequentMiddleware(t *testing.T) {\n\tapp := fiber.New(fiber.Config{BodyLimit: 8})\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tc.Set(\"X-Custom-Middleware\", \"ran\")\n\t\treturn c.Next()\n\t})\n\tapp.Use(cors.New())\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, nil)\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, \"ran\", string(resp.Header.Peek(\"X-Custom-Middleware\")))\n\trequire.Equal(t, \"*\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n\nfunc Test_Integration_App_ServerErrorHandler_WithCustomCtx(t *testing.T) {\n\tapp := fiber.NewWithCustomCtx(newIntegrationCustomCtx, fiber.Config{BodyLimit: 16})\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tcustomCtx, ok := c.(*integrationCustomCtx)\n\t\trequire.True(t, ok)\n\t\tcustomCtx.Set(\"X-Custom-Ctx\", \"true\")\n\t\treturn c.Next()\n\t})\n\tapp.Use(cors.New(cors.Config{AllowOrigins: []string{\"https://example.org\"}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp := performOversizedRequest(t, app, func(req *fasthttp.Request) {\n\t\treq.Header.Set(fiber.HeaderOrigin, \"https://example.org\")\n\t})\n\n\trequire.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())\n\trequire.Equal(t, \"true\", string(resp.Header.Peek(\"X-Custom-Ctx\")))\n\trequire.Equal(t, \"https://example.org\", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n"
  },
  {
    "path": "app_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/gofiber/utils/v2\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n)\n\ntype fileView struct {\n\tpath    string\n\tcontent string\n\tloads   int\n}\n\nfunc (v *fileView) Load() error {\n\tcontents, err := os.ReadFile(v.path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read template: %w\", err)\n\t}\n\n\tv.content = string(contents)\n\tv.loads++\n\treturn nil\n}\n\nfunc (*fileView) Render(io.Writer, string, any, ...string) error { return nil }\n\nfunc testEmptyHandler(_ Ctx) error {\n\treturn nil\n}\n\nfunc testStatus200(t *testing.T, app *App, url, method string) {\n\tt.Helper()\n\n\treq := httptest.NewRequest(method, url, http.NoBody)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBodyError string) {\n\tt.Helper()\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 500, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectedBodyError, string(body), \"Response body\")\n}\n\nfunc Test_App_Test_Goroutine_Leak_Compare(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\thandler    Handler\n\t\tname       string\n\t\ttimeout    time.Duration\n\t\tsleepTime  time.Duration\n\t\texpectLeak bool\n\t}{\n\t\t{\n\t\t\tname: \"With timeout (potential leak)\",\n\t\t\thandler: func(c Ctx) error {\n\t\t\t\ttime.Sleep(300 * time.Millisecond) // Simulate time-consuming operation\n\t\t\t\treturn c.SendString(\"ok\")\n\t\t\t},\n\t\t\ttimeout:    50 * time.Millisecond,  // // Short timeout to ensure triggering\n\t\t\tsleepTime:  500 * time.Millisecond, // Wait time longer than handler execution time\n\t\t\texpectLeak: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Without timeout (no leak)\",\n\t\t\thandler: func(c Ctx) error {\n\t\t\t\treturn c.SendString(\"ok\") // Return immediately\n\t\t\t},\n\t\t\ttimeout:    0, // Disable timeout\n\t\t\tsleepTime:  100 * time.Millisecond,\n\t\t\texpectLeak: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\n\t\t\t// Record initial goroutine count\n\t\t\tinitialGoroutines := runtime.NumGoroutine()\n\t\t\tt.Logf(\"[%s] Initial goroutines: %d\", tc.name, initialGoroutines)\n\n\t\t\tapp.Get(\"/\", tc.handler)\n\n\t\t\t// Send 10 requests\n\t\t\tnumRequests := 10\n\t\t\tfor range numRequests {\n\t\t\t\treq := httptest.NewRequest(MethodGet, \"/\", http.NoBody)\n\n\t\t\t\tif tc.timeout > 0 {\n\t\t\t\t\t_, err := app.Test(req, TestConfig{\n\t\t\t\t\t\tTimeout:       tc.timeout,\n\t\t\t\t\t\tFailOnTimeout: true,\n\t\t\t\t\t})\n\t\t\t\t\trequire.Error(t, err)\n\t\t\t\t\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n\t\t\t\t} else if resp, err := app.Test(req); err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t} else {\n\t\t\t\t\trequire.Equal(t, 200, resp.StatusCode)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Wait for normal goroutines to complete\n\t\t\ttime.Sleep(tc.sleepTime)\n\n\t\t\t// Check final goroutine count\n\t\t\tfinalGoroutines := runtime.NumGoroutine()\n\t\t\tleakedGoroutines := finalGoroutines - initialGoroutines\n\t\t\tif leakedGoroutines < 0 {\n\t\t\t\tleakedGoroutines = 0\n\t\t\t}\n\t\t\tt.Logf(\"[%s] Final goroutines: %d (leaked: %d)\",\n\t\t\t\ttc.name, finalGoroutines, leakedGoroutines)\n\n\t\t\tif tc.expectLeak {\n\t\t\t\t// We allow up to 3x the request count to account for noise; zero is tolerated.\n\t\t\t\tmaxLeak := numRequests * 3\n\t\t\t\tif leakedGoroutines > maxLeak {\n\t\t\t\t\tt.Errorf(\"[%s] Expected at most %d leaked goroutines, but got %d\",\n\t\t\t\t\t\ttc.name, maxLeak, leakedGoroutines)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// No-leak scenario: allow a small buffer for runtime noise.\n\t\t\t// Increase slack to reduce flakes from background goroutines.\n\t\t\tif leakedGoroutines > numRequests {\n\t\t\t\tt.Errorf(\"[%s] Expected at most %d leaked goroutines, but got %d\",\n\t\t\t\t\ttc.name, numRequests, leakedGoroutines)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_App_MethodNotAllowed(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\n\tapp.Post(\"/\", testEmptyHandler)\n\n\tapp.Options(\"/\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodPost, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Empty(t, resp.Header.Get(HeaderAllow))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodPatch, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodPut, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n\n\tapp.Get(\"/\", testEmptyHandler)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodTrace, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"GET, HEAD, POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodPatch, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"GET, HEAD, POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n\n\tapp.Head(\"/\", testEmptyHandler)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodPut, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 405, resp.StatusCode)\n\trequire.Equal(t, \"GET, HEAD, POST, OPTIONS\", resp.Header.Get(HeaderAllow))\n}\n\nfunc Test_App_RegisterNetHTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname       string\n\t\tregister   func(app *App, path string, handler any)\n\t\tmethod     string\n\t\texpectBody bool\n\t}{\n\t\t{\n\t\t\tname: \"Get\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Get(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodGet,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Head\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Head(path, handler)\n\t\t\t},\n\t\t\tmethod: http.MethodHead,\n\t\t},\n\t\t{\n\t\t\tname: \"Post\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Post(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodPost,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Put\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Put(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodPut,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Delete\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Delete(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodDelete,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Connect\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Connect(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodConnect,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Options\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Options(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodOptions,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Trace\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Trace(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodTrace,\n\t\t\texpectBody: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Patch\",\n\t\t\tregister: func(app *App, path string, handler any) {\n\t\t\t\tapp.Patch(path, handler)\n\t\t\t},\n\t\t\tmethod:     http.MethodPatch,\n\t\t\texpectBody: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := New()\n\t\t\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.Header().Set(\"X-Test\", r.Method)\n\t\t\t\tw.WriteHeader(http.StatusAccepted)\n\t\t\t\tif r.Method == http.MethodHead {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t_, err := w.Write([]byte(\"hello from net/http \" + r.Method))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\n\t\t\ttt.register(app, \"/foo\", http.HandlerFunc(handler))\n\n\t\t\treq := httptest.NewRequest(tt.method, \"/foo\", http.NoBody)\n\t\t\tif tt.method == http.MethodConnect {\n\t\t\t\treq.URL.Scheme = \"http\"\n\t\t\t\treq.URL.Host = \"example.com\"\n\t\t\t}\n\n\t\t\tresp, err := app.Test(req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, http.StatusAccepted, resp.StatusCode)\n\t\t\trequire.Equal(t, tt.method, resp.Header.Get(\"X-Test\"))\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.expectBody {\n\t\t\t\trequire.Equal(t, \"hello from net/http \"+tt.method, string(body))\n\t\t\t} else {\n\t\t\t\trequire.Empty(t, body)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_App_Custom_Middleware_404_Should_Not_SetMethodNotAllowed(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.SendStatus(404)\n\t})\n\n\tapp.Post(\"/\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 404, resp.StatusCode)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Not Found\", string(body))\n\trequire.Equal(t, strconv.Itoa(len(\"Not Found\")), resp.Header.Get(HeaderContentLength))\n\n\tg := app.Group(\"/with-next\", func(c Ctx) error {\n\t\treturn c.Status(404).Next()\n\t})\n\n\tg.Post(\"/\", testEmptyHandler)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/with-next\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 404, resp.StatusCode)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Not Found\", string(body))\n\trequire.Equal(t, strconv.Itoa(len(\"Not Found\")), resp.Header.Get(HeaderContentLength))\n}\n\nfunc Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) {\n\tt.Parallel()\n\texpectedError := regexp.MustCompile(\n\t\t`error when reading request headers: small read buffer\\. Increase ReadBufferSize\\. Buffer size=4096, contents: \"GET / HTTP/1.1\\\\r\\\\nHost: example\\.com\\\\r\\\\nVery-Long-Header: -+`,\n\t)\n\tapp := New()\n\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\tpanic(errors.New(\"should never called\"))\n\t})\n\n\trequest := httptest.NewRequest(MethodGet, \"/\", http.NoBody)\n\tlogHeaderSlice := make([]string, 5000)\n\trequest.Header.Set(\"Very-Long-Header\", strings.Join(logHeaderSlice, \"-\"))\n\t_, err := app.Test(request)\n\tif err == nil {\n\t\tt.Error(\"Expect an error at app.Test(request)\")\n\t}\n\n\trequire.Regexp(t, expectedError, err.Error())\n}\n\nfunc Test_App_Errors(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tBodyLimit: 4,\n\t})\n\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"hi, i'm an error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 500, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hi, i'm an error\", string(body))\n\n\t_, err = app.Test(httptest.NewRequest(MethodGet, \"/\", strings.NewReader(\"big body\")))\n\tif err != nil {\n\t\trequire.Equal(t, \"body size exceeds the given limit\", err.Error(), \"app.Test(req)\")\n\t}\n}\n\nfunc Test_App_BodyLimit_Negative(t *testing.T) {\n\tt.Parallel()\n\n\tlimits := []int{-1, -512}\n\tfor _, limit := range limits {\n\t\tapp := New(Config{BodyLimit: limit})\n\n\t\tapp.Post(\"/\", func(c Ctx) error {\n\t\t\treturn c.SendStatus(StatusOK)\n\t\t})\n\n\t\tlargeBody := bytes.Repeat([]byte{'a'}, DefaultBodyLimit+1)\n\t\treq := httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(largeBody))\n\t\t_, err := app.Test(req)\n\t\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)\n\n\t\tsmallBody := bytes.Repeat([]byte{'a'}, DefaultBodyLimit-1)\n\t\treq = httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(smallBody))\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t}\n}\n\nfunc Test_App_BodyLimit_Zero(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{BodyLimit: 0})\n\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tlargeBody := bytes.Repeat([]byte{'a'}, DefaultBodyLimit+1)\n\treq := httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(largeBody))\n\t_, err := app.Test(req)\n\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)\n\n\tsmallBody := bytes.Repeat([]byte{'a'}, DefaultBodyLimit-1)\n\treq = httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(smallBody))\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\nfunc Test_App_BodyLimit_LargerThanDefault(t *testing.T) {\n\tt.Parallel()\n\n\tlimit := DefaultBodyLimit*2 + 1024 // slightly above double the default\n\tapp := New(Config{BodyLimit: limit})\n\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\t// Body larger than the default but within our custom limit should succeed\n\tmidBody := bytes.Repeat([]byte{'a'}, DefaultBodyLimit+512)\n\treq := httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(midBody))\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\t// Body above the custom limit should fail\n\tlargeBody := bytes.Repeat([]byte{'a'}, limit+1)\n\treq = httptest.NewRequest(MethodPost, \"/\", bytes.NewReader(largeBody))\n\t_, err = app.Test(req)\n\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)\n}\n\ntype customConstraint struct{}\n\nfunc (*customConstraint) Name() string {\n\treturn \"test\"\n}\n\nfunc (*customConstraint) Execute(param string, args ...string) bool {\n\tif param == \"test\" && len(args) == 1 && args[0] == \"test\" {\n\t\treturn true\n\t}\n\n\tif len(args) == 0 && param == \"c\" {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc Test_App_CustomConstraint(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.RegisterCustomConstraint(&customConstraint{})\n\n\tapp.Get(\"/test/:param<test(test)>\", func(c Ctx) error {\n\t\treturn c.SendString(\"test\")\n\t})\n\n\tapp.Get(\"/test2/:param<test>\", func(c Ctx) error {\n\t\treturn c.SendString(\"test\")\n\t})\n\n\tapp.Get(\"/test3/:param<test()>\", func(c Ctx) error {\n\t\treturn c.SendString(\"test\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/test2\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/c\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/cc\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test3/cc\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_ErrorHandler_Custom(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tErrorHandler: func(c Ctx, _ error) error {\n\t\t\treturn c.Status(200).SendString(\"hi, i'm a custom error\")\n\t\t},\n\t})\n\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"hi, i'm an error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hi, i'm a custom error\", string(body))\n}\n\nfunc Test_App_ErrorHandler_HandlerStack(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tErrorHandler: func(c Ctx, err error) error {\n\t\t\trequire.Equal(t, \"1: USE error\", err.Error())\n\t\t\treturn DefaultErrorHandler(c, err)\n\t\t},\n\t})\n\tapp.Use(\"/\", func(c Ctx) error {\n\t\terr := c.Next() // call next USE\n\t\trequire.Equal(t, \"2: USE error\", err.Error())\n\t\treturn errors.New(\"1: USE error\")\n\t}, func(c Ctx) error {\n\t\terr := c.Next() // call [0] GET\n\t\trequire.Equal(t, \"0: GET error\", err.Error())\n\t\treturn errors.New(\"2: USE error\")\n\t})\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"0: GET error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 500, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1: USE error\", string(body))\n}\n\nfunc Test_App_ErrorHandler_RouteStack(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tErrorHandler: func(c Ctx, err error) error {\n\t\t\trequire.Equal(t, \"1: USE error\", err.Error())\n\t\t\treturn DefaultErrorHandler(c, err)\n\t\t},\n\t})\n\tapp.Use(\"/\", func(c Ctx) error {\n\t\terr := c.Next()\n\t\trequire.Equal(t, \"0: GET error\", err.Error())\n\t\treturn errors.New(\"1: USE error\") // [2] call ErrorHandler\n\t})\n\tapp.Get(\"/test\", func(_ Ctx) error {\n\t\treturn errors.New(\"0: GET error\") // [1] return to USE\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 500, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1: USE error\", string(body))\n}\n\nfunc Test_App_serverErrorHandler_Internal_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tmsg := \"test err\"\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tapp.serverErrorHandler(c.fasthttp, errors.New(msg))\n\trequire.Equal(t, string(c.fasthttp.Response.Body()), msg)\n\trequire.Equal(t, StatusBadRequest, c.fasthttp.Response.StatusCode())\n}\n\nfunc Test_App_serverErrorHandler_Network_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tapp.serverErrorHandler(c.fasthttp, &net.DNSError{\n\t\tErr:       \"test error\",\n\t\tName:      \"test host\",\n\t\tIsTimeout: false,\n\t})\n\trequire.Equal(t, string(c.fasthttp.Response.Body()), utils.StatusMessage(StatusBadGateway))\n\trequire.Equal(t, StatusBadGateway, c.fasthttp.Response.StatusCode())\n}\n\nfunc Test_App_serverErrorHandler_Unsupported_Method_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tapp.serverErrorHandler(c.fasthttp, errors.New(\"unsupported http request method 'FOO'\"))\n\trequire.Equal(t, utils.StatusMessage(StatusNotImplemented), string(c.fasthttp.Response.Body()))\n\trequire.Equal(t, StatusNotImplemented, c.fasthttp.Response.StatusCode())\n}\n\nfunc Test_App_serverErrorHandler_Unsupported_Method_Request(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/bar\", func(c Ctx) error {\n\t\treturn c.SendString(\"bar\")\n\t})\n\n\tln := fasthttputil.NewInmemoryListener()\n\n\tserverStarted := make(chan struct{}, 1)\n\tserverErr := make(chan error, 1)\n\n\tgo func() {\n\t\tserverStarted <- struct{}{}\n\t\tif err := app.Listener(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\tserverErr <- err\n\t\t\treturn\n\t\t}\n\t\tserverErr <- nil\n\t}()\n\n\t<-serverStarted\n\n\tconn, err := ln.Dial()\n\trequire.NoError(t, err)\n\trequire.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))\n\n\t_, err = conn.Write([]byte(\"FOO /bar HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\"))\n\trequire.NoError(t, err)\n\n\tresp, err := http.ReadResponse(bufio.NewReader(conn), nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNotImplemented, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, utils.StatusMessage(StatusNotImplemented), string(body))\n\trequire.NoError(t, resp.Body.Close())\n\trequire.NoError(t, conn.Close())\n\n\trequire.NoError(t, app.Shutdown())\n\trequire.NoError(t, <-serverErr)\n}\n\nfunc Test_App_Nested_Params(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.Status(400).Send([]byte(\"Should move on\"))\n\t})\n\tapp.Get(\"/test/:param\", func(c Ctx) error {\n\t\treturn c.Status(400).Send([]byte(\"Should move on\"))\n\t})\n\tapp.Get(\"/test/:param/test\", func(c Ctx) error {\n\t\treturn c.Status(400).Send([]byte(\"Should move on\"))\n\t})\n\tapp.Get(\"/test/:param/test/:param2\", func(c Ctx) error {\n\t\treturn c.Status(200).Send([]byte(\"Good job\"))\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/test/john/test/doe\", http.NoBody)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Use_Params(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(\"/prefix/:param\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.Params(\"param\"))\n\t\treturn nil\n\t})\n\n\tapp.Use(\"/foo/:bar?\", func(c Ctx) error {\n\t\trequire.Equal(t, \"foobar\", c.Params(\"bar\", \"foobar\"))\n\t\treturn nil\n\t})\n\n\tapp.Use(\"/:param/*\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.Params(\"param\"))\n\t\trequire.Equal(t, \"doe\", c.Params(\"*\"))\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/prefix/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/john/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\trequire.PanicsWithValue(t, \"use: invalid handler func()\\n\", func() {\n\t\tapp.Use(\"/:param/*\", func() {\n\t\t\t// this should panic\n\t\t})\n\t})\n}\n\nfunc Test_App_Use_UnescapedPath(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{UnescapePath: true, CaseSensitive: true})\n\n\tapp.Use(\"/cRéeR/:param\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/cRéeR/اختبار\", c.Path())\n\t\treturn c.SendString(c.Params(\"param\"))\n\t})\n\n\tapp.Use(\"/abc\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/AbC\", c.Path())\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/cR%C3%A9eR/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\t// check the param result\n\trequire.Equal(t, \"اختبار\", app.toString(body))\n\n\t// with lowercase letters\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/cr%C3%A9er/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Use_CaseSensitive(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{CaseSensitive: true})\n\n\tapp.Use(\"/abc\", func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\t// wrong letters in the requested route -> 404\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/AbC\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n\n\t// right letters in the requested route -> 200\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/abc\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// check the detected path when the case-insensitive recognition is activated\n\tapp.config.CaseSensitive = false\n\t// check the case-sensitive feature\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/AbC\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\t// check the detected path result\n\trequire.Equal(t, \"/AbC\", app.toString(body))\n}\n\nfunc Test_App_Not_Use_StrictRouting(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(\"/abc\", func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\tg := app.Group(\"/foo\")\n\tg.Use(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\t// wrong path in the requested route -> 404\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/abc/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// right path in the requested route -> 200\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/abc\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// wrong path with group in the requested route -> 404\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// right path with group in the requested route -> 200\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Use_MultiplePrefix(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use([]string{\"/john\", \"/doe\"}, func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\tg := app.Group(\"/test\")\n\tg.Use([]string{\"/john\", \"/doe\"}, func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/john\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/doe\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/test/john\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/test/doe\", string(body))\n}\n\nfunc Test_Group_Use_NoBoundary(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tgrp := app.Group(\"/api\")\n\n\tgrp.Use(\"/foo\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api/foo/bar\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/api/foobar\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Use_StrictRouting(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{StrictRouting: true})\n\n\tapp.Get(\"/abc\", func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\tg := app.Group(\"/foo\")\n\tg.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(c.Path())\n\t})\n\n\t// wrong path in the requested route -> 404\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/abc/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n\n\t// right path in the requested route -> 200\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/abc\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// wrong path with group in the requested route -> 404\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n\n\t// right path with group in the requested route -> 200\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Add_Method_Test(t *testing.T) {\n\tt.Parallel()\n\n\tmethods := append(DefaultMethods, \"JOHN\") //nolint:gocritic // We want a new slice here\n\tapp := New(Config{\n\t\tRequestMethods: methods,\n\t})\n\n\tapp.Add([]string{\"JOHN\"}, \"/john\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(\"JOHN\", \"/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusMethodNotAllowed, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(\"UNKNOWN\", \"/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotImplemented, resp.StatusCode, \"Status code\")\n\n\t// Add a new method\n\trequire.Panics(t, func() {\n\t\tapp.Add([]string{\"JANE\"}, \"/jane\", testEmptyHandler)\n\t})\n}\n\nfunc Test_App_All_Method_Test(t *testing.T) {\n\tt.Parallel()\n\n\tmethods := append(DefaultMethods, \"JOHN\") //nolint:gocritic // We want a new slice here\n\tapp := New(Config{\n\t\tRequestMethods: methods,\n\t})\n\n\t// Add a new method with All\n\tapp.All(\"/doe\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(\"JOHN\", \"/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// Add a new method\n\trequire.Panics(t, func() {\n\t\tapp.Add([]string{\"JANE\"}, \"/jane\", testEmptyHandler)\n\t})\n}\n\n// go test -run Test_App_GETOnly\nfunc Test_App_GETOnly(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tGETOnly: true,\n\t})\n\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(\"Hello 👋!\")\n\t})\n\n\treq := httptest.NewRequest(MethodPost, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusMethodNotAllowed, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Use_Params_Group(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tgroup := app.Group(\"/prefix/:param/*\")\n\tgroup.Use(\"/\", func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tgroup.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.Params(\"param\"))\n\t\trequire.Equal(t, \"doe\", c.Params(\"*\"))\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/prefix/john/doe/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Chaining(t *testing.T) {\n\tt.Parallel()\n\tn := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\tapp := New()\n\tapp.Use(\"/john\", n, n, n, n, func(c Ctx) error {\n\t\treturn c.SendStatus(202)\n\t})\n\t// check handler count for registered HEAD route\n\trequire.Len(t, app.stack[app.methodInt(MethodHead)][0].Handlers, 5, \"app.Test(req)\")\n\n\treq := httptest.NewRequest(MethodPost, \"/john\", http.NoBody)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 202, resp.StatusCode, \"Status code\")\n\n\tapp.Get(\"/test\", n, n, n, n, func(c Ctx) error {\n\t\treturn c.SendStatus(203)\n\t})\n\n\treq = httptest.NewRequest(MethodGet, \"/test\", http.NoBody)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 203, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Order(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\t_, err := c.WriteString(\"1\")\n\t\trequire.NoError(t, err)\n\t\treturn c.Next()\n\t})\n\n\tapp.All(\"/test\", func(c Ctx) error {\n\t\t_, err := c.WriteString(\"2\")\n\t\trequire.NoError(t, err)\n\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(func(c Ctx) error {\n\t\t_, err := c.WriteString(\"3\")\n\t\trequire.NoError(t, err)\n\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/test\", http.NoBody)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n}\n\nfunc Test_App_AutoHead_Compliance(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/hello\", func(c Ctx) error {\n\t\tc.Set(\"X-Test\", \"string\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\tapp.startupProcess()\n\n\tgetReq := httptest.NewRequest(MethodGet, \"/hello\", http.NoBody)\n\tgetResp, err := app.Test(getReq)\n\trequire.NoError(t, err, \"app.Test(get)\")\n\tdefer func() {\n\t\trequire.NoError(t, getResp.Body.Close())\n\t}()\n\n\tbody, err := io.ReadAll(getResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", string(body))\n\trequire.Equal(t, \"string\", getResp.Header.Get(\"X-Test\"))\n\n\theadReq := httptest.NewRequest(MethodHead, \"/hello\", http.NoBody)\n\theadResp, err := app.Test(headReq)\n\trequire.NoError(t, err, \"app.Test(head)\")\n\tdefer func() {\n\t\trequire.NoError(t, headResp.Body.Close())\n\t}()\n\n\trequire.Equal(t, getResp.StatusCode, headResp.StatusCode)\n\trequire.Equal(t, strconv.Itoa(len(body)), headResp.Header.Get(HeaderContentLength))\n\trequire.Equal(t, getResp.Header.Get(HeaderContentType), headResp.Header.Get(HeaderContentType))\n\trequire.Equal(t, getResp.Header.Get(\"X-Test\"), headResp.Header.Get(\"X-Test\"))\n\n\theadBody, err := io.ReadAll(headResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, headBody)\n}\n\nfunc Test_App_AutoHead_Compliance_SendFile(t *testing.T) {\n\tt.Parallel()\n\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"SendFile auto-HEAD test is skipped on Windows due to file locking semantics\")\n\t}\n\n\ttmpDir := t.TempDir()\n\tfilePath := filepath.Join(tmpDir, \"hello.txt\")\n\tfileContent := []byte(\"file-body\")\n\trequire.NoError(t, os.WriteFile(filePath, fileContent, 0o644)) //nolint:gosec // permissions match test fixtures\n\n\tapp := New()\n\tapp.Get(\"/file\", func(c Ctx) error {\n\t\tc.Set(\"X-Test\", \"file\")\n\t\treturn c.SendFile(filePath)\n\t})\n\tapp.startupProcess()\n\n\tgetReq := httptest.NewRequest(MethodGet, \"/file\", http.NoBody)\n\tgetResp, err := app.Test(getReq)\n\trequire.NoError(t, err, \"app.Test(get)\")\n\tdefer func() {\n\t\trequire.NoError(t, getResp.Body.Close())\n\t}()\n\n\tbody, err := io.ReadAll(getResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fileContent, body)\n\trequire.Equal(t, \"file\", getResp.Header.Get(\"X-Test\"))\n\n\theadReq := httptest.NewRequest(MethodHead, \"/file\", http.NoBody)\n\theadResp, err := app.Test(headReq)\n\trequire.NoError(t, err, \"app.Test(head)\")\n\tdefer func() {\n\t\trequire.NoError(t, headResp.Body.Close())\n\t}()\n\n\trequire.Equal(t, getResp.StatusCode, headResp.StatusCode)\n\trequire.Equal(t, strconv.Itoa(len(fileContent)), headResp.Header.Get(HeaderContentLength))\n\trequire.Equal(t, getResp.Header.Get(HeaderContentType), headResp.Header.Get(HeaderContentType))\n\trequire.Equal(t, getResp.Header.Get(\"X-Test\"), headResp.Header.Get(\"X-Test\"))\n\n\theadBody, err := io.ReadAll(headResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, headBody)\n}\n\nfunc Test_App_Methods(t *testing.T) {\n\tt.Parallel()\n\tdummyHandler := testEmptyHandler\n\n\tapp := New()\n\n\tapp.Connect(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", \"CONNECT\")\n\n\tapp.Put(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodPut)\n\n\tapp.Post(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodPost)\n\n\tapp.Delete(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodDelete)\n\n\tapp.Head(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodHead)\n\n\tapp.Patch(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodPatch)\n\n\tapp.Options(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodOptions)\n\n\tapp.Trace(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodTrace)\n\n\tapp.Get(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodGet)\n\n\tapp.All(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodPost)\n\n\tapp.Use(\"/:john?/:doe?\", dummyHandler)\n\ttestStatus200(t, app, \"/john/doe\", MethodGet)\n}\n\nfunc Test_App_Route_Naming(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\thandler := func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t}\n\tapp.Get(\"/john\", handler).Name(\"john\")\n\tapp.Delete(\"/doe\", handler)\n\tapp.Name(\"doe\")\n\n\tjane := app.Group(\"/jane\").Name(\"jane.\")\n\tgroup := app.Group(\"/group\")\n\tsubGroup := jane.Group(\"/sub-group\").Name(\"sub.\")\n\n\tjane.Get(\"/test\", handler).Name(\"test\")\n\tjane.Trace(\"/trace\", handler).Name(\"trace\")\n\n\tgroup.Get(\"/test\", handler).Name(\"test\")\n\n\tapp.Post(\"/post\", handler).Name(\"post\")\n\n\tsubGroup.Get(\"/done\", handler).Name(\"done\")\n\n\trequire.Equal(t, \"post\", app.GetRoute(\"post\").Name)\n\trequire.Equal(t, \"john\", app.GetRoute(\"john\").Name)\n\trequire.Equal(t, \"jane.test\", app.GetRoute(\"jane.test\").Name)\n\trequire.Equal(t, \"jane.trace\", app.GetRoute(\"jane.trace\").Name)\n\trequire.Equal(t, \"jane.sub.done\", app.GetRoute(\"jane.sub.done\").Name)\n\trequire.Equal(t, \"test\", app.GetRoute(\"test\").Name)\n}\n\nfunc Test_App_New(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", testEmptyHandler)\n\n\tappConfig := New(Config{\n\t\tImmutable: true,\n\t})\n\tappConfig.Get(\"/\", testEmptyHandler)\n}\n\nfunc Test_App_Config(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tStrictRouting: true,\n\t})\n\trequire.True(t, app.Config().StrictRouting)\n}\n\nfunc Test_App_GetString(t *testing.T) {\n\tt.Parallel()\n\n\theap := string([]byte(\"fiber\"))\n\tappMutable := New()\n\tsame := appMutable.GetString(heap)\n\tif unsafe.StringData(same) != unsafe.StringData(heap) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected original string when immutable is disabled\")\n\t}\n\n\tappImmutable := New(Config{Immutable: true})\n\tcopied := appImmutable.GetString(heap)\n\tif unsafe.StringData(copied) == unsafe.StringData(heap) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected a copy for heap-backed string when immutable is enabled\")\n\t}\n\n\tliteral := \"fiber\"\n\tsameLit := appImmutable.GetString(literal)\n\tif unsafe.StringData(sameLit) != unsafe.StringData(literal) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected original literal when immutable is enabled\")\n\t}\n}\n\nfunc Test_App_GetBytes(t *testing.T) {\n\tt.Parallel()\n\n\tb := []byte(\"fiber\")\n\tappMutable := New()\n\tsame := appMutable.GetBytes(b)\n\tif unsafe.SliceData(same) != unsafe.SliceData(b) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected original slice when immutable is disabled\")\n\t}\n\n\talias := make([]byte, 10)\n\tcopy(alias, b)\n\tsub := alias[:5]\n\tappImmutable := New(Config{Immutable: true})\n\tcopied := appImmutable.GetBytes(sub)\n\tif unsafe.SliceData(copied) == unsafe.SliceData(sub) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected a copy for aliased slice when immutable is enabled\")\n\t}\n\n\tfull := make([]byte, 5)\n\tcopy(full, b)\n\tdetached := appImmutable.GetBytes(full)\n\tif unsafe.SliceData(detached) == unsafe.SliceData(full) { //nolint:gosec // compare pointer addresses\n\t\tt.Error(\"expected a copy even when cap==len\")\n\t}\n}\n\nfunc Test_App_Shutdown(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\tt.Run(\"no server\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := &App{}\n\t\tif err := app.Shutdown(); err != nil {\n\t\t\trequire.ErrorContains(t, err, \"shutdown: server is not running\")\n\t\t}\n\t})\n}\n\nfunc Test_App_ShutdownWithTimeout(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\ttime.Sleep(5 * time.Second)\n\t\treturn c.SendString(\"body\")\n\t})\n\n\tln := fasthttputil.NewInmemoryListener()\n\tserverReady := make(chan struct{}) // Signal that the server is ready to start\n\n\tgo func() {\n\t\tserverReady <- struct{}{}\n\t\terr := app.Listener(ln)\n\t\tassert.NoError(t, err)\n\t}()\n\n\t<-serverReady // Waiting for the server to be ready\n\n\t// Create a connection and send a request\n\tconnReady := make(chan struct{})\n\tgo func() {\n\t\tconn, err := ln.Dial()\n\t\tassert.NoError(t, err)\n\n\t\t_, err = conn.Write([]byte(\"GET / HTTP/1.1\\r\\nHost: google.com\\r\\n\\r\\n\"))\n\t\tassert.NoError(t, err)\n\n\t\tconnReady <- struct{}{} // Signal that the request has been sent\n\t}()\n\n\t<-connReady // Waiting for the request to be sent\n\n\tshutdownErr := make(chan error)\n\tgo func() {\n\t\tshutdownErr <- app.ShutdownWithTimeout(1 * time.Second)\n\t}()\n\n\ttimer := time.NewTimer(time.Second * 5)\n\tselect {\n\tcase <-timer.C:\n\t\tt.Fatal(\"idle connections not closed on shutdown\")\n\tcase err := <-shutdownErr:\n\t\tif err == nil || !errors.Is(err, context.DeadlineExceeded) {\n\t\t\tt.Fatalf(\"unexpected err %v. Expecting %v\", err, context.DeadlineExceeded)\n\t\t}\n\t}\n}\n\nfunc Test_App_ShutdownWithContext(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"successful shutdown\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\n\t\t// Fast request that should complete\n\t\tapp.Get(\"/\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tserverStarted := make(chan bool, 1)\n\n\t\tgo func() {\n\t\t\tserverStarted <- true\n\t\t\tif err := app.Listener(ln); err != nil {\n\t\t\t\tt.Errorf(\"Failed to start listener: %v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t<-serverStarted\n\n\t\t// Execute normal request\n\t\tconn, err := ln.Dial()\n\t\trequire.NoError(t, err)\n\t\t_, err = conn.Write([]byte(\"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\"))\n\t\trequire.NoError(t, err)\n\n\t\t// Shutdown with sufficient timeout\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\terr = app.ShutdownWithContext(ctx)\n\t\trequire.NoError(t, err, \"Expected successful shutdown\")\n\t})\n\n\tt.Run(\"shutdown with hooks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\n\t\thookOrder := make([]string, 0)\n\t\tvar hookMutex sync.Mutex\n\n\t\tapp.Hooks().OnPreShutdown(func() error {\n\t\t\thookMutex.Lock()\n\t\t\thookOrder = append(hookOrder, \"pre\")\n\t\t\thookMutex.Unlock()\n\t\t\treturn nil\n\t\t})\n\n\t\tapp.Hooks().OnPostShutdown(func(_ error) error {\n\t\t\thookMutex.Lock()\n\t\t\thookOrder = append(hookOrder, \"post\")\n\t\t\thookMutex.Unlock()\n\t\t\treturn nil\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tgo func() {\n\t\t\tif err := app.Listener(ln); err != nil {\n\t\t\t\tt.Errorf(\"Failed to start listener: %v\", err)\n\t\t\t}\n\t\t}()\n\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\terr := app.ShutdownWithContext(context.Background())\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, []string{\"pre\", \"post\"}, hookOrder, \"Hooks should execute in order\")\n\t})\n\n\tt.Run(\"timeout with long running request\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\n\t\trequestStarted := make(chan struct{})\n\t\trequestProcessing := make(chan struct{})\n\n\t\tapp.Get(\"/\", func(c Ctx) error {\n\t\t\tclose(requestStarted)\n\t\t\t// Wait for signal to continue processing the request\n\t\t\t<-requestProcessing\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tgo func() {\n\t\t\tif err := app.Listener(ln); err != nil {\n\t\t\t\tt.Errorf(\"Failed to start listener: %v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t// Ensure server is fully started\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t// Start a long-running request\n\t\tgo func() {\n\t\t\tconn, err := ln.Dial()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Failed to dial: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, err := conn.Write([]byte(\"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\")); err != nil {\n\t\t\t\tt.Errorf(\"Failed to write: %v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t// Wait for request to start\n\t\tselect {\n\t\tcase <-requestStarted:\n\t\t\t// Request has started, signal to continue processing\n\t\t\tclose(requestProcessing)\n\t\tcase <-time.After(2 * time.Second):\n\t\t\tt.Fatal(\"Request did not start in time\")\n\t\t}\n\n\t\t// Attempt shutdown, should timeout\n\t\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\t\tdefer cancel()\n\n\t\terr := app.ShutdownWithContext(ctx)\n\t\trequire.ErrorIs(t, err, context.DeadlineExceeded)\n\t})\n}\n\nfunc Test_App_OptionsAsterisk(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Options(\"/resource\", func(c Ctx) error {\n\t\tc.Set(HeaderAllow, \"GET\")\n\t\tc.Status(StatusNoContent)\n\n\t\treturn nil\n\t})\n\tapp.Options(\"*\", func(c Ctx) error {\n\t\tc.Set(HeaderAllow, \"GET, POST\")\n\t\tc.Status(StatusOK)\n\n\t\treturn nil\n\t})\n\n\tln := fasthttputil.NewInmemoryListener()\n\terrCh := make(chan error, 1)\n\tserverReady := make(chan struct{})\n\n\tgo func() {\n\t\tserverReady <- struct{}{}\n\t\terrCh <- app.Listener(ln)\n\t}()\n\n\t<-serverReady\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, app.Shutdown())\n\t\trequire.NoError(t, <-errCh)\n\t})\n\n\twriteRequest := func(conn net.Conn, raw string) {\n\t\tt.Helper()\n\t\t_, err := conn.Write([]byte(raw))\n\t\trequire.NoError(t, err)\n\t}\n\n\tconn, err := ln.Dial()\n\trequire.NoError(t, err)\n\n\twriteRequest(conn, \"OPTIONS * HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\")\n\n\tresp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: http.MethodOptions})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"GET, POST\", resp.Header.Get(HeaderAllow))\n\trequire.Zero(t, resp.ContentLength)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, body)\n\trequire.NoError(t, resp.Body.Close())\n\trequire.NoError(t, conn.Close())\n\n\tcontrolConn, err := ln.Dial()\n\trequire.NoError(t, err)\n\n\twriteRequest(controlConn, \"OPTIONS /resource HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\")\n\n\tcontrolResp, err := http.ReadResponse(bufio.NewReader(controlConn), &http.Request{Method: http.MethodOptions})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNoContent, controlResp.StatusCode)\n\trequire.Equal(t, \"GET\", controlResp.Header.Get(HeaderAllow))\n\trequire.Zero(t, controlResp.ContentLength)\n\tcontrolBody, err := io.ReadAll(controlResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, controlBody)\n\trequire.NoError(t, controlResp.Body.Close())\n\trequire.NoError(t, controlConn.Close())\n}\n\n// go test -run Test_App_Mixed_Routes_WithSameLen\nfunc Test_App_Mixed_Routes_WithSameLen(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// middleware\n\tapp.Use(func(c Ctx) error {\n\t\tc.Set(\"TestHeader\", \"TestValue\")\n\t\treturn c.Next()\n\t})\n\t// routes with the same length\n\tapp.Get(\"/tesbar\", func(c Ctx) error {\n\t\tc.Type(\"html\")\n\t\treturn c.Send([]byte(\"TEST_BAR\"))\n\t})\n\tapp.Get(\"/foobar\", func(c Ctx) error {\n\t\tc.Type(\"html\")\n\t\treturn c.Send([]byte(\"FOO_BAR\"))\n\t})\n\n\t// match get route\n\treq := httptest.NewRequest(MethodGet, \"/foobar\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(HeaderContentLength))\n\trequire.Equal(t, \"TestValue\", resp.Header.Get(\"TestHeader\"))\n\trequire.Equal(t, \"text/html; charset=utf-8\", resp.Header.Get(HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"FOO_BAR\", string(body))\n\n\t// match static route\n\treq = httptest.NewRequest(MethodGet, \"/tesbar\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(HeaderContentLength))\n\trequire.Equal(t, \"TestValue\", resp.Header.Get(\"TestHeader\"))\n\trequire.Equal(t, \"text/html; charset=utf-8\", resp.Header.Get(HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"TEST_BAR\")\n}\n\nfunc Test_App_Group_Invalid(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithValue(t, \"use: invalid handler int\\n\", func() {\n\t\tNew().Group(\"/\").Use(1)\n\t})\n}\n\nfunc Test_App_Group(t *testing.T) {\n\tt.Parallel()\n\tdummyHandler := testEmptyHandler\n\n\tapp := New()\n\n\tgrp := app.Group(\"/test\")\n\tgrp.Get(\"/\", dummyHandler)\n\ttestStatus200(t, app, \"/test\", MethodGet)\n\n\tgrp.Get(\"/:demo?\", dummyHandler)\n\ttestStatus200(t, app, \"/test/john\", MethodGet)\n\n\tgrp.Connect(\"/CONNECT\", dummyHandler)\n\ttestStatus200(t, app, \"/test/CONNECT\", MethodConnect)\n\n\tgrp.Put(\"/PUT\", dummyHandler)\n\ttestStatus200(t, app, \"/test/PUT\", MethodPut)\n\n\tgrp.Post(\"/POST\", dummyHandler)\n\ttestStatus200(t, app, \"/test/POST\", MethodPost)\n\n\tgrp.Delete(\"/DELETE\", dummyHandler)\n\ttestStatus200(t, app, \"/test/DELETE\", MethodDelete)\n\n\tgrp.Head(\"/HEAD\", dummyHandler)\n\ttestStatus200(t, app, \"/test/HEAD\", MethodHead)\n\n\tgrp.Patch(\"/PATCH\", dummyHandler)\n\ttestStatus200(t, app, \"/test/PATCH\", MethodPatch)\n\n\tgrp.Options(\"/OPTIONS\", dummyHandler)\n\ttestStatus200(t, app, \"/test/OPTIONS\", MethodOptions)\n\n\tgrp.Trace(\"/TRACE\", dummyHandler)\n\ttestStatus200(t, app, \"/test/TRACE\", MethodTrace)\n\n\tgrp.All(\"/ALL\", dummyHandler)\n\ttestStatus200(t, app, \"/test/ALL\", MethodPost)\n\n\tgrp.Use(dummyHandler)\n\ttestStatus200(t, app, \"/test/oke\", MethodGet)\n\n\tgrp.Use(\"/USE\", dummyHandler)\n\ttestStatus200(t, app, \"/test/USE/oke\", MethodGet)\n\n\tapi := grp.Group(\"/v1\")\n\tapi.Post(\"/\", dummyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodPost, \"/test/v1/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tapi.Get(\"/users\", dummyHandler)\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/v1/UsErS\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_RouteChain(t *testing.T) {\n\tt.Parallel()\n\tdummyHandler := testEmptyHandler\n\n\tapp := New()\n\n\tregister := app.RouteChain(\"/test\").\n\t\tGet(dummyHandler).\n\t\tHead(dummyHandler).\n\t\tPost(dummyHandler).\n\t\tPut(dummyHandler).\n\t\tDelete(dummyHandler).\n\t\tConnect(dummyHandler).\n\t\tOptions(dummyHandler).\n\t\tTrace(dummyHandler).\n\t\tPatch(dummyHandler)\n\n\ttestStatus200(t, app, \"/test\", MethodGet)\n\ttestStatus200(t, app, \"/test\", MethodHead)\n\ttestStatus200(t, app, \"/test\", MethodPost)\n\ttestStatus200(t, app, \"/test\", MethodPut)\n\ttestStatus200(t, app, \"/test\", MethodDelete)\n\ttestStatus200(t, app, \"/test\", MethodConnect)\n\ttestStatus200(t, app, \"/test\", MethodOptions)\n\ttestStatus200(t, app, \"/test\", MethodTrace)\n\ttestStatus200(t, app, \"/test\", MethodPatch)\n\n\tregister.RouteChain(\"/v1\").Get(dummyHandler).Post(dummyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodPost, \"/test/v1\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/v1\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tregister.RouteChain(\"/v1\").RouteChain(\"/v2\").RouteChain(\"/v3\").Get(dummyHandler).Trace(dummyHandler)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodTrace, \"/test/v1/v2/v3\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test/v1/v2/v3\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_App_Route(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Route(\"/test\", func(api Router) {\n\t\tapi.Get(\"/foo\", testEmptyHandler).Name(\"foo\")\n\n\t\tapi.Route(\"/bar\", func(bar Router) {\n\t\t\tbar.Get(\"/\", testEmptyHandler).Name(\"index\")\n\t\t}, \"bar.\")\n\t}, \"test.\")\n\n\ttestStatus200(t, app, \"/test/foo\", MethodGet)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test/bar/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, http.StatusOK, resp.StatusCode, \"Status code\")\n\n\trequire.Equal(t, \"/test/foo\", app.GetRoute(\"test.foo\").Path)\n\trequire.Equal(t, \"/test/bar/\", app.GetRoute(\"test.bar.index\").Path)\n}\n\nfunc Test_App_Route_nilFuncPanics(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\trequire.PanicsWithValue(t, \"route handler 'fn' cannot be nil\", func() {\n\t\tapp.Route(\"/panic\", nil)\n\t})\n}\n\nfunc Test_Group_Route_nilFuncPanics(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tgrp := app.Group(\"/api\")\n\n\trequire.PanicsWithValue(t, \"route handler 'fn' cannot be nil\", func() {\n\t\tgrp.Route(\"/panic\", nil)\n\t})\n}\n\nfunc Test_Group_RouteChain_All(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tvar calls []string\n\tgrp := app.Group(\"/api\", func(c Ctx) error {\n\t\tcalls = append(calls, \"group\")\n\t\treturn c.Next()\n\t})\n\n\tgrp.RouteChain(\"/users\").All(func(c Ctx) error {\n\t\tcalls = append(calls, \"routechain\")\n\t\treturn c.SendStatus(http.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api/users\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, http.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, []string{\"group\", \"routechain\"}, calls)\n}\n\nfunc Test_App_Deep_Group(t *testing.T) {\n\tt.Parallel()\n\trunThroughCount := 0\n\tdummyHandler := func(c Ctx) error {\n\t\trunThroughCount++\n\t\treturn c.Next()\n\t}\n\n\tapp := New()\n\tgAPI := app.Group(\"/api\", dummyHandler)\n\tgV1 := gAPI.Group(\"/v1\", dummyHandler)\n\tgUser := gV1.Group(\"/user\", dummyHandler)\n\tgUser.Get(\"/authenticate\", func(c Ctx) error {\n\t\trunThroughCount++\n\t\treturn c.SendStatus(200)\n\t})\n\ttestStatus200(t, app, \"/api/v1/user/authenticate\", MethodGet)\n\trequire.Equal(t, 4, runThroughCount, \"Loop count\")\n}\n\n// go test -run Test_App_Next_Method\nfunc Test_App_Next_Method(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\trequire.Equal(t, MethodGet, c.Method())\n\t\terr := c.Next()\n\t\trequire.Equal(t, MethodGet, c.Method())\n\t\treturn err\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_NewError -benchmem -count=4\nfunc Benchmark_NewError(b *testing.B) {\n\tfor b.Loop() {\n\t\tNewError(200, \"test\") //nolint:errcheck // not needed\n\t}\n}\n\nfunc Benchmark_NewError_Parallel(b *testing.B) {\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tNewError(200, \"test\") //nolint:errcheck // not needed\n\t\t}\n\t})\n}\n\n// go test -run Test_NewError\nfunc Test_NewError(t *testing.T) {\n\tt.Parallel()\n\te := NewError(StatusForbidden, \"permission denied\")\n\trequire.Equal(t, StatusForbidden, e.Code)\n\trequire.Equal(t, \"permission denied\", e.Message)\n}\n\n// go test -run Test_NewError_Format\nfunc Test_NewErrorf_Format(t *testing.T) {\n\tt.Parallel()\n\n\ttype args []any\n\n\ttests := []struct {\n\t\tname string\n\t\twant string\n\t\tin   args\n\t\tcode int\n\t}{\n\t\t{\n\t\t\tname: \"no-args → default text\",\n\t\t\tcode: StatusNotFound,\n\t\t\tin:   nil,\n\t\t\twant: utils.StatusMessage(StatusNotFound),\n\t\t},\n\t\t{\n\t\t\tname: \"single-string arg overrides\",\n\t\t\tcode: StatusBadRequest,\n\t\t\tin:   args{\"custom bad request\"},\n\t\t\twant: \"custom bad request\",\n\t\t},\n\t\t{\n\t\t\tname: \"single non-string arg stringified\",\n\t\t\tcode: StatusInternalServerError,\n\t\t\tin:   args{errors.New(\"db down\")},\n\t\t\twant: \"db down\",\n\t\t},\n\t\t{\n\t\t\tname: \"single nil interface\",\n\t\t\tcode: StatusInternalServerError,\n\t\t\tin:   args{any(nil)},\n\t\t\twant: \"<nil>\",\n\t\t},\n\t\t{\n\t\t\tname: \"format string + args\",\n\t\t\tcode: StatusBadRequest,\n\t\t\tin:   args{\"invalid id %d\", 10},\n\t\t\twant: \"invalid id 10\",\n\t\t},\n\t\t{\n\t\t\tname: \"format string + excess args\",\n\t\t\tcode: StatusBadRequest,\n\t\t\tin:   args{\"odd %d\", 1, 2, 3},\n\t\t\twant: \"odd 1%!(EXTRA int=2, int=3)\",\n\t\t},\n\t\t{\n\t\t\tname: \"≥2 args but first not string\",\n\t\t\tcode: StatusBadRequest,\n\t\t\tin:   args{errors.New(\"boom\"), 42},\n\t\t\twant: \"boom\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\te := NewErrorf(tt.code, tt.in...)\n\t\t\trequire.Equal(t, tt.code, e.Code)\n\t\t\trequire.Equal(t, tt.want, e.Message)\n\t\t})\n\t}\n}\n\n// go test -run Test_Test_Timeout\nfunc Test_Test_Timeout(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout: 0,\n\t})\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tapp.Get(\"timeout\", func(_ Ctx) error {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\treturn nil\n\t})\n\n\t_, err = app.Test(httptest.NewRequest(MethodGet, \"/timeout\", http.NoBody), TestConfig{\n\t\tTimeout:       20 * time.Millisecond,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.Error(t, err, \"app.Test(req)\")\n}\n\ntype errorReader int\n\nvar errErrorReader = errors.New(\"errorReader\")\n\nfunc (errorReader) Read([]byte) (int, error) {\n\treturn 0, errErrorReader\n}\n\n// go test -run Test_Test_DumpError\nfunc Test_Test_DumpError(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/\", testEmptyHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", errorReader(0)))\n\trequire.Nil(t, resp)\n\trequire.ErrorIs(t, err, errErrorReader)\n}\n\n// go test -run Test_App_Handler\nfunc Test_App_Handler(t *testing.T) {\n\tt.Parallel()\n\th := New().Handler()\n\trequire.Equal(t, \"fasthttp.RequestHandler\", reflect.TypeOf(h).String())\n}\n\ntype invalidView struct{}\n\nfunc (invalidView) Load() error { return errors.New(\"invalid view\") }\n\nfunc (invalidView) Render(io.Writer, string, any, ...string) error { panic(\"implement me\") }\n\ntype countingView struct {\n\tloadErr error\n\tloads   int\n}\n\nfunc (v *countingView) Load() error {\n\tv.loads++\n\treturn v.loadErr\n}\n\nfunc (*countingView) Render(io.Writer, string, any, ...string) error { return nil }\n\nfunc Test_App_ReloadViews_Success(t *testing.T) {\n\tt.Parallel()\n\tview := &countingView{}\n\tapp := New(Config{Views: view})\n\tinitialLoads := view.loads\n\n\trequire.NoError(t, app.ReloadViews())\n\trequire.Equal(t, initialLoads+1, view.loads)\n\n\trequire.NoError(t, app.ReloadViews())\n\trequire.Equal(t, initialLoads+2, view.loads)\n}\n\nfunc Test_App_ReloadViews_Error(t *testing.T) {\n\tt.Parallel()\n\twantedErr := errors.New(\"boom\")\n\tview := &countingView{loadErr: wantedErr}\n\tapp := New(Config{Views: view})\n\n\terr := app.ReloadViews()\n\trequire.Error(t, err)\n\trequire.ErrorIs(t, err, wantedErr)\n}\n\nfunc Test_App_ReloadViews_NoEngine(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\terr := app.ReloadViews()\n\trequire.ErrorIs(t, err, ErrNoViewEngineConfigured)\n}\n\nfunc Test_App_ReloadViews_InterfaceNilPointer(t *testing.T) {\n\tt.Parallel()\n\tvar view *countingView\n\tapp := &App{config: Config{Views: view}}\n\n\terr := app.ReloadViews()\n\trequire.ErrorIs(t, err, ErrNoViewEngineConfigured)\n}\n\nfunc Test_App_ReloadViews_MountedViews(t *testing.T) {\n\tt.Parallel()\n\ttempDir := t.TempDir()\n\ttemplatePath := filepath.Join(tempDir, \"template.html\")\n\n\trequire.NoError(t, os.WriteFile(templatePath, []byte(\"before\"), 0o600))\n\n\tview := &fileView{path: templatePath}\n\tsubApp := New(Config{Views: view})\n\tapp := New()\n\tapp.Use(\"/sub\", subApp)\n\n\trequire.NoError(t, view.Load())\n\tinitialLoads := view.loads\n\trequire.Equal(t, \"before\", view.content)\n\n\trequire.NoError(t, os.WriteFile(templatePath, []byte(\"after\"), 0o600))\n\trequire.NoError(t, app.ReloadViews())\n\n\trequire.Equal(t, \"after\", view.content)\n\trequire.Greater(t, view.loads, initialLoads)\n}\n\nfunc Test_App_ReloadViews_MountedViews_Error(t *testing.T) {\n\tt.Parallel()\n\texpectedErr := errors.New(\"sub view error\")\n\tsubView := &countingView{loadErr: expectedErr}\n\tsubApp := New(Config{Views: subView})\n\tapp := New()\n\tapp.Use(\"/sub\", subApp)\n\n\terr := app.ReloadViews()\n\trequire.ErrorIs(t, err, expectedErr)\n}\n\nfunc Test_App_ReloadViews_MountedViews_MultipleApps(t *testing.T) {\n\tt.Parallel()\n\tviewA := &countingView{}\n\tviewB := &countingView{}\n\tsubAppA := New(Config{Views: viewA})\n\tsubAppB := New(Config{Views: viewB})\n\tapp := New()\n\tapp.Use(\"/a\", subAppA)\n\tapp.Use(\"/b\", subAppB)\n\n\tinitialLoadsA := viewA.loads\n\tinitialLoadsB := viewB.loads\n\n\trequire.NoError(t, app.ReloadViews())\n\n\trequire.Equal(t, initialLoadsA+1, viewA.loads)\n\trequire.Equal(t, initialLoadsB+1, viewB.loads)\n}\n\nfunc Test_App_ReloadViews_MountedViews_WithParentViews(t *testing.T) {\n\tt.Parallel()\n\tparentView := &countingView{}\n\tsubView := &countingView{}\n\tsubApp := New(Config{Views: subView})\n\tapp := New(Config{Views: parentView})\n\tapp.Use(\"/sub\", subApp)\n\n\tinitialParentLoads := parentView.loads\n\tinitialSubLoads := subView.loads\n\n\trequire.NoError(t, app.ReloadViews())\n\n\trequire.Equal(t, initialParentLoads+1, parentView.loads)\n\trequire.Equal(t, initialSubLoads+1, subView.loads)\n}\n\n// go test -run Test_App_Init_Error_View\nfunc Test_App_Init_Error_View(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{Views: invalidView{}})\n\n\trequire.PanicsWithValue(t, \"implement me\", func() {\n\t\t//nolint:errcheck // not needed\n\t\t_ = app.config.Views.Render(nil, \"\", nil)\n\t})\n}\n\n// go test -run Test_App_Stack\nfunc Test_App_Stack(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(\"/path0\", testEmptyHandler)\n\tapp.Get(\"/path1\", testEmptyHandler)\n\tapp.Get(\"/path2\", testEmptyHandler)\n\tapp.Post(\"/path3\", testEmptyHandler)\n\n\tapp.startupProcess()\n\n\tstack := app.Stack()\n\tmethodList := app.config.RequestMethods\n\trequire.Len(t, methodList, len(stack))\n\trequire.Len(t, stack[app.methodInt(MethodGet)], 3)\n\trequire.Len(t, stack[app.methodInt(MethodHead)], 3)\n\trequire.Len(t, stack[app.methodInt(MethodPost)], 2)\n\trequire.Len(t, stack[app.methodInt(MethodPut)], 1)\n\trequire.Len(t, stack[app.methodInt(MethodPatch)], 1)\n\trequire.Len(t, stack[app.methodInt(MethodDelete)], 1)\n\trequire.Len(t, stack[app.methodInt(MethodConnect)], 1)\n\trequire.Len(t, stack[app.methodInt(MethodOptions)], 1)\n\trequire.Len(t, stack[app.methodInt(MethodTrace)], 1)\n}\n\n// go test -run Test_App_HandlersCount\nfunc Test_App_HandlersCount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(\"/path0\", testEmptyHandler)\n\tapp.Get(\"/path2\", testEmptyHandler)\n\tapp.Post(\"/path3\", testEmptyHandler)\n\n\tapp.startupProcess()\n\n\tcount := app.HandlersCount()\n\trequire.Equal(t, uint32(4), count)\n}\n\n// go test -run Test_App_ReadTimeout\nfunc Test_App_ReadTimeout(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tReadTimeout:      time.Nanosecond,\n\t\tIdleTimeout:      time.Minute,\n\t\tDisableKeepalive: true,\n\t})\n\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr := ln.Addr().String()\n\n\tapp.Get(\"/read-timeout\", func(c Ctx) error {\n\t\treturn c.SendString(\"I should not be sent\")\n\t})\n\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\tconn, err := net.Dial(NetworkTCP4, addr)\n\t\tassert.NoError(t, err)\n\t\tdefer func(conn net.Conn) {\n\t\t\tcloseErr := conn.Close()\n\t\t\tassert.NoError(t, closeErr)\n\t\t}(conn)\n\n\t\t_, err = conn.Write([]byte(\"HEAD /read-timeout HTTP/1.1\\r\\n\"))\n\t\tassert.NoError(t, err)\n\n\t\tbuf := make([]byte, 1024)\n\t\tvar n int\n\t\tn, err = conn.Read(buf)\n\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, bytes.Contains(buf[:n], []byte(\"408 Request Timeout\")))\n\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listener(ln, ListenConfig{DisableStartupMessage: true}))\n}\n\n// go test -run Test_App_BadRequest\nfunc Test_App_BadRequest(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/bad-request\", func(c Ctx) error {\n\t\treturn c.SendString(\"I should not be sent\")\n\t})\n\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr := ln.Addr().String()\n\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tconn, err := net.Dial(NetworkTCP4, addr)\n\t\tassert.NoError(t, err)\n\t\tdefer func(conn net.Conn) {\n\t\t\tcloseErr := conn.Close()\n\t\t\tassert.NoError(t, closeErr)\n\t\t}(conn)\n\n\t\t_, err = conn.Write([]byte(\"BadRequest\\r\\n\"))\n\t\tassert.NoError(t, err)\n\n\t\tbuf := make([]byte, 1024)\n\t\tvar n int\n\t\tn, err = conn.Read(buf)\n\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, bytes.Contains(buf[:n], []byte(\"400 Bad Request\")))\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listener(ln, ListenConfig{DisableStartupMessage: true}))\n}\n\n// go test -run Test_App_SmallReadBuffer\nfunc Test_App_SmallReadBuffer(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tReadBufferSize: 1,\n\t})\n\n\tapp.Get(\"/small-read-buffer\", func(c Ctx) error {\n\t\treturn c.SendString(\"I should not be sent\")\n\t})\n\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr := ln.Addr().String()\n\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\treq, err := http.NewRequestWithContext(context.Background(), MethodGet, fmt.Sprintf(\"http://%s/small-read-buffer\", addr), http.NoBody)\n\t\tassert.NoError(t, err)\n\t\tvar client http.Client\n\t\tresp, err := client.Do(req)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 431, resp.StatusCode)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listener(ln, ListenConfig{DisableStartupMessage: true}))\n}\n\nfunc Test_App_Server(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\trequire.NotNil(t, app.Server())\n}\n\nfunc Test_App_Error_In_Fasthttp_Server(t *testing.T) {\n\tapp := New()\n\tapp.config.ErrorHandler = func(_ Ctx, _ error) error {\n\t\treturn errors.New(\"fake error\")\n\t}\n\tapp.server.GetOnly = true\n\n\tresp, err := app.Test(httptest.NewRequest(MethodPost, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 500, resp.StatusCode)\n}\n\n// go test -race -run Test_App_New_Test_Parallel\nfunc Test_App_New_Test_Parallel(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"Test_App_New_Test_Parallel_1\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New(Config{Immutable: true})\n\t\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t})\n\tt.Run(\"Test_App_New_Test_Parallel_2\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New(Config{Immutable: true})\n\t\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_App_ReadBodyStream(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{StreamRequestBody: true})\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\t// Calling c.Body() automatically reads the entire stream.\n\t\treturn c.SendString(fmt.Sprintf(\"%v %s\", c.Request().IsBodyStream(), c.Body()))\n\t})\n\ttestString := \"this is a test\"\n\tresp, err := app.Test(httptest.NewRequest(MethodPost, \"/\", bytes.NewBufferString(testString)))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Equal(t, \"true \"+testString, string(body))\n}\n\nfunc Test_App_DisablePreParseMultipartForm(t *testing.T) {\n\tt.Parallel()\n\t// Must be used with both otherwise there is no point.\n\ttestString := \"this is a test\"\n\n\tapp := New(Config{DisablePreParseMultipartForm: true, StreamRequestBody: true})\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\treq := c.Request()\n\t\tmpf, err := req.MultipartForm()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !req.IsBodyStream() {\n\t\t\treturn errors.New(\"not a body stream\")\n\t\t}\n\t\tfile, err := mpf.File[\"test\"][0].Open()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open: %w\", err)\n\t\t}\n\t\tbuffer := make([]byte, len(testString))\n\t\tn, err := file.Read(buffer)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read: %w\", err)\n\t\t}\n\t\tif n != len(testString) {\n\t\t\treturn errors.New(\"bad read length\")\n\t\t}\n\t\treturn c.Send(buffer)\n\t})\n\tb := &bytes.Buffer{}\n\tw := multipart.NewWriter(b)\n\twriter, err := w.CreateFormFile(\"test\", \"test\")\n\trequire.NoError(t, err, \"w.CreateFormFile\")\n\tn, err := writer.Write([]byte(testString))\n\trequire.NoError(t, err, \"writer.Write\")\n\trequire.Len(t, testString, n, \"writer n\")\n\trequire.NoError(t, w.Close(), \"w.Close()\")\n\n\treq := httptest.NewRequest(MethodPost, \"/\", b)\n\treq.Header.Set(\"Content-Type\", w.FormDataContentType())\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\n\trequire.Equal(t, testString, string(body))\n}\n\nfunc Test_App_Test_no_timeout_infinitely(t *testing.T) {\n\tt.Parallel()\n\tvar err error\n\tc := make(chan int)\n\n\tgo func() {\n\t\tdefer func() { c <- 0 }()\n\t\tapp := New()\n\t\tapp.Get(\"/\", func(_ Ctx) error {\n\t\t\truntime.Goexit()\n\t\t\treturn nil\n\t\t})\n\n\t\treq := httptest.NewRequest(MethodGet, \"/\", http.NoBody)\n\t\t_, err = app.Test(req, TestConfig{\n\t\t\tTimeout: 0,\n\t\t})\n\t}()\n\n\ttk := time.NewTimer(5 * time.Second)\n\tdefer tk.Stop()\n\n\tselect {\n\tcase <-tk.C:\n\t\tt.Error(\"hanging test\")\n\t\tt.FailNow()\n\tcase <-c:\n\t}\n\n\tif err == nil {\n\t\tt.Error(\"unexpected success request\")\n\t\tt.FailNow()\n\t}\n}\n\nfunc Test_App_Test_timeout(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\ttime.Sleep(1 * time.Second)\n\t\treturn nil\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout:       100 * time.Millisecond,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n}\n\nfunc Test_App_Test_timeout_empty_response(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\treturn nil\n\t})\n\n\t// When FailOnTimeout is false, the test should return whatever response is available\n\t// at timeout without failing\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout:       10 * time.Millisecond,\n\t\tFailOnTimeout: false,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\nfunc Test_App_Test_drop_empty_response(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.Drop()\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout:       0,\n\t\tFailOnTimeout: false,\n\t})\n\trequire.ErrorIs(t, err, ErrTestGotEmptyResponse)\n}\n\nfunc Test_App_Test_response_error(t *testing.T) {\n\t// Note: Test cannot run in parallel due to\n\t// overriding the httpReadResponse global variable.\n\t// t.Parallel()\n\n\t// Override httpReadResponse temporarily\n\toldHTTPReadResponse := httpReadResponse\n\tdefer func() {\n\t\thttpReadResponse = oldHTTPReadResponse\n\t}()\n\thttpReadResponse = func(_ *bufio.Reader, _ *http.Request) (*http.Response, error) {\n\t\treturn nil, errErrorReader\n\t}\n\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody), TestConfig{\n\t\tTimeout:       0,\n\t\tFailOnTimeout: false,\n\t})\n\trequire.ErrorIs(t, err, errErrorReader)\n}\n\ntype errorReadCloser int\n\nvar errInvalidReadOnBody = errors.New(\"test: invalid Read on body\")\n\nfunc (errorReadCloser) Read(_ []byte) (int, error) {\n\treturn 0, errInvalidReadOnBody\n}\n\nfunc (errorReadCloser) Close() error {\n\treturn nil\n}\n\nfunc Test_App_Test_ReadFail(t *testing.T) {\n\t// Note: Test cannot run in parallel due to\n\t// overriding the httpReadResponse global variable.\n\t// t.Parallel()\n\n\t// Override httpReadResponse temporarily\n\toldHTTPReadResponse := httpReadResponse\n\tdefer func() {\n\t\thttpReadResponse = oldHTTPReadResponse\n\t}()\n\n\thttpReadResponse = func(r *bufio.Reader, req *http.Request) (*http.Response, error) {\n\t\tresp, err := http.ReadResponse(r, req)\n\t\trequire.NoError(t, resp.Body.Close())\n\t\tresp.Body = errorReadCloser(0)\n\t\treturn resp, err //nolint:wrapcheck // unnecessary to wrap it\n\t}\n\n\tapp := New()\n\thints := []string{\"<https://cdn.com>; rel=preload; as=script\"}\n\tapp.Get(\"/early\", func(c Ctx) error {\n\t\terr := c.SendEarlyHints(hints)\n\t\trequire.NoError(t, err)\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/early\", http.NoBody)\n\t_, err := app.Test(req)\n\n\trequire.ErrorIs(t, err, errInvalidReadOnBody)\n}\n\nvar errDoubleClose = errors.New(\"test: double close\")\n\ntype doubleCloseBody struct {\n\tisClosed bool\n}\n\nfunc (b *doubleCloseBody) Read(_ []byte) (int, error) {\n\tif b.isClosed {\n\t\treturn 0, errInvalidReadOnBody\n\t}\n\n\t// Close after reading EOF\n\t_ = b.Close() //nolint:errcheck // It is fine to ignore the error here\n\treturn 0, io.EOF\n}\n\nfunc (b *doubleCloseBody) Close() error {\n\tif b.isClosed {\n\t\treturn errDoubleClose\n\t}\n\n\tb.isClosed = true\n\treturn nil\n}\n\nfunc Test_App_Test_CloseFail(t *testing.T) {\n\t// Note: Test cannot run in parallel due to\n\t// overriding the httpReadResponse global variable.\n\t// t.Parallel()\n\n\t// Override httpReadResponse temporarily\n\toldHTTPReadResponse := httpReadResponse\n\tdefer func() {\n\t\thttpReadResponse = oldHTTPReadResponse\n\t}()\n\n\thttpReadResponse = func(r *bufio.Reader, req *http.Request) (*http.Response, error) {\n\t\tresp, err := http.ReadResponse(r, req)\n\t\t_ = resp.Body.Close() //nolint:errcheck // It is fine to ignore the error here\n\t\tresp.Body = &doubleCloseBody{}\n\t\treturn resp, err //nolint:wrapcheck // unnecessary to wrap it\n\t}\n\n\tapp := New()\n\thints := []string{\"<https://cdn.com>; rel=preload; as=script\"}\n\tapp.Get(\"/early\", func(c Ctx) error {\n\t\terr := c.SendEarlyHints(hints)\n\t\trequire.NoError(t, err)\n\t\treturn c.Status(StatusOK).SendString(\"done\")\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/early\", http.NoBody)\n\t_, err := app.Test(req)\n\n\trequire.ErrorIs(t, err, errDoubleClose)\n}\n\nfunc Test_App_SetTLSHandler(t *testing.T) {\n\tt.Parallel()\n\ttlsHandler := &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{\n\t\tServerName: \"example.golang\",\n\t}}\n\n\tapp := New()\n\tapp.SetTLSHandler(tlsHandler)\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\trequire.Equal(t, \"example.golang\", c.ClientHelloInfo().ServerName)\n}\n\nfunc Test_App_AddCustomRequestMethod(t *testing.T) {\n\tt.Parallel()\n\tmethods := append(DefaultMethods, \"TEST\") //nolint:gocritic // We want a new slice here\n\tapp := New(Config{\n\t\tRequestMethods: methods,\n\t})\n\tappMethods := app.config.RequestMethods\n\n\t// method name is always uppercase - https://datatracker.ietf.org/doc/html/rfc7231#section-4.1\n\trequire.Len(t, app.stack, len(appMethods))\n\trequire.Equal(t, \"TEST\", appMethods[len(appMethods)-1])\n}\n\nfunc Test_App_GetRoutes(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\thandler := func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t}\n\tapp.Delete(\"/delete\", handler).Name(\"delete\")\n\tapp.Post(\"/post\", handler).Name(\"post\")\n\troutes := app.GetRoutes(false)\n\trequire.Len(t, routes, 2+len(app.config.RequestMethods))\n\tmethodMap := map[string]string{\"/delete\": \"delete\", \"/post\": \"post\"}\n\tfor _, route := range routes {\n\t\tname, ok := methodMap[route.Path]\n\t\tif ok {\n\t\t\trequire.Equal(t, name, route.Name)\n\t\t}\n\t}\n\n\troutes = app.GetRoutes(true)\n\trequire.Len(t, routes, 2)\n\tfor _, route := range routes {\n\t\tname, ok := methodMap[route.Path]\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, name, route.Name)\n\t}\n}\n\nfunc Test_Middleware_Route_Naming_With_Use(t *testing.T) {\n\tt.Parallel()\n\tnamed := \"named\"\n\tapp := New()\n\n\tapp.Get(\"/unnamed\", func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\n\tapp.Post(\"/named\", func(c Ctx) error {\n\t\treturn c.Next()\n\t}).Name(named)\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t}) // no name - logging MW\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t}).Name(\"corsMW\")\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t}).Name(\"compressMW\")\n\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t}) // no name - cache MW\n\n\tgrp := app.Group(\"/pages\").Name(\"pages.\")\n\tgrp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t}).Name(\"csrfMW\")\n\n\tgrp.Get(\"/home\", func(c Ctx) error {\n\t\treturn c.Next()\n\t}).Name(\"home\")\n\n\tgrp.Get(\"/unnamed\", func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\n\tfor _, route := range app.GetRoutes() {\n\t\tswitch route.Path {\n\t\tcase \"/\":\n\t\t\trequire.Equal(t, \"compressMW\", route.Name)\n\t\tcase \"/unnamed\", \"/pages/unnamed\":\n\t\t\trequire.Empty(t, route.Name)\n\t\tcase \"/named\":\n\t\t\trequire.Equal(t, named, route.Name)\n\t\tcase \"/pages\":\n\t\t\trequire.Equal(t, \"pages.csrfMW\", route.Name)\n\t\tcase \"/pages/home\":\n\t\t\trequire.Equal(t, \"pages.home\", route.Name)\n\t\tdefault:\n\t\t\tt.Errorf(\"unknown route: %s\", route.Path)\n\t\t}\n\t}\n}\n\nfunc Test_Route_Naming_Issue_2671_2685(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/\", emptyHandler).Name(\"index\")\n\trequire.Equal(t, \"/\", app.GetRoute(\"index\").Path)\n\n\tapp.Get(\"/a/:a_id\", emptyHandler).Name(\"a\")\n\trequire.Equal(t, \"/a/:a_id\", app.GetRoute(\"a\").Path)\n\n\tapp.Post(\"/b/:bId\", emptyHandler).Name(\"b\")\n\trequire.Equal(t, \"/b/:bId\", app.GetRoute(\"b\").Path)\n\n\tc := app.Group(\"/c\")\n\tc.Get(\"\", emptyHandler).Name(\"c.get\")\n\trequire.Equal(t, \"/c\", app.GetRoute(\"c.get\").Path)\n\n\tc.Post(\"\", emptyHandler).Name(\"c.post\")\n\trequire.Equal(t, \"/c\", app.GetRoute(\"c.post\").Path)\n\n\tc.Get(\"/d\", emptyHandler).Name(\"c.get.d\")\n\trequire.Equal(t, \"/c/d\", app.GetRoute(\"c.get.d\").Path)\n\n\td := app.Group(\"/d/:d_id\")\n\td.Get(\"\", emptyHandler).Name(\"d.get\")\n\trequire.Equal(t, \"/d/:d_id\", app.GetRoute(\"d.get\").Path)\n\n\td.Post(\"\", emptyHandler).Name(\"d.post\")\n\trequire.Equal(t, \"/d/:d_id\", app.GetRoute(\"d.post\").Path)\n\n\te := app.Group(\"/e/:eId\")\n\te.Get(\"\", emptyHandler).Name(\"e.get\")\n\trequire.Equal(t, \"/e/:eId\", app.GetRoute(\"e.get\").Path)\n\n\te.Post(\"\", emptyHandler).Name(\"e.post\")\n\trequire.Equal(t, \"/e/:eId\", app.GetRoute(\"e.post\").Path)\n\n\te.Get(\"f\", emptyHandler).Name(\"e.get.f\")\n\trequire.Equal(t, \"/e/:eId/f\", app.GetRoute(\"e.get.f\").Path)\n\n\tpostGroup := app.Group(\"/post/:postId\")\n\tpostGroup.Get(\"\", emptyHandler).Name(\"post.get\")\n\trequire.Equal(t, \"/post/:postId\", app.GetRoute(\"post.get\").Path)\n\n\tpostGroup.Post(\"\", emptyHandler).Name(\"post.update\")\n\trequire.Equal(t, \"/post/:postId\", app.GetRoute(\"post.update\").Path)\n\n\t// Add testcase for routes use the same PATH on different methods\n\tapp.Get(\"/users\", emptyHandler).Name(\"get-users\")\n\tapp.Post(\"/users\", emptyHandler).Name(\"add-user\")\n\tgetUsers := app.GetRoute(\"get-users\")\n\trequire.Equal(t, \"/users\", getUsers.Path)\n\n\taddUser := app.GetRoute(\"add-user\")\n\trequire.Equal(t, \"/users\", addUser.Path)\n\n\t// Add testcase for routes use the same PATH on different methods (for groups)\n\tnewGrp := app.Group(\"/name-test\")\n\tnewGrp.Get(\"/users\", emptyHandler).Name(\"grp-get-users\")\n\tnewGrp.Post(\"/users\", emptyHandler).Name(\"grp-add-user\")\n\tgetUsers = app.GetRoute(\"grp-get-users\")\n\trequire.Equal(t, \"/name-test/users\", getUsers.Path)\n\n\taddUser = app.GetRoute(\"grp-add-user\")\n\trequire.Equal(t, \"/name-test/users\", addUser.Path)\n\n\t// Add testcase for HEAD route naming\n\tapp.Get(\"/simple-route\", emptyHandler).Name(\"simple-route\")\n\tapp.Head(\"/simple-route\", emptyHandler).Name(\"simple-route2\")\n\n\tsRoute := app.GetRoute(\"simple-route\")\n\trequire.Equal(t, \"/simple-route\", sRoute.Path)\n\n\tsRoute2 := app.GetRoute(\"simple-route2\")\n\trequire.Equal(t, \"/simple-route\", sRoute2.Path)\n}\n\nfunc Test_App_State(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.State().Set(\"key\", \"value\")\n\tstr, ok := app.State().GetString(\"key\")\n\trequire.True(t, ok)\n\trequire.Equal(t, \"value\", str)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Communication_Flow -benchmem -count=4\nfunc Benchmark_Communication_Flow(b *testing.B) {\n\tapp := New()\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, 200, fctx.Response.Header.StatusCode())\n\trequire.Equal(b, \"Hello, World!\", string(fctx.Response.Body()))\n}\n\nfunc Benchmark_Communication_Flow_Parallel(b *testing.B) {\n\tapp := New()\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.Header.SetMethod(MethodGet)\n\t\tfctx.Request.SetRequestURI(\"/\")\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n\n\tverifyCtx := &fasthttp.RequestCtx{}\n\tverifyCtx.Request.Header.SetMethod(MethodGet)\n\tverifyCtx.Request.SetRequestURI(\"/\")\n\th(verifyCtx)\n\n\trequire.Equal(b, 200, verifyCtx.Response.Header.StatusCode())\n\trequire.Equal(b, \"Hello, World!\", string(verifyCtx.Response.Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AcquireReleaseFlow -benchmem -count=4\nfunc Benchmark_Ctx_AcquireReleaseFlow(b *testing.B) {\n\tapp := New()\n\n\tfctx := &fasthttp.RequestCtx{}\n\n\tb.Run(\"withoutRequestCtx\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor b.Loop() {\n\t\t\tc, _ := app.AcquireCtx(fctx).(*DefaultCtx) //nolint:errcheck // not needed\n\t\t\tapp.ReleaseCtx(c)\n\t\t}\n\t})\n\n\tb.Run(\"withRequestCtx\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tfor b.Loop() {\n\t\t\tc, _ := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck // not needed\n\t\t\tapp.ReleaseCtx(c)\n\t\t}\n\t})\n}\n\nfunc acquireDefaultCtxForAppBenchmark(b *testing.B, app *App, fctx *fasthttp.RequestCtx) *DefaultCtx {\n\tb.Helper()\n\n\tctx := app.AcquireCtx(fctx)\n\tdefaultCtx, ok := ctx.(*DefaultCtx)\n\tif !ok {\n\t\tb.Fatal(\"AcquireCtx did not return *DefaultCtx\")\n\t}\n\treturn defaultCtx\n}\n\nfunc Benchmark_Ctx_AcquireReleaseFlow_Parallel(b *testing.B) {\n\tapp := New()\n\n\tb.Run(\"withoutRequestCtx\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\tfor pb.Next() {\n\t\t\t\tc := acquireDefaultCtxForAppBenchmark(b, app, fctx)\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"withRequestCtx\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tc := acquireDefaultCtxForAppBenchmark(b, app, &fasthttp.RequestCtx{})\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc TestErrorHandler_PicksRightOne(t *testing.T) {\n\tt.Parallel()\n\t// common handler to be used by all routes,\n\t// it will always fail by returning an error since\n\t// we need to test that the right ErrorHandler is invoked\n\thandler := func(_ Ctx) error {\n\t\treturn errors.New(\"random error\")\n\t}\n\n\t// subapp /api/v1/users [no custom error handler]\n\tappAPIV1Users := New()\n\tappAPIV1Users.Get(\"/\", handler)\n\n\t// subapp /api/v1/use [with custom error handler]\n\tappAPIV1UseEH := func(c Ctx, _ error) error {\n\t\treturn c.SendString(\"/api/v1/use error handler\")\n\t}\n\tappAPIV1Use := New(Config{ErrorHandler: appAPIV1UseEH})\n\tappAPIV1Use.Get(\"/\", handler)\n\n\t// subapp: /api/v1 [with custom error handler]\n\tappV1EH := func(c Ctx, _ error) error {\n\t\treturn c.SendString(\"/api/v1 error handler\")\n\t}\n\tappV1 := New(Config{ErrorHandler: appV1EH})\n\tappV1.Get(\"/\", handler)\n\tappV1.Use(\"/users\", appAPIV1Users)\n\tappV1.Use(\"/use\", appAPIV1Use)\n\n\t// root app [no custom error handler]\n\tapp := New()\n\tapp.Get(\"/\", handler)\n\tapp.Use(\"/api/v1\", appV1)\n\n\ttestCases := []struct {\n\t\tpath     string // the endpoint url to test\n\t\texpected string // the expected error response\n\t}{\n\t\t// /api/v1/users mount doesn't have custom ErrorHandler\n\t\t// so it should use the upper-nearest one (/api/v1)\n\t\t{\"/api/v1/users\", \"/api/v1 error handler\"},\n\n\t\t// /api/v1/use mount has a custom ErrorHandler\n\t\t{\"/api/v1/use\", \"/api/v1/use error handler\"},\n\n\t\t// /api/v1 mount has a custom ErrorHandler\n\t\t{\"/api/v1\", \"/api/v1 error handler\"},\n\n\t\t// / mount doesn't have custom ErrorHandler, since is\n\t\t// the root path i will use Fiber's default Error Handler\n\t\t{\"/\", \"random error\"},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.path, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, testCase.path, http.NoBody))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\trequire.Equal(t, testCase.expected, string(body))\n\t\t})\n\t}\n}\n\ntype groupIDResponse struct {\n\tGroupID string `json:\"group_id\"`\n}\n\n// Test for the reported bug where Test method returns \"test: got empty response\" error\n// when using a very small timeout value (e.g., 5 microseconds instead of 5 seconds).\n// With FailOnTimeout: false, the test should return whatever response is available without error.\nfunc Test_App_Test_SmallTimeout_WithFailOnTimeoutFalse(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Post(\"/admin/api/groups\", func(c Ctx) error {\n\t\t// Add a small delay to ensure timeout is triggered\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\tgroupID := \"g.test123\"\n\t\treturn c.JSON(groupIDResponse{\n\t\t\tGroupID: groupID,\n\t\t})\n\t})\n\n\treq := httptest.NewRequest(MethodPost, \"/admin/api/groups\", http.NoBody)\n\n\t// Using 5 microseconds which is too short for the handler to complete\n\t// But with FailOnTimeout: false, it should return whatever is available without error\n\tresp, err := app.Test(req, TestConfig{\n\t\tTimeout:       5 * time.Microsecond,\n\t\tFailOnTimeout: false,\n\t})\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tvar response groupIDResponse\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\terr = json.Unmarshal(body, &response)\n\trequire.NoError(t, err)\n\n\trequire.NotEmpty(t, response.GroupID)\n\trequire.Equal(t, \"g.test123\", response.GroupID)\n}\n\n// Test that FailOnTimeout: true still works as expected\nfunc Test_App_Test_SmallTimeout_WithFailOnTimeoutTrue(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Post(\"/admin/api/groups\", func(c Ctx) error {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tgroupID := \"g.test123\"\n\t\treturn c.JSON(groupIDResponse{\n\t\t\tGroupID: groupID,\n\t\t})\n\t})\n\n\treq := httptest.NewRequest(MethodPost, \"/admin/api/groups\", http.NoBody)\n\n\t// With FailOnTimeout: true (default), it should fail fast with timeout error\n\t_, err := app.Test(req, TestConfig{\n\t\tTimeout:       10 * time.Millisecond,\n\t\tFailOnTimeout: true,\n\t})\n\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n}\n"
  },
  {
    "path": "bind.go",
    "content": "package fiber\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3/binder\"\n\t\"github.com/gofiber/schema\"\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n)\n\n// CustomBinder An interface to register custom binders.\ntype CustomBinder interface {\n\tName() string\n\tMIMETypes() []string\n\tParse(c Ctx, out any) error\n}\n\n// StructValidator is an interface to register custom struct validator for binding.\ntype StructValidator interface {\n\tValidate(out any) error\n}\n\nvar bindPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &Bind{\n\t\t\tdontHandleErrs: true,\n\t\t}\n\t},\n}\n\n// Bind provides helper methods for binding request data to Go values.\n// By default (manual mode), parsing failures are returned as *BindError; use errors.As to extract source and field details.\n// With WithAutoHandling(), parsing failures set HTTP 400 and return *Error instead.\ntype Bind struct {\n\tctx            Ctx\n\tdontHandleErrs bool\n\tskipValidation bool\n}\n\n// BindError source constants for BindError.Source.\nconst (\n\tBindSourceURI        = \"uri\"\n\tBindSourceQuery      = \"query\"\n\tBindSourceHeader     = \"header\"\n\tBindSourceCookie     = \"cookie\"\n\tBindSourceBody       = \"body\"\n\tBindSourceRespHeader = \"respHeader\"\n)\n\n// BindError wraps a binding failure with the source and field that failed.\n// Use errors.As(err, &be) to extract it when you need to branch on source\n// (e.g. 404 for URI vs 400 for body).\ntype BindError struct {\n\tErr    error  // underlying error; use errors.As to inspect\n\tSource string // binding source: uri, query, body, header, cookie, or respHeader (see BindSource* constants)\n\tField  string // struct field or tag key that failed (best-effort, may be empty)\n}\n\nfunc (e *BindError) Error() string {\n\tif e.Field != \"\" {\n\t\treturn fmt.Sprintf(\"bind %q from %s: %v\", e.Field, e.Source, e.Err)\n\t}\n\treturn fmt.Sprintf(\"bind from %s: %v\", e.Source, e.Err)\n}\n\nfunc (e *BindError) Unwrap() error {\n\treturn e.Err\n}\n\nfunc extractFieldFromError(err error) string {\n\tvar convErr schema.ConversionError\n\tif errors.As(err, &convErr) {\n\t\treturn convErr.Key\n\t}\n\tvar unknownKey schema.UnknownKeyError\n\tif errors.As(err, &unknownKey) {\n\t\treturn unknownKey.Key\n\t}\n\tvar emptyField schema.EmptyFieldError\n\tif errors.As(err, &emptyField) {\n\t\treturn emptyField.Key\n\t}\n\tvar multiErr schema.MultiError\n\tif errors.As(err, &multiErr) {\n\t\tfor k := range multiErr {\n\t\t\treturn k\n\t\t}\n\t}\n\tvar unmarshalErr *json.UnmarshalTypeError\n\tif errors.As(err, &unmarshalErr) {\n\t\treturn unmarshalErr.Field\n\t}\n\treturn \"\"\n}\n\nfunc newBindError(source string, raw error) *BindError {\n\treturn &BindError{Source: source, Field: extractFieldFromError(raw), Err: raw}\n}\n\n// AcquireBind returns Bind reference from bind pool.\nfunc AcquireBind() *Bind {\n\tb, ok := bindPool.Get().(*Bind)\n\tif !ok {\n\t\tpanic(errBindPoolTypeAssertion)\n\t}\n\n\treturn b\n}\n\n// ReleaseBind returns b acquired via Bind to bind pool.\nfunc ReleaseBind(b *Bind) {\n\tb.release()\n\tbindPool.Put(b)\n}\n\n// releasePooledBinder resets a binder and returns it to its pool.\n// It should be used with defer to ensure proper cleanup of pooled binders.\nfunc releasePooledBinder[T interface{ Reset() }](pool *sync.Pool, bind T) {\n\tbind.Reset()\n\tbinder.PutToThePool(pool, bind)\n}\n\nfunc (b *Bind) release() {\n\tb.ctx = nil\n\tb.dontHandleErrs = true\n\tb.skipValidation = false\n}\n\n// WithoutAutoHandling If you want to handle binder errors manually, you can use `WithoutAutoHandling`.\n// It's default behavior of binder.\nfunc (b *Bind) WithoutAutoHandling() *Bind {\n\tb.dontHandleErrs = true\n\n\treturn b\n}\n\n// WithAutoHandling If you want to handle binder errors automatically, you can use `WithAutoHandling`.\n// If there's an error, it will return the error and set HTTP status to `400 Bad Request`.\n// You must still return on error explicitly\nfunc (b *Bind) WithAutoHandling() *Bind {\n\tb.dontHandleErrs = false\n\n\treturn b\n}\n\n// SkipValidation enables or disables struct validation for the current bind chain.\nfunc (b *Bind) SkipValidation(skip bool) *Bind {\n\tb.skipValidation = skip\n\n\treturn b\n}\n\n// Check WithAutoHandling/WithoutAutoHandling errors and return it by usage.\nfunc (b *Bind) returnErr(err error) error {\n\tif err == nil || b.dontHandleErrs {\n\t\treturn err\n\t}\n\n\tb.ctx.Status(StatusBadRequest)\n\treturn NewError(StatusBadRequest, \"Bad request: \"+err.Error())\n}\n\n// returnBindErr runs returnErr and, if the result is not a *Error, wraps it in *BindError.\n// Use for binding parse failures; use returnErr directly for Custom and validation errors.\nfunc (b *Bind) returnBindErr(err error, source string) error {\n\tif retErr := b.returnErr(err); retErr != nil {\n\t\tvar fiberErr *Error\n\t\tif errors.As(retErr, &fiberErr) {\n\t\t\treturn fiberErr\n\t\t}\n\t\treturn newBindError(source, retErr)\n\t}\n\treturn nil\n}\n\n// Struct validation.\nfunc (b *Bind) validateStruct(out any) error {\n\tif b.skipValidation {\n\t\treturn nil\n\t}\n\n\tvalidator := b.ctx.App().config.StructValidator\n\tif validator == nil {\n\t\treturn nil\n\t}\n\n\tt := reflect.TypeOf(out)\n\tif t == nil {\n\t\treturn nil\n\t}\n\n\t// Unwrap pointers (e.g. *T, **T) to inspect the underlying destination type.\n\tfor t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\n\tif t.Kind() != reflect.Struct {\n\t\treturn nil\n\t}\n\n\treturn validator.Validate(out)\n}\n\n// Custom To use custom binders, you have to use this method.\n// You can register them from RegisterCustomBinder method of Fiber instance.\n// They're checked by name, if it's not found, it will return an error.\n// NOTE: WithAutoHandling/WithAutoHandling is still valid for Custom binders.\nfunc (b *Bind) Custom(name string, dest any) error {\n\tbinders := b.ctx.App().customBinders\n\tfor _, customBinder := range binders {\n\t\tif customBinder.Name() == name {\n\t\t\tif err := b.returnBindErr(customBinder.Parse(b.ctx, dest), name); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn b.validateStruct(dest)\n\t\t}\n\t}\n\n\treturn ErrCustomBinderNotFound\n}\n\n// Header binds the request header strings into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) Header(out any) error {\n\tbind := binder.GetFromThePool[*binder.HeaderBinding](&binder.HeaderBinderPool)\n\tbind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers\n\n\tdefer releasePooledBinder(&binder.HeaderBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Request(), out), BindSourceHeader); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) RespHeader(out any) error {\n\tbind := binder.GetFromThePool[*binder.RespHeaderBinding](&binder.RespHeaderBinderPool)\n\tbind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers\n\n\tdefer releasePooledBinder(&binder.RespHeaderBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Response(), out), BindSourceRespHeader); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// Cookie binds the request cookie strings into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\n// NOTE: If your cookie is like key=val1,val2; they'll be bound as a slice if your map is map[string][]string. Else, it'll use last element of cookie.\nfunc (b *Bind) Cookie(out any) error {\n\tbind := binder.GetFromThePool[*binder.CookieBinding](&binder.CookieBinderPool)\n\tbind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers\n\n\tdefer releasePooledBinder(&binder.CookieBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(&b.ctx.RequestCtx().Request, out), BindSourceCookie); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// Query binds the query string into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) Query(out any) error {\n\tbind := binder.GetFromThePool[*binder.QueryBinding](&binder.QueryBinderPool)\n\tbind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers\n\n\tdefer releasePooledBinder(&binder.QueryBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(&b.ctx.RequestCtx().Request, out), BindSourceQuery); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// JSON binds the body string into the struct.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) JSON(out any) error {\n\tbind := binder.GetFromThePool[*binder.JSONBinding](&binder.JSONBinderPool)\n\tbind.JSONDecoder = b.ctx.App().Config().JSONDecoder\n\n\tdefer releasePooledBinder(&binder.JSONBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Body(), out), BindSourceBody); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// CBOR binds the body string into the struct.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) CBOR(out any) error {\n\tbind := binder.GetFromThePool[*binder.CBORBinding](&binder.CBORBinderPool)\n\tbind.CBORDecoder = b.ctx.App().Config().CBORDecoder\n\n\tdefer releasePooledBinder(&binder.CBORBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Body(), out), BindSourceBody); err != nil {\n\t\treturn err\n\t}\n\treturn b.validateStruct(out)\n}\n\n// XML binds the body string into the struct.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) XML(out any) error {\n\tbind := binder.GetFromThePool[*binder.XMLBinding](&binder.XMLBinderPool)\n\tbind.XMLDecoder = b.ctx.App().config.XMLDecoder\n\n\tdefer releasePooledBinder(&binder.XMLBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Body(), out), BindSourceBody); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// Form binds the form into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\n// If Content-Type is \"application/x-www-form-urlencoded\" or \"multipart/form-data\", it will bind the form values.\n// Multipart file fields are supported using *multipart.FileHeader, []*multipart.FileHeader, or *[]*multipart.FileHeader.\nfunc (b *Bind) Form(out any) error {\n\tbind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool)\n\tbind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers\n\n\tdefer releasePooledBinder(&binder.FormBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(&b.ctx.RequestCtx().Request, out), BindSourceBody); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// URI binds the route parameters into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) URI(out any) error {\n\tbind := binder.GetFromThePool[*binder.URIBinding](&binder.URIBinderPool)\n\n\tdefer releasePooledBinder(&binder.URIBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out), BindSourceURI); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// MsgPack binds the body string into the struct.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) MsgPack(out any) error {\n\tbind := binder.GetFromThePool[*binder.MsgPackBinding](&binder.MsgPackBinderPool)\n\tbind.MsgPackDecoder = b.ctx.App().Config().MsgPackDecoder\n\n\tdefer releasePooledBinder(&binder.MsgPackBinderPool, bind)\n\n\tif err := b.returnBindErr(bind.Bind(b.ctx.Body(), out), BindSourceBody); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.validateStruct(out)\n}\n\n// Body binds the request body into the struct, map[string]string and map[string][]string.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\n// It supports decoding the following content types based on the Content-Type header:\n// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data\n// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder.\n// If there is no custom binder for mime type of body, it will return a ErrUnprocessableEntity error.\nfunc (b *Bind) Body(out any) error {\n\t// Get content-type\n\tctype := utils.UnsafeString(utilsbytes.UnsafeToLower(b.ctx.RequestCtx().Request.Header.ContentType()))\n\tctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))\n\n\t// Check custom binders\n\tbinders := b.ctx.App().customBinders\n\tfor _, customBinder := range binders {\n\t\tif slices.Contains(customBinder.MIMETypes(), ctype) {\n\t\t\tif err := b.returnBindErr(customBinder.Parse(b.ctx, out), BindSourceBody); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn b.validateStruct(out)\n\t\t}\n\t}\n\n\t// Parse body accordingly\n\tswitch ctype {\n\tcase MIMEApplicationJSON:\n\t\treturn b.JSON(out)\n\tcase MIMEApplicationMsgPack:\n\t\treturn b.MsgPack(out)\n\tcase MIMETextXML, MIMEApplicationXML:\n\t\treturn b.XML(out)\n\tcase MIMEApplicationCBOR:\n\t\treturn b.CBOR(out)\n\tcase MIMEApplicationForm, MIMEMultipartForm:\n\t\treturn b.Form(out)\n\t}\n\n\t// No suitable content type found\n\treturn ErrUnprocessableEntity\n}\n\n// All binds values from URI params, the request body, the query string,\n// headers, and cookies into the provided struct in precedence order.\n// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).\nfunc (b *Bind) All(out any) error {\n\toutVal := reflect.ValueOf(out)\n\tif outVal.Kind() != reflect.Ptr || outVal.Elem().Kind() != reflect.Struct {\n\t\treturn ErrUnprocessableEntity\n\t}\n\n\toutElem := outVal.Elem()\n\n\t// Precedence: URL Params -> Body -> Query -> Headers -> Cookies\n\tsources := []func(any) error{b.URI}\n\n\t// Check if both Body and Content-Type are set\n\tif len(b.ctx.Request().Body()) > 0 && len(b.ctx.RequestCtx().Request.Header.ContentType()) > 0 {\n\t\tsources = append(sources, b.Body)\n\t}\n\tsources = append(sources, b.Query, b.Header, b.Cookie)\n\tprevSkip := b.skipValidation\n\tb.skipValidation = true\n\n\t// TODO: Support custom precedence with an optional binding_source tag\n\t// TODO: Create WithOverrideEmptyValues\n\t// Bind from each source, but only update unset fields\n\tfor _, bindFunc := range sources {\n\t\ttempStruct := reflect.New(outElem.Type()).Interface()\n\t\tif err := bindFunc(tempStruct); err != nil {\n\t\t\tb.skipValidation = prevSkip\n\t\t\treturn err\n\t\t}\n\n\t\ttempStructVal := reflect.ValueOf(tempStruct).Elem()\n\t\tmergeStruct(outElem, tempStructVal)\n\t}\n\n\tb.skipValidation = prevSkip\n\treturn b.returnErr(b.validateStruct(out))\n}\n\nfunc mergeStruct(dst, src reflect.Value) {\n\tdstFields := dst.NumField()\n\tfor i := range dstFields {\n\t\tdstField := dst.Field(i)\n\t\tsrcField := src.Field(i)\n\n\t\t// Skip if the destination field is already set\n\t\tif isZero(dstField.Interface()) {\n\t\t\tif dstField.CanSet() && srcField.IsValid() {\n\t\t\t\tdstField.Set(srcField)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isZero(value any) bool {\n\tv := reflect.ValueOf(value)\n\treturn v.IsZero()\n}\n"
  },
  {
    "path": "bind_test.go",
    "content": "//nolint:wrapcheck,tagliatelle // We must not wrap errors in tests\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gofiber/fiber/v3/binder\"\n\t\"github.com/gofiber/schema\"\n\t\"github.com/shamaton/msgpack/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nconst helloWorld = \"hello world\"\n\n// go test -run Test_returnErr -v\nfunc Test_returnErr(t *testing.T) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Bind().WithAutoHandling().returnErr(nil)\n\trequire.NoError(t, err)\n}\n\n// go test -run Test_AcquireReleaseBind -v\nfunc Test_AcquireReleaseBind(t *testing.T) {\n\tb := AcquireBind()\n\tb.dontHandleErrs = false\n\tb.skipValidation = true\n\tb.ctx = &DefaultCtx{}\n\tReleaseBind(b)\n\n\tb2 := AcquireBind()\n\trequire.Nil(t, b2.ctx)\n\trequire.True(t, b2.dontHandleErrs)\n\trequire.False(t, b2.skipValidation)\n\tReleaseBind(b2)\n}\n\n// go test -run Test_BindError -v\n\nfunc Test_BindError_Unwrap(t *testing.T) {\n\tt.Parallel()\n\tinner := errors.New(\"inner\")\n\tbe := &BindError{Source: BindSourceQuery, Err: inner}\n\trequire.ErrorIs(t, be, inner)\n\trequire.Equal(t, inner, errors.Unwrap(be))\n\n\tvar extracted *BindError\n\trequire.ErrorAs(t, be, &extracted)\n\trequire.Equal(t, BindSourceQuery, extracted.Source)\n}\n\nfunc Test_BindError_ErrorFormat(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with field\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tbe := &BindError{Source: BindSourceQuery, Field: \"id\", Err: errors.New(\"conversion failed\")}\n\t\trequire.Contains(t, be.Error(), `\"id\"`)\n\t\trequire.Contains(t, be.Error(), \"query\")\n\t\trequire.Contains(t, be.Error(), \"conversion failed\")\n\t})\n\n\tt.Run(\"without field\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tbe := &BindError{Source: BindSourceBody, Field: \"\", Err: errors.New(\"parse failed\")}\n\t\trequire.Equal(t, \"bind from body: parse failed\", be.Error())\n\t})\n}\n\nfunc Test_BindError_FieldExtraction(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"QueryConversionError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype Q struct {\n\t\t\tID int `query:\"id\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().URI().SetQueryString(\"id=notanint\")\n\t\terr := c.Bind().Query(new(Q))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceQuery, be.Source)\n\t\trequire.Equal(t, \"id\", be.Field)\n\t\trequire.ErrorAs(t, err, &MultiError{})\n\t})\n\n\tt.Run(\"ConversionError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tconvErrBinder := &customBinderReturningError{\n\t\t\terr:      schema.ConversionError{Key: \"count\"},\n\t\t\tmimeType: \"application/x-conversion-error-test\",\n\t\t}\n\t\tapp := New()\n\t\tapp.RegisterCustomBinder(convErrBinder)\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(\"{}\"))\n\t\tc.Request().Header.SetContentType(\"application/x-conversion-error-test\")\n\t\ttype D struct{ Name string }\n\t\terr := c.Bind().Body(new(D))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\trequire.Equal(t, \"count\", be.Field)\n\t})\n\n\tt.Run(\"JSONUnmarshalTypeError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype J struct {\n\t\t\tCount int `json:\"count\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(`{\"count\":\"notanint\"}`))\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\terr := c.Bind().Body(new(J))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\trequire.Equal(t, \"count\", be.Field)\n\t\tvar unmarshalErr *json.UnmarshalTypeError\n\t\trequire.ErrorAs(t, err, &unmarshalErr)\n\t})\n\n\tt.Run(\"UnknownKeyError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tunknownKeyBinder := &customBinderReturningError{\n\t\t\terr:      schema.UnknownKeyError{Key: \"extra_field\"},\n\t\t\tmimeType: \"application/x-unknown-key-test\",\n\t\t}\n\t\tapp := New()\n\t\tapp.RegisterCustomBinder(unknownKeyBinder)\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(\"{}\"))\n\t\tc.Request().Header.SetContentType(\"application/x-unknown-key-test\")\n\t\ttype D struct{ Name string }\n\t\terr := c.Bind().Body(new(D))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\trequire.Equal(t, \"extra_field\", be.Field)\n\t})\n\n\tt.Run(\"EmptyFieldError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\temptyFieldBinder := &customBinderReturningError{\n\t\t\terr:      schema.EmptyFieldError{Key: \"required_field\"},\n\t\t\tmimeType: \"application/x-empty-field-test\",\n\t\t}\n\t\tapp := New()\n\t\tapp.RegisterCustomBinder(emptyFieldBinder)\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(\"{}\"))\n\t\tc.Request().Header.SetContentType(\"application/x-empty-field-test\")\n\t\ttype D struct{ Name string }\n\t\terr := c.Bind().Body(new(D))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\trequire.Equal(t, \"required_field\", be.Field)\n\t})\n\n\tt.Run(\"MultiError\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype Q struct {\n\t\t\tA string `query:\"a,required\"`\n\t\t\tB string `query:\"b,required\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().URI().SetQueryString(\"\")\n\t\terr := c.Bind().Query(new(Q))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceQuery, be.Source)\n\t\trequire.Contains(t, []string{\"a\", \"b\"}, be.Field)\n\t\trequire.ErrorAs(t, err, &MultiError{})\n\t})\n\n\tt.Run(\"NoRecognizedErrorType\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tgenericErrBinder := &customBinderReturningError{\n\t\t\terr:      errors.New(\"generic parse failure\"),\n\t\t\tmimeType: \"application/x-generic-error\",\n\t\t}\n\t\tapp := New()\n\t\tapp.RegisterCustomBinder(genericErrBinder)\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(\"{}\"))\n\t\tc.Request().Header.SetContentType(\"application/x-generic-error\")\n\t\ttype D struct{ Name string }\n\t\terr := c.Bind().Body(new(D))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\trequire.Empty(t, be.Field)\n\t})\n}\n\nfunc Test_BindError_Sources(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"URI\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype U struct {\n\t\t\tID int `uri:\"id\"`\n\t\t}\n\t\tapp := New()\n\t\tapp.Get(\"/user/:id\", func(ctx Ctx) error {\n\t\t\terr := ctx.Bind().URI(new(U))\n\t\t\trequire.Error(t, err)\n\t\t\tvar be *BindError\n\t\t\trequire.ErrorAs(t, err, &be)\n\t\t\trequire.Equal(t, BindSourceURI, be.Source)\n\t\t\trequire.ErrorAs(t, err, &MultiError{})\n\t\t\treturn nil\n\t\t})\n\t\t_, err := app.Test(httptest.NewRequest(http.MethodGet, \"/user/notanint\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"Query\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype Q struct {\n\t\t\tID int `query:\"id,required\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().URI().SetQueryString(\"\")\n\t\terr := c.Bind().Query(new(Q))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceQuery, be.Source)\n\t\trequire.ErrorAs(t, err, &MultiError{})\n\t})\n\n\tt.Run(\"Header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype H struct {\n\t\t\tID int `header:\"x-id,required\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().Header.Del(\"X-Id\")\n\t\terr := c.Bind().Header(new(H))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceHeader, be.Source)\n\t\trequire.ErrorAs(t, err, &MultiError{})\n\t})\n\n\tt.Run(\"Cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype C struct {\n\t\t\tID int `cookie:\"id,required\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().Header.DelCookie(\"id\")\n\t\terr := c.Bind().Cookie(new(C))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceCookie, be.Source)\n\t\trequire.ErrorAs(t, err, &MultiError{})\n\t})\n\n\tt.Run(\"Body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype J struct {\n\t\t\tX int `json:\"x\"`\n\t\t}\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\t\tc.Request().SetBody([]byte(`{\"x\":\"bad\"}`))\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\terr := c.Bind().Body(new(J))\n\t\trequire.Error(t, err)\n\t\tvar be *BindError\n\t\trequire.ErrorAs(t, err, &be)\n\t\trequire.Equal(t, BindSourceBody, be.Source)\n\t\tvar unmarshalErr *json.UnmarshalTypeError\n\t\trequire.ErrorAs(t, err, &unmarshalErr)\n\t})\n}\n\nfunc Test_BindError_All(t *testing.T) {\n\tt.Parallel()\n\n\ttype Req struct {\n\t\tName string `json:\"name\"`\n\t\tID   int    `uri:\"id\" json:\"id\"`\n\t}\n\n\tt.Run(\"URIFailsFirst\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tapp.Get(\"/users/:id\", func(ctx Ctx) error {\n\t\t\terr := ctx.Bind().All(new(Req))\n\t\t\trequire.Error(t, err)\n\t\t\tvar be *BindError\n\t\t\trequire.ErrorAs(t, err, &be)\n\t\t\trequire.Equal(t, BindSourceURI, be.Source)\n\t\t\trequire.ErrorAs(t, err, &MultiError{})\n\t\t\treturn nil\n\t\t})\n\t\treq := httptest.NewRequest(http.MethodGet, \"/users/notanint\", bytes.NewReader([]byte(`{\"name\":\"ok\"}`)))\n\t\treq.Header.Set(\"Content-Type\", MIMEApplicationJSON)\n\t\t_, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t})\n}\n\n// go test -run Test_Bind_Query -v\nfunc Test_Bind_Query(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Query struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball&hobby=football\")\n\tq := new(Query)\n\trequire.NoError(t, c.Bind().Query(q))\n\trequire.Len(t, q.Hobby, 2)\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football\")\n\tq = new(Query)\n\trequire.NoError(t, c.Bind().Query(q))\n\trequire.Len(t, q.Hobby, 2)\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=soccer&hobby=basketball,football\")\n\tq = new(Query)\n\trequire.NoError(t, c.Bind().Query(q))\n\trequire.Len(t, q.Hobby, 3)\n\n\tempty := new(Query)\n\tc.Request().URI().SetQueryString(\"\")\n\trequire.NoError(t, c.Bind().Query(empty))\n\trequire.Empty(t, empty.Hobby)\n\n\ttype Query2 struct {\n\t\tName            string\n\t\tHobby           string\n\t\tDefault         string `query:\"default,default:hello\"`\n\t\tFavouriteDrinks []string\n\t\tEmpty           []string\n\t\tAlloc           []string\n\t\tDefaults        []string `query:\"defaults,default:hello|world\"`\n\t\tNo              []int64\n\t\tID              int\n\t\tBool            bool\n\t}\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1\")\n\tq2 := new(Query2)\n\tq2.Bool = true\n\tq2.Name = helloWorld\n\trequire.NoError(t, c.Bind().Query(q2))\n\trequire.Equal(t, \"basketball,football\", q2.Hobby)\n\trequire.True(t, q2.Bool)\n\trequire.Equal(t, \"tom\", q2.Name) // check value get overwritten\n\trequire.Equal(t, []string{\"milo\", \"coke\", \"pepsi\"}, q2.FavouriteDrinks)\n\tvar nilSlice []string\n\trequire.Equal(t, nilSlice, q2.Empty)\n\trequire.Equal(t, []string{\"\"}, q2.Alloc)\n\trequire.Equal(t, []int64{1}, q2.No)\n\trequire.Equal(t, \"hello\", q2.Default)\n\trequire.Equal(t, []string{\"hello\", \"world\"}, q2.Defaults)\n\n\ttype RequiredQuery struct {\n\t\tName string `query:\"name,required\"`\n\t}\n\trq := new(RequiredQuery)\n\tc.Request().URI().SetQueryString(\"\")\n\terr := c.Bind().Query(rq)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"name\\\" from query: name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\ttype ArrayQuery struct {\n\t\tData []string\n\t}\n\taq := new(ArrayQuery)\n\tc.Request().URI().SetQueryString(\"data[]=john&data[]=doe\")\n\trequire.NoError(t, c.Bind().Query(aq))\n\trequire.Len(t, aq.Data, 2)\n}\n\n// go test -run Test_Bind_Query_Map -v\nfunc Test_Bind_Query_Map(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball&hobby=football\")\n\tq := make(map[string][]string)\n\trequire.NoError(t, c.Bind().Query(&q))\n\trequire.Len(t, q[\"hobby\"], 2)\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football\")\n\tq = make(map[string][]string)\n\trequire.NoError(t, c.Bind().Query(&q))\n\trequire.Len(t, q[\"hobby\"], 2)\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=soccer&hobby=basketball,football\")\n\tq = make(map[string][]string)\n\trequire.NoError(t, c.Bind().Query(&q))\n\trequire.Len(t, q[\"hobby\"], 3)\n\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=soccer\")\n\tqq := make(map[string]string)\n\trequire.NoError(t, c.Bind().Query(&qq))\n\trequire.Equal(t, \"1\", qq[\"id\"])\n\n\tempty := make(map[string][]string)\n\tc.Request().URI().SetQueryString(\"\")\n\trequire.NoError(t, c.Bind().Query(&empty))\n\trequire.Empty(t, empty[\"hobby\"])\n\n\tem := make(map[string][]int)\n\tc.Request().URI().SetQueryString(\"\")\n\trequire.ErrorIs(t, c.Bind().Query(&em), binder.ErrMapNotConvertible)\n}\n\n// go test -run Test_Bind_Query_WithSetParserDecoder -v\nfunc Test_Bind_Query_WithSetParserDecoder(t *testing.T) {\n\ttype NonRFCTime time.Time\n\n\tnonRFCConverter := func(value string) reflect.Value {\n\t\tif v, err := time.Parse(\"2006-01-02\", value); err == nil {\n\t\t\treturn reflect.ValueOf(v)\n\t\t}\n\t\treturn reflect.Value{}\n\t}\n\n\tnonRFCTime := binder.ParserType{\n\t\tCustomType: NonRFCTime{},\n\t\tConverter:  nonRFCConverter,\n\t}\n\n\tbinder.SetParserDecoder(binder.ParserConfig{\n\t\tIgnoreUnknownKeys: true,\n\t\tParserType:        []binder.ParserType{nonRFCTime},\n\t\tZeroEmpty:         true,\n\t\tSetAliasTag:       \"query\",\n\t})\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype NonRFCTimeInput struct {\n\t\tDate  NonRFCTime `query:\"date\"`\n\t\tTitle string     `query:\"title\"`\n\t\tBody  string     `query:\"body\"`\n\t}\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tq := new(NonRFCTimeInput)\n\n\tc.Request().URI().SetQueryString(\"date=2021-04-10&title=CustomDateTest&Body=October\")\n\trequire.NoError(t, c.Bind().Query(q))\n\trequire.Equal(t, \"CustomDateTest\", q.Title)\n\tdate := fmt.Sprintf(\"%v\", q.Date)\n\trequire.Equal(t, \"{0 63753609600 <nil>}\", date)\n\trequire.Equal(t, \"October\", q.Body)\n\n\tc.Request().URI().SetQueryString(\"date=2021-04-10&title&Body=October\")\n\tq = &NonRFCTimeInput{\n\t\tTitle: \"Existing title\",\n\t\tBody:  \"Existing Body\",\n\t}\n\trequire.NoError(t, c.Bind().Query(q))\n\trequire.Empty(t, q.Title)\n}\n\n// go test -run Test_Bind_Query_Schema -v\nfunc Test_Bind_Query_Schema(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Query1 struct {\n\t\tName   string `query:\"name,required\"`\n\t\tNested struct {\n\t\t\tAge int `query:\"age\"`\n\t\t} `query:\"nested,required\"`\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"name=tom&nested.age=10\")\n\tq := new(Query1)\n\trequire.NoError(t, c.Bind().Query(q))\n\n\tc.Request().URI().SetQueryString(\"namex=tom&nested.age=10\")\n\tq = new(Query1)\n\terr := c.Bind().Query(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"name\\\" from query: name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().URI().SetQueryString(\"name=tom&nested.agex=10\")\n\tq = new(Query1)\n\trequire.NoError(t, c.Bind().Query(q))\n\n\tc.Request().URI().SetQueryString(\"name=tom&test.age=10\")\n\tq = new(Query1)\n\terr = c.Bind().Query(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"nested\\\" from query: nested is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\ttype Query2 struct {\n\t\tName   string `query:\"name\"`\n\t\tNested struct {\n\t\t\tAge int `query:\"age,required\"`\n\t\t} `query:\"nested\"`\n\t}\n\tc.Request().URI().SetQueryString(\"name=tom&nested.age=10\")\n\tq2 := new(Query2)\n\trequire.NoError(t, c.Bind().Query(q2))\n\n\tc.Request().URI().SetQueryString(\"nested.age=10\")\n\tq2 = new(Query2)\n\trequire.NoError(t, c.Bind().Query(q2))\n\n\tc.Request().URI().SetQueryString(\"nested.agex=10\")\n\tq2 = new(Query2)\n\terr = c.Bind().Query(q2)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"nested.age\\\" from query: nested.age is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().URI().SetQueryString(\"nested.agex=10\")\n\tq2 = new(Query2)\n\terr = c.Bind().Query(q2)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"nested.age\\\" from query: nested.age is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\ttype Node struct {\n\t\tNext  *Node `query:\"next,required\"`\n\t\tValue int   `query:\"val,required\"`\n\t}\n\tc.Request().URI().SetQueryString(\"val=1&next.val=3\")\n\tn := new(Node)\n\trequire.NoError(t, c.Bind().Query(n))\n\trequire.Equal(t, 1, n.Value)\n\trequire.Equal(t, 3, n.Next.Value)\n\n\tc.Request().URI().SetQueryString(\"next.val=2\")\n\tn = new(Node)\n\terr = c.Bind().Query(n)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"val\\\" from query: val is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().URI().SetQueryString(\"val=3&next.value=2\")\n\tn = new(Node)\n\tn.Next = new(Node)\n\trequire.NoError(t, c.Bind().Query(n))\n\trequire.Equal(t, 3, n.Value)\n\trequire.Equal(t, 0, n.Next.Value)\n\n\ttype Person struct {\n\t\tName string `query:\"name\"`\n\t\tAge  int    `query:\"age\"`\n\t}\n\n\ttype CollectionQuery struct {\n\t\tData []Person `query:\"data\"`\n\t}\n\n\tc.Request().URI().SetQueryString(\"data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12\")\n\tcq := new(CollectionQuery)\n\trequire.NoError(t, c.Bind().Query(cq))\n\trequire.Len(t, cq.Data, 2)\n\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\trequire.Equal(t, 10, cq.Data[0].Age)\n\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\trequire.Equal(t, 12, cq.Data[1].Age)\n\n\tc.Request().URI().SetQueryString(\"data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12\")\n\tcq = new(CollectionQuery)\n\trequire.NoError(t, c.Bind().Query(cq))\n\trequire.Len(t, cq.Data, 2)\n\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\trequire.Equal(t, 10, cq.Data[0].Age)\n\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\trequire.Equal(t, 12, cq.Data[1].Age)\n}\n\n// go test -run Test_Bind_Header -v\nfunc Test_Bind_Header(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Header struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.Add(\"id\", \"1\")\n\tc.Request().Header.Add(\"Name\", \"John Doe\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber\")\n\tq := new(Header)\n\trequire.NoError(t, c.Bind().Header(q))\n\trequire.Len(t, q.Hobby, 1)\n\n\tc.Request().Header.Del(\"hobby\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber,go\")\n\tq = new(Header)\n\trequire.NoError(t, c.Bind().Header(q))\n\trequire.Len(t, q.Hobby, 1)\n\n\tempty := new(Header)\n\tc.Request().Header.Del(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(empty))\n\trequire.Empty(t, empty.Hobby)\n\n\ttype Header2 struct {\n\t\tName            string\n\t\tHobby           string\n\t\tFavouriteDrinks []string\n\t\tEmpty           []string\n\t\tAlloc           []string\n\t\tNo              []int64\n\t\tID              int\n\t\tBool            bool\n\t}\n\n\tc.Request().Header.Add(\"id\", \"2\")\n\tc.Request().Header.Add(\"Name\", \"Jane Doe\")\n\tc.Request().Header.Del(\"hobby\")\n\tc.Request().Header.Add(\"Hobby\", \"go,fiber\")\n\tc.Request().Header.Add(\"favouriteDrinks\", \"milo,coke,pepsi\")\n\tc.Request().Header.Add(\"alloc\", \"\")\n\tc.Request().Header.Add(\"no\", \"1\")\n\n\th2 := new(Header2)\n\th2.Bool = true\n\th2.Name = helloWorld\n\trequire.NoError(t, c.Bind().Header(h2))\n\trequire.Equal(t, \"go,fiber\", h2.Hobby)\n\trequire.True(t, h2.Bool)\n\trequire.Equal(t, \"Jane Doe\", h2.Name) // check value get overwritten\n\trequire.Equal(t, []string{\"milo,coke,pepsi\"}, h2.FavouriteDrinks)\n\tvar nilSlice []string\n\trequire.Equal(t, nilSlice, h2.Empty)\n\trequire.Equal(t, []string{\"\"}, h2.Alloc)\n\trequire.Equal(t, []int64{1}, h2.No)\n\n\ttype RequiredHeader struct {\n\t\tName string `header:\"name,required\"`\n\t}\n\trh := new(RequiredHeader)\n\tc.Request().Header.Del(\"name\")\n\terr := c.Bind().Header(rh)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"name\\\" from header: name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n}\n\n// go test -run Test_Bind_Header_Map -v\nfunc Test_Bind_Header_Map(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.Add(\"id\", \"1\")\n\tc.Request().Header.Add(\"Name\", \"John Doe\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber\")\n\tq := make(map[string][]string, 0)\n\trequire.NoError(t, c.Bind().Header(&q))\n\trequire.Len(t, q[\"Hobby\"], 1)\n\n\tc.Request().Header.Del(\"hobby\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber,go\")\n\tq = make(map[string][]string, 0)\n\trequire.NoError(t, c.Bind().Header(&q))\n\trequire.Len(t, q[\"Hobby\"], 1)\n\n\tempty := make(map[string][]string, 0)\n\tc.Request().Header.Del(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(&empty))\n\trequire.Empty(t, empty[\"Hobby\"])\n}\n\n// go test -run Test_Bind_Header_WithSetParserDecoder -v\nfunc Test_Bind_Header_WithSetParserDecoder(t *testing.T) {\n\ttype NonRFCTime time.Time\n\n\tnonRFCConverter := func(value string) reflect.Value {\n\t\tif v, err := time.Parse(\"2006-01-02\", value); err == nil {\n\t\t\treturn reflect.ValueOf(v)\n\t\t}\n\t\treturn reflect.Value{}\n\t}\n\n\tnonRFCTime := binder.ParserType{\n\t\tCustomType: NonRFCTime{},\n\t\tConverter:  nonRFCConverter,\n\t}\n\n\tbinder.SetParserDecoder(binder.ParserConfig{\n\t\tIgnoreUnknownKeys: true,\n\t\tParserType:        []binder.ParserType{nonRFCTime},\n\t\tZeroEmpty:         true,\n\t\tSetAliasTag:       \"req\",\n\t})\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype NonRFCTimeInput struct {\n\t\tDate  NonRFCTime `req:\"date\"`\n\t\tTitle string     `req:\"title\"`\n\t\tBody  string     `req:\"body\"`\n\t}\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tr := new(NonRFCTimeInput)\n\n\tc.Request().Header.Add(\"Date\", \"2021-04-10\")\n\tc.Request().Header.Add(\"Title\", \"CustomDateTest\")\n\tc.Request().Header.Add(\"Body\", \"October\")\n\n\trequire.NoError(t, c.Bind().Header(r))\n\trequire.Equal(t, \"CustomDateTest\", r.Title)\n\tdate := fmt.Sprintf(\"%v\", r.Date)\n\trequire.Equal(t, \"{0 63753609600 <nil>}\", date)\n\trequire.Equal(t, \"October\", r.Body)\n\n\tc.Request().Header.Add(\"Title\", \"\")\n\tr = &NonRFCTimeInput{\n\t\tTitle: \"Existing title\",\n\t\tBody:  \"Existing Body\",\n\t}\n\trequire.NoError(t, c.Bind().Header(r))\n\trequire.Empty(t, r.Title)\n}\n\n// go test -run Test_Bind_Header_Schema -v\nfunc Test_Bind_Header_Schema(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Header1 struct {\n\t\tName   string `header:\"Name,required\"`\n\t\tNested struct {\n\t\t\tAge int `header:\"Age\"`\n\t\t} `header:\"Nested,required\"`\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.Add(\"Name\", \"tom\")\n\tc.Request().Header.Add(\"Nested.Age\", \"10\")\n\tq := new(Header1)\n\trequire.NoError(t, c.Bind().Header(q))\n\n\tc.Request().Header.Del(\"Name\")\n\tq = new(Header1)\n\terr := c.Bind().Header(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Name\\\" from header: Name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.Add(\"Name\", \"tom\")\n\tc.Request().Header.Del(\"Nested.Age\")\n\tc.Request().Header.Add(\"Nested.Agex\", \"10\")\n\tq = new(Header1)\n\trequire.NoError(t, c.Bind().Header(q))\n\n\tc.Request().Header.Del(\"Nested.Agex\")\n\tq = new(Header1)\n\terr = c.Bind().Header(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Nested\\\" from header: Nested is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.Del(\"Nested.Agex\")\n\tc.Request().Header.Del(\"Name\")\n\n\ttype Header2 struct {\n\t\tName   string `header:\"Name\"`\n\t\tNested struct {\n\t\t\tAge int `header:\"age,required\"`\n\t\t} `header:\"Nested\"`\n\t}\n\n\tc.Request().Header.Add(\"Name\", \"tom\")\n\tc.Request().Header.Add(\"Nested.Age\", \"10\")\n\n\th2 := new(Header2)\n\trequire.NoError(t, c.Bind().Header(h2))\n\n\tc.Request().Header.Del(\"Name\")\n\th2 = new(Header2)\n\trequire.NoError(t, c.Bind().Header(h2))\n\n\tc.Request().Header.Del(\"Name\")\n\tc.Request().Header.Del(\"Nested.Age\")\n\tc.Request().Header.Add(\"Nested.Agex\", \"10\")\n\th2 = new(Header2)\n\terr = c.Bind().Header(h2)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Nested.age\\\" from header: Nested.age is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\ttype Node struct {\n\t\tNext  *Node `header:\"Next,required\"`\n\t\tValue int   `header:\"Val,required\"`\n\t}\n\tc.Request().Header.Add(\"Val\", \"1\")\n\tc.Request().Header.Add(\"Next.Val\", \"3\")\n\tn := new(Node)\n\trequire.NoError(t, c.Bind().Header(n))\n\trequire.Equal(t, 1, n.Value)\n\trequire.Equal(t, 3, n.Next.Value)\n\n\tc.Request().Header.Del(\"Val\")\n\tn = new(Node)\n\terr = c.Bind().Header(n)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Val\\\" from header: Val is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.Add(\"Val\", \"3\")\n\tc.Request().Header.Del(\"Next.Val\")\n\tc.Request().Header.Add(\"Next.Value\", \"2\")\n\tn = new(Node)\n\tn.Next = new(Node)\n\trequire.NoError(t, c.Bind().Header(n))\n\trequire.Equal(t, 3, n.Value)\n\trequire.Equal(t, 0, n.Next.Value)\n}\n\n// go test -run Test_Bind_Resp_Header -v\nfunc Test_Bind_RespHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Header struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Response().Header.Add(\"id\", \"1\")\n\tc.Response().Header.Add(\"Name\", \"John Doe\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber\")\n\tq := new(Header)\n\trequire.NoError(t, c.Bind().RespHeader(q))\n\trequire.Len(t, q.Hobby, 2)\n\n\tc.Response().Header.Del(\"hobby\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber,go\")\n\tq = new(Header)\n\trequire.NoError(t, c.Bind().RespHeader(q))\n\trequire.Len(t, q.Hobby, 3)\n\n\tempty := new(Header)\n\tc.Response().Header.Del(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(empty))\n\trequire.Empty(t, empty.Hobby)\n\n\ttype Header2 struct {\n\t\tName            string\n\t\tHobby           string\n\t\tFavouriteDrinks []string\n\t\tEmpty           []string\n\t\tAlloc           []string\n\t\tNo              []int64\n\t\tID              int\n\t\tBool            bool\n\t}\n\n\tc.Response().Header.Add(\"id\", \"2\")\n\tc.Response().Header.Add(\"Name\", \"Jane Doe\")\n\tc.Response().Header.Del(\"hobby\")\n\tc.Response().Header.Add(\"Hobby\", \"go,fiber\")\n\tc.Response().Header.Add(\"favouriteDrinks\", \"milo,coke,pepsi\")\n\tc.Response().Header.Add(\"alloc\", \"\")\n\tc.Response().Header.Add(\"no\", \"1\")\n\n\th2 := new(Header2)\n\th2.Bool = true\n\th2.Name = helloWorld\n\trequire.NoError(t, c.Bind().RespHeader(h2))\n\trequire.Equal(t, \"go,fiber\", h2.Hobby)\n\trequire.True(t, h2.Bool)\n\trequire.Equal(t, \"Jane Doe\", h2.Name) // check value get overwritten\n\trequire.Equal(t, []string{\"milo\", \"coke\", \"pepsi\"}, h2.FavouriteDrinks)\n\tvar nilSlice []string\n\trequire.Equal(t, nilSlice, h2.Empty)\n\trequire.Equal(t, []string{\"\"}, h2.Alloc)\n\trequire.Equal(t, []int64{1}, h2.No)\n\n\ttype RequiredHeader struct {\n\t\tName string `respHeader:\"name,required\"`\n\t}\n\trh := new(RequiredHeader)\n\tc.Response().Header.Del(\"name\")\n\terr := c.Bind().RespHeader(rh)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"name\\\" from respHeader: name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n}\n\n// go test -run Test_Bind_RespHeader_Map -v\nfunc Test_Bind_RespHeader_Map(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Response().Header.Add(\"id\", \"1\")\n\tc.Response().Header.Add(\"Name\", \"John Doe\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber\")\n\tq := make(map[string][]string, 0)\n\trequire.NoError(t, c.Bind().RespHeader(&q))\n\trequire.Len(t, q[\"Hobby\"], 1)\n\n\tc.Response().Header.Del(\"hobby\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber,go\")\n\tq = make(map[string][]string, 0)\n\trequire.NoError(t, c.Bind().RespHeader(&q))\n\trequire.Len(t, q[\"Hobby\"], 1)\n\n\tempty := make(map[string][]string, 0)\n\tc.Response().Header.Del(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(&empty))\n\trequire.Empty(t, empty[\"Hobby\"])\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Query -benchmem -count=4\nfunc Benchmark_Bind_Query(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Query struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball&hobby=football\")\n\tq := new(Query)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Query(q)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"tom\", q.Name)\n\trequire.Equal(b, 1, q.ID)\n\trequire.Len(b, q.Hobby, 2)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Query_Default -benchmem -count=4\nfunc Benchmark_Bind_Query_Default(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Query struct {\n\t\tName  string   `query:\"name,default:tom\"`\n\t\tHobby []string `query:\"hobby,default:football|basketball\"`\n\t\tID    int      `query:\"id,default:1\"`\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"\")\n\tq := new(Query)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t*q = Query{}\n\t\terr = c.Bind().Query(q)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"tom\", q.Name)\n\trequire.Equal(b, 1, q.ID)\n\trequire.Len(b, q.Hobby, 2)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Query_Map -benchmem -count=4\nfunc Benchmark_Bind_Query_Map(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball&hobby=football\")\n\tq := make(map[string][]string)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Query(&q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Query_WithParseParam -benchmem -count=4\nfunc Benchmark_Bind_Query_WithParseParam(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Person struct {\n\t\tName string `query:\"name\"`\n\t\tAge  int    `query:\"age\"`\n\t}\n\n\ttype CollectionQuery struct {\n\t\tData []Person `query:\"data\"`\n\t}\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"data[0][name]=john&data[0][age]=10\")\n\tcq := new(CollectionQuery)\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Query(cq)\n\t}\n\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Query_Comma -benchmem -count=4\nfunc Benchmark_Bind_Query_Comma(b *testing.B) {\n\tvar err error\n\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Query struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football\")\n\tq := new(Query)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Query(q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Header -benchmem -count=4\nfunc Benchmark_Bind_Header(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype ReqHeader struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.Add(\"id\", \"1\")\n\tc.Request().Header.Add(\"Name\", \"John Doe\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber\")\n\n\tq := new(ReqHeader)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Header(q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Header_Map -benchmem -count=4\nfunc Benchmark_Bind_Header_Map(b *testing.B) {\n\tvar err error\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.Add(\"id\", \"1\")\n\tc.Request().Header.Add(\"Name\", \"John Doe\")\n\tc.Request().Header.Add(\"Hobby\", \"golang,fiber\")\n\n\tq := make(map[string][]string)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().Header(&q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_RespHeader -benchmem -count=4\nfunc Benchmark_Bind_RespHeader(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype ReqHeader struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Response().Header.Add(\"id\", \"1\")\n\tc.Response().Header.Add(\"Name\", \"John Doe\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber\")\n\n\tq := new(ReqHeader)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().RespHeader(q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_RespHeader_Map -benchmem -count=4\nfunc Benchmark_Bind_RespHeader_Map(b *testing.B) {\n\tvar err error\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Response().Header.Add(\"id\", \"1\")\n\tc.Response().Header.Add(\"Name\", \"John Doe\")\n\tc.Response().Header.Add(\"Hobby\", \"golang,fiber\")\n\n\tq := make(map[string][]string)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Bind().RespHeader(&q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -run Test_Bind_Body_Compression\nfunc Test_Bind_Body(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tMsgPackEncoder: msgpack.Marshal,\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\tCBOREncoder:    cbor.Marshal,\n\t\tCBORDecoder:    cbor.Unmarshal,\n\t})\n\treqBody := []byte(`{\"name\":\"john\"}`)\n\n\ttype Demo struct {\n\t\tName  string   `json:\"name\" xml:\"name\" form:\"name\" query:\"name\" msgpack:\"name\"`\n\t\tNames []string `json:\"names\" xml:\"names\" form:\"names\" query:\"names\" msgpack:\"names\"`\n\t}\n\n\t// Helper function to test compressed bodies\n\ttestCompressedBody := func(t *testing.T, compressedBody []byte, encoding string) {\n\t\tt.Helper()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\tc.Request().Header.Set(fasthttp.HeaderContentEncoding, encoding)\n\t\tc.Request().SetBody(compressedBody)\n\t\tc.Request().Header.SetContentLength(len(compressedBody))\n\t\td := new(Demo)\n\t\trequire.NoError(t, c.Bind().Body(d))\n\t\trequire.Equal(t, \"john\", d.Name)\n\t\tc.Request().Header.Del(fasthttp.HeaderContentEncoding)\n\t}\n\n\tt.Run(\"Gzip\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcompressedBody := fasthttp.AppendGzipBytes(nil, reqBody)\n\t\trequire.NotEqual(t, reqBody, compressedBody)\n\t\ttestCompressedBody(t, compressedBody, \"gzip\")\n\t})\n\n\tt.Run(\"Deflate\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcompressedBody := fasthttp.AppendDeflateBytes(nil, reqBody)\n\t\trequire.NotEqual(t, reqBody, compressedBody)\n\t\ttestCompressedBody(t, compressedBody, \"deflate\")\n\t})\n\n\tt.Run(\"Brotli\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcompressedBody := fasthttp.AppendBrotliBytes(nil, reqBody)\n\t\trequire.NotEqual(t, reqBody, compressedBody)\n\t\ttestCompressedBody(t, compressedBody, \"br\")\n\t})\n\n\tt.Run(\"Zstd\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcompressedBody := fasthttp.AppendZstdBytes(nil, reqBody)\n\t\trequire.NotEqual(t, reqBody, compressedBody)\n\t\ttestCompressedBody(t, compressedBody, \"zstd\")\n\t})\n\n\ttestDecodeParser := func(t *testing.T, contentType string, body []byte) {\n\t\tt.Helper()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.SetContentType(contentType)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\td := new(Demo)\n\t\trequire.NoError(t, c.Bind().Body(d))\n\t\trequire.Equal(t, \"john\", d.Name)\n\t}\n\n\ttestErrorParser := func(t *testing.T, contentType string, body []byte) {\n\t\tt.Helper()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.SetContentType(contentType)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\td := new(Demo)\n\t\terr := c.Bind().Body(d)\n\t\trequire.Error(t, err)\n\t}\n\n\tt.Run(\"JSON\", func(t *testing.T) {\n\t\ttestDecodeParser(t, MIMEApplicationJSON, []byte(`{\"name\":\"john\"}`))\n\t})\n\tt.Run(\"MsgPack\", func(t *testing.T) {\n\t\ttestDecodeParser(t, MIMEApplicationMsgPack, []byte{0x81, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa4, 0x6a, 0x6f, 0x68, 0x6e})\n\t\ttestErrorParser(t, MIMEApplicationMsgPack, []byte{0xFF, 0xFF})\n\t})\n\tt.Run(\"CBOR\", func(t *testing.T) {\n\t\tenc, err := cbor.Marshal(&Demo{Name: \"john\"})\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\ttestDecodeParser(t, MIMEApplicationCBOR, enc)\n\n\t\t// Test invalid CBOR data\n\t\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\t\tinvalidData := []byte{0xFF, 0xFF} // Invalid CBOR data\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().Header.SetContentType(MIMEApplicationCBOR)\n\t\t\tc.Request().SetBody(invalidData)\n\t\t\td := new(Demo)\n\t\t\trequire.Error(t, c.Bind().Body(d))\n\t\t})\n\t})\n\n\tt.Run(\"XML\", func(t *testing.T) {\n\t\ttestDecodeParser(t, MIMEApplicationXML, []byte(`<Demo><name>john</name></Demo>`))\n\t})\n\n\tt.Run(\"Form\", func(t *testing.T) {\n\t\ttestDecodeParser(t, MIMEApplicationForm, []byte(\"name=john\"))\n\t})\n\n\tt.Run(\"MultipartForm\", func(t *testing.T) {\n\t\ttestDecodeParser(t, MIMEMultipartForm+`;boundary=\"b\"`, []byte(\"--b\\r\\nContent-Disposition: form-data; name=\\\"name\\\"\\r\\n\\r\\njohn\\r\\n--b--\"))\n\t})\n\n\ttestDecodeParserError := func(t *testing.T, contentType, body string) {\n\t\tt.Helper()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.SetContentType(contentType)\n\t\tc.Request().SetBody([]byte(body))\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\trequire.Error(t, c.Bind().Body(nil))\n\t}\n\n\tt.Run(\"ErrorInvalidContentType\", func(t *testing.T) {\n\t\ttestDecodeParserError(t, \"invalid-content-type\", \"\")\n\t})\n\n\tt.Run(\"ErrorUnknownContentTypeReturnsUnprocessableEntity\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.SetContentType(\"application/unknown-type\")\n\t\tc.Request().SetBody([]byte(\"some body\"))\n\t\tc.Request().Header.SetContentLength(9)\n\t\td := new(Demo)\n\t\terr := c.Bind().Body(d)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, ErrUnprocessableEntity)\n\t})\n\n\tt.Run(\"ErrorMalformedMultipart\", func(t *testing.T) {\n\t\ttestDecodeParserError(t, MIMEMultipartForm+`;boundary=\"b\"`, \"--b\")\n\t})\n\n\ttype CollectionQuery struct {\n\t\tData []Demo `query:\"data\"`\n\t}\n\n\tt.Run(\"MultipartCollectionQueryDotNotation\", func(t *testing.T) {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Reset()\n\n\t\tbuf := &bytes.Buffer{}\n\t\twriter := multipart.NewWriter(buf)\n\t\trequire.NoError(t, writer.WriteField(\"data.0.name\", \"john\"))\n\t\trequire.NoError(t, writer.WriteField(\"data.1.name\", \"doe\"))\n\t\trequire.NoError(t, writer.Close())\n\n\t\tc.Request().Header.SetContentType(writer.FormDataContentType())\n\t\tc.Request().SetBody(buf.Bytes())\n\t\tc.Request().Header.SetContentLength(len(c.Body()))\n\n\t\tcq := new(CollectionQuery)\n\t\trequire.NoError(t, c.Bind().Body(cq))\n\t\trequire.Len(t, cq.Data, 2)\n\t\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\t\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\t})\n\n\tt.Run(\"MultipartCollectionQuerySquareBrackets\", func(t *testing.T) {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Reset()\n\n\t\tbuf := &bytes.Buffer{}\n\t\twriter := multipart.NewWriter(buf)\n\t\trequire.NoError(t, writer.WriteField(\"data[0][name]\", \"john\"))\n\t\trequire.NoError(t, writer.WriteField(\"data[1][name]\", \"doe\"))\n\t\trequire.NoError(t, writer.Close())\n\n\t\tc.Request().Header.SetContentType(writer.FormDataContentType())\n\t\tc.Request().SetBody(buf.Bytes())\n\t\tc.Request().Header.SetContentLength(len(c.Body()))\n\n\t\tcq := new(CollectionQuery)\n\t\trequire.NoError(t, c.Bind().Body(cq))\n\t\trequire.Len(t, cq.Data, 2)\n\t\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\t\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\t})\n\n\tt.Run(\"CollectionQuerySquareBrackets\", func(t *testing.T) {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Reset()\n\t\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\t\tc.Request().SetBody([]byte(\"data[0][name]=john&data[1][name]=doe\"))\n\t\tc.Request().Header.SetContentLength(len(c.Body()))\n\t\tcq := new(CollectionQuery)\n\t\trequire.NoError(t, c.Bind().Body(cq))\n\t\trequire.Len(t, cq.Data, 2)\n\t\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\t\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\t})\n\n\tt.Run(\"CollectionQueryDotNotation\", func(t *testing.T) {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Reset()\n\t\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\t\tc.Request().SetBody([]byte(\"data.0.name=john&data.1.name=doe\"))\n\t\tc.Request().Header.SetContentLength(len(c.Body()))\n\t\tcq := new(CollectionQuery)\n\t\trequire.NoError(t, c.Bind().Body(cq))\n\t\trequire.Len(t, cq.Data, 2)\n\t\trequire.Equal(t, \"john\", cq.Data[0].Name)\n\t\trequire.Equal(t, \"doe\", cq.Data[1].Name)\n\t})\n}\n\n// go test -run Test_Bind_Body_WithSetParserDecoder\nfunc Test_Bind_Body_WithSetParserDecoder(t *testing.T) {\n\ttype CustomTime time.Time\n\n\ttimeConverter := func(value string) reflect.Value {\n\t\tif v, err := time.Parse(\"2006-01-02\", value); err == nil {\n\t\t\treturn reflect.ValueOf(v)\n\t\t}\n\t\treturn reflect.Value{}\n\t}\n\n\tcustomTime := binder.ParserType{\n\t\tCustomType: CustomTime{},\n\t\tConverter:  timeConverter,\n\t}\n\n\tbinder.SetParserDecoder(binder.ParserConfig{\n\t\tIgnoreUnknownKeys: true,\n\t\tParserType:        []binder.ParserType{customTime},\n\t\tZeroEmpty:         true,\n\t\tSetAliasTag:       \"form\",\n\t})\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tDate  CustomTime `form:\"date\"`\n\t\tTitle string     `form:\"title\"`\n\t\tBody  string     `form:\"body\"`\n\t}\n\n\ttestDecodeParser := func(contentType, body string) {\n\t\tc.Request().Header.SetContentType(contentType)\n\t\tc.Request().SetBody([]byte(body))\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\td := Demo{\n\t\t\tTitle: \"Existing title\",\n\t\t\tBody:  \"Existing Body\",\n\t\t}\n\t\trequire.NoError(t, c.Bind().Body(&d))\n\t\tdate := fmt.Sprintf(\"%v\", d.Date)\n\t\trequire.Equal(t, \"{0 63743587200 <nil>}\", date)\n\t\trequire.Empty(t, d.Title)\n\t\trequire.Equal(t, \"New Body\", d.Body)\n\t}\n\n\ttestDecodeParser(MIMEApplicationForm, \"date=2020-12-15&title=&body=New Body\")\n\ttestDecodeParser(MIMEMultipartForm+`; boundary=\"b\"`, \"--b\\r\\nContent-Disposition: form-data; name=\\\"date\\\"\\r\\n\\r\\n2020-12-15\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"title\\\"\\r\\n\\r\\n\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"body\\\"\\r\\n\\r\\nNew Body\\r\\n--b--\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_JSON -benchmem -count=4\nfunc Benchmark_Bind_Body_JSON(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `json:\"name\"`\n\t}\n\tbody, err := json.Marshal(&Demo{Name: \"john\"})\n\tif err != nil {\n\t\tb.Error(err)\n\t}\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_MsgPack -benchmem -count=4\nfunc Benchmark_Bind_Body_MsgPack(b *testing.B) {\n\tvar err error\n\n\tapp := New(\n\t\tConfig{\n\t\t\tMsgPackEncoder: msgpack.Marshal,\n\t\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\t\tCBOREncoder:    cbor.Marshal,\n\t\t\tCBORDecoder:    cbor.Unmarshal,\n\t\t},\n\t)\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `msgpack:\"name\"`\n\t}\n\tbody := []byte{0x81, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa4, 0x6a, 0x6f, 0x68, 0x6e} // {\"name\":\"john\"}\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationMsgPack)\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_XML -benchmem -count=4\nfunc Benchmark_Bind_Body_XML(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `xml:\"name\"`\n\t}\n\tbody := []byte(\"<Demo><name>john</name></Demo>\")\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationXML)\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_CBOR -benchmem -count=4\nfunc Benchmark_Bind_Body_CBOR(b *testing.B) {\n\tvar err error\n\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `json:\"name\"`\n\t}\n\tbody, err := cbor.Marshal(&Demo{Name: \"john\"})\n\tif err != nil {\n\t\tb.Error(err)\n\t}\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationCBOR)\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4\nfunc Benchmark_Bind_Body_Form(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `form:\"name\"`\n\t}\n\tbody := []byte(\"name=john\")\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm -benchmem -count=4\nfunc Benchmark_Bind_Body_MultipartForm(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Demo struct {\n\t\tName string `form:\"name\"`\n\t}\n\n\tbuf := &bytes.Buffer{}\n\twriter := multipart.NewWriter(buf)\n\trequire.NoError(b, writer.WriteField(\"name\", \"john\"))\n\trequire.NoError(b, writer.Close())\n\tbody := buf.Bytes()\n\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm_Nested -benchmem -count=4\nfunc Benchmark_Bind_Body_MultipartForm_Nested(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Person struct {\n\t\tName string `form:\"name\"`\n\t\tAge  int    `form:\"age\"`\n\t}\n\n\ttype Demo struct {\n\t\tName    string   `form:\"name\"`\n\t\tPersons []Person `form:\"persons\"`\n\t}\n\n\tbuf := &bytes.Buffer{}\n\twriter := multipart.NewWriter(buf)\n\trequire.NoError(b, writer.WriteField(\"name\", \"john\"))\n\trequire.NoError(b, writer.WriteField(\"persons.0.name\", \"john\"))\n\trequire.NoError(b, writer.WriteField(\"persons[0][age]\", \"10\"))\n\trequire.NoError(b, writer.WriteField(\"persons[1][name]\", \"doe\"))\n\trequire.NoError(b, writer.WriteField(\"persons.1.age\", \"20\"))\n\trequire.NoError(b, writer.Close())\n\tbody := buf.Bytes()\n\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(d)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d.Name)\n\trequire.Equal(b, \"john\", d.Persons[0].Name)\n\trequire.Equal(b, 10, d.Persons[0].Age)\n\trequire.Equal(b, \"doe\", d.Persons[1].Name)\n\trequire.Equal(b, 20, d.Persons[1].Age)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4\nfunc Benchmark_Bind_Body_Form_Map(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tbody := []byte(\"name=john\")\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\tc.Request().Header.SetContentLength(len(body))\n\td := make(map[string]string)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Body(&d)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", d[\"name\"])\n}\n\n// go test -run Test_Bind_URI\nfunc Test_Bind_URI(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/test1/userId/role/:roleId\", func(c Ctx) error {\n\t\ttype Demo struct {\n\t\t\tUserID uint `uri:\"userId\"`\n\t\t\tRoleID uint `uri:\"roleId\"`\n\t\t}\n\t\td := new(Demo)\n\t\tif err := c.Bind().URI(d); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\trequire.Equal(t, uint(111), d.UserID)\n\t\trequire.Equal(t, uint(222), d.RoleID)\n\t\treturn nil\n\t})\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/test1/111/role/222\", http.NoBody))\n\trequire.NoError(t, err)\n\t_, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/111/role/222\", http.NoBody))\n\trequire.NoError(t, err)\n}\n\n// go test -run Test_Bind_URI_Map\nfunc Test_Bind_URI_Map(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/test1/userId/role/:roleId\", func(c Ctx) error {\n\t\td := make(map[string]string)\n\n\t\tif err := c.Bind().URI(&d); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\trequire.Equal(t, uint(111), d[\"userId\"])\n\t\trequire.Equal(t, uint(222), d[\"roleId\"])\n\t\treturn nil\n\t})\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/test1/111/role/222\", http.NoBody))\n\trequire.NoError(t, err)\n\t_, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/111/role/222\", http.NoBody))\n\trequire.NoError(t, err)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_URI -benchmem -count=4\nfunc Benchmark_Bind_URI(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.route = &Route{\n\t\tParams: []string{\n\t\t\t\"param1\", \"param2\", \"param3\", \"param4\",\n\t\t},\n\t}\n\tc.values = [maxParams]string{\n\t\t\"john\", \"doe\", \"is\", \"awesome\",\n\t}\n\n\tvar res struct {\n\t\tParam1 string `uri:\"param1\"`\n\t\tParam2 string `uri:\"param2\"`\n\t\tParam3 string `uri:\"param3\"`\n\t\tParam4 string `uri:\"param4\"`\n\t}\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().URI(&res)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", res.Param1)\n\trequire.Equal(b, \"doe\", res.Param2)\n\trequire.Equal(b, \"is\", res.Param3)\n\trequire.Equal(b, \"awesome\", res.Param4)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_URI_Map -benchmem -count=4\nfunc Benchmark_Bind_URI_Map(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.route = &Route{\n\t\tParams: []string{\n\t\t\t\"param1\", \"param2\", \"param3\", \"param4\",\n\t\t},\n\t}\n\tc.values = [maxParams]string{\n\t\t\"john\", \"doe\", \"is\", \"awesome\",\n\t}\n\n\tres := make(map[string]string)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().URI(&res)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", res[\"param1\"])\n\trequire.Equal(b, \"doe\", res[\"param2\"])\n\trequire.Equal(b, \"is\", res[\"param3\"])\n\trequire.Equal(b, \"awesome\", res[\"param4\"])\n}\n\n// go test -run Test_Bind_Cookie -v\nfunc Test_Bind_Cookie(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Cookie struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.SetCookie(\"id\", \"1\")\n\tc.Request().Header.SetCookie(\"Name\", \"John Doe\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber\")\n\tq := new(Cookie)\n\trequire.NoError(t, c.Bind().Cookie(q))\n\trequire.Len(t, q.Hobby, 2)\n\n\tc.Request().Header.DelCookie(\"hobby\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber,go\")\n\tq = new(Cookie)\n\trequire.NoError(t, c.Bind().Cookie(q))\n\trequire.Len(t, q.Hobby, 3)\n\n\tempty := new(Cookie)\n\tc.Request().Header.DelCookie(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(empty))\n\trequire.Empty(t, empty.Hobby)\n\n\ttype Cookie2 struct {\n\t\tName            string\n\t\tHobby           string\n\t\tFavouriteDrinks []string\n\t\tEmpty           []string\n\t\tAlloc           []string\n\t\tNo              []int64\n\t\tID              int\n\t\tBool            bool\n\t}\n\n\tc.Request().Header.SetCookie(\"id\", \"2\")\n\tc.Request().Header.SetCookie(\"Name\", \"Jane Doe\")\n\tc.Request().Header.DelCookie(\"hobby\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"go,fiber\")\n\tc.Request().Header.SetCookie(\"favouriteDrinks\", \"milo,coke,pepsi\")\n\tc.Request().Header.SetCookie(\"alloc\", \"\")\n\tc.Request().Header.SetCookie(\"no\", \"1\")\n\n\th2 := new(Cookie2)\n\th2.Bool = true\n\th2.Name = helloWorld\n\trequire.NoError(t, c.Bind().Cookie(h2))\n\trequire.Equal(t, \"go,fiber\", h2.Hobby)\n\trequire.True(t, h2.Bool)\n\trequire.Equal(t, \"Jane Doe\", h2.Name) // check value get overwritten\n\trequire.Equal(t, []string{\"milo\", \"coke\", \"pepsi\"}, h2.FavouriteDrinks)\n\tvar nilSlice []string\n\trequire.Equal(t, nilSlice, h2.Empty)\n\trequire.Equal(t, []string{\"\"}, h2.Alloc)\n\trequire.Equal(t, []int64{1}, h2.No)\n\n\ttype RequiredCookie struct {\n\t\tName string `cookie:\"name,required\"`\n\t}\n\trh := new(RequiredCookie)\n\tc.Request().Header.DelCookie(\"name\")\n\terr := c.Bind().Cookie(rh)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"name\\\" from cookie: name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n}\n\n// go test -run Test_Bind_Cookie_Map -v\nfunc Test_Bind_Cookie_Map(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.SetCookie(\"id\", \"1\")\n\tc.Request().Header.SetCookie(\"Name\", \"John Doe\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber\")\n\tq := make(map[string][]string)\n\trequire.NoError(t, c.Bind().Cookie(&q))\n\trequire.Len(t, q[\"Hobby\"], 2)\n\n\tc.Request().Header.DelCookie(\"hobby\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber,go\")\n\tq = make(map[string][]string)\n\trequire.NoError(t, c.Bind().Cookie(&q))\n\trequire.Len(t, q[\"Hobby\"], 3)\n\n\tempty := make(map[string][]string)\n\tc.Request().Header.DelCookie(\"hobby\")\n\trequire.NoError(t, c.Bind().Query(&empty))\n\trequire.Empty(t, empty[\"Hobby\"])\n}\n\n// go test -run Test_Bind_Cookie_WithSetParserDecoder -v\nfunc Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) {\n\ttype NonRFCTime time.Time\n\n\tnonRFCConverter := func(value string) reflect.Value {\n\t\tif v, err := time.Parse(\"2006-01-02\", value); err == nil {\n\t\t\treturn reflect.ValueOf(v)\n\t\t}\n\t\treturn reflect.Value{}\n\t}\n\n\tnonRFCTime := binder.ParserType{\n\t\tCustomType: NonRFCTime{},\n\t\tConverter:  nonRFCConverter,\n\t}\n\n\tbinder.SetParserDecoder(binder.ParserConfig{\n\t\tIgnoreUnknownKeys: true,\n\t\tParserType:        []binder.ParserType{nonRFCTime},\n\t\tZeroEmpty:         true,\n\t\tSetAliasTag:       \"cerez\",\n\t})\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype NonRFCTimeInput struct {\n\t\tDate  NonRFCTime `cerez:\"date\"`\n\t\tTitle string     `cerez:\"title\"`\n\t\tBody  string     `cerez:\"body\"`\n\t}\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tr := new(NonRFCTimeInput)\n\n\tc.Request().Header.SetCookie(\"Date\", \"2021-04-10\")\n\tc.Request().Header.SetCookie(\"Title\", \"CustomDateTest\")\n\tc.Request().Header.SetCookie(\"Body\", \"October\")\n\n\trequire.NoError(t, c.Bind().Cookie(r))\n\trequire.Equal(t, \"CustomDateTest\", r.Title)\n\tdate := fmt.Sprintf(\"%v\", r.Date)\n\trequire.Equal(t, \"{0 63753609600 <nil>}\", date)\n\trequire.Equal(t, \"October\", r.Body)\n\n\tc.Request().Header.SetCookie(\"Title\", \"\")\n\tr = &NonRFCTimeInput{\n\t\tTitle: \"Existing title\",\n\t\tBody:  \"Existing Body\",\n\t}\n\trequire.NoError(t, c.Bind().Cookie(r))\n\trequire.Empty(t, r.Title)\n}\n\n// go test -run Test_Bind_Cookie_Schema -v\nfunc Test_Bind_Cookie_Schema(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Cookie1 struct {\n\t\tName   string `cookie:\"Name,required\"`\n\t\tNested struct {\n\t\t\tAge int `cookie:\"Age\"`\n\t\t} `cookie:\"Nested,required\"`\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.SetCookie(\"Name\", \"tom\")\n\tc.Request().Header.SetCookie(\"Nested.Age\", \"10\")\n\tq := new(Cookie1)\n\trequire.NoError(t, c.Bind().Cookie(q))\n\n\tc.Request().Header.DelCookie(\"Name\")\n\tq = new(Cookie1)\n\terr := c.Bind().Cookie(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Name\\\" from cookie: Name is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.SetCookie(\"Name\", \"tom\")\n\tc.Request().Header.DelCookie(\"Nested.Age\")\n\tc.Request().Header.SetCookie(\"Nested.Agex\", \"10\")\n\tq = new(Cookie1)\n\trequire.NoError(t, c.Bind().Cookie(q))\n\n\tc.Request().Header.DelCookie(\"Nested.Agex\")\n\tq = new(Cookie1)\n\terr = c.Bind().Cookie(q)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Nested\\\" from cookie: Nested is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.DelCookie(\"Nested.Agex\")\n\tc.Request().Header.DelCookie(\"Name\")\n\n\ttype Cookie2 struct {\n\t\tName   string `cookie:\"Name\"`\n\t\tNested struct {\n\t\t\tAge int `cookie:\"Age,required\"`\n\t\t} `cookie:\"Nested\"`\n\t}\n\n\tc.Request().Header.SetCookie(\"Name\", \"tom\")\n\tc.Request().Header.SetCookie(\"Nested.Age\", \"10\")\n\n\th2 := new(Cookie2)\n\trequire.NoError(t, c.Bind().Cookie(h2))\n\n\tc.Request().Header.DelCookie(\"Name\")\n\th2 = new(Cookie2)\n\trequire.NoError(t, c.Bind().Cookie(h2))\n\n\tc.Request().Header.DelCookie(\"Name\")\n\tc.Request().Header.DelCookie(\"Nested.Age\")\n\tc.Request().Header.SetCookie(\"Nested.Agex\", \"10\")\n\th2 = new(Cookie2)\n\terr = c.Bind().Cookie(h2)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Nested.Age\\\" from cookie: Nested.Age is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\ttype Node struct {\n\t\tNext  *Node `cookie:\"Next,required\"`\n\t\tValue int   `cookie:\"Val,required\"`\n\t}\n\tc.Request().Header.SetCookie(\"Val\", \"1\")\n\tc.Request().Header.SetCookie(\"Next.Val\", \"3\")\n\tn := new(Node)\n\trequire.NoError(t, c.Bind().Cookie(n))\n\trequire.Equal(t, 1, n.Value)\n\trequire.Equal(t, 3, n.Next.Value)\n\n\tc.Request().Header.DelCookie(\"Val\")\n\tn = new(Node)\n\terr = c.Bind().Cookie(n)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"bind \\\"Val\\\" from cookie: Val is empty\", err.Error())\n\trequire.ErrorAs(t, err, &MultiError{})\n\n\tc.Request().Header.SetCookie(\"Val\", \"3\")\n\tc.Request().Header.DelCookie(\"Next.Val\")\n\tc.Request().Header.SetCookie(\"Next.Value\", \"2\")\n\tn = new(Node)\n\tn.Next = new(Node)\n\trequire.NoError(t, c.Bind().Cookie(n))\n\trequire.Equal(t, 3, n.Value)\n\trequire.Equal(t, 0, n.Next.Value)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Cookie -benchmem -count=4\nfunc Benchmark_Bind_Cookie(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Cookie struct {\n\t\tName  string\n\t\tHobby []string\n\t\tID    int\n\t}\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.SetCookie(\"id\", \"1\")\n\tc.Request().Header.SetCookie(\"Name\", \"John Doe\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber\")\n\n\tq := new(Cookie)\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Cookie(q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Bind_Cookie_Map -benchmem -count=4\nfunc Benchmark_Bind_Cookie_Map(b *testing.B) {\n\tvar err error\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\n\tc.Request().Header.SetCookie(\"id\", \"1\")\n\tc.Request().Header.SetCookie(\"Name\", \"John Doe\")\n\tc.Request().Header.SetCookie(\"Hobby\", \"golang,fiber\")\n\n\tq := make(map[string][]string)\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Bind().Cookie(&q)\n\t}\n\trequire.NoError(b, err)\n}\n\n// custom binder for testing\ntype customBinder struct{}\n\nfunc (*customBinder) Name() string {\n\treturn \"custom\"\n}\n\nfunc (*customBinder) MIMETypes() []string {\n\treturn []string{\"test\", \"test2\"}\n}\n\nfunc (*customBinder) Parse(c Ctx, out any) error {\n\treturn json.Unmarshal(c.Body(), out)\n}\n\n// customBinderReturningError returns a fixed error for testing extractFieldFromError branches.\ntype customBinderReturningError struct {\n\terr      error\n\tmimeType string\n}\n\nfunc (*customBinderReturningError) Name() string {\n\treturn \"error-binder\"\n}\n\nfunc (b *customBinderReturningError) MIMETypes() []string {\n\tif b.mimeType != \"\" {\n\t\treturn []string{b.mimeType}\n\t}\n\treturn []string{\"application/x-unknown-key-test\", \"application/x-empty-field-test\"}\n}\n\nfunc (b *customBinderReturningError) Parse(_ Ctx, _ any) error {\n\treturn b.err\n}\n\n// go test -run Test_Bind_CustomBinder\nfunc Test_Bind_CustomBinder(t *testing.T) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// Register binder\n\tcustomBinder := &customBinder{}\n\tapp.RegisterCustomBinder(customBinder)\n\n\ttype Demo struct {\n\t\tName string `json:\"name\"`\n\t}\n\tbody := []byte(`{\"name\":\"john\"}`)\n\tc.Request().SetBody(body)\n\tc.Request().Header.SetContentType(\"test\")\n\tc.Request().Header.SetContentLength(len(body))\n\td := new(Demo)\n\n\trequire.NoError(t, c.Bind().Body(d))\n\trequire.NoError(t, c.Bind().Custom(\"custom\", d))\n\trequire.Equal(t, ErrCustomBinderNotFound, c.Bind().Custom(\"not_custom\", d))\n\trequire.Equal(t, \"john\", d.Name)\n}\n\n// go test -run Test_Bind_CustomBinder_Source\nfunc Test_Bind_CustomBinder_Source(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\n\tapp.RegisterCustomBinder(&customBinder{})\n\n\ttype Demo struct {\n\t\tName string `json:\"name\"`\n\t}\n\tc.Request().SetBody([]byte(`{invalid json`))\n\tc.Request().Header.SetContentLength(14)\n\n\terr := c.Bind().Custom(\"custom\", new(Demo))\n\trequire.Error(t, err)\n\tvar be *BindError\n\trequire.ErrorAs(t, err, &be)\n\trequire.Equal(t, \"custom\", be.Source)\n}\n\n// go test -run Test_Bind_CustomBinder_Validation\nfunc Test_Bind_CustomBinder_Validation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{StructValidator: &structValidator{}})\n\tapp.RegisterCustomBinder(&customBinder{})\n\n\tt.Run(\"Body_custom_binder_validation_pass\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\n\t\tbody := []byte(`{\"name\":\"john\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(\"test\")\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\tout := new(simpleQuery)\n\t\trequire.NoError(t, c.Bind().Body(out))\n\t\trequire.Equal(t, \"john\", out.Name)\n\t})\n\n\tt.Run(\"Body_custom_binder_validation_fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\n\t\tbody := []byte(`{\"name\":\"invalid\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(\"test\")\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\tout := new(simpleQuery)\n\t\trequire.EqualError(t, c.Bind().Body(out), \"you should have entered right name\")\n\t})\n\n\tt.Run(\"Custom_binder_validation_pass\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\n\t\tbody := []byte(`{\"name\":\"john\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\tout := new(simpleQuery)\n\t\trequire.NoError(t, c.Bind().Custom(\"custom\", out))\n\t\trequire.Equal(t, \"john\", out.Name)\n\t})\n\n\tt.Run(\"Custom_binder_validation_fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(c) })\n\n\t\tbody := []byte(`{\"name\":\"invalid\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\tout := new(simpleQuery)\n\t\trequire.EqualError(t, c.Bind().Custom(\"custom\", out), \"you should have entered right name\")\n\t})\n}\n\n// go test -run Test_Bind_WithAutoHandling\nfunc Test_Bind_WithAutoHandling(t *testing.T) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype RequiredQuery struct {\n\t\tName string `query:\"name,required\"`\n\t}\n\trq := new(RequiredQuery)\n\tc.Request().URI().SetQueryString(\"\")\n\terr := c.Bind().WithAutoHandling().Query(rq)\n\trequire.Equal(t, StatusBadRequest, c.Response().StatusCode())\n\trequire.Equal(t, \"Bad request: name is empty\", err.Error())\n}\n\n// simple struct validator for testing\ntype structValidator struct{}\n\nfunc (*structValidator) Validate(out any) error {\n\tout = reflect.ValueOf(out).Elem().Interface()\n\tsq, ok := out.(simpleQuery)\n\tif !ok {\n\t\treturn errors.New(\"failed to type-assert to simpleQuery\")\n\t}\n\n\tif sq.Name != \"john\" {\n\t\treturn errors.New(\"you should have entered right name\")\n\t}\n\n\treturn nil\n}\n\ntype simpleQuery struct {\n\tName string `query:\"name\" json:\"name\"`\n}\n\ntype countingStructValidator struct {\n\tcalls int\n}\n\nfunc (v *countingStructValidator) Validate(_ any) error {\n\tv.calls++\n\n\treturn nil\n}\n\n// go test -run Test_Bind_Form_Map_SkipsStructValidator\nfunc Test_Bind_Form_Map_SkipsStructValidator(t *testing.T) {\n\tt.Parallel()\n\n\tmakeRequest := func(c Ctx) {\n\t\tbody := []byte(\"name=john\")\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\t\tc.Request().Header.SetContentLength(len(body))\n\t}\n\n\tt.Run(\"without validator\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\tmakeRequest(c)\n\t\treq := make(map[string]string)\n\t\trequire.NoError(t, c.Bind().Form(&req))\n\t\trequire.Equal(t, \"john\", req[\"name\"])\n\t})\n\n\tt.Run(\"with struct validator configured\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvalidator := &countingStructValidator{}\n\t\tapp := New(Config{StructValidator: validator})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\tmakeRequest(c)\n\t\treq := make(map[string]string)\n\t\trequire.NoError(t, c.Bind().Form(&req))\n\t\trequire.Equal(t, \"john\", req[\"name\"])\n\t\trequire.Equal(t, 0, validator.calls)\n\t})\n}\n\n// go test -run Test_Bind_SkipValidation\nfunc Test_Bind_SkipValidation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{StructValidator: &structValidator{}})\n\n\tt.Run(\"validation enabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\trq := new(simpleQuery)\n\t\tc.Request().URI().SetQueryString(\"name=efe\")\n\t\trequire.Equal(t, \"you should have entered right name\", c.Bind().SkipValidation(false).Query(rq).Error())\n\t})\n\n\tt.Run(\"validation skipped\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\trq := new(simpleQuery)\n\t\tc.Request().URI().SetQueryString(\"name=efe\")\n\t\trequire.NoError(t, c.Bind().SkipValidation(true).Query(rq))\n\t\trequire.Equal(t, \"efe\", rq.Name)\n\t})\n}\n\n// go test -run Test_Bind_SkipValidation_Usage\nfunc Test_Bind_SkipValidation_Usage(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{StructValidator: &structValidator{}})\n\n\tt.Run(\"skip validation for json\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\tbody := []byte(`{\"name\":\"efe\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\treq := new(simpleQuery)\n\t\trequire.NoError(t, c.Bind().SkipValidation(true).JSON(req))\n\t\trequire.Equal(t, \"efe\", req.Name)\n\t})\n\n\tt.Run(\"re-enable validation explicitly\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\tbody := []byte(`{\"name\":\"efe\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\treq := new(simpleQuery)\n\t\trequire.EqualError(t, c.Bind().SkipValidation(false).JSON(req), \"you should have entered right name\")\n\t})\n\n\tt.Run(\"toggle on same bind instance\", func(t *testing.T) {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() {\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\n\t\tbody := []byte(`{\"name\":\"efe\"}`)\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\tc.Request().Header.SetContentLength(len(body))\n\n\t\treq := new(simpleQuery)\n\t\trequire.NoError(t, c.Bind().SkipValidation(true).JSON(req))\n\n\t\tc.Request().SetBody(body)\n\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\treq = new(simpleQuery)\n\t\trequire.EqualError(t, c.Bind().SkipValidation(false).JSON(req), \"you should have entered right name\")\n\t})\n}\n\n// go test -run Test_Bind_ValidateStruct_NilTarget\nfunc Test_Bind_ValidateStruct_NilTarget(t *testing.T) {\n\tt.Parallel()\n\n\tvalidator := &countingStructValidator{}\n\tapp := New(Config{StructValidator: validator})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\trequire.NoError(t, c.Bind().validateStruct(nil))\n\trequire.Equal(t, 0, validator.calls)\n}\n\n// go test -run Test_Bind_StructValidator\nfunc Test_Bind_StructValidator(t *testing.T) {\n\tapp := New(Config{StructValidator: &structValidator{}})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trq := new(simpleQuery)\n\tc.Request().URI().SetQueryString(\"name=efe\")\n\trequire.Equal(t, \"you should have entered right name\", c.Bind().Query(rq).Error())\n\n\trq = new(simpleQuery)\n\tc.Request().URI().SetQueryString(\"name=john\")\n\trequire.NoError(t, c.Bind().Query(rq))\n}\n\n// go test -run Test_Bind_RepeatParserWithSameStruct -v\nfunc Test_Bind_RepeatParserWithSameStruct(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\ttype Request struct {\n\t\tQueryParam  string `query:\"query_param\"`\n\t\tHeaderParam string `header:\"header_param\"`\n\t\tBodyParam   string `json:\"body_param\" xml:\"body_param\" form:\"body_param\"`\n\t}\n\n\tr := new(Request)\n\n\tc.Request().URI().SetQueryString(\"query_param=query_param\")\n\trequire.NoError(t, c.Bind().Query(r))\n\trequire.Equal(t, \"query_param\", r.QueryParam)\n\n\tc.Request().Header.Add(\"header_param\", \"header_param\")\n\trequire.NoError(t, c.Bind().Header(r))\n\trequire.Equal(t, \"header_param\", r.HeaderParam)\n\n\tvar gzipJSON bytes.Buffer\n\tw := gzip.NewWriter(&gzipJSON)\n\t_, err := w.Write([]byte(`{\"body_param\":\"body_param\"}`))\n\trequire.NoError(t, err)\n\terr = w.Close()\n\trequire.NoError(t, err)\n\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\tc.Request().Header.Set(HeaderContentEncoding, \"gzip\")\n\tc.Request().SetBody(gzipJSON.Bytes())\n\tc.Request().Header.SetContentLength(len(gzipJSON.Bytes()))\n\trequire.NoError(t, c.Bind().Body(r))\n\trequire.Equal(t, \"body_param\", r.BodyParam)\n\tc.Request().Header.Del(HeaderContentEncoding)\n\n\ttestDecodeParser := func(contentType, body string) {\n\t\tc.Request().Header.SetContentType(contentType)\n\t\tc.Request().SetBody([]byte(body))\n\t\tc.Request().Header.SetContentLength(len(body))\n\t\trequire.NoError(t, c.Bind().Body(r))\n\t\trequire.Equal(t, \"body_param\", r.BodyParam)\n\t}\n\n\tcb, err := cbor.Marshal(&Request{BodyParam: \"body_param\"})\n\trequire.NoError(t, err, \"Failed to marshal CBOR data\")\n\n\ttestDecodeParser(MIMEApplicationJSON, `{\"body_param\":\"body_param\"}`)\n\ttestDecodeParser(MIMEApplicationXML, `<Demo><body_param>body_param</body_param></Demo>`)\n\ttestDecodeParser(MIMEApplicationCBOR, string(cb))\n\ttestDecodeParser(MIMEApplicationForm, \"body_param=body_param\")\n\ttestDecodeParser(MIMEMultipartForm+`;boundary=\"b\"`, \"--b\\r\\nContent-Disposition: form-data; name=\\\"body_param\\\"\\r\\n\\r\\nbody_param\\r\\n--b--\")\n}\n\ntype RequestConfig struct {\n\tHeaders     map[string]string\n\tCookies     map[string]string\n\tContentType string\n\tQuery       string\n\tBody        []byte\n}\n\nfunc (rc *RequestConfig) ApplyTo(ctx Ctx) {\n\tif rc.Body != nil {\n\t\tctx.Request().SetBody(rc.Body)\n\t\tctx.Request().Header.SetContentLength(len(rc.Body))\n\t}\n\tif rc.ContentType != \"\" {\n\t\tctx.Request().Header.SetContentType(rc.ContentType)\n\t}\n\tfor k, v := range rc.Headers {\n\t\tctx.Request().Header.Set(k, v)\n\t}\n\tfor k, v := range rc.Cookies {\n\t\tctx.Request().Header.SetCookie(k, v)\n\t}\n\tif rc.Query != \"\" {\n\t\tctx.Request().URI().SetQueryString(rc.Query)\n\t}\n}\n\n// go test -run Test_Bind_All\nfunc Test_Bind_All(t *testing.T) {\n\tt.Parallel()\n\ttype User struct {\n\t\tAvatar    *multipart.FileHeader `form:\"avatar\"`\n\t\tName      string                `query:\"name\" json:\"name\" form:\"name\"`\n\t\tEmail     string                `json:\"email\" form:\"email\"`\n\t\tRole      string                `header:\"X-User-Role\"`\n\t\tSessionID string                `json:\"session_id\" cookie:\"session_id\"`\n\t\tID        int                   `uri:\"id\" query:\"id\" json:\"id\" form:\"id\"`\n\t}\n\tnewBind := func(app *App) *Bind {\n\t\treturn &Bind{\n\t\t\tctx: app.AcquireCtx(&fasthttp.RequestCtx{}),\n\t\t}\n\t}\n\n\tdefaultConfig := func() *RequestConfig {\n\t\treturn &RequestConfig{\n\t\t\tContentType: MIMEApplicationJSON,\n\t\t\tBody:        []byte(`{\"name\":\"john\", \"email\": \"john@doe.com\", \"session_id\": \"abc1234\", \"id\": 1}`),\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"X-User-Role\": \"admin\",\n\t\t\t},\n\t\t\tCookies: map[string]string{\n\t\t\t\t\"session_id\": \"abc123\",\n\t\t\t},\n\t\t\tQuery: \"id=1&name=john\",\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tout      any\n\t\texpected *User\n\t\tconfig   *RequestConfig\n\t\tname     string\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname:    \"Invalid output type\",\n\t\t\tout:     123,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"Successful binding\",\n\t\t\tout:    new(User),\n\t\t\tconfig: defaultConfig(),\n\t\t\texpected: &User{\n\t\t\t\tID:        1,\n\t\t\t\tName:      \"john\",\n\t\t\t\tEmail:     \"john@doe.com\",\n\t\t\t\tRole:      \"admin\",\n\t\t\t\tSessionID: \"abc1234\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Missing fields (partial JSON only)\",\n\t\t\tout:  new(User),\n\t\t\tconfig: &RequestConfig{\n\t\t\t\tContentType: MIMEApplicationJSON,\n\t\t\t\tBody:        []byte(`{\"name\":\"partial\"}`),\n\t\t\t},\n\t\t\texpected: &User{\n\t\t\t\tName: \"partial\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Override query with JSON\",\n\t\t\tout:  new(User),\n\t\t\tconfig: &RequestConfig{\n\t\t\t\tContentType: MIMEApplicationJSON,\n\t\t\t\tBody:        []byte(`{\"name\":\"fromjson\", \"id\": 99}`),\n\t\t\t\tQuery:       \"id=1&name=queryname\",\n\t\t\t},\n\t\t\texpected: &User{\n\t\t\t\tName: \"fromjson\",\n\t\t\t\tID:   99,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Form binding\",\n\t\t\tout:  new(User),\n\t\t\tconfig: &RequestConfig{\n\t\t\t\tContentType: MIMEApplicationForm,\n\t\t\t\tBody:        []byte(\"id=2&name=formname&email=form@doe.com\"),\n\t\t\t},\n\t\t\texpected: &User{\n\t\t\t\tID:    2,\n\t\t\t\tName:  \"formname\",\n\t\t\t\tEmail: \"form@doe.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Skip body when content-type missing\",\n\t\t\tout:  new(User),\n\t\t\tconfig: &RequestConfig{\n\t\t\t\tBody:  []byte(`{\"name\":\"bodyname\"}`),\n\t\t\t\tQuery: \"name=queryname\",\n\t\t\t},\n\t\t\texpected: &User{\n\t\t\t\tName: \"queryname\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Skip empty body despite content-type\",\n\t\t\tout:  new(User),\n\t\t\tconfig: &RequestConfig{\n\t\t\t\tContentType: MIMEApplicationJSON,\n\t\t\t\tBody:        []byte{},\n\t\t\t\tQuery:       \"name=queryname\",\n\t\t\t},\n\t\t\texpected: &User{\n\t\t\t\tName: \"queryname\",\n\t\t\t},\n\t\t},\n\t}\n\n\tapp := New()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tbind := newBind(app)\n\n\t\t\tif tt.config != nil {\n\t\t\t\ttt.config.ApplyTo(bind.ctx)\n\t\t\t}\n\n\t\t\terr := bind.All(tt.out)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.expected != nil {\n\t\t\t\tactual, ok := tt.out.(*User)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\trequire.Equal(t, tt.expected.ID, actual.ID)\n\t\t\t\trequire.Equal(t, tt.expected.Name, actual.Name)\n\t\t\t\trequire.Equal(t, tt.expected.Email, actual.Email)\n\t\t\t\trequire.Equal(t, tt.expected.Role, actual.Role)\n\t\t\t\trequire.Equal(t, tt.expected.SessionID, actual.SessionID)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -run Test_Bind_All_Uri_Precedence\nfunc Test_Bind_All_Uri_Precedence(t *testing.T) {\n\tt.Parallel()\n\ttype User struct {\n\t\tName  string `json:\"name\"`\n\t\tEmail string `json:\"email\"`\n\t\tID    int    `uri:\"id\" json:\"id\" query:\"id\" form:\"id\"`\n\t}\n\n\tapp := New()\n\n\tapp.Post(\"/test1/:id\", func(c Ctx) error {\n\t\td := new(User)\n\t\tif err := c.Bind().All(d); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\trequire.Equal(t, 111, d.ID)\n\t\trequire.Equal(t, \"john\", d.Name)\n\t\trequire.Equal(t, \"john@doe.com\", d.Email)\n\t\treturn nil\n\t})\n\n\tbody := strings.NewReader(`{\"id\": 999, \"name\": \"john\", \"email\": \"john@doe.com\"}`)\n\treq := httptest.NewRequest(MethodPost, \"/test1/111?id=888\", body)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, res.StatusCode)\n}\n\n// go test -run Test_Bind_All_Query_Precedence\nfunc Test_Bind_All_Query_Precedence(t *testing.T) {\n\tt.Parallel()\n\ttype Data struct {\n\t\tID int `query:\"id\" header:\"id\" cookie:\"id\"`\n\t}\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().URI().SetQueryString(\"id=5\")\n\tc.Request().Header.Set(\"id\", \"3\")\n\tc.Request().Header.SetCookie(\"id\", \"2\")\n\n\td := new(Data)\n\trequire.NoError(t, (&Bind{ctx: c}).All(d))\n\trequire.Equal(t, 5, d.ID)\n}\n\n// go test -run Test_Bind_All_StructValidator\nfunc Test_Bind_All_StructValidator(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{StructValidator: &structValidator{}})\n\n\t// Success case: name comes from body only\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx.Request().Header.SetContentType(MIMEApplicationJSON)\n\tctx.Request().SetBody([]byte(`{\"name\":\"john\"}`))\n\tsq := new(simpleQuery)\n\trequire.NoError(t, (&Bind{ctx: ctx}).All(sq))\n\trequire.Equal(t, \"john\", sq.Name)\n\n\t// Failure: missing name everywhere\n\tctxFail := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctxFail.Request().Header.SetContentType(MIMEApplicationJSON)\n\tctxFail.Request().SetBody([]byte(`{}`))\n\tsqFail := new(simpleQuery)\n\terr := (&Bind{ctx: ctxFail}).WithoutAutoHandling().All(sqFail)\n\trequire.EqualError(t, err, \"you should have entered right name\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_Bind_All -benchmem -count=4\nfunc BenchmarkBind_All(b *testing.B) {\n\ttype User struct {\n\t\tSessionID string `json:\"session_id\" cookie:\"session_id\"`\n\t\tName      string `query:\"name\" json:\"name\" form:\"name\"`\n\t\tEmail     string `json:\"email\" form:\"email\"`\n\t\tRole      string `header:\"X-User-Role\"`\n\t\tID        int    `uri:\"id\" query:\"id\" json:\"id\" form:\"id\"`\n\t}\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tconfig := &RequestConfig{\n\t\tContentType: MIMEApplicationJSON,\n\t\tBody:        []byte(`{\"name\":\"john\", \"email\": \"john@doe.com\", \"session_id\": \"abc1234\", \"id\": 1}`),\n\t\tHeaders: map[string]string{\n\t\t\t\"X-User-Role\": \"admin\",\n\t\t},\n\t\tCookies: map[string]string{\n\t\t\t\"session_id\": \"abc123\",\n\t\t},\n\t\tQuery: \"id=1&name=john\",\n\t}\n\n\tbind := &Bind{\n\t\tctx: c,\n\t}\n\n\tfor b.Loop() {\n\t\tuser := &User{}\n\t\tconfig.ApplyTo(c)\n\t\tif err := bind.All(user); err != nil {\n\t\t\tb.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "binder/README.md",
    "content": "# Fiber Binders\n\n**Binder** is a new request/response binding feature for Fiber introduced in Fiber v3. It replaces the old Fiber parsers and offers enhanced capabilities such as custom binder registration, struct validation, support for `map[string]string`, `map[string][]string`, and more. Binder replaces the following components:\n\n- `BodyParser`\n- `ParamsParser`\n- `GetReqHeaders`\n- `GetRespHeaders`\n- `AllParams`\n- `QueryParser`\n- `ReqHeaderParser`\n\n## Default Binders\n\nFiber provides several default binders out of the box:\n\n- [Form](form.go)\n- [Query](query.go)\n- [URI](uri.go)\n- [Header](header.go)\n- [Response Header](resp_header.go)\n- [Cookie](cookie.go)\n- [JSON](json.go)\n- [XML](xml.go)\n- [CBOR](cbor.go)\n\n## Guides\n\n### Binding into a Struct\n\nFiber supports binding request data directly into a struct using [gofiber/schema](https://github.com/gofiber/schema). Here's an example:\n\n```go\n// Field names must start with an uppercase letter\ntype Person struct {\n    Name string `json:\"name\" xml:\"name\" form:\"name\"`\n    Pass string `json:\"pass\" xml:\"pass\" form:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Body(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // Output: john\n    log.Println(p.Pass) // Output: doe\n\n    // Additional logic...\n})\n\n// Run tests with the following curl commands:\n\n// JSON\ncurl -X POST -H \"Content-Type: application/json\" --data \"{\\\"name\\\":\\\"john\\\",\\\"pass\\\":\\\"doe\\\"}\" localhost:3000\n\n// XML\ncurl -X POST -H \"Content-Type: application/xml\" --data \"<login><name>john</name><pass>doe</pass></login>\" localhost:3000\n\n// URL-Encoded Form\ncurl -X POST -H \"Content-Type: application/x-www-form-urlencoded\" --data \"name=john&pass=doe\" localhost:3000\n\n// Multipart Form\ncurl -X POST -F name=john -F pass=doe http://localhost:3000\n\n// Query Parameters\ncurl -X POST \"http://localhost:3000/?name=john&pass=doe\"\n```\n\n### Binding into a Map\n\nFiber allows binding request data into a `map[string]string` or `map[string][]string`. Here's an example:\n\n```go\napp.Get(\"/\", func(c fiber.Ctx) error {\n    params := make(map[string][]string)\n\n    if err := c.Bind().Query(params); err != nil {\n        return err\n    }\n\n    log.Println(params[\"name\"])     // Output: [john]\n    log.Println(params[\"pass\"])     // Output: [doe]\n    log.Println(params[\"products\"]) // Output: [shoe hat]\n\n    // Additional logic...\n    return nil\n})\n\n// Run tests with the following curl command:\n\ncurl \"http://localhost:3000/?name=john&pass=doe&products=shoe&products=hat\"\n```\n\n### Automatic Error Handling with `WithAutoHandling`\n\nBy default, Fiber returns binder errors directly. To handle errors automatically and return a `400 Bad Request` status, use the `WithAutoHandling()` method.\n\n**Example:**\n\n```go\n// Field names must start with an uppercase letter\ntype Person struct {\n    Name string `json:\"name,required\"`\n    Pass string `json:\"pass\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().WithAutoHandling().JSON(p); err != nil {\n        return err \n        // Automatically returns status code 400\n        // Response: Bad request: name is empty\n    }\n\n    // Additional logic...\n    return nil\n})\n\n// Run tests with the following curl command:\n\ncurl -X GET -H \"Content-Type: application/json\" --data \"{\\\"pass\\\":\\\"doe\\\"}\" localhost:3000\n```\n\n### Defining a Custom Binder\n\nFiber maintains a minimal codebase by not including every possible binder. If you need to use a custom binder, you can easily register and utilize it. Here's an example of creating a `toml` binder.\n\n```go\ntype Person struct {\n    Name string `toml:\"name\"`\n    Pass string `toml:\"pass\"`\n}\n\ntype tomlBinding struct{}\n\nfunc (b *tomlBinding) Name() string {\n    return \"toml\"\n}\n\nfunc (b *tomlBinding) MIMETypes() []string {\n    return []string{\"application/toml\"}\n}\n\nfunc (b *tomlBinding) Parse(c fiber.Ctx, out any) error {\n    return toml.Unmarshal(c.Body(), out)\n}\n\nfunc main() {\n    app := fiber.New()\n    app.RegisterCustomBinder(&tomlBinding{})\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        out := new(Person)\n        if err := c.Bind().Body(out); err != nil {\n            return err\n        }\n\n        // Alternatively, specify the custom binder:\n        // if err := c.Bind().Custom(\"toml\", out); err != nil {\n        //     return err\n        // }\n\n        return c.SendString(out.Pass) // Output: test\n    })\n\n    app.Listen(\":3000\")\n}\n\n// Run tests with the following curl command:\n\ncurl -X GET -H \"Content-Type: application/toml\" --data \"name = 'bar'\npass = 'test'\" localhost:3000\n```\n\n### Defining a Custom Validator\n\nAll Fiber binders support struct validation if a validator is defined in the configuration. You can create your own validator or use existing ones like [go-playground/validator](https://github.com/go-playground/validator) or [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation). Here's an example of a simple custom validator:\n\n```go\ntype Query struct {\n    Name string `query:\"name\"`\n}\n\ntype structValidator struct{}\n\nfunc (v *structValidator) Engine() any {\n    return nil // Implement if using an external validation engine\n}\n\nfunc (v *structValidator) ValidateStruct(out any) error {\n    data := reflect.ValueOf(out).Elem().Interface()\n    query := data.(Query)\n\n    if query.Name != \"john\" {\n        return errors.New(\"you should have entered the correct name!\")\n    }\n\n    return nil\n}\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        StructValidator: &structValidator{},\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        out := new(Query)\n        if err := c.Bind().Query(out); err != nil {\n            return err // Returns: you should have entered the correct name!\n        }\n        return c.SendString(out.Name)\n    })\n\n    app.Listen(\":3000\")\n}\n\n// Run tests with the following curl command:\n\ncurl \"http://localhost:3000/?name=efe\"\n```\n"
  },
  {
    "path": "binder/binder.go",
    "content": "package binder\n\nimport (\n\t\"errors\"\n\t\"sync\"\n)\n\n// Binder errors\nvar (\n\tErrSuitableContentNotFound = errors.New(\"binder: suitable content not found to parse body\")\n\tErrMapNotConvertible       = errors.New(\"binder: map is not convertible to map[string]string or map[string][]string\")\n\tErrMapNilDestination       = errors.New(\"binder: map destination is nil and cannot be initialized\")\n\tErrInvalidDestinationValue = errors.New(\"binder: invalid destination value\")\n\tErrUnmatchedBrackets       = errors.New(\"unmatched brackets\")\n)\n\nvar errPoolTypeAssertion = errors.New(\"failed to type-assert to T\")\n\nvar HeaderBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &HeaderBinding{}\n\t},\n}\n\nvar RespHeaderBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &RespHeaderBinding{}\n\t},\n}\n\nvar CookieBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &CookieBinding{}\n\t},\n}\n\nvar QueryBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &QueryBinding{}\n\t},\n}\n\nvar FormBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &FormBinding{}\n\t},\n}\n\nvar URIBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &URIBinding{}\n\t},\n}\n\nvar XMLBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &XMLBinding{}\n\t},\n}\n\nvar JSONBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &JSONBinding{}\n\t},\n}\n\nvar CBORBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &CBORBinding{}\n\t},\n}\n\nvar MsgPackBinderPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &MsgPackBinding{}\n\t},\n}\n\n// GetFromThePool retrieves a binder from the provided sync.Pool and panics if\n// the stored value cannot be cast to the requested type.\nfunc GetFromThePool[T any](pool *sync.Pool) T {\n\tbinder, ok := pool.Get().(T)\n\tif !ok {\n\t\tpanic(errPoolTypeAssertion)\n\t}\n\n\treturn binder\n}\n\n// PutToThePool returns the binder to the provided sync.Pool.\nfunc PutToThePool[T any](pool *sync.Pool, binder T) {\n\tpool.Put(binder)\n}\n"
  },
  {
    "path": "binder/binder_test.go",
    "content": "package binder\n\nimport (\n\t\"mime/multipart\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_GetAndPutToThePool(t *testing.T) {\n\tt.Parallel()\n\n\t// Panics in case we get from another pool\n\trequire.Panics(t, func() {\n\t\t_ = GetFromThePool[*HeaderBinding](&CookieBinderPool)\n\t})\n\n\t// We get from the pool\n\tbinder := GetFromThePool[*HeaderBinding](&HeaderBinderPool)\n\tPutToThePool(&HeaderBinderPool, binder)\n\n\t_ = GetFromThePool[*RespHeaderBinding](&RespHeaderBinderPool)\n\t_ = GetFromThePool[*QueryBinding](&QueryBinderPool)\n\t_ = GetFromThePool[*FormBinding](&FormBinderPool)\n\t_ = GetFromThePool[*URIBinding](&URIBinderPool)\n\t_ = GetFromThePool[*XMLBinding](&XMLBinderPool)\n\t_ = GetFromThePool[*JSONBinding](&JSONBinderPool)\n\t_ = GetFromThePool[*CBORBinding](&CBORBinderPool)\n\t_ = GetFromThePool[*MsgPackBinding](&MsgPackBinderPool)\n}\n\nfunc Test_Binders_ErrorPaths(t *testing.T) {\n\tt.Run(\"query binder invalid key\", func(t *testing.T) {\n\t\tb := &QueryBinding{}\n\t\treq := fasthttp.AcquireRequest()\n\t\treq.URI().SetQueryString(\"invalid[%3Dval&name=john\")\n\t\tdefer fasthttp.ReleaseRequest(req)\n\t\terr := b.Bind(req, &struct{}{})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unmatched brackets\")\n\t})\n\n\tt.Run(\"form binder invalid key\", func(t *testing.T) {\n\t\tb := &FormBinding{}\n\t\treq := fasthttp.AcquireRequest()\n\t\treq.SetBodyString(\"invalid[=val\")\n\t\treq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\t\tdefer fasthttp.ReleaseRequest(req)\n\t\terr := b.Bind(req, &struct{}{})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"unmatched brackets\")\n\t})\n\n\tt.Run(\"form binder bad multipart\", func(t *testing.T) {\n\t\tb := &FormBinding{}\n\t\treq := fasthttp.AcquireRequest()\n\t\treq.Header.SetContentType(MIMEMultipartForm)\n\t\tdefer fasthttp.ReleaseRequest(req)\n\t\terr := b.Bind(req, &struct{}{})\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc Test_GetFieldCache_Panic(t *testing.T) {\n\tt.Parallel()\n\trequire.Panics(t, func() { getFieldCache(\"unknown\") })\n}\n\nfunc Test_parseToMap_defaultCase(t *testing.T) {\n\tt.Parallel()\n\tm := map[string]int{}\n\terr := parseToMap(reflect.ValueOf(m), map[string][]string{\"a\": {\"1\"}})\n\trequire.NoError(t, err)\n\trequire.Empty(t, m)\n\n\tm2 := map[string]string{}\n\terr = parseToMap(reflect.ValueOf(m2), map[string][]string{\"empty\": {}})\n\trequire.NoError(t, err)\n\trequire.Empty(t, m2[\"empty\"])\n\n\tvar zeroStringMap map[string]string\n\terr = parseToMap(reflect.ValueOf(&zeroStringMap).Elem(), map[string][]string{\"name\": {\"john\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", zeroStringMap[\"name\"])\n}\n\nfunc Test_parse_function_maps(t *testing.T) {\n\tt.Parallel()\n\n\tm := map[string][]string{}\n\terr := parse(\"query\", &m, map[string][]string{\"a\": {\"b\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"b\"}, m[\"a\"])\n\n\tm2 := map[string]string{}\n\terr = parse(\"query\", &m2, map[string][]string{\"a\": {\"b\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"b\", m2[\"a\"])\n\n\tvar zeroStringMap map[string]string\n\terr = parse(\"query\", &zeroStringMap, map[string][]string{\"foo\": {\"bar\", \"baz\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"baz\", zeroStringMap[\"foo\"])\n\n\tvar zeroSliceMap map[string][]string\n\terr = parse(\"query\", &zeroSliceMap, map[string][]string{\"foo\": {\"bar\", \"baz\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"bar\", \"baz\"}, zeroSliceMap[\"foo\"])\n}\n\nfunc Test_SetParserDecoder_UnknownKeys(t *testing.T) {\n\tSetParserDecoder(ParserConfig{IgnoreUnknownKeys: false})\n\tdefer SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})\n\ttype user struct {\n\t\tName string `query:\"name\"`\n\t}\n\tdata := map[string][]string{\"name\": {\"john\"}, \"foo\": {\"bar\"}}\n\terr := parseToStruct(\"query\", &user{}, data)\n\trequire.Error(t, err)\n\tSetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})\n}\n\nfunc Test_SetParserDecoder_CustomConverter(t *testing.T) {\n\ttype myInt int\n\tconv := func(s string) reflect.Value {\n\t\tv, _ := strconv.Atoi(s) //nolint:errcheck // not needed\n\t\tmi := myInt(v)\n\t\treturn reflect.ValueOf(mi)\n\t}\n\n\tSetParserDecoder(ParserConfig{ParserType: []ParserType{{CustomType: myInt(0), Converter: conv}}})\n\tdefer SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})\n\n\ttype data struct {\n\t\tV myInt `query:\"v\"`\n\t}\n\td := new(data)\n\terr := parse(\"query\", d, map[string][]string{\"v\": {\"5\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, myInt(5), d.V)\n}\n\nfunc Test_formatBindData_typeMismatch(t *testing.T) {\n\tt.Parallel()\n\tout := struct{}{}\n\tfiles := map[string][]*multipart.FileHeader{}\n\terr := formatBindData(\"query\", out, files, \"file\", 123, false, false)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"unsupported value type: int\", err.Error())\n}\n"
  },
  {
    "path": "binder/cbor.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// CBORBinding is the CBOR binder for CBOR request body.\ntype CBORBinding struct {\n\tCBORDecoder utils.CBORUnmarshal\n}\n\n// Name returns the binding name.\nfunc (*CBORBinding) Name() string {\n\treturn \"cbor\"\n}\n\n// Bind parses the request body as CBOR and returns the result.\nfunc (b *CBORBinding) Bind(body []byte, out any) error {\n\treturn b.CBORDecoder(body, out)\n}\n\n// Reset resets the CBORBinding binder.\nfunc (b *CBORBinding) Reset() {\n\tb.CBORDecoder = nil\n}\n\n// UnimplementedCborMarshal panics to signal that a CBOR marshaler must be\n// configured before CBOR support can be used.\nfunc UnimplementedCborMarshal(_ any) ([]byte, error) {\n\tpanic(\"Must explicitly setup CBOR, please check docs: https://docs.gofiber.io/next/guide/advance-format#cbor\")\n}\n\n// UnimplementedCborUnmarshal panics to signal that a CBOR unmarshaler must be\n// configured before CBOR support can be used.\nfunc UnimplementedCborUnmarshal(_ []byte, _ any) error {\n\tpanic(\"Must explicitly setup CBOR, please check docs: https://docs.gofiber.io/next/guide/advance-format#cbor\")\n}\n"
  },
  {
    "path": "binder/cbor_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_CBORBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &CBORBinding{\n\t\tCBORDecoder: cbor.Unmarshal,\n\t}\n\trequire.Equal(t, \"cbor\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `cbor:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string   `cbor:\"name\"`\n\t\tPosts []Post   `cbor:\"posts\"`\n\t\tNames []string `cbor:\"names\"`\n\t\tAge   int      `cbor:\"age\"`\n\t}\n\tvar user User\n\n\twantedUser := User{\n\t\tName: \"john\",\n\t\tNames: []string{\n\t\t\t\"john\",\n\t\t\t\"doe\",\n\t\t},\n\t\tAge: 42,\n\t\tPosts: []Post{\n\t\t\t{Title: \"post1\"},\n\t\t\t{Title: \"post2\"},\n\t\t\t{Title: \"post3\"},\n\t\t},\n\t}\n\n\tbody, err := cbor.Marshal(wantedUser)\n\trequire.NoError(t, err)\n\n\terr = b.Bind(body, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\n\tb.Reset()\n\trequire.Nil(t, b.CBORDecoder)\n}\n\nfunc Benchmark_CBORBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &CBORBinding{\n\t\tCBORDecoder: cbor.Unmarshal,\n\t}\n\n\ttype User struct {\n\t\tName string `cbor:\"name\"`\n\t\tAge  int    `cbor:\"age\"`\n\t}\n\n\tvar user User\n\twantedUser := User{\n\t\tName: \"john\",\n\t\tAge:  42,\n\t}\n\n\tbody, err := cbor.Marshal(wantedUser)\n\trequire.NoError(b, err)\n\n\tfor b.Loop() {\n\t\terr = binder.Bind(body, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n}\n\nfunc Test_UnimplementedCborMarshal_Panics(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Panics(t, func() {\n\t\t_, _ = UnimplementedCborMarshal(struct{ Name string }{Name: \"test\"}) //nolint:errcheck // this is just a test to trigger the panic\n\t})\n}\n\nfunc Test_UnimplementedCborUnmarshal_Panics(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Panics(t, func() {\n\t\tvar out any\n\t\t_ = UnimplementedCborUnmarshal([]byte{0xa0}, &out) //nolint:errcheck // this is just a test to trigger the panic\n\t})\n}\n\nfunc Test_UnimplementedCborMarshal_PanicMessage(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trequire.Contains(t, r, \"Must explicitly setup CBOR\")\n\t\t}\n\t}()\n\t_, _ = UnimplementedCborMarshal(struct{ Name string }{Name: \"test\"}) //nolint:errcheck // this is just a test to trigger the panic\n}\n\nfunc Test_UnimplementedCborUnmarshal_PanicMessage(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trequire.Contains(t, r, \"Must explicitly setup CBOR\")\n\t\t}\n\t}()\n\tvar out any\n\t_ = UnimplementedCborUnmarshal([]byte{0xa0}, &out) //nolint:errcheck // this is just a test to trigger the panic\n}\n"
  },
  {
    "path": "binder/cookie.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// CookieBinding is the cookie binder for cookie request body.\ntype CookieBinding struct {\n\tEnableSplitting bool\n}\n\n// Name returns the binding name.\nfunc (*CookieBinding) Name() string {\n\treturn \"cookie\"\n}\n\n// Bind parses the request cookie and returns the result.\nfunc (b *CookieBinding) Bind(req *fasthttp.Request, out any) error {\n\tdata := make(map[string][]string)\n\n\tfor key, val := range req.Header.Cookies() {\n\t\tk := utils.UnsafeString(key)\n\t\tv := utils.UnsafeString(val)\n\t\tif err := formatBindData(b.Name(), out, data, k, v, b.EnableSplitting, false); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// Reset resets the CookieBinding binder.\nfunc (b *CookieBinding) Reset() {\n\tb.EnableSplitting = false\n}\n"
  },
  {
    "path": "binder/cookie_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_CookieBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &CookieBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"cookie\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `form:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string   `form:\"name\"`\n\t\tNames []string `form:\"names\"`\n\t\tPosts []Post   `form:\"posts\"`\n\t\tAge   int      `form:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\n\treq.Header.SetCookie(\"name\", \"john\")\n\treq.Header.SetCookie(\"names\", \"john,doe\")\n\treq.Header.SetCookie(\"age\", \"42\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr := b.Bind(req, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\n\tb.Reset()\n\trequire.False(t, b.EnableSplitting)\n}\n\nfunc Benchmark_CookieBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &CookieBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `query:\"name\"`\n\t\tPosts []string `query:\"posts\"`\n\t\tAge   int      `query:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\tb.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\treq.Header.SetCookie(\"name\", \"john\")\n\treq.Header.SetCookie(\"age\", \"42\")\n\treq.Header.SetCookie(\"posts\", \"post1,post2,post3\")\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(req, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n\trequire.Contains(b, user.Posts, \"post1\")\n\trequire.Contains(b, user.Posts, \"post2\")\n\trequire.Contains(b, user.Posts, \"post3\")\n}\n\nfunc Test_CookieBinder_Bind_ParseError(t *testing.T) {\n\tb := &CookieBinding{}\n\ttype User struct {\n\t\tAge int `cookie:\"age\"`\n\t}\n\tvar user User\n\treq := fasthttp.AcquireRequest()\n\treq.Header.SetCookie(\"age\", \"invalid\")\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(req) })\n\terr := b.Bind(req, &user)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "binder/form.go",
    "content": "package binder\n\nimport (\n\t\"mime/multipart\"\n\t\"sync\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nconst MIMEMultipartForm string = \"multipart/form-data\"\n\nvar (\n\tformMapPool = sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn make(map[string][]string)\n\t\t},\n\t}\n\tformFileMapPool = sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn make(map[string][]*multipart.FileHeader)\n\t\t},\n\t}\n)\n\n// FormBinding is the form binder for form request body.\ntype FormBinding struct {\n\tEnableSplitting bool\n}\n\n// Name returns the binding name.\nfunc (*FormBinding) Name() string {\n\treturn \"form\"\n}\n\n// Bind parses the request body and returns the result.\nfunc (b *FormBinding) Bind(req *fasthttp.Request, out any) error {\n\t// Handle multipart form\n\tif FilterFlags(utils.UnsafeString(req.Header.ContentType())) == MIMEMultipartForm {\n\t\treturn b.bindMultipart(req, out)\n\t}\n\n\tdata := acquireFormMap()\n\tdefer releaseFormMap(data)\n\n\tfor key, val := range req.PostArgs().All() {\n\t\tk := utils.UnsafeString(key)\n\t\tv := utils.UnsafeString(val)\n\t\tif err := formatBindData(b.Name(), out, data, k, v, b.EnableSplitting, true); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// bindMultipart parses the request body and returns the result.\nfunc (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {\n\tmultipartForm, err := req.MultipartForm()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata := acquireFormMap()\n\tdefer releaseFormMap(data)\n\n\tfor key, values := range multipartForm.Value {\n\t\terr = formatBindData(b.Name(), out, data, key, values, b.EnableSplitting, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfiles := acquireFileHeaderMap()\n\tdefer releaseFileHeaderMap(files)\n\n\tfor key, values := range multipartForm.File {\n\t\terr = formatBindData(b.Name(), out, files, key, values, b.EnableSplitting, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data, files)\n}\n\n// Reset resets the FormBinding binder.\nfunc (b *FormBinding) Reset() {\n\tb.EnableSplitting = false\n}\n\nfunc acquireFormMap() map[string][]string {\n\tm, ok := formMapPool.Get().(map[string][]string)\n\tif !ok {\n\t\tm = make(map[string][]string)\n\t}\n\treturn m\n}\n\nfunc releaseFormMap(m map[string][]string) {\n\tclearFormMap(m)\n\tformMapPool.Put(m)\n}\n\nfunc acquireFileHeaderMap() map[string][]*multipart.FileHeader {\n\tm, ok := formFileMapPool.Get().(map[string][]*multipart.FileHeader)\n\tif !ok {\n\t\tm = make(map[string][]*multipart.FileHeader)\n\t}\n\treturn m\n}\n\nfunc releaseFileHeaderMap(m map[string][]*multipart.FileHeader) {\n\tclearFileHeaderMap(m)\n\tformFileMapPool.Put(m)\n}\n\nfunc clearFormMap(m map[string][]string) {\n\tclear(m)\n}\n\nfunc clearFileHeaderMap(m map[string][]*multipart.FileHeader) {\n\tclear(m)\n}\n"
  },
  {
    "path": "binder/form_test.go",
    "content": "package binder\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_FormBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &FormBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"form\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `form:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string   `form:\"name\"`\n\t\tNames []string `form:\"names\"`\n\t\tPosts []Post   `form:\"posts\"`\n\t\tAge   int      `form:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\treq.SetBodyString(\"name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3\")\n\treq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr := b.Bind(req, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\n\tb.Reset()\n\trequire.False(t, b.EnableSplitting)\n}\n\nfunc Test_FormBinder_Bind_ParseError(t *testing.T) {\n\tb := &FormBinding{}\n\ttype User struct {\n\t\tAge int `form:\"age\"`\n\t}\n\tvar user User\n\treq := fasthttp.AcquireRequest()\n\treq.SetBodyString(\"age=invalid\")\n\treq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(req) })\n\terr := b.Bind(req, &user)\n\trequire.Error(t, err)\n}\n\nfunc Benchmark_FormBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &FormBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `form:\"name\"`\n\t\tPosts []string `form:\"posts\"`\n\t\tAge   int      `form:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\treq.SetBodyString(\"name=john&age=42&posts=post1,post2,post3\")\n\treq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(req, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n}\n\nfunc Test_FormBinder_BindMultipart(t *testing.T) {\n\tt.Parallel()\n\n\tb := &FormBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"form\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `form:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tAvatar  *multipart.FileHeader   `form:\"avatar\"`\n\t\tName    string                  `form:\"name\"`\n\t\tNames   []string                `form:\"names\"`\n\t\tPosts   []Post                  `form:\"posts\"`\n\t\tAvatars []*multipart.FileHeader `form:\"avatars\"`\n\t\tAge     int                     `form:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\n\tbuf := &bytes.Buffer{}\n\tmw := multipart.NewWriter(buf)\n\n\trequire.NoError(t, mw.WriteField(\"name\", \"john\"))\n\trequire.NoError(t, mw.WriteField(\"names\", \"john,eric\"))\n\trequire.NoError(t, mw.WriteField(\"names\", \"doe\"))\n\trequire.NoError(t, mw.WriteField(\"age\", \"42\"))\n\trequire.NoError(t, mw.WriteField(\"posts[0][title]\", \"post1\"))\n\trequire.NoError(t, mw.WriteField(\"posts[1][title]\", \"post2\"))\n\trequire.NoError(t, mw.WriteField(\"posts[2][title]\", \"post3\"))\n\n\twriter, err := mw.CreateFormFile(\"avatar\", \"avatar.txt\")\n\trequire.NoError(t, err)\n\n\t_, err = writer.Write([]byte(\"avatar\"))\n\trequire.NoError(t, err)\n\n\twriter, err = mw.CreateFormFile(\"avatars\", \"avatar1.txt\")\n\trequire.NoError(t, err)\n\n\t_, err = writer.Write([]byte(\"avatar1\"))\n\trequire.NoError(t, err)\n\n\twriter, err = mw.CreateFormFile(\"avatars\", \"avatar2.txt\")\n\trequire.NoError(t, err)\n\n\t_, err = writer.Write([]byte(\"avatar2\"))\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, mw.Close())\n\n\treq.Header.SetContentType(mw.FormDataContentType())\n\treq.SetBody(buf.Bytes())\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr = b.Bind(req, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\trequire.Contains(t, user.Names, \"eric\")\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\n\trequire.NotNil(t, user.Avatar)\n\trequire.Equal(t, \"avatar.txt\", user.Avatar.Filename)\n\trequire.Equal(t, \"application/octet-stream\", user.Avatar.Header.Get(\"Content-Type\"))\n\n\tfile, err := user.Avatar.Open()\n\trequire.NoError(t, err)\n\n\tcontent, err := io.ReadAll(file)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"avatar\", string(content))\n\n\trequire.Len(t, user.Avatars, 2)\n\trequire.Equal(t, \"avatar1.txt\", user.Avatars[0].Filename)\n\trequire.Equal(t, \"application/octet-stream\", user.Avatars[0].Header.Get(\"Content-Type\"))\n\n\tfile, err = user.Avatars[0].Open()\n\trequire.NoError(t, err)\n\n\tcontent, err = io.ReadAll(file)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"avatar1\", string(content))\n\n\trequire.Equal(t, \"avatar2.txt\", user.Avatars[1].Filename)\n\trequire.Equal(t, \"application/octet-stream\", user.Avatars[1].Header.Get(\"Content-Type\"))\n\n\tfile, err = user.Avatars[1].Open()\n\trequire.NoError(t, err)\n\n\tcontent, err = io.ReadAll(file)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"avatar2\", string(content))\n}\n\nfunc Test_FormBinder_BindMultipart_ValueError(t *testing.T) {\n\tb := &FormBinding{}\n\treq := fasthttp.AcquireRequest()\n\tbuf := &bytes.Buffer{}\n\tmw := multipart.NewWriter(buf)\n\trequire.NoError(t, mw.WriteField(\"invalid[\", \"val\"))\n\trequire.NoError(t, mw.Close())\n\treq.Header.SetContentType(mw.FormDataContentType())\n\treq.SetBody(buf.Bytes())\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(req) })\n\terr := b.Bind(req, &struct{}{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"unmatched brackets\")\n}\n\nfunc Test_FormBinder_BindMultipart_FileError(t *testing.T) {\n\tb := &FormBinding{}\n\treq := fasthttp.AcquireRequest()\n\tbuf := &bytes.Buffer{}\n\tmw := multipart.NewWriter(buf)\n\twriter, err := mw.CreateFormFile(\"invalid[\", \"file.txt\")\n\trequire.NoError(t, err)\n\t_, err = writer.Write([]byte(\"content\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, mw.Close())\n\treq.Header.SetContentType(mw.FormDataContentType())\n\treq.SetBody(buf.Bytes())\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(req) })\n\terr = b.Bind(req, &struct{}{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"unmatched brackets\")\n}\n\nfunc Test_FormBinder_Bind_MapClearedBetweenRequests(t *testing.T) {\n\tt.Parallel()\n\n\tb := &FormBinding{}\n\n\ttype payload struct {\n\t\tName string `form:\"name\"`\n\t\tAge  int    `form:\"age\"`\n\t}\n\n\tfirstReq := fasthttp.AcquireRequest()\n\tfirstReq.SetBodyString(\"name=john&age=21\")\n\tfirstReq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(firstReq) })\n\n\tvar first payload\n\trequire.NoError(t, b.Bind(firstReq, &first))\n\trequire.Equal(t, \"john\", first.Name)\n\trequire.Equal(t, 21, first.Age)\n\n\tsecondReq := fasthttp.AcquireRequest()\n\tsecondReq.SetBodyString(\"age=42\")\n\tsecondReq.Header.SetContentType(\"application/x-www-form-urlencoded\")\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(secondReq) })\n\n\tvar second payload\n\trequire.NoError(t, b.Bind(secondReq, &second))\n\trequire.Empty(t, second.Name)\n\trequire.Equal(t, 42, second.Age)\n}\n\nfunc Test_FormBinder_BindMultipart_MapsClearedBetweenRequests(t *testing.T) {\n\tt.Parallel()\n\n\tb := &FormBinding{}\n\n\ttype payload struct { // betteralign:ignore - test payload prioritizes readability over alignment\n\t\tAvatar *multipart.FileHeader `form:\"avatar\"`\n\t\tName   string                `form:\"name\"`\n\t\tAge    int                   `form:\"age\"`\n\t}\n\n\tfirstReq := fasthttp.AcquireRequest()\n\tfirstBuffer := &bytes.Buffer{}\n\tfirstWriter := multipart.NewWriter(firstBuffer)\n\n\trequire.NoError(t, firstWriter.WriteField(\"name\", \"john\"))\n\trequire.NoError(t, firstWriter.WriteField(\"age\", \"21\"))\n\n\tfirstFile, err := firstWriter.CreateFormFile(\"avatar\", \"avatar.txt\")\n\trequire.NoError(t, err)\n\t_, err = firstFile.Write([]byte(\"avatar-content\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, firstWriter.Close())\n\n\tfirstReq.Header.SetContentType(firstWriter.FormDataContentType())\n\tfirstReq.SetBody(firstBuffer.Bytes())\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(firstReq) })\n\n\tvar first payload\n\trequire.NoError(t, b.Bind(firstReq, &first))\n\trequire.Equal(t, \"john\", first.Name)\n\trequire.Equal(t, 21, first.Age)\n\trequire.NotNil(t, first.Avatar)\n\trequire.Equal(t, \"avatar.txt\", first.Avatar.Filename)\n\n\tsecondReq := fasthttp.AcquireRequest()\n\tsecondBuffer := &bytes.Buffer{}\n\tsecondWriter := multipart.NewWriter(secondBuffer)\n\trequire.NoError(t, secondWriter.WriteField(\"age\", \"42\"))\n\trequire.NoError(t, secondWriter.Close())\n\n\tsecondReq.Header.SetContentType(secondWriter.FormDataContentType())\n\tsecondReq.SetBody(secondBuffer.Bytes())\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(secondReq) })\n\n\tvar second payload\n\trequire.NoError(t, b.Bind(secondReq, &second))\n\trequire.Empty(t, second.Name)\n\trequire.Equal(t, 42, second.Age)\n\trequire.Nil(t, second.Avatar)\n}\n\nfunc Benchmark_FormBinder_BindMultipart(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &FormBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `form:\"name\"`\n\t\tPosts []string `form:\"posts\"`\n\t\tAge   int      `form:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\tb.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\tbuf := &bytes.Buffer{}\n\tmw := multipart.NewWriter(buf)\n\n\trequire.NoError(b, mw.WriteField(\"name\", \"john\"))\n\trequire.NoError(b, mw.WriteField(\"age\", \"42\"))\n\trequire.NoError(b, mw.WriteField(\"posts\", \"post1\"))\n\trequire.NoError(b, mw.WriteField(\"posts\", \"post2\"))\n\trequire.NoError(b, mw.WriteField(\"posts\", \"post3\"))\n\trequire.NoError(b, mw.Close())\n\n\treq.Header.SetContentType(mw.FormDataContentType())\n\treq.SetBody(buf.Bytes())\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(req, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n}\n"
  },
  {
    "path": "binder/header.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// HeaderBinding is the binder implementation used to populate values from HTTP headers.\ntype HeaderBinding struct {\n\tEnableSplitting bool\n}\n\n// Name returns the binding name.\nfunc (*HeaderBinding) Name() string {\n\treturn \"header\"\n}\n\n// Bind parses the request header and returns the result.\nfunc (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error {\n\tdata := make(map[string][]string)\n\tfor key, val := range req.Header.All() {\n\t\tk := utils.UnsafeString(key)\n\t\tv := utils.UnsafeString(val)\n\t\tif err := formatBindData(b.Name(), out, data, k, v, b.EnableSplitting, false); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// Reset resets the HeaderBinding binder.\nfunc (b *HeaderBinding) Reset() {\n\tb.EnableSplitting = false\n}\n"
  },
  {
    "path": "binder/header_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_HeaderBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &HeaderBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"header\", b.Name())\n\n\ttype User struct {\n\t\tName  string   `header:\"Name\"`\n\t\tNames []string `header:\"Names\"`\n\t\tPosts []string `header:\"Posts\"`\n\t\tAge   int      `header:\"Age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\treq.Header.Set(\"name\", \"john\")\n\treq.Header.Set(\"names\", \"john,doe\")\n\treq.Header.Set(\"age\", \"42\")\n\treq.Header.Set(\"posts\", \"post1,post2,post3\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr := b.Bind(req, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0])\n\trequire.Equal(t, \"post2\", user.Posts[1])\n\trequire.Equal(t, \"post3\", user.Posts[2])\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\n\tb.Reset()\n\trequire.False(t, b.EnableSplitting)\n}\n\nfunc Benchmark_HeaderBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &HeaderBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `header:\"Name\"`\n\t\tPosts []string `header:\"Posts\"`\n\t\tAge   int      `header:\"Age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\tb.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\treq.Header.Set(\"name\", \"john\")\n\treq.Header.Set(\"age\", \"42\")\n\treq.Header.Set(\"posts\", \"post1,post2,post3\")\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(req, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n\trequire.Contains(b, user.Posts, \"post1\")\n\trequire.Contains(b, user.Posts, \"post2\")\n\trequire.Contains(b, user.Posts, \"post3\")\n}\n\nfunc Test_HeaderBinder_Bind_ParseError(t *testing.T) {\n\tb := &HeaderBinding{}\n\ttype User struct {\n\t\tAge int `header:\"Age\"`\n\t}\n\tvar user User\n\treq := fasthttp.AcquireRequest()\n\treq.Header.Set(\"age\", \"invalid\")\n\tt.Cleanup(func() { fasthttp.ReleaseRequest(req) })\n\terr := b.Bind(req, &user)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "binder/json.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// JSONBinding is the JSON binder for JSON request body.\ntype JSONBinding struct {\n\tJSONDecoder utils.JSONUnmarshal\n}\n\n// Name returns the binding name.\nfunc (*JSONBinding) Name() string {\n\treturn \"json\"\n}\n\n// Bind parses the request body as JSON and returns the result.\nfunc (b *JSONBinding) Bind(body []byte, out any) error {\n\treturn b.JSONDecoder(body, out)\n}\n\n// Reset resets the JSONBinding binder.\nfunc (b *JSONBinding) Reset() {\n\tb.JSONDecoder = nil\n}\n"
  },
  {
    "path": "binder/json_test.go",
    "content": "package binder\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_JSON_Binding_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &JSONBinding{\n\t\tJSONDecoder: json.Unmarshal,\n\t}\n\trequire.Equal(t, \"json\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `json:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string `json:\"name\"`\n\t\tPosts []Post `json:\"posts\"`\n\t\tAge   int    `json:\"age\"`\n\t}\n\tvar user User\n\n\terr := b.Bind([]byte(`{\"name\":\"john\",\"age\":42,\"posts\":[{\"title\":\"post1\"},{\"title\":\"post2\"},{\"title\":\"post3\"}]}`), &user)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\n\tb.Reset()\n\trequire.Nil(t, b.JSONDecoder)\n}\n\nfunc Benchmark_JSON_Binding_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &JSONBinding{\n\t\tJSONDecoder: json.Unmarshal,\n\t}\n\n\ttype User struct {\n\t\tName  string   `json:\"name\"`\n\t\tPosts []string `json:\"posts\"`\n\t\tAge   int      `json:\"age\"`\n\t}\n\n\tvar user User\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind([]byte(`{\"name\":\"john\",\"age\":42,\"posts\":[\"post1\",\"post2\",\"post3\"]}`), &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n\trequire.Equal(b, \"post1\", user.Posts[0])\n\trequire.Equal(b, \"post2\", user.Posts[1])\n\trequire.Equal(b, \"post3\", user.Posts[2])\n}\n"
  },
  {
    "path": "binder/mapping.go",
    "content": "package binder\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"mime/multipart\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/valyala/bytebufferpool\"\n\n\t\"github.com/gofiber/schema\"\n)\n\n// ParserConfig form decoder config for SetParserDecoder\ntype ParserConfig struct {\n\tSetAliasTag       string\n\tParserType        []ParserType\n\tIgnoreUnknownKeys bool\n\tZeroEmpty         bool\n}\n\n// ParserType require two element, type and converter for register.\n// Use ParserType with BodyParser for parsing custom type in form data.\ntype ParserType struct {\n\tCustomType any\n\tConverter  func(string) reflect.Value\n}\n\nvar (\n\tdecoderPoolMu sync.RWMutex\n\t// decoderPoolMap helps to improve binders\n\tdecoderPoolMap = map[string]*sync.Pool{}\n\t// tags is used to classify parser's pool\n\ttags = []string{\"header\", \"respHeader\", \"cookie\", \"query\", \"form\", \"uri\"}\n)\n\nfunc getDecoderPool(tag string) *sync.Pool {\n\tdecoderPoolMu.RLock()\n\tpool := decoderPoolMap[tag]\n\tif pool == nil {\n\t\tdecoderPoolMu.RUnlock()\n\t\tpanic(fmt.Sprintf(\"decoder pool not initialized for tag %q\", tag))\n\t}\n\tdecoderPoolMu.RUnlock()\n\n\treturn pool\n}\n\n// SetParserDecoder allow globally change the option of form decoder, update decoderPool\nfunc SetParserDecoder(parserConfig ParserConfig) {\n\tdecoderPoolMu.Lock()\n\tdefer decoderPoolMu.Unlock()\n\n\tfor _, tag := range tags {\n\t\tdecoderPoolMap[tag] = &sync.Pool{New: func() any {\n\t\t\treturn decoderBuilder(parserConfig)\n\t\t}}\n\t}\n}\n\nfunc decoderBuilder(parserConfig ParserConfig) any {\n\tdecoder := schema.NewDecoder()\n\tdecoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys)\n\tif parserConfig.SetAliasTag != \"\" {\n\t\tdecoder.SetAliasTag(parserConfig.SetAliasTag)\n\t}\n\tfor _, v := range parserConfig.ParserType {\n\t\tdecoder.RegisterConverter(reflect.ValueOf(v.CustomType).Interface(), v.Converter)\n\t}\n\tdecoder.ZeroEmpty(parserConfig.ZeroEmpty)\n\treturn decoder\n}\n\nfunc init() {\n\tdecoderPoolMu.Lock()\n\tdefer decoderPoolMu.Unlock()\n\n\tfor _, tag := range tags {\n\t\tdecoderPoolMap[tag] = &sync.Pool{New: func() any {\n\t\t\treturn decoderBuilder(ParserConfig{\n\t\t\t\tIgnoreUnknownKeys: true,\n\t\t\t\tZeroEmpty:         true,\n\t\t\t})\n\t\t}}\n\t}\n}\n\n// parse data into the map or struct\nfunc parse(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error {\n\tptrVal := reflect.ValueOf(out)\n\n\t// Get pointer value\n\tif ptrVal.Kind() == reflect.Ptr {\n\t\tptrVal = ptrVal.Elem()\n\t}\n\n\t// Parse into the map\n\tif ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String {\n\t\treturn parseToMap(ptrVal, data)\n\t}\n\n\t// Parse into the struct\n\treturn parseToStruct(aliasTag, out, data, files...)\n}\n\n// Parse data into the struct with gofiber/schema\nfunc parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error {\n\t// Get decoder from pool\n\tpool := getDecoderPool(aliasTag)\n\tschemaDecoder := pool.Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed\n\tdefer pool.Put(schemaDecoder)\n\n\t// Set alias tag\n\tschemaDecoder.SetAliasTag(aliasTag)\n\n\tif err := schemaDecoder.Decode(out, data, files...); err != nil {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\treturn nil\n}\n\n// Parse data into the map\n// thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go\nfunc parseToMap(target reflect.Value, data map[string][]string) error {\n\tif !target.IsValid() {\n\t\treturn ErrInvalidDestinationValue\n\t}\n\n\tif target.Kind() == reflect.Interface && !target.IsNil() {\n\t\ttarget = target.Elem()\n\t}\n\n\tif target.Kind() != reflect.Map || target.Type().Key().Kind() != reflect.String {\n\t\treturn nil // nothing to do for non-map destinations\n\t}\n\n\tif target.IsNil() {\n\t\tif !target.CanSet() {\n\t\t\treturn ErrMapNilDestination\n\t\t}\n\t\ttarget.Set(reflect.MakeMap(target.Type()))\n\t}\n\n\tswitch target.Type().Elem().Kind() {\n\tcase reflect.Slice:\n\t\tnewMap, ok := target.Interface().(map[string][]string)\n\t\tif !ok {\n\t\t\treturn ErrMapNotConvertible\n\t\t}\n\n\t\tmaps.Copy(newMap, data)\n\tcase reflect.String:\n\t\tnewMap, ok := target.Interface().(map[string]string)\n\t\tif !ok {\n\t\t\treturn ErrMapNotConvertible\n\t\t}\n\n\t\tfor k, v := range data {\n\t\t\tif len(v) == 0 {\n\t\t\t\tnewMap[k] = \"\"\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewMap[k] = v[len(v)-1]\n\t\t}\n\tdefault:\n\t\t// Interface element maps (e.g. map[string]any) are left untouched because\n\t\t// the binder cannot safely infer element conversions without mutating\n\t\t// caller-provided values. These destinations therefore see a successful\n\t\t// no-op parse.\n\t\treturn nil // it's not necessary to check all types\n\t}\n\n\treturn nil\n}\n\nfunc parseParamSquareBrackets(k string) (string, error) {\n\tbb := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(bb)\n\n\tkbytes := []byte(k)\n\topenBracketsCount := 0\n\n\tfor i, b := range kbytes {\n\t\tif b == '[' {\n\t\t\topenBracketsCount++\n\t\t\tif i+1 < len(kbytes) && kbytes[i+1] != ']' {\n\t\t\t\tif err := bb.WriteByte('.'); err != nil {\n\t\t\t\t\treturn \"\", err //nolint:wrapcheck // unnecessary to wrap it\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif b == ']' {\n\t\t\topenBracketsCount--\n\t\t\tif openBracketsCount < 0 {\n\t\t\t\treturn \"\", ErrUnmatchedBrackets\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := bb.WriteByte(b); err != nil {\n\t\t\treturn \"\", err //nolint:wrapcheck // unnecessary to wrap it\n\t\t}\n\t}\n\n\tif openBracketsCount > 0 {\n\t\treturn \"\", ErrUnmatchedBrackets\n\t}\n\n\treturn bb.String(), nil\n}\n\nfunc isStringKeyMap(t reflect.Type) bool {\n\treturn t.Kind() == reflect.Map && t.Key().Kind() == reflect.String\n}\n\nfunc isExported(f *reflect.StructField) bool {\n\tif f == nil {\n\t\treturn false\n\t}\n\treturn f.PkgPath == \"\"\n}\n\nfunc fieldName(f *reflect.StructField, aliasTag string) string {\n\tif f == nil {\n\t\treturn \"\"\n\t}\n\n\tname := f.Tag.Get(aliasTag)\n\tif name == \"\" {\n\t\tname = f.Name\n\t} else if first, _, found := strings.Cut(name, \",\"); found {\n\t\tname = first\n\t}\n\n\treturn utilsstrings.ToLower(name)\n}\n\ntype fieldInfo struct {\n\tnames       map[string]reflect.Kind\n\tnestedKinds map[reflect.Kind]struct{}\n}\n\nfunc unwrapType(t reflect.Type) reflect.Type {\n\tfor t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\n\treturn t\n}\n\nvar (\n\theaderFieldCache     sync.Map\n\trespHeaderFieldCache sync.Map\n\tcookieFieldCache     sync.Map\n\tqueryFieldCache      sync.Map\n\tformFieldCache       sync.Map\n\turiFieldCache        sync.Map\n)\n\nfunc getFieldCache(aliasTag string) *sync.Map {\n\tswitch aliasTag {\n\tcase \"header\":\n\t\treturn &headerFieldCache\n\tcase \"respHeader\":\n\t\treturn &respHeaderFieldCache\n\tcase \"cookie\":\n\t\treturn &cookieFieldCache\n\tcase \"form\":\n\t\treturn &formFieldCache\n\tcase \"uri\":\n\t\treturn &uriFieldCache\n\tcase \"query\":\n\t\treturn &queryFieldCache\n\t}\n\n\tpanic(\"unknown alias tag: \" + aliasTag)\n}\n\nfunc buildFieldInfo(t reflect.Type, aliasTag string) fieldInfo {\n\tinfo := fieldInfo{\n\t\tnames:       make(map[string]reflect.Kind),\n\t\tnestedKinds: make(map[reflect.Kind]struct{}),\n\t}\n\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tf := t.Field(i)\n\t\tif !isExported(&f) {\n\t\t\tcontinue\n\t\t}\n\t\tfieldType := unwrapType(f.Type)\n\t\tinfo.names[fieldName(&f, aliasTag)] = fieldType.Kind()\n\n\t\tif fieldType.Kind() == reflect.Struct {\n\t\t\tfor j := 0; j < fieldType.NumField(); j++ {\n\t\t\t\tsf := fieldType.Field(j)\n\t\t\t\tif !isExported(&sf) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnestedType := unwrapType(sf.Type)\n\t\t\t\tinfo.nestedKinds[nestedType.Kind()] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn info\n}\n\nfunc equalFieldType(out any, kind reflect.Kind, key, aliasTag string) bool {\n\ttyp := reflect.TypeOf(out).Elem()\n\tkey = utilsstrings.ToLower(key)\n\n\tif isStringKeyMap(typ) {\n\t\treturn true\n\t}\n\n\tif typ.Kind() != reflect.Struct {\n\t\treturn false\n\t}\n\n\tcache := getFieldCache(aliasTag)\n\tval, ok := cache.Load(typ)\n\tif !ok {\n\t\tinfo := buildFieldInfo(typ, aliasTag)\n\t\tval, _ = cache.LoadOrStore(typ, info)\n\t}\n\n\tinfo, ok := val.(fieldInfo)\n\tif !ok {\n\t\treturn false\n\t}\n\n\tif k, ok := info.names[key]; ok && k == kind {\n\t\treturn true\n\t}\n\tif _, ok := info.nestedKinds[kind]; ok {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// FilterFlags returns the media type value by trimming any parameters from a Content-Type header.\nfunc FilterFlags(content string) string {\n\tif i := strings.IndexAny(content, \" ;\"); i >= 0 {\n\t\treturn content[:i]\n\t}\n\treturn content\n}\n\nfunc formatBindData[T, K any](aliasTag string, out any, data map[string][]T, key string, value K, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay\n\tvar err error\n\tif supportBracketNotation && strings.IndexByte(key, '[') >= 0 {\n\t\tkey, err = parseParamSquareBrackets(key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tswitch v := any(value).(type) {\n\tcase string:\n\t\tdataMap, ok := any(data).(map[string][]string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unsupported value type: %T\", value)\n\t\t}\n\n\t\tassignBindData(aliasTag, out, dataMap, key, v, enableSplitting)\n\tcase []string:\n\t\tdataMap, ok := any(data).(map[string][]string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unsupported value type: %T\", value)\n\t\t}\n\n\t\tfor _, val := range v {\n\t\t\tassignBindData(aliasTag, out, dataMap, key, val, enableSplitting)\n\t\t}\n\tcase []*multipart.FileHeader:\n\t\tfor _, val := range v {\n\t\t\tvalT, ok := any(val).(T)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"unsupported value type: %T\", value)\n\t\t\t}\n\t\t\tdata[key] = append(data[key], valT)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported value type: %T\", value)\n\t}\n\n\treturn err\n}\n\nfunc assignBindData(aliasTag string, out any, data map[string][]string, key, value string, enableSplitting bool) { //nolint:revive // it's okay\n\tif enableSplitting && strings.IndexByte(value, ',') >= 0 && equalFieldType(out, reflect.Slice, key, aliasTag) {\n\t\tfor v := range strings.SplitSeq(value, \",\") {\n\t\t\tdata[key] = append(data[key], v)\n\t\t}\n\t} else {\n\t\tdata[key] = append(data[key], value)\n\t}\n}\n"
  },
  {
    "path": "binder/mapping_test.go",
    "content": "package binder\n\nimport (\n\t\"fmt\"\n\t\"mime/multipart\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gofiber/schema\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_EqualFieldType(t *testing.T) {\n\tt.Parallel()\n\n\tvar out int\n\trequire.False(t, equalFieldType(&out, reflect.Int, \"key\", \"query\"))\n\n\tvar dummy struct{ f string }\n\trequire.False(t, equalFieldType(&dummy, reflect.String, \"key\", \"query\"))\n\n\tvar dummy2 struct{ f string }\n\trequire.False(t, equalFieldType(&dummy2, reflect.String, \"f\", \"query\"))\n\n\tvar user struct {\n\t\tName    string\n\t\tAddress string `query:\"address\"`\n\t\tAge     int    `query:\"AGE\"`\n\t}\n\trequire.True(t, equalFieldType(&user, reflect.String, \"name\", \"query\"))\n\trequire.True(t, equalFieldType(&user, reflect.String, \"Name\", \"query\"))\n\trequire.True(t, equalFieldType(&user, reflect.String, \"address\", \"query\"))\n\trequire.True(t, equalFieldType(&user, reflect.String, \"Address\", \"query\"))\n\trequire.True(t, equalFieldType(&user, reflect.Int, \"AGE\", \"query\"))\n\trequire.True(t, equalFieldType(&user, reflect.Int, \"age\", \"query\"))\n\n\tvar user2 struct {\n\t\tUser struct {\n\t\t\tName    string\n\t\t\tAddress string `query:\"address\"`\n\t\t\tAge     int    `query:\"AGE\"`\n\t\t} `query:\"user\"`\n\t}\n\n\trequire.True(t, equalFieldType(&user2, reflect.String, \"user.name\", \"query\"))\n\trequire.True(t, equalFieldType(&user2, reflect.String, \"user.Name\", \"query\"))\n\trequire.True(t, equalFieldType(&user2, reflect.String, \"user.address\", \"query\"))\n\trequire.True(t, equalFieldType(&user2, reflect.String, \"user.Address\", \"query\"))\n\trequire.True(t, equalFieldType(&user2, reflect.Int, \"user.AGE\", \"query\"))\n\trequire.True(t, equalFieldType(&user2, reflect.Int, \"user.age\", \"query\"))\n\n\tvar pointerUser struct {\n\t\tTags *[]string `query:\"tags\"`\n\t}\n\trequire.True(t, equalFieldType(&pointerUser, reflect.Slice, \"tags\", \"query\"))\n\n\ttype nested struct {\n\t\tValues []string `query:\"values\"`\n\t}\n\tvar nestedWrapper struct {\n\t\tNested *nested `query:\"nested\"`\n\t}\n\trequire.True(t, equalFieldType(&nestedWrapper, reflect.Slice, \"nested.values\", \"query\"))\n\n\ttype nestedPointerSlice struct {\n\t\tValues *[]string `query:\"values\"`\n\t}\n\tvar nestedPointerWrapper struct {\n\t\tNested *nestedPointerSlice `query:\"nested\"`\n\t}\n\trequire.True(t, equalFieldType(&nestedPointerWrapper, reflect.Slice, \"nested.values\", \"query\"))\n}\n\nfunc Test_ParseParamSquareBrackets(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\terr      error\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"foo[bar]\",\n\t\t\texpected: \"foo.bar\",\n\t\t},\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"foo[bar][baz]\",\n\t\t\texpected: \"foo.bar.baz\",\n\t\t},\n\t\t{\n\t\t\terr:      ErrUnmatchedBrackets,\n\t\t\tinput:    \"foo[bar\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\terr:      ErrUnmatchedBrackets,\n\t\t\tinput:    \"foo[bar][baz\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\terr:      ErrUnmatchedBrackets,\n\t\t\tinput:    \"foo]bar[\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"foo[bar[baz]]\",\n\t\t\texpected: \"foo.bar.baz\",\n\t\t},\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"[]\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\terr:      nil,\n\t\t\tinput:    \"foo[]\",\n\t\t\texpected: \"foo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult, err := parseParamSquareBrackets(tt.input)\n\t\t\tif tt.err != nil {\n\t\t\t\trequire.ErrorIs(t, err, tt.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_parseToMap(t *testing.T) {\n\tt.Parallel()\n\n\tinputMap := map[string][]string{\n\t\t\"key1\": {\"value1\", \"value2\"},\n\t\t\"key2\": {\"value3\"},\n\t\t\"key3\": {\"value4\"},\n\t}\n\n\t// Test map[string]string\n\tm := make(map[string]string)\n\terr := parseToMap(reflect.ValueOf(m), inputMap)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"value2\", m[\"key1\"])\n\trequire.Equal(t, \"value3\", m[\"key2\"])\n\trequire.Equal(t, \"value4\", m[\"key3\"])\n\n\t// Test map[string][]string\n\tm2 := make(map[string][]string)\n\terr = parseToMap(reflect.ValueOf(m2), inputMap)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, m2[\"key1\"], 2)\n\trequire.Contains(t, m2[\"key1\"], \"value1\")\n\trequire.Contains(t, m2[\"key1\"], \"value2\")\n\trequire.Len(t, m2[\"key2\"], 1)\n\trequire.Len(t, m2[\"key3\"], 1)\n\n\t// Test map[string]any\n\tm3 := make(map[string]any)\n\terr = parseToMap(reflect.ValueOf(m3), inputMap)\n\trequire.NoError(t, err)\n\trequire.Empty(t, m3)\n\n\tvar zeroStringMap map[string]string\n\terr = parseToMap(reflect.ValueOf(&zeroStringMap).Elem(), inputMap)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"value2\", zeroStringMap[\"key1\"])\n\n\tvar zeroSliceMap map[string][]string\n\terr = parseToMap(reflect.ValueOf(&zeroSliceMap).Elem(), inputMap)\n\trequire.NoError(t, err)\n\trequire.Len(t, zeroSliceMap[\"key1\"], 2)\n\n\terr = parseToMap(reflect.ValueOf(map[string]string(nil)), inputMap)\n\trequire.ErrorIs(t, err, ErrMapNilDestination)\n}\n\nfunc Test_FilterFlags(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tinput:    \"text/javascript; charset=utf-8\",\n\t\t\texpected: \"text/javascript\",\n\t\t},\n\t\t{\n\t\t\tinput:    \"text/javascript\",\n\t\t\texpected: \"text/javascript\",\n\t\t},\n\n\t\t{\n\t\t\tinput:    \"text/javascript; charset=utf-8; foo=bar\",\n\t\t\texpected: \"text/javascript\",\n\t\t},\n\t\t{\n\t\t\tinput:    \"text/javascript charset=utf-8\",\n\t\t\texpected: \"text/javascript\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := FilterFlags(tt.input)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc Benchmark_FilterFlags(b *testing.B) {\n\tb.ReportAllocs()\n\n\tcases := []string{\n\t\t\"text/javascript; charset=utf-8\",\n\t\t\"application/json\",\n\t\t\"text/plain; charset=utf-8; foo=bar\",\n\t\t\"text/javascript charset=utf-8\",\n\t}\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = FilterFlags(cases[i&3])\n\t}\n}\n\nfunc TestFormatBindData(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"string value with valid key\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"name\", \"John\", false, false)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif len(data[\"name\"]) != 1 || data[\"name\"][0] != \"John\" {\n\t\t\tt.Fatalf(\"expected data[\\\"name\\\"] = [John], got %v\", data[\"name\"])\n\t\t}\n\t})\n\n\tt.Run(\"unsupported value type\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"age\", 30, false, false) // int is unsupported\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected an error, got nil\")\n\t\t}\n\t})\n\n\tt.Run(\"bracket notation parsing error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"invalid[\", \"value\", false, true) // malformed bracket notation\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected an error, got nil\")\n\t\t}\n\t})\n\n\tt.Run(\"handling multipart file headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]*multipart.FileHeader)\n\t\tfiles := []*multipart.FileHeader{\n\t\t\t{Filename: \"file1.txt\"},\n\t\t\t{Filename: \"file2.txt\"},\n\t\t}\n\t\terr := formatBindData(\"query\", out, data, \"files\", files, false, false)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif len(data[\"files\"]) != 2 {\n\t\t\tt.Fatalf(\"expected 2 files, got %d\", len(data[\"files\"]))\n\t\t}\n\t})\n\n\tt.Run(\"type casting error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := map[string][]int{} // Incorrect type to force a casting error\n\t\terr := formatBindData(\"query\", out, data, \"key\", \"value\", false, false)\n\t\trequire.Equal(t, \"unsupported value type: string\", err.Error())\n\t})\n}\n\nfunc TestAssignBindData(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"splitting enabled with comma\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct {\n\t\t\tColors []string `query:\"colors\"`\n\t\t}{}\n\t\tdata := make(map[string][]string)\n\t\tassignBindData(\"query\", &out, data, \"colors\", \"red,blue,green\", true)\n\t\trequire.Len(t, data[\"colors\"], 3)\n\t})\n\n\tt.Run(\"splitting disabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar out []string\n\t\tdata := make(map[string][]string)\n\t\tassignBindData(\"query\", out, data, \"color\", \"red,blue\", false)\n\t\trequire.Len(t, data[\"color\"], 1)\n\t})\n}\n\nfunc Test_parseToStruct_MismatchedData(t *testing.T) {\n\tt.Parallel()\n\n\ttype User struct {\n\t\tName string `query:\"name\"`\n\t\tAge  int    `query:\"age\"`\n\t}\n\n\tdata := map[string][]string{\n\t\t\"name\": {\"John\"},\n\t\t\"age\":  {\"invalidAge\"},\n\t}\n\n\terr := parseToStruct(\"query\", &User{}, data)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"schema: error converting value for \\\"age\\\"\")\n}\n\nfunc Test_formatBindData_ErrorCases(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"unsupported value type int\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"age\", 30, false, false) // int is unsupported\n\t\trequire.Error(t, err)\n\t\trequire.EqualError(t, err, \"unsupported value type: int\")\n\t})\n\n\tt.Run(\"unsupported value type map\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"map\", map[string]string{\"key\": \"value\"}, false, false) // map is unsupported\n\t\trequire.Error(t, err)\n\t\trequire.EqualError(t, err, \"unsupported value type: map[string]string\")\n\t})\n\n\tt.Run(\"bracket notation parsing error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"invalid[\", \"value\", false, true) // malformed bracket notation\n\t\trequire.Error(t, err)\n\t\trequire.EqualError(t, err, \"unmatched brackets\")\n\t})\n\n\tt.Run(\"type casting error for []string\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tout := struct{}{}\n\t\tdata := make(map[string][]string)\n\t\terr := formatBindData(\"query\", out, data, \"names\", 123, false, false) // invalid type for []string\n\t\trequire.Error(t, err)\n\t\trequire.EqualError(t, err, \"unsupported value type: int\")\n\t})\n}\n\nfunc Test_decoderBuilder(t *testing.T) {\n\tt.Parallel()\n\ttype customInt int\n\tconv := func(s string) reflect.Value {\n\t\ti, err := strconv.Atoi(s)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn reflect.ValueOf(customInt(i))\n\t}\n\tparserConfig := ParserConfig{\n\t\tSetAliasTag: \"custom\",\n\t\tParserType: []ParserType{{\n\t\t\tCustomType: customInt(0),\n\t\t\tConverter:  conv,\n\t\t}},\n\t\tIgnoreUnknownKeys: false,\n\t\tZeroEmpty:         false,\n\t}\n\tdecAny := decoderBuilder(parserConfig)\n\tdec, ok := decAny.(*schema.Decoder)\n\trequire.True(t, ok)\n\tvar out struct {\n\t\tX customInt `custom:\"x\"`\n\t}\n\terr := dec.Decode(&out, map[string][]string{\"x\": {\"7\"}})\n\trequire.NoError(t, err)\n\trequire.Equal(t, customInt(7), out.X)\n}\n\nfunc Test_parseToMap_Extended(t *testing.T) {\n\tt.Parallel()\n\tdata := map[string][]string{\n\t\t\"empty\": {},\n\t\t\"key1\":  {\"value1\"},\n\t}\n\n\tm := make(map[string]string)\n\terr := parseToMap(reflect.ValueOf(m), data)\n\trequire.NoError(t, err)\n\trequire.Empty(t, m[\"empty\"])\n\n\tm2 := make(map[string][]int)\n\terr = parseToMap(reflect.ValueOf(m2), data)\n\trequire.ErrorIs(t, err, ErrMapNotConvertible)\n\n\tm3 := make(map[string]int)\n\terr = parseToMap(reflect.ValueOf(m3), data)\n\trequire.NoError(t, err)\n}\n\nfunc Test_decoderPoolMapInit(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, tag := range tags {\n\t\tdecAny := getDecoderPool(tag).Get()\n\t\tdec, ok := decAny.(*schema.Decoder)\n\t\trequire.True(t, ok)\n\t\trequire.NotNil(t, dec)\n\t\tgetDecoderPool(tag).Put(decAny)\n\t}\n}\n\nfunc TestSetParserDecoderConcurrentAccess(t *testing.T) {\n\tt.Parallel()\n\n\tt.Cleanup(func() {\n\t\tSetParserDecoder(ParserConfig{\n\t\t\tIgnoreUnknownKeys: true,\n\t\t\tZeroEmpty:         true,\n\t\t})\n\t})\n\n\ttype queryUser struct {\n\t\tName string `query:\"name\"`\n\t}\n\n\tdata := map[string][]string{\n\t\t\"name\": {\"fiber\"},\n\t}\n\tparserConfig := ParserConfig{\n\t\tIgnoreUnknownKeys: true,\n\t\tZeroEmpty:         true,\n\t}\n\n\tstart := make(chan struct{})\n\tconst workers = 25\n\terrCh := make(chan error, workers*2)\n\tvar wg sync.WaitGroup\n\n\trunWorker := func(fn func() error) {\n\t\twg.Go(func() {\n\t\t\t<-start\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic: %v\", r)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tif err := fn(); err != nil {\n\t\t\t\terrCh <- err\n\t\t\t}\n\t\t})\n\t}\n\n\tfor i := 0; i < workers; i++ {\n\t\trunWorker(func() error {\n\t\t\tSetParserDecoder(parserConfig)\n\t\t\treturn nil\n\t\t})\n\n\t\trunWorker(func() error {\n\t\t\tvar out queryUser\n\t\t\tif err := parseToStruct(\"query\", &out, data); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif out.Name != \"fiber\" {\n\t\t\t\treturn fmt.Errorf(\"unexpected name %q\", out.Name)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tclose(start)\n\twg.Wait()\n\tclose(errCh)\n\n\tfor err := range errCh {\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc Test_getFieldCache(t *testing.T) {\n\tt.Parallel()\n\trequire.NotNil(t, getFieldCache(\"header\"))\n\trequire.NotNil(t, getFieldCache(\"respHeader\"))\n\trequire.NotNil(t, getFieldCache(\"cookie\"))\n\trequire.NotNil(t, getFieldCache(\"form\"))\n\trequire.NotNil(t, getFieldCache(\"uri\"))\n\trequire.NotNil(t, getFieldCache(\"query\"))\n\trequire.Panics(t, func() { getFieldCache(\"unknown\") })\n}\n\nfunc Test_EqualFieldType_Map(t *testing.T) {\n\tt.Parallel()\n\tm := map[string]int{}\n\trequire.True(t, equalFieldType(&m, reflect.Int, \"any\", \"query\"))\n}\n\nfunc Test_equalFieldType_CacheTypeMismatch(t *testing.T) {\n\ttype Sample struct {\n\t\tField string `query:\"field\"`\n\t}\n\tcache := getFieldCache(\"query\")\n\ttyp := reflect.TypeOf(Sample{})\n\tcache.Store(typ, 1)\n\tdefer cache.Delete(typ)\n\tvar s Sample\n\trequire.False(t, equalFieldType(&s, reflect.String, \"field\", \"query\"))\n}\n\nfunc Test_buildFieldInfo_Unexported(t *testing.T) {\n\tt.Parallel()\n\ttype nested struct {\n\t\texport   int\n\t\tExported int\n\t}\n\t_ = nested{export: 0}\n\ttype outer struct {\n\t\tName   string\n\t\tNested nested\n\t}\n\tinfo := buildFieldInfo(reflect.TypeOf(outer{}), \"query\")\n\trequire.Contains(t, info.names, \"name\")\n\t_, ok := info.nestedKinds[reflect.Int]\n\trequire.True(t, ok)\n}\n\nfunc Test_formatBindData_BracketNotationSuccess(t *testing.T) {\n\tt.Parallel()\n\tout := struct{}{}\n\tdata := make(map[string][]string)\n\terr := formatBindData(\"query\", out, data, \"user[name]\", \"john\", false, true)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", data[\"user.name\"][0])\n}\n\nfunc Test_formatBindData_FileHeaderTypeMismatch(t *testing.T) {\n\tt.Parallel()\n\tout := struct{}{}\n\tdata := map[string][]int{}\n\tfiles := []*multipart.FileHeader{{Filename: \"file1.txt\"}}\n\terr := formatBindData(\"query\", out, data, \"file\", files, false, false)\n\trequire.EqualError(t, err, \"unsupported value type: []*multipart.FileHeader\")\n}\n\nfunc Benchmark_equalFieldType(b *testing.B) {\n\ttype Nested struct {\n\t\tName string `query:\"name\"`\n\t}\n\ttype User struct {\n\t\tName   string `query:\"name\"`\n\t\tNested Nested `query:\"user\"`\n\t\tAge    int    `query:\"age\"`\n\t}\n\tvar user User\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tequalFieldType(&user, reflect.String, \"name\", \"query\")\n\t\tequalFieldType(&user, reflect.Int, \"age\", \"query\")\n\t\tequalFieldType(&user, reflect.String, \"user.name\", \"query\")\n\t}\n}\n"
  },
  {
    "path": "binder/msgpack.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// MsgPackBinding is the MsgPack binder for MsgPack request body.\ntype MsgPackBinding struct {\n\tMsgPackDecoder utils.MsgPackUnmarshal\n}\n\n// Name returns the binding name.\nfunc (*MsgPackBinding) Name() string {\n\treturn \"msgpack\"\n}\n\n// Bind parses the request body as MsgPack and returns the result.\nfunc (b *MsgPackBinding) Bind(body []byte, out any) error {\n\treturn b.MsgPackDecoder(body, out)\n}\n\n// Reset resets the MsgPackBinding binder.\nfunc (b *MsgPackBinding) Reset() {\n\tb.MsgPackDecoder = nil\n}\n\n// UnimplementedMsgpackMarshal panics to signal that a Msgpack marshaler must\n// be configured before MsgPack support can be used.\nfunc UnimplementedMsgpackMarshal(_ any) ([]byte, error) {\n\tpanic(\"Must explicit setup Msgpack, please check docs: https://docs.gofiber.io/next/guide/advance-format#msgpack\")\n}\n\n// UnimplementedMsgpackUnmarshal panics to signal that a Msgpack unmarshaler\n// must be configured before MsgPack support can be used.\nfunc UnimplementedMsgpackUnmarshal(_ []byte, _ any) error {\n\tpanic(\"Must explicit setup Msgpack, please check docs: https://docs.gofiber.io/next/guide/advance-format#msgpack\")\n}\n"
  },
  {
    "path": "binder/msgpack_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/shamaton/msgpack/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_Msgpack_Binding_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &MsgPackBinding{\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t}\n\trequire.Equal(t, \"msgpack\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `msgpack:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string `msgpack:\"name\"`\n\t\tPosts []Post `msgpack:\"posts\"`\n\t\tAge   int    `msgpack:\"age\"`\n\t}\n\tvar user User\n\n\t// Prepare msgpack data\n\tinput := map[string]any{\n\t\t\"name\": \"john\",\n\t\t\"age\":  42,\n\t\t\"posts\": []map[string]any{\n\t\t\t{\"title\": \"post1\"},\n\t\t\t{\"title\": \"post2\"},\n\t\t\t{\"title\": \"post3\"},\n\t\t},\n\t}\n\tdata, err := msgpack.Marshal(input)\n\trequire.NoError(t, err)\n\n\terr = b.Bind(data, &user)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\n\tb.Reset()\n\trequire.Nil(t, b.MsgPackDecoder)\n}\n\nfunc Benchmark_Msgpack_Binding_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &MsgPackBinding{\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t}\n\n\ttype User struct {\n\t\tName  string   `msgpack:\"name\"`\n\t\tPosts []string `msgpack:\"posts\"`\n\t\tAge   int      `msgpack:\"age\"`\n\t}\n\n\tvar user User\n\tvar err error\n\tfor b.Loop() {\n\t\t// {\"name\":\"john\",\"age\":42,\"posts\":[{\"title\":\"post1\"},{\"title\":\"post2\"},{\"title\":\"post3\"}]}\n\t\terr = binder.Bind([]byte{\n\t\t\t0x83, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa4, 0x6a, 0x6f, 0x68, 0x6e, 0xa3, 0x61, 0x67, 0x65, 0x2a,\n\t\t\t0xa5, 0x70, 0x6f, 0x73, 0x74, 0x73, 0x93, 0xa5, 0x70, 0x6f, 0x73, 0x74, 0x31, 0xa5, 0x70, 0x6f,\n\t\t\t0x73, 0x74, 0x32, 0xa5, 0x70, 0x6f, 0x73, 0x74, 0x33,\n\t\t},\n\t\t\t&user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n\trequire.Equal(b, \"post1\", user.Posts[0])\n\trequire.Equal(b, \"post2\", user.Posts[1])\n\trequire.Equal(b, \"post3\", user.Posts[2])\n}\n\nfunc Test_UnimplementedMsgpackMarshal_Panics(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Panics(t, func() {\n\t\t_, err := UnimplementedMsgpackMarshal(struct{ Name string }{Name: \"test\"})\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_UnimplementedMsgpackUnmarshal_Panics(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Panics(t, func() {\n\t\tvar out any\n\t\terr := UnimplementedMsgpackUnmarshal([]byte{0x80}, &out)\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_UnimplementedMsgpackMarshal_PanicMessage(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trequire.Contains(t, r, \"Must explicit setup Msgpack\")\n\t\t}\n\t}()\n\t_, err := UnimplementedMsgpackMarshal(struct{ Name string }{Name: \"test\"})\n\n\trequire.NoError(t, err)\n}\n\nfunc Test_UnimplementedMsgpackUnmarshal_PanicMessage(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trequire.Contains(t, r, \"Must explicit setup Msgpack\")\n\t\t}\n\t}()\n\tvar out any\n\terr := UnimplementedMsgpackUnmarshal([]byte{0x80}, &out)\n\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "binder/query.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// QueryBinding is the query binder for query request body.\ntype QueryBinding struct {\n\tEnableSplitting bool\n}\n\n// Name returns the binding name.\nfunc (*QueryBinding) Name() string {\n\treturn \"query\"\n}\n\n// Bind parses the request query and returns the result.\nfunc (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error {\n\tdata := make(map[string][]string)\n\tfor key, val := range reqCtx.URI().QueryArgs().All() {\n\t\tk := utils.UnsafeString(key)\n\t\tv := utils.UnsafeString(val)\n\t\tif err := formatBindData(b.Name(), out, data, k, v, b.EnableSplitting, true); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// Reset resets the QueryBinding binder.\nfunc (b *QueryBinding) Reset() {\n\tb.EnableSplitting = false\n}\n"
  },
  {
    "path": "binder/query_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_QueryBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &QueryBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"query\", b.Name())\n\n\ttype Post struct {\n\t\tTitle string `query:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string   `query:\"name\"`\n\t\tNames []string `query:\"names\"`\n\t\tPosts []Post   `query:\"posts\"`\n\t\tAge   int      `query:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\treq.URI().SetQueryString(\"name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr := b.Bind(req, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Len(t, user.Posts, 3)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\trequire.Equal(t, \"post3\", user.Posts[2].Title)\n\trequire.Contains(t, user.Names, \"john\")\n\trequire.Contains(t, user.Names, \"doe\")\n\n\tb.Reset()\n\trequire.False(t, b.EnableSplitting)\n}\n\nfunc Benchmark_QueryBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &QueryBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `query:\"name\"`\n\t\tPosts []string `query:\"posts\"`\n\t\tAge   int      `query:\"age\"`\n\t}\n\tvar user User\n\n\treq := fasthttp.AcquireRequest()\n\tb.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\treq.URI().SetQueryString(\"name=john&age=42&posts=post1,post2,post3\")\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(req, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Len(b, user.Posts, 3)\n\trequire.Contains(b, user.Posts, \"post1\")\n\trequire.Contains(b, user.Posts, \"post2\")\n\trequire.Contains(b, user.Posts, \"post3\")\n}\n\nfunc Test_QueryBinder_Bind_PointerSlices(t *testing.T) {\n\tt.Parallel()\n\n\tbinder := &QueryBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype Preferences struct {\n\t\tTags *[]string `query:\"tags\"`\n\t}\n\n\ttype Profile struct {\n\t\tEmails *[]string    `query:\"emails\"`\n\t\tPrefs  *Preferences `query:\"preferences\"`\n\t}\n\n\tvar profile Profile\n\n\treq := fasthttp.AcquireRequest()\n\treq.URI().SetQueryString(\"emails=work,personal&preferences[tags]=golang,api\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseRequest(req)\n\t})\n\n\terr := binder.Bind(req, &profile)\n\trequire.NoError(t, err)\n\n\trequire.NotNil(t, profile.Emails)\n\trequire.ElementsMatch(t, []string{\"work\", \"personal\"}, *profile.Emails)\n\n\trequire.NotNil(t, profile.Prefs)\n\trequire.NotNil(t, profile.Prefs.Tags)\n\trequire.ElementsMatch(t, []string{\"golang\", \"api\"}, *profile.Prefs.Tags)\n}\n"
  },
  {
    "path": "binder/resp_header.go",
    "content": "package binder\n\nimport (\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// RespHeaderBinding is the respHeader binder for response header.\ntype RespHeaderBinding struct {\n\tEnableSplitting bool\n}\n\n// Name returns the binding name.\nfunc (*RespHeaderBinding) Name() string {\n\treturn \"respHeader\"\n}\n\n// Bind parses the response header and returns the result.\nfunc (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error {\n\tdata := make(map[string][]string)\n\n\tfor key, val := range resp.Header.All() {\n\t\tk := utils.UnsafeString(key)\n\t\tv := utils.UnsafeString(val)\n\t\tif err := formatBindData(b.Name(), out, data, k, v, b.EnableSplitting, false); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// Reset resets the RespHeaderBinding binder.\nfunc (b *RespHeaderBinding) Reset() {\n\tb.EnableSplitting = false\n}\n"
  },
  {
    "path": "binder/resp_header_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_RespHeaderBinder_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &RespHeaderBinding{\n\t\tEnableSplitting: true,\n\t}\n\trequire.Equal(t, \"respHeader\", b.Name())\n\n\ttype User struct {\n\t\tName  string   `respHeader:\"name\"`\n\t\tPosts []string `respHeader:\"posts\"`\n\t\tAge   int      `respHeader:\"age\"`\n\t}\n\tvar user User\n\n\tresp := fasthttp.AcquireResponse()\n\tresp.Header.Set(\"name\", \"john\")\n\tresp.Header.Set(\"age\", \"42\")\n\tresp.Header.Set(\"posts\", \"post1,post2,post3\")\n\n\tt.Cleanup(func() {\n\t\tfasthttp.ReleaseResponse(resp)\n\t})\n\n\terr := b.Bind(resp, &user)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Equal(t, []string{\"post1\", \"post2\", \"post3\"}, user.Posts)\n\n\tb.Reset()\n\trequire.False(t, b.EnableSplitting)\n}\n\nfunc Benchmark_RespHeaderBinder_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &RespHeaderBinding{\n\t\tEnableSplitting: true,\n\t}\n\n\ttype User struct {\n\t\tName  string   `respHeader:\"name\"`\n\t\tPosts []string `respHeader:\"posts\"`\n\t\tAge   int      `respHeader:\"age\"`\n\t}\n\tvar user User\n\n\tresp := fasthttp.AcquireResponse()\n\tresp.Header.Set(\"name\", \"john\")\n\tresp.Header.Set(\"age\", \"42\")\n\tresp.Header.Set(\"posts\", \"post1,post2,post3\")\n\n\tb.Cleanup(func() {\n\t\tfasthttp.ReleaseResponse(resp)\n\t})\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(resp, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Equal(b, []string{\"post1\", \"post2\", \"post3\"}, user.Posts)\n}\n\nfunc Test_RespHeaderBinder_Bind_ParseError(t *testing.T) {\n\tb := &RespHeaderBinding{}\n\ttype User struct {\n\t\tAge int `respHeader:\"age\"`\n\t}\n\tvar user User\n\tresp := fasthttp.AcquireResponse()\n\tresp.Header.Set(\"age\", \"invalid\")\n\tt.Cleanup(func() { fasthttp.ReleaseResponse(resp) })\n\terr := b.Bind(resp, &user)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "binder/uri.go",
    "content": "package binder\n\n// URIBinding is the binder implementation for populating values from route parameters.\ntype URIBinding struct{}\n\n// Name returns the binding name.\nfunc (*URIBinding) Name() string {\n\treturn \"uri\"\n}\n\n// Bind parses the URI parameters and returns the result.\nfunc (b *URIBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error {\n\tdata := make(map[string][]string, len(params))\n\tfor _, param := range params {\n\t\tdata[param] = append(data[param], paramsFunc(param))\n\t}\n\n\treturn parse(b.Name(), out, data)\n}\n\n// Reset resets URIBinding binder.\nfunc (*URIBinding) Reset() {\n\t// Nothing to reset\n}\n"
  },
  {
    "path": "binder/uri_test.go",
    "content": "package binder\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_URIBinding_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &URIBinding{}\n\trequire.Equal(t, \"uri\", b.Name())\n\n\ttype User struct {\n\t\tName  string   `uri:\"name\"`\n\t\tPosts []string `uri:\"posts\"`\n\t\tAge   int      `uri:\"age\"`\n\t}\n\tvar user User\n\n\tparamsKey := []string{\"name\", \"age\", \"posts\"}\n\tparamsVals := []string{\"john\", \"42\", \"post1,post2,post3\"}\n\tparamsFunc := func(key string, _ ...string) string {\n\t\tfor i, k := range paramsKey {\n\t\t\tif k == key {\n\t\t\t\treturn paramsVals[i]\n\t\t\t}\n\t\t}\n\n\t\treturn \"\"\n\t}\n\n\terr := b.Bind(paramsKey, paramsFunc, &user)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Equal(t, []string{\"post1,post2,post3\"}, user.Posts)\n\n\tb.Reset()\n}\n\nfunc Benchmark_URIBinding_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &URIBinding{}\n\n\ttype User struct {\n\t\tName  string   `uri:\"name\"`\n\t\tPosts []string `uri:\"posts\"`\n\t\tAge   int      `uri:\"age\"`\n\t}\n\tvar user User\n\n\tparamsKey := []string{\"name\", \"age\", \"posts\"}\n\tparamsVals := []string{\"john\", \"42\", \"post1,post2,post3\"}\n\tparamsFunc := func(key string, _ ...string) string {\n\t\tfor i, k := range paramsKey {\n\t\t\tif k == key {\n\t\t\t\treturn paramsVals[i]\n\t\t\t}\n\t\t}\n\n\t\treturn \"\"\n\t}\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(paramsKey, paramsFunc, &user)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\trequire.Equal(b, []string{\"post1,post2,post3\"}, user.Posts)\n}\n"
  },
  {
    "path": "binder/xml.go",
    "content": "package binder\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// XMLBinding is the XML binder for XML request body.\ntype XMLBinding struct {\n\tXMLDecoder utils.XMLUnmarshal\n}\n\n// Name returns the binding name.\nfunc (*XMLBinding) Name() string {\n\treturn \"xml\"\n}\n\n// Bind parses the request body as XML and returns the result.\nfunc (b *XMLBinding) Bind(body []byte, out any) error {\n\tif err := b.XMLDecoder(body, out); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal xml: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Reset resets the XMLBinding binder.\nfunc (b *XMLBinding) Reset() {\n\tb.XMLDecoder = nil\n}\n"
  },
  {
    "path": "binder/xml_test.go",
    "content": "package binder\n\nimport (\n\t\"encoding/xml\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_XMLBinding_Bind(t *testing.T) {\n\tt.Parallel()\n\n\tb := &XMLBinding{\n\t\tXMLDecoder: xml.Unmarshal,\n\t}\n\trequire.Equal(t, \"xml\", b.Name())\n\n\ttype Posts struct {\n\t\tXMLName xml.Name `xml:\"post\"`\n\t\tTitle   string   `xml:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName   string  `xml:\"name\"`\n\t\tIgnore string  `xml:\"-\"`\n\t\tPosts  []Posts `xml:\"posts>post\"`\n\t\tAge    int     `xml:\"age\"`\n\t}\n\n\tuser := new(User)\n\terr := b.Bind([]byte(`\n\t\t<user>\n\t\t\t<name>john</name>\n\t\t\t<age>42</age>\n\t\t\t<ignore>ignore</ignore>\n\t\t\t<posts>\n\t\t\t\t<post>\n\t\t\t\t\t<title>post1</title>\n\t\t\t\t</post>\n\t\t\t\t<post>\n\t\t\t\t\t<title>post2</title>\n\t\t\t\t</post>\n\t\t\t</posts>\n\t\t</user>\n\t`), user)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"john\", user.Name)\n\trequire.Equal(t, 42, user.Age)\n\trequire.Empty(t, user.Ignore)\n\n\trequire.Len(t, user.Posts, 2)\n\trequire.Equal(t, \"post1\", user.Posts[0].Title)\n\trequire.Equal(t, \"post2\", user.Posts[1].Title)\n\n\tb.Reset()\n\trequire.Nil(t, b.XMLDecoder)\n}\n\nfunc Test_XMLBinding_Bind_error(t *testing.T) {\n\tt.Parallel()\n\tb := &XMLBinding{\n\t\tXMLDecoder: xml.Unmarshal,\n\t}\n\n\ttype User struct {\n\t\tName string `xml:\"name\"`\n\t\tAge  int    `xml:\"age\"`\n\t}\n\n\tuser := new(User)\n\terr := b.Bind([]byte(`\n\t\t<user>\n\t\t\t<name>john</name>\n\t\t\t<age>42</age>\n\t\t\t<unknown>unknown</unknown>\n\t\t</user\n\t`), user)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"failed to unmarshal xml\")\n}\n\nfunc Benchmark_XMLBinding_Bind(b *testing.B) {\n\tb.ReportAllocs()\n\n\tbinder := &XMLBinding{\n\t\tXMLDecoder: xml.Unmarshal,\n\t}\n\n\ttype Posts struct {\n\t\tXMLName xml.Name `xml:\"post\"`\n\t\tTitle   string   `xml:\"title\"`\n\t}\n\n\ttype User struct {\n\t\tName  string  `xml:\"name\"`\n\t\tPosts []Posts `xml:\"posts>post\"`\n\t\tAge   int     `xml:\"age\"`\n\t}\n\n\tuser := new(User)\n\tdata := []byte(`\n\t\t<user>\n\t\t\t<name>john</name>\n\t\t\t<age>42</age>\n\t\t\t<ignore>ignore</ignore>\n\t\t\t<posts>\n\t\t\t\t<post>\n\t\t\t\t\t<title>post1</title>\n\t\t\t\t</post>\n\t\t\t\t<post>\n\t\t\t\t\t<title>post2</title>\n\t\t\t\t</post>\n\t\t\t</posts>\n\t\t</user>\n\t`)\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = binder.Bind(data, user)\n\t}\n\trequire.NoError(b, err)\n\n\tuser = new(User)\n\terr = binder.Bind(data, user)\n\trequire.NoError(b, err)\n\n\trequire.Equal(b, \"john\", user.Name)\n\trequire.Equal(b, 42, user.Age)\n\n\trequire.Len(b, user.Posts, 2)\n\trequire.Equal(b, \"post1\", user.Posts[0].Title)\n\trequire.Equal(b, \"post2\", user.Posts[1].Title)\n}\n"
  },
  {
    "path": "client/README.md",
    "content": "<h1 align=\"center\">Fiber Client</h1>\n<p align=\"center\">Easy-to-use HTTP client based on fasthttp (inspired by <a href=\"https://github.com/go-resty/resty\">resty</a> and <a href=\"https://github.com/axios/axios\">axios</a>)</p>\n<p align=\"center\"><a href=\"#features\">Features</a> section describes in detail about Resty capabilities</p>\n\n## Features\n\n> The characteristics have not yet been written.\n\n- GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, etc.\n- Simple and chainable methods for settings and request\n- Request Body can be `string`, `[]byte`, `map`, `slice`\n  - Auto detects `Content-Type`\n  - Buffer processing for `files`\n  - Native `*fasthttp.Request` instance can be accessed during middleware and request execution via `Request.RawRequest`\n  - Request Body can be read multiple time via `Request.RawRequest.GetBody()`\n- Response object gives you more possibility\n  - Access as `[]byte` by `response.Body()` or access as `string` by `response.String()`\n- Automatic marshal and unmarshal for JSON and XML content type\n  - Default is JSON, if you supply struct/map without header Content-Type\n  - For auto-unmarshal, refer to -\n    - Success scenario Request.SetResult() and Response.Result().\n    - Error scenario Request.SetError() and Response.Error().\n    - Supports RFC7807 - application/problem+json & application/problem+xml\n  - Provide an option to override JSON Marshal/Unmarshal and XML Marshal/Unmarshal\n\n## Usage\n\nThe following samples will assist you to become as comfortable as possible with `Fiber Client` library.\n\n```go\n// Import Fiber Client into your code and refer it as `client`.\nimport \"github.com/gofiber/fiber/client\"\n```\n\n### Simple GET\n"
  },
  {
    "path": "client/client.go",
    "content": "// Package client exposes Fiber's HTTP client built on top of fasthttp.\n//\n// It allows constructing new clients or wrapping existing fasthttp transports\n// so applications can share pools, dialers, and TLS settings between Fiber and\n// lower-level fasthttp integrations.\npackage client\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\n\t\"github.com/gofiber/utils/v2\"\n\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttpproxy\"\n)\n\nvar ErrFailedToAppendCert = errors.New(\"failed to append certificate\")\n\n// Client provides Fiber's high-level HTTP API while delegating transport work\n// to fasthttp.Client, fasthttp.HostClient, or fasthttp.LBClient implementations.\n//\n// Settings configured on the client are shared across every request and may be\n// overridden per request when needed.\ntype Client struct {\n\tlogger    log.CommonLogger\n\ttransport httpClientTransport\n\n\theader  *Header\n\tparams  *QueryParam\n\tcookies *Cookie\n\tpath    *PathParam\n\n\tjsonMarshal   utils.JSONMarshal\n\tjsonUnmarshal utils.JSONUnmarshal\n\txmlMarshal    utils.XMLMarshal\n\txmlUnmarshal  utils.XMLUnmarshal\n\tcborMarshal   utils.CBORMarshal\n\tcborUnmarshal utils.CBORUnmarshal\n\n\tcookieJar            *CookieJar\n\tretryConfig          *RetryConfig\n\tbaseURL              string\n\tuserAgent            string\n\treferer              string\n\tuserRequestHooks     []RequestHook\n\tbuiltinRequestHooks  []RequestHook\n\tuserResponseHooks    []ResponseHook\n\tbuiltinResponseHooks []ResponseHook\n\n\ttimeout                time.Duration\n\tmu                     sync.RWMutex\n\tdebug                  bool\n\tdisablePathNormalizing bool\n}\n\n// Do executes the request using the underlying fasthttp transport.\n//\n// It mirrors [fasthttp.Client.Do], [fasthttp.HostClient.Do], or\n// [fasthttp.LBClient.Do] depending on how the Fiber client was constructed.\nfunc (c *Client) Do(req *fasthttp.Request, resp *fasthttp.Response) error {\n\treturn c.transport.Do(req, resp)\n}\n\n// DoTimeout executes the request and waits for a response up to the provided timeout.\n// It mirrors the behavior of the respective fasthttp client's DoTimeout implementation.\nfunc (c *Client) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {\n\treturn c.transport.DoTimeout(req, resp, timeout)\n}\n\n// DoDeadline executes the request and waits for a response until the provided deadline.\n// It mirrors the behavior of the respective fasthttp client's DoDeadline implementation.\nfunc (c *Client) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {\n\treturn c.transport.DoDeadline(req, resp, deadline)\n}\n\n// DoRedirects executes the request following redirects up to maxRedirects.\nfunc (c *Client) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {\n\treturn c.transport.DoRedirects(req, resp, maxRedirects)\n}\n\n// CloseIdleConnections closes idle connections on the underlying fasthttp transport when supported.\nfunc (c *Client) CloseIdleConnections() {\n\tc.transport.CloseIdleConnections()\n}\n\nfunc (c *Client) currentTLSConfig() *tls.Config {\n\treturn c.transport.TLSConfig()\n}\n\nfunc (c *Client) applyTLSConfig(config *tls.Config) {\n\tc.transport.SetTLSConfig(config)\n}\n\nfunc (c *Client) applyDial(dial fasthttp.DialFunc) {\n\tc.transport.SetDial(dial)\n}\n\n// FasthttpClient returns the underlying *fasthttp.Client if the client was created with one.\nfunc (c *Client) FasthttpClient() *fasthttp.Client {\n\tif client, ok := c.transport.(*standardClientTransport); ok {\n\t\treturn client.client\n\t}\n\treturn nil\n}\n\n// HostClient returns the underlying fasthttp.HostClient if the client was created with one.\nfunc (c *Client) HostClient() *fasthttp.HostClient {\n\tif client, ok := c.transport.(*hostClientTransport); ok {\n\t\treturn client.client\n\t}\n\treturn nil\n}\n\n// LBClient returns the underlying fasthttp.LBClient if the client was created with one.\nfunc (c *Client) LBClient() *fasthttp.LBClient {\n\tif client, ok := c.transport.(*lbClientTransport); ok {\n\t\treturn client.client\n\t}\n\treturn nil\n}\n\n// R creates a new Request associated with the client.\nfunc (c *Client) R() *Request {\n\treturn AcquireRequest().SetClient(c)\n}\n\n// RequestHook returns the user-defined request hooks.\nfunc (c *Client) RequestHook() []RequestHook {\n\treturn c.userRequestHooks\n}\n\n// AddRequestHook adds user-defined request hooks.\nfunc (c *Client) AddRequestHook(h ...RequestHook) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.userRequestHooks = append(c.userRequestHooks, h...)\n\treturn c\n}\n\n// ResponseHook returns the user-defined response hooks.\nfunc (c *Client) ResponseHook() []ResponseHook {\n\treturn c.userResponseHooks\n}\n\n// AddResponseHook adds user-defined response hooks.\nfunc (c *Client) AddResponseHook(h ...ResponseHook) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.userResponseHooks = append(c.userResponseHooks, h...)\n\treturn c\n}\n\n// JSONMarshal returns the JSON marshal function used by the client.\nfunc (c *Client) JSONMarshal() utils.JSONMarshal {\n\treturn c.jsonMarshal\n}\n\n// SetJSONMarshal sets the JSON marshal function to use.\nfunc (c *Client) SetJSONMarshal(f utils.JSONMarshal) *Client {\n\tc.jsonMarshal = f\n\treturn c\n}\n\n// JSONUnmarshal returns the JSON unmarshal function used by the client.\nfunc (c *Client) JSONUnmarshal() utils.JSONUnmarshal {\n\treturn c.jsonUnmarshal\n}\n\n// SetJSONUnmarshal sets the JSON unmarshal function to use.\nfunc (c *Client) SetJSONUnmarshal(f utils.JSONUnmarshal) *Client {\n\tc.jsonUnmarshal = f\n\treturn c\n}\n\n// XMLMarshal returns the XML marshal function used by the client.\nfunc (c *Client) XMLMarshal() utils.XMLMarshal {\n\treturn c.xmlMarshal\n}\n\n// SetXMLMarshal sets the XML marshal function to use.\nfunc (c *Client) SetXMLMarshal(f utils.XMLMarshal) *Client {\n\tc.xmlMarshal = f\n\treturn c\n}\n\n// XMLUnmarshal returns the XML unmarshal function used by the client.\nfunc (c *Client) XMLUnmarshal() utils.XMLUnmarshal {\n\treturn c.xmlUnmarshal\n}\n\n// SetXMLUnmarshal sets the XML unmarshal function to use.\nfunc (c *Client) SetXMLUnmarshal(f utils.XMLUnmarshal) *Client {\n\tc.xmlUnmarshal = f\n\treturn c\n}\n\n// CBORMarshal returns the CBOR marshal function used by the client.\nfunc (c *Client) CBORMarshal() utils.CBORMarshal {\n\treturn c.cborMarshal\n}\n\n// SetCBORMarshal sets the CBOR marshal function to use.\nfunc (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client {\n\tc.cborMarshal = f\n\treturn c\n}\n\n// CBORUnmarshal returns the CBOR unmarshal function used by the client.\nfunc (c *Client) CBORUnmarshal() utils.CBORUnmarshal {\n\treturn c.cborUnmarshal\n}\n\n// SetCBORUnmarshal sets the CBOR unmarshal function to use.\nfunc (c *Client) SetCBORUnmarshal(f utils.CBORUnmarshal) *Client {\n\tc.cborUnmarshal = f\n\treturn c\n}\n\n// TLSConfig returns the client's TLS configuration.\n// If none is set, it initializes a new one.\nfunc (c *Client) TLSConfig() *tls.Config {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif cfg := c.currentTLSConfig(); cfg != nil {\n\t\treturn cfg\n\t}\n\tcfg := &tls.Config{MinVersion: tls.VersionTLS12}\n\tc.applyTLSConfig(cfg)\n\treturn cfg\n}\n\n// SetTLSConfig sets the TLS configuration for the client.\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.applyTLSConfig(config)\n\treturn c\n}\n\n// SetCertificates adds certificates to the client's TLS configuration.\nfunc (c *Client) SetCertificates(certs ...tls.Certificate) *Client {\n\tconfig := c.TLSConfig()\n\tconfig.Certificates = append(config.Certificates, certs...)\n\treturn c\n}\n\n// SetRootCertificate adds one or more root certificates to the client's TLS configuration.\nfunc (c *Client) SetRootCertificate(path string) *Client {\n\tcleanPath := filepath.Clean(path)\n\tfile, err := os.Open(cleanPath)\n\tif err != nil {\n\t\tc.logger.Panicf(\"client: %v\", err)\n\t}\n\tdefer func() {\n\t\tif closeErr := file.Close(); closeErr != nil {\n\t\t\tc.logger.Panicf(\"client: failed to close file: %v\", closeErr)\n\t\t}\n\t}()\n\n\tpem, err := io.ReadAll(file)\n\tif err != nil {\n\t\tc.logger.Panicf(\"client: %v\", err)\n\t}\n\n\tconfig := c.TLSConfig()\n\tif config.RootCAs == nil {\n\t\tconfig.RootCAs = x509.NewCertPool()\n\t}\n\n\tif !config.RootCAs.AppendCertsFromPEM(pem) {\n\t\tc.logger.Panicf(\"client: %v\", ErrFailedToAppendCert)\n\t}\n\n\treturn c\n}\n\n// SetRootCertificateFromString adds one or more root certificates from a string to the client's TLS configuration.\nfunc (c *Client) SetRootCertificateFromString(pem string) *Client {\n\tconfig := c.TLSConfig()\n\n\tif config.RootCAs == nil {\n\t\tconfig.RootCAs = x509.NewCertPool()\n\t}\n\n\tif !config.RootCAs.AppendCertsFromPEM([]byte(pem)) {\n\t\tc.logger.Panicf(\"client: %v\", ErrFailedToAppendCert)\n\t}\n\n\treturn c\n}\n\n// SetProxyURL sets the proxy URL for the client. This affects all subsequent requests.\nfunc (c *Client) SetProxyURL(proxyURL string) error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.applyDial(fasthttpproxy.FasthttpHTTPDialer(proxyURL))\n\treturn nil\n}\n\n// RetryConfig returns the current retry configuration.\nfunc (c *Client) RetryConfig() *RetryConfig {\n\treturn c.retryConfig\n}\n\n// SetRetryConfig sets the retry configuration for the client.\nfunc (c *Client) SetRetryConfig(config *RetryConfig) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.retryConfig = config\n\treturn c\n}\n\n// BaseURL returns the client's base URL.\nfunc (c *Client) BaseURL() string {\n\treturn c.baseURL\n}\n\n// SetBaseURL sets the base URL prefix for all requests made by the client.\nfunc (c *Client) SetBaseURL(url string) *Client {\n\tc.baseURL = url\n\treturn c\n}\n\n// Header returns all header values associated with the provided key.\nfunc (c *Client) Header(key string) []string {\n\treturn c.header.PeekMultiple(key)\n}\n\n// AddHeader adds a single header field and its value to the client. These headers apply to all requests.\nfunc (c *Client) AddHeader(key, val string) *Client {\n\tc.header.Add(key, val)\n\treturn c\n}\n\n// SetHeader sets a single header field and its value in the client.\nfunc (c *Client) SetHeader(key, val string) *Client {\n\tc.header.Set(key, val)\n\treturn c\n}\n\n// AddHeaders adds multiple header fields and their values to the client.\nfunc (c *Client) AddHeaders(h map[string][]string) *Client {\n\tc.header.AddHeaders(h)\n\treturn c\n}\n\n// SetHeaders method sets multiple headers field and its values at one go in the client instance.\n// These headers will be applied to all requests created from this client instance. Also it can be\n// overridden at request level headers options.\nfunc (c *Client) SetHeaders(h map[string]string) *Client {\n\tc.header.SetHeaders(h)\n\treturn c\n}\n\n// Param returns all values of the specified query parameter.\nfunc (c *Client) Param(key string) []string {\n\ttmp := c.params.PeekMulti(key)\n\tres := make([]string, 0, len(tmp))\n\tfor _, v := range tmp {\n\t\tres = append(res, utils.UnsafeString(v))\n\t}\n\n\treturn res\n}\n\n// AddParam adds a single query parameter and its value to the client.\n// These params will be applied to all requests created from this client instance.\nfunc (c *Client) AddParam(key, val string) *Client {\n\tc.params.Add(key, val)\n\treturn c\n}\n\n// SetParam sets a single query parameter and its value in the client.\nfunc (c *Client) SetParam(key, val string) *Client {\n\tc.params.Set(key, val)\n\treturn c\n}\n\n// AddParams adds multiple query parameters and their values to the client.\nfunc (c *Client) AddParams(m map[string][]string) *Client {\n\tc.params.AddParams(m)\n\treturn c\n}\n\n// SetParams sets multiple query parameters and their values in the client.\nfunc (c *Client) SetParams(m map[string]string) *Client {\n\tc.params.SetParams(m)\n\treturn c\n}\n\n// SetParamsWithStruct sets multiple query parameters and their values using a struct.\nfunc (c *Client) SetParamsWithStruct(v any) *Client {\n\tc.params.SetParamsWithStruct(v)\n\treturn c\n}\n\n// DelParams deletes one or more query parameters and their values from the client.\nfunc (c *Client) DelParams(key ...string) *Client {\n\tfor _, v := range key {\n\t\tc.params.Del(v)\n\t}\n\treturn c\n}\n\n// SetUserAgent sets the User-Agent header for the client.\nfunc (c *Client) SetUserAgent(ua string) *Client {\n\tc.userAgent = ua\n\treturn c\n}\n\n// SetReferer sets the Referer header for the client.\nfunc (c *Client) SetReferer(r string) *Client {\n\tc.referer = r\n\treturn c\n}\n\n// DisablePathNormalizing reports whether path normalizing is disabled for the client.\nfunc (c *Client) DisablePathNormalizing() bool {\n\treturn c.disablePathNormalizing\n}\n\n// SetDisablePathNormalizing configures the client to disable or enable path normalizing.\nfunc (c *Client) SetDisablePathNormalizing(disable bool) *Client {\n\tc.disablePathNormalizing = disable\n\treturn c\n}\n\n// PathParam returns the value of the specified path parameter. Returns an empty string if it does not exist.\nfunc (c *Client) PathParam(key string) string {\n\tif val, ok := (*c.path)[key]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n\n// SetPathParam sets a single path parameter and its value in the client.\nfunc (c *Client) SetPathParam(key, val string) *Client {\n\tc.path.SetParam(key, val)\n\treturn c\n}\n\n// SetPathParams sets multiple path parameters and their values in the client.\nfunc (c *Client) SetPathParams(m map[string]string) *Client {\n\tc.path.SetParams(m)\n\treturn c\n}\n\n// SetPathParamsWithStruct sets multiple path parameters and their values using a struct.\nfunc (c *Client) SetPathParamsWithStruct(v any) *Client {\n\tc.path.SetParamsWithStruct(v)\n\treturn c\n}\n\n// DelPathParams deletes one or more path parameters and their values from the client.\nfunc (c *Client) DelPathParams(key ...string) *Client {\n\tc.path.DelParams(key...)\n\treturn c\n}\n\n// Cookie returns the value of the specified cookie. Returns an empty string if it does not exist.\nfunc (c *Client) Cookie(key string) string {\n\tif val, ok := (*c.cookies)[key]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n\n// SetCookie sets a single cookie and its value in the client.\nfunc (c *Client) SetCookie(key, val string) *Client {\n\tc.cookies.SetCookie(key, val)\n\treturn c\n}\n\n// SetCookies sets multiple cookies and their values in the client.\nfunc (c *Client) SetCookies(m map[string]string) *Client {\n\tc.cookies.SetCookies(m)\n\treturn c\n}\n\n// SetCookiesWithStruct sets multiple cookies and their values using a struct.\nfunc (c *Client) SetCookiesWithStruct(v any) *Client {\n\tc.cookies.SetCookiesWithStruct(v)\n\treturn c\n}\n\n// DelCookies deletes one or more cookies and their values from the client.\nfunc (c *Client) DelCookies(key ...string) *Client {\n\tc.cookies.DelCookies(key...)\n\treturn c\n}\n\n// SetTimeout sets the timeout value for the client. This applies to all requests unless overridden at the request level.\nfunc (c *Client) SetTimeout(t time.Duration) *Client {\n\tc.timeout = t\n\treturn c\n}\n\n// Debug enables debug-level logging output.\nfunc (c *Client) Debug() *Client {\n\tc.debug = true\n\treturn c\n}\n\n// DisableDebug disables debug-level logging output.\nfunc (c *Client) DisableDebug() *Client {\n\tc.debug = false\n\treturn c\n}\n\n// StreamResponseBody returns the current StreamResponseBody setting.\nfunc (c *Client) StreamResponseBody() bool {\n\treturn c.transport.StreamResponseBody()\n}\n\n// SetStreamResponseBody enables or disables response body streaming.\n// When enabled, the response body can be read as a stream using BodyStream()\n// instead of being fully loaded into memory. This is useful for large responses\n// or server-sent events.\nfunc (c *Client) SetStreamResponseBody(enable bool) *Client {\n\tc.transport.SetStreamResponseBody(enable)\n\treturn c\n}\n\n// SetCookieJar sets the cookie jar for the client.\nfunc (c *Client) SetCookieJar(cookieJar *CookieJar) *Client {\n\tc.cookieJar = cookieJar\n\treturn c\n}\n\n// Get sends a GET request to the specified URL, similar to axios.\nfunc (c *Client) Get(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Get(url)\n}\n\n// Post sends a POST request to the specified URL, similar to axios.\nfunc (c *Client) Post(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Post(url)\n}\n\n// Head sends a HEAD request to the specified URL, similar to axios.\nfunc (c *Client) Head(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Head(url)\n}\n\n// Put sends a PUT request to the specified URL, similar to axios.\nfunc (c *Client) Put(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Put(url)\n}\n\n// Delete sends a DELETE request to the specified URL, similar to axios.\nfunc (c *Client) Delete(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Delete(url)\n}\n\n// Options sends an OPTIONS request to the specified URL, similar to axios.\nfunc (c *Client) Options(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Options(url)\n}\n\n// Patch sends a PATCH request to the specified URL, similar to axios.\nfunc (c *Client) Patch(url string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Patch(url)\n}\n\n// Custom sends a request with a custom method to the specified URL, similar to axios.\nfunc (c *Client) Custom(url, method string, cfg ...Config) (*Response, error) {\n\treq := AcquireRequest().SetClient(c)\n\tsetConfigToRequest(req, cfg...)\n\treturn req.Custom(url, method)\n}\n\n// SetDial sets the custom dial function for the client.\nfunc (c *Client) SetDial(dial fasthttp.DialFunc) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.applyDial(dial)\n\treturn c\n}\n\n// SetLogger sets the logger instance used by the client.\nfunc (c *Client) SetLogger(logger log.CommonLogger) *Client {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tc.logger = logger\n\treturn c\n}\n\n// Logger returns the logger instance used by the client.\nfunc (c *Client) Logger() log.CommonLogger {\n\treturn c.logger\n}\n\n// Reset resets the client to its default state, clearing most configurations\n// and replacing the underlying transport with a new fasthttp.Client so future\n// requests resume with Fiber's standard transport settings.\nfunc (c *Client) Reset() {\n\tc.transport = newStandardClientTransport(&fasthttp.Client{})\n\tc.baseURL = \"\"\n\tc.timeout = 0\n\tc.userAgent = \"\"\n\tc.referer = \"\"\n\tc.retryConfig = nil\n\tc.debug = false\n\tc.disablePathNormalizing = false\n\n\tif c.cookieJar != nil {\n\t\tc.cookieJar.Release()\n\t\tc.cookieJar = nil\n\t}\n\n\tc.path.Reset()\n\tc.cookies.Reset()\n\tc.header.Reset()\n\tc.params.Reset()\n}\n\n// Config is used to easily set request parameters. Note that when setting a request body,\n// JSON is used as the default serialization mechanism. The priority is:\n// Body > FormData > File.\ntype Config struct {\n\tCtx                    context.Context //nolint:containedctx // It's needed to be stored in the config.\n\tBody                   any\n\tHeader                 map[string]string\n\tParam                  map[string]string\n\tCookie                 map[string]string\n\tPathParam              map[string]string\n\tFormData               map[string]string\n\tUserAgent              string\n\tReferer                string\n\tFile                   []*File\n\tTimeout                time.Duration\n\tMaxRedirects           int\n\tDisablePathNormalizing bool\n}\n\n// setConfigToRequest sets the parameters passed via Config to the Request.\nfunc setConfigToRequest(req *Request, config ...Config) {\n\tif len(config) == 0 {\n\t\treturn\n\t}\n\tcfg := config[0]\n\n\tif cfg.Ctx != nil {\n\t\treq.SetContext(cfg.Ctx)\n\t}\n\n\tif cfg.UserAgent != \"\" {\n\t\treq.SetUserAgent(cfg.UserAgent)\n\t}\n\n\tif cfg.Referer != \"\" {\n\t\treq.SetReferer(cfg.Referer)\n\t}\n\n\tif cfg.Header != nil {\n\t\treq.SetHeaders(cfg.Header)\n\t}\n\n\tif cfg.Param != nil {\n\t\treq.SetParams(cfg.Param)\n\t}\n\n\tif cfg.Cookie != nil {\n\t\treq.SetCookies(cfg.Cookie)\n\t}\n\n\tif cfg.PathParam != nil {\n\t\treq.SetPathParams(cfg.PathParam)\n\t}\n\n\tif cfg.Timeout != 0 {\n\t\treq.SetTimeout(cfg.Timeout)\n\t}\n\n\tif cfg.MaxRedirects != 0 {\n\t\treq.SetMaxRedirects(cfg.MaxRedirects)\n\t}\n\n\tif cfg.DisablePathNormalizing {\n\t\treq.SetDisablePathNormalizing(true)\n\t}\n\n\tif cfg.Body != nil {\n\t\tswitch v := cfg.Body.(type) {\n\t\tcase []byte:\n\t\t\treq.SetRawBody(v)\n\t\tcase string:\n\t\t\treq.SetRawBody([]byte(v))\n\t\tdefault:\n\t\t\treq.SetJSON(cfg.Body)\n\t\t}\n\t\treturn\n\t}\n\n\tif cfg.FormData != nil {\n\t\treq.SetFormDataWithMap(cfg.FormData)\n\t\treturn\n\t}\n\n\tif len(cfg.File) != 0 {\n\t\treq.AddFiles(cfg.File...)\n\t\treturn\n\t}\n}\n\nvar (\n\tdefaultClient    *Client\n\treplaceMu        = sync.Mutex{}\n\tdefaultUserAgent = \"fiber\"\n)\n\nfunc init() {\n\tdefaultClient = New()\n}\n\n// New creates and returns a new Client object.\nfunc New() *Client {\n\t// Follow-up performance optimizations:\n\t// Try to use a pool to reduce the memory allocation cost for the Fiber client and the fasthttp client.\n\t// If possible, also consider pooling other structs (e.g., request headers, cookies, query parameters, path parameters).\n\treturn NewWithClient(&fasthttp.Client{})\n}\n\n// NewWithClient creates and returns a new Client object from an existing fasthttp.Client.\nfunc NewWithClient(c *fasthttp.Client) *Client {\n\tif c == nil {\n\t\tpanic(\"fasthttp.Client must not be nil\")\n\t}\n\treturn newClient(newStandardClientTransport(c))\n}\n\n// NewWithHostClient creates and returns a new Client object from an existing fasthttp.HostClient.\nfunc NewWithHostClient(c *fasthttp.HostClient) *Client {\n\tif c == nil {\n\t\tpanic(\"fasthttp.HostClient must not be nil\")\n\t}\n\treturn newClient(newHostClientTransport(c))\n}\n\n// NewWithLBClient creates and returns a new Client object from an existing fasthttp.LBClient.\nfunc NewWithLBClient(c *fasthttp.LBClient) *Client {\n\tif c == nil {\n\t\tpanic(\"fasthttp.LBClient must not be nil\")\n\t}\n\treturn newClient(newLBClientTransport(c))\n}\n\nfunc newClient(transport httpClientTransport) *Client {\n\treturn &Client{\n\t\ttransport: transport,\n\t\theader: &Header{\n\t\t\tRequestHeader: &fasthttp.RequestHeader{},\n\t\t},\n\t\tparams: &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t},\n\t\tcookies: &Cookie{},\n\t\tpath:    &PathParam{},\n\n\t\tuserRequestHooks:     []RequestHook{},\n\t\tbuiltinRequestHooks:  []RequestHook{parserRequestURL, parserRequestHeader, parserRequestBody},\n\t\tuserResponseHooks:    []ResponseHook{},\n\t\tbuiltinResponseHooks: []ResponseHook{parserResponseCookie, logger},\n\t\tjsonMarshal:          json.Marshal,\n\t\tjsonUnmarshal:        json.Unmarshal,\n\t\txmlMarshal:           xml.Marshal,\n\t\tcborMarshal:          cbor.Marshal,\n\t\tcborUnmarshal:        cbor.Unmarshal,\n\t\txmlUnmarshal:         xml.Unmarshal,\n\t\tlogger:               log.DefaultLogger[*log.Logger](),\n\t}\n}\n\n// C returns the default client.\nfunc C() *Client {\n\treturn defaultClient\n}\n\n// Replace replaces the defaultClient with a new one, returning a function to restore the old client.\nfunc Replace(c *Client) func() {\n\treplaceMu.Lock()\n\tdefer replaceMu.Unlock()\n\n\toldClient := defaultClient\n\tdefaultClient = c\n\n\treturn func() {\n\t\treplaceMu.Lock()\n\t\tdefer replaceMu.Unlock()\n\n\t\tdefaultClient = oldClient\n\t}\n}\n\n// Get sends a GET request using the default client.\nfunc Get(url string, cfg ...Config) (*Response, error) {\n\treturn C().Get(url, cfg...)\n}\n\n// Post sends a POST request using the default client.\nfunc Post(url string, cfg ...Config) (*Response, error) {\n\treturn C().Post(url, cfg...)\n}\n\n// Head sends a HEAD request using the default client.\nfunc Head(url string, cfg ...Config) (*Response, error) {\n\treturn C().Head(url, cfg...)\n}\n\n// Put sends a PUT request using the default client.\nfunc Put(url string, cfg ...Config) (*Response, error) {\n\treturn C().Put(url, cfg...)\n}\n\n// Delete sends a DELETE request using the default client.\nfunc Delete(url string, cfg ...Config) (*Response, error) {\n\treturn C().Delete(url, cfg...)\n}\n\n// Options sends an OPTIONS request using the default client.\nfunc Options(url string, cfg ...Config) (*Response, error) {\n\treturn C().Options(url, cfg...)\n}\n\n// Patch sends a PATCH request using the default client.\nfunc Patch(url string, cfg ...Config) (*Response, error) {\n\treturn C().Patch(url, cfg...)\n}\n"
  },
  {
    "path": "client/client_test.go",
    "content": "package client\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/addon/retry\"\n\t\"github.com/gofiber/fiber/v3/internal/tlstest\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc startTestServerWithPort(t *testing.T, beforeStarting func(app *fiber.App)) (app *fiber.App, addr string) { //nolint:nonamedreturns // gocritic unnamedResult requires explicit result names for clarity when returning app and address\n\tt.Helper()\n\n\tapp = fiber.New()\n\n\tif beforeStarting != nil {\n\t\tbeforeStarting(app)\n\t}\n\n\taddrChan := make(chan string)\n\terrChan := make(chan error, 1)\n\tgo func(server *fiber.App) {\n\t\terr := server.Listen(\":0\", fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t}(app)\n\n\tselect {\n\tcase addr = <-addrChan:\n\t\treturn app, addr\n\tcase err := <-errChan:\n\t\tt.Fatalf(\"Failed to start test server: %v\", err)\n\t}\n\n\treturn nil, \"\"\n}\n\nfunc Test_New_With_Client(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with valid client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := &fasthttp.Client{\n\t\t\tMaxConnsPerHost: 5,\n\t\t}\n\t\tclient := NewWithClient(c)\n\n\t\trequire.NotNil(t, client)\n\t})\n\n\tt.Run(\"with nil client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.PanicsWithValue(t, \"fasthttp.Client must not be nil\", func() {\n\t\t\tNewWithClient(nil)\n\t\t})\n\t})\n}\n\nfunc Test_New_With_HostClient(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with valid host client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thc := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\t\tclient := NewWithHostClient(hc)\n\n\t\trequire.NotNil(t, client)\n\t\trequire.Equal(t, hc, client.HostClient())\n\t\trequire.Nil(t, client.FasthttpClient())\n\t})\n\n\tt.Run(\"with nil host client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.PanicsWithValue(t, \"fasthttp.HostClient must not be nil\", func() {\n\t\t\tNewWithHostClient(nil)\n\t\t})\n\t})\n}\n\nfunc Test_New_With_LBClient(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with valid lb client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlb := &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\t&fasthttp.HostClient{Addr: \"example.com:80\"},\n\t\t\t},\n\t\t}\n\n\t\tclient := NewWithLBClient(lb)\n\n\t\trequire.NotNil(t, client)\n\t\trequire.Equal(t, lb, client.LBClient())\n\t\trequire.Nil(t, client.FasthttpClient())\n\t\trequire.Nil(t, client.HostClient())\n\t})\n\n\tt.Run(\"with nil lb client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.PanicsWithValue(t, \"fasthttp.LBClient must not be nil\", func() {\n\t\t\tNewWithLBClient(nil)\n\t\t})\n\t})\n}\n\nfunc TestClientUnderlyingTransports(t *testing.T) {\n\tt.Parallel()\n\n\tstd := New()\n\trequire.NotNil(t, std.FasthttpClient())\n\trequire.Nil(t, std.HostClient())\n\trequire.Nil(t, std.LBClient())\n\n\thostTransport := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\thost := NewWithHostClient(hostTransport)\n\trequire.Nil(t, host.FasthttpClient())\n\trequire.Same(t, hostTransport, host.HostClient())\n\trequire.Nil(t, host.LBClient())\n\n\tlbClient := &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{hostTransport}}\n\tlb := NewWithLBClient(lbClient)\n\trequire.Nil(t, lb.FasthttpClient())\n\trequire.Nil(t, lb.HostClient())\n\trequire.Same(t, lbClient, lb.LBClient())\n}\n\nfunc TestClientCBORUnmarshalOverride(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\tinitial := client.CBORUnmarshal()\n\trequire.NotNil(t, initial)\n\n\tcalled := false\n\tcustom := func(b []byte, v any) error {\n\t\t_ = b\n\t\t_ = v\n\t\tcalled = true\n\t\treturn nil\n\t}\n\n\tclient.SetCBORUnmarshal(custom)\n\tfn := client.CBORUnmarshal()\n\trequire.NotNil(t, fn)\n\n\tvar payload []byte\n\trequire.NoError(t, fn(payload, nil))\n\trequire.True(t, called)\n}\n\nfunc TestClientSetRootCertificateErrors(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\trequire.Panics(t, func() {\n\t\tclient.SetRootCertificate(\"does-not-exist.pem\")\n\t})\n\n\ttmpDir := t.TempDir()\n\tbadPath := filepath.Join(tmpDir, \"invalid.pem\")\n\trequire.NoError(t, os.WriteFile(badPath, []byte(\"not a pem\"), 0o600))\n\n\trequire.Panics(t, func() {\n\t\tclient.SetRootCertificate(badPath)\n\t})\n}\n\nfunc TestClientSetRootCertificateFromStringError(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\trequire.Panics(t, func() {\n\t\tclient.SetRootCertificateFromString(\"invalid pem data\")\n\t})\n}\n\nfunc TestClientLoggerAccessors(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\t_ = client.Logger()\n\tcontextual := log.WithContext(context.Background())\n\tclient.SetLogger(contextual)\n\trequire.Equal(t, contextual, client.Logger())\n}\n\nfunc TestClientResetClearsState(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\n\tjar := AcquireCookieJar()\n\tjar.hostCookies = map[string][]*fasthttp.Cookie{\"example.com\": {}}\n\tclient.SetCookieJar(jar)\n\n\tclient.SetBaseURL(\"http://example.com\")\n\tclient.SetTimeout(2 * time.Second)\n\tclient.SetUserAgent(\"reset-agent\")\n\tclient.SetReferer(\"reset-ref\")\n\tclient.SetRetryConfig(&RetryConfig{MaxRetryCount: 3})\n\tclient.Debug()\n\tclient.SetDisablePathNormalizing(true)\n\tclient.SetHeaders(map[string]string{\"X-Test\": \"value\"})\n\tclient.SetParams(map[string]string{\"p\": \"1\"})\n\tclient.SetCookies(map[string]string{\"cookie\": \"value\"})\n\tclient.SetPathParams(map[string]string{\"id\": \"123\"})\n\n\tclient.Reset()\n\n\trequire.NotNil(t, client.FasthttpClient())\n\trequire.Nil(t, client.HostClient())\n\trequire.Nil(t, client.LBClient())\n\trequire.Empty(t, client.BaseURL())\n\trequire.Zero(t, client.timeout)\n\trequire.Empty(t, client.userAgent)\n\trequire.Empty(t, client.referer)\n\trequire.Nil(t, client.retryConfig)\n\trequire.False(t, client.debug)\n\trequire.False(t, client.disablePathNormalizing)\n\trequire.Nil(t, client.cookieJar)\n\trequire.Nil(t, jar.hostCookies)\n\trequire.Empty(t, *client.path)\n\trequire.Empty(t, *client.cookies)\n\trequire.Equal(t, 0, client.header.Len())\n\trequire.Equal(t, 0, client.params.Len())\n}\n\nfunc Test_Client_Add_Hook(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add request hooks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbuf := bytebufferpool.Get()\n\t\tdefer bytebufferpool.Put(buf)\n\n\t\tclient := New().AddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook1\")\n\t\t\treturn nil\n\t\t})\n\n\t\trequire.Len(t, client.RequestHook(), 1)\n\n\t\tclient.AddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook2\")\n\t\t\treturn nil\n\t\t}, func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook3\")\n\t\t\treturn nil\n\t\t})\n\n\t\trequire.Len(t, client.RequestHook(), 3)\n\t})\n\n\tt.Run(\"add response hooks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().AddResponseHook(func(_ *Client, _ *Response, _ *Request) error {\n\t\t\treturn nil\n\t\t})\n\n\t\trequire.Len(t, client.ResponseHook(), 1)\n\n\t\thook1 := func(_ *Client, _ *Response, _ *Request) error {\n\t\t\treturn nil\n\t\t}\n\t\thook2 := func(_ *Client, _ *Response, _ *Request) error {\n\t\t\treturn nil\n\t\t}\n\t\tclient.AddResponseHook(hook1, hook2)\n\n\t\trequire.Len(t, client.ResponseHook(), 3)\n\t})\n}\n\nfunc Test_Client_HostClient_Behavior(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"do and redirects\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"ok\")\n\t\t\t})\n\t\t\tapp.Get(\"/redirect\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Redirect().Status(fiber.StatusFound).To(\"/ok\")\n\t\t\t})\n\t\t})\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := NewWithHostClient(&fasthttp.HostClient{Addr: addr})\n\n\t\tresp, err := client.Get(\"http://\" + addr + \"/ok\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"ok\", resp.String())\n\n\t\tresp, err = client.Get(\"http://\"+addr+\"/redirect\", Config{MaxRedirects: 1})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"ok\", resp.String())\n\t})\n\n\tt.Run(\"retries respect dial overrides\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"retry\")\n\t\t\t})\n\t\t})\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := NewWithHostClient(&fasthttp.HostClient{Addr: addr})\n\t\tclient.SetRetryConfig(&RetryConfig{\n\t\t\tInitialInterval: time.Millisecond,\n\t\t\tMaxRetryCount:   2,\n\t\t})\n\n\t\tvar attempts int32\n\t\tclient.SetDial(func(address string) (net.Conn, error) {\n\t\t\tif atomic.AddInt32(&attempts, 1) == 1 {\n\t\t\t\treturn nil, errors.New(\"dial failure\")\n\t\t\t}\n\t\t\treturn fasthttp.Dial(address)\n\t\t})\n\n\t\tresp, err := client.Get(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"retry\", resp.String())\n\t\trequire.EqualValues(t, 2, atomic.LoadInt32(&attempts))\n\t})\n\n\tt.Run(\"tls configuration propagates\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\t\trequire.NoError(t, err)\n\n\t\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\t\trequire.NoError(t, err)\n\t\tln = tls.NewListener(ln, serverTLSConf)\n\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"tls-host\")\n\t\t})\n\n\t\tgo func() {\n\t\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\t\tDisableStartupMessage: true,\n\t\t\t}))\n\t\t}()\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tclient := NewWithHostClient(&fasthttp.HostClient{Addr: ln.Addr().String(), IsTLS: true})\n\n\t\tresp, err := client.SetTLSConfig(clientTLSConf).Get(\"https://\" + ln.Addr().String())\n\t\trequire.NoError(t, err)\n\t\tcfg := client.TLSConfig()\n\t\trequire.Same(t, clientTLSConf, cfg)\n\t\trequire.NotNil(t, cfg.RootCAs)\n\t\trequire.Equal(t, clientTLSConf, client.HostClient().TLSConfig)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"tls-host\", resp.String())\n\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\tt.Run(\"proxy overrides and reset\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := NewWithHostClient(&fasthttp.HostClient{Addr: \"example.com:80\"})\n\n\t\trequire.NoError(t, client.SetProxyURL(\"http://127.0.0.1:8080\"))\n\t\trequire.NotNil(t, client.HostClient().Dial)\n\n\t\tvar called int32\n\t\tcustomDial := func(addr string) (net.Conn, error) {\n\t\t\t_ = addr\n\t\t\tatomic.AddInt32(&called, 1)\n\t\t\treturn nil, errors.New(\"dial\")\n\t\t}\n\t\tclient.SetDial(customDial)\n\n\t\t_, err := client.HostClient().Dial(\"example.com:80\")\n\t\trequire.Error(t, err)\n\t\trequire.EqualValues(t, 1, atomic.LoadInt32(&called))\n\n\t\tclient.Reset()\n\t\trequire.NotNil(t, client.FasthttpClient())\n\t\trequire.Nil(t, client.HostClient())\n\t\trequire.Nil(t, client.LBClient())\n\t})\n\n\tt.Run(\"timeouts and close idle\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := NewWithHostClient(&fasthttp.HostClient{Addr: \"example.com:80\"})\n\n\t\tvar dialCalls int32\n\t\tdialErr := errors.New(\"dial failed\")\n\t\tclient.HostClient().Dial = func(addr string) (net.Conn, error) {\n\t\t\t_ = addr\n\t\t\tatomic.AddInt32(&dialCalls, 1)\n\t\t\treturn nil, dialErr\n\t\t}\n\n\t\treq := fasthttp.AcquireRequest()\n\t\tresp := fasthttp.AcquireResponse()\n\t\treq.SetRequestURI(\"http://example.com/\")\n\n\t\terr := client.DoTimeout(req, resp, 5*time.Millisecond)\n\t\trequire.ErrorIs(t, err, dialErr)\n\n\t\treq.SetRequestURI(\"http://example.com/\")\n\t\terr = client.DoDeadline(req, resp, time.Now().Add(5*time.Millisecond))\n\t\trequire.ErrorIs(t, err, dialErr)\n\n\t\tfasthttp.ReleaseRequest(req)\n\t\tfasthttp.ReleaseResponse(resp)\n\n\t\trequire.EqualValues(t, 2, atomic.LoadInt32(&dialCalls))\n\t\trequire.NotPanics(t, client.CloseIdleConnections)\n\t})\n}\n\nfunc Test_Client_LBClient_Behavior(t *testing.T) {\n\tt.Parallel()\n\n\tnewLBClient := func(addr string) *fasthttp.LBClient {\n\t\treturn &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\t&fasthttp.HostClient{Addr: addr},\n\t\t\t},\n\t\t}\n\t}\n\n\tt.Run(\"do and redirects\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"ok\")\n\t\t\t})\n\t\t\tapp.Get(\"/redirect\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Redirect().Status(fiber.StatusFound).To(\"/ok\")\n\t\t\t})\n\t\t})\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := NewWithLBClient(newLBClient(addr))\n\n\t\tresp, err := client.Get(\"http://\" + addr + \"/ok\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"ok\", resp.String())\n\n\t\tresp, err = client.Get(\"http://\"+addr+\"/redirect\", Config{MaxRedirects: 1})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"ok\", resp.String())\n\t})\n\n\tt.Run(\"retries respect dial overrides\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"retry\")\n\t\t\t})\n\t\t})\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := NewWithLBClient(newLBClient(addr))\n\t\tclient.SetRetryConfig(&RetryConfig{\n\t\t\tInitialInterval: time.Millisecond,\n\t\t\tMaxRetryCount:   2,\n\t\t})\n\n\t\tvar attempts int32\n\t\tclient.SetDial(func(address string) (net.Conn, error) {\n\t\t\tif atomic.AddInt32(&attempts, 1) == 1 {\n\t\t\t\treturn nil, errors.New(\"dial failure\")\n\t\t\t}\n\t\t\treturn fasthttp.Dial(address)\n\t\t})\n\n\t\tresp, err := client.Get(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"retry\", resp.String())\n\t\trequire.EqualValues(t, 2, atomic.LoadInt32(&attempts))\n\t})\n\n\tt.Run(\"tls configuration propagates\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\t\trequire.NoError(t, err)\n\n\t\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\t\trequire.NoError(t, err)\n\t\tln = tls.NewListener(ln, serverTLSConf)\n\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"tls-lb\")\n\t\t})\n\n\t\tgo func() {\n\t\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\t\tDisableStartupMessage: true,\n\t\t\t}))\n\t\t}()\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tlb := newLBClient(ln.Addr().String())\n\t\thost, ok := lb.Clients[0].(*fasthttp.HostClient)\n\t\trequire.True(t, ok)\n\t\thost.IsTLS = true\n\t\tclient := NewWithLBClient(lb)\n\n\t\tresp, err := client.SetTLSConfig(clientTLSConf).Get(\"https://\" + ln.Addr().String())\n\t\trequire.NoError(t, err)\n\t\tcfg := client.TLSConfig()\n\t\trequire.Same(t, clientTLSConf, cfg)\n\t\trequire.NotNil(t, cfg.RootCAs)\n\n\t\thc, ok := client.LBClient().Clients[0].(*fasthttp.HostClient)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, clientTLSConf, hc.TLSConfig)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"tls-lb\", resp.String())\n\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\tt.Run(\"proxy overrides and reset\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := NewWithLBClient(&fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\t&fasthttp.HostClient{Addr: \"example.com:80\"},\n\t\t\t},\n\t\t})\n\n\t\trequire.NoError(t, client.SetProxyURL(\"http://127.0.0.1:8080\"))\n\n\t\thc, ok := client.LBClient().Clients[0].(*fasthttp.HostClient)\n\t\trequire.True(t, ok)\n\t\trequire.NotNil(t, hc.Dial)\n\n\t\tvar called int32\n\t\tcustomDial := func(addr string) (net.Conn, error) {\n\t\t\t_ = addr\n\t\t\tatomic.AddInt32(&called, 1)\n\t\t\treturn nil, errors.New(\"dial\")\n\t\t}\n\t\tclient.SetDial(customDial)\n\n\t\t_, err := hc.Dial(\"example.com:80\")\n\t\trequire.Error(t, err)\n\t\trequire.EqualValues(t, 1, atomic.LoadInt32(&called))\n\n\t\tclient.Reset()\n\t\trequire.NotNil(t, client.FasthttpClient())\n\t\trequire.Nil(t, client.LBClient())\n\t\trequire.Nil(t, client.HostClient())\n\t})\n\n\tt.Run(\"timeouts and close idle\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := NewWithLBClient(&fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\t&fasthttp.HostClient{Addr: \"example.com:80\"},\n\t\t\t\t&fasthttp.HostClient{Addr: \"example.org:80\"},\n\t\t\t},\n\t\t})\n\n\t\tvar dialCalls int32\n\t\tdialErr := errors.New(\"dial failed\")\n\t\tfor _, bc := range client.LBClient().Clients {\n\t\t\tif hc, ok := bc.(*fasthttp.HostClient); ok {\n\t\t\t\thc.Dial = func(addr string) (net.Conn, error) {\n\t\t\t\t\t_ = addr\n\t\t\t\t\tatomic.AddInt32(&dialCalls, 1)\n\t\t\t\t\treturn nil, dialErr\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treq := fasthttp.AcquireRequest()\n\t\tresp := fasthttp.AcquireResponse()\n\t\treq.SetRequestURI(\"http://example.com/\")\n\n\t\terr := client.DoTimeout(req, resp, 5*time.Millisecond)\n\t\trequire.ErrorIs(t, err, dialErr)\n\n\t\treq.SetRequestURI(\"http://example.com/\")\n\t\terr = client.DoDeadline(req, resp, time.Now().Add(5*time.Millisecond))\n\t\trequire.ErrorIs(t, err, dialErr)\n\n\t\tfasthttp.ReleaseRequest(req)\n\t\tfasthttp.ReleaseResponse(resp)\n\n\t\trequire.GreaterOrEqual(t, atomic.LoadInt32(&dialCalls), int32(2))\n\t\trequire.NotPanics(t, client.CloseIdleConnections)\n\t})\n}\n\nfunc Test_Client_Add_Hook_CheckOrder(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tclient := New().\n\t\tAddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook1\")\n\t\t\treturn nil\n\t\t}).\n\t\tAddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook2\")\n\t\t\treturn nil\n\t\t}).\n\t\tAddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\tbuf.WriteString(\"hook3\")\n\t\t\treturn nil\n\t\t})\n\n\tfor _, hook := range client.RequestHook() {\n\t\trequire.NoError(t, hook(client, &Request{}))\n\t}\n\n\trequire.Equal(t, \"hook1hook2hook3\", buf.String())\n}\n\nfunc Test_Client_Marshal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set json marshal\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetJSONMarshal(func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(\"hello\"), nil\n\t\t\t})\n\t\tval, err := client.JSONMarshal()(nil)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"hello\"), val)\n\t})\n\n\tt.Run(\"set json marshal error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\temptyErr := errors.New(\"empty json\")\n\t\tclient := New().\n\t\t\tSetJSONMarshal(func(_ any) ([]byte, error) {\n\t\t\t\treturn nil, emptyErr\n\t\t\t})\n\n\t\tval, err := client.JSONMarshal()(nil)\n\t\trequire.Nil(t, val)\n\t\trequire.ErrorIs(t, err, emptyErr)\n\t})\n\n\tt.Run(\"set json unmarshal\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetJSONUnmarshal(func(_ []byte, _ any) error {\n\t\t\t\treturn errors.New(\"empty json\")\n\t\t\t})\n\n\t\terr := client.JSONUnmarshal()(nil, nil)\n\t\trequire.Equal(t, errors.New(\"empty json\"), err)\n\t})\n\n\tt.Run(\"set json unmarshal error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetJSONUnmarshal(func(_ []byte, _ any) error {\n\t\t\t\treturn errors.New(\"empty json\")\n\t\t\t})\n\n\t\terr := client.JSONUnmarshal()(nil, nil)\n\t\trequire.Equal(t, errors.New(\"empty json\"), err)\n\t})\n\n\tt.Run(\"set xml marshal\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetXMLMarshal(func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(\"hello\"), nil\n\t\t\t})\n\t\tval, err := client.XMLMarshal()(nil)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"hello\"), val)\n\t})\n\n\tt.Run(\"set xml marshal error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetXMLMarshal(func(_ any) ([]byte, error) {\n\t\t\t\treturn nil, errors.New(\"empty xml\")\n\t\t\t})\n\n\t\tval, err := client.XMLMarshal()(nil)\n\t\trequire.Nil(t, val)\n\t\trequire.Equal(t, errors.New(\"empty xml\"), err)\n\t})\n\n\tt.Run(\"set cbor marshal\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tbs, err := hex.DecodeString(\"f6\")\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tclient := New().\n\t\t\tSetCBORMarshal(func(_ any) ([]byte, error) {\n\t\t\t\treturn bs, nil\n\t\t\t})\n\t\tval, err := client.CBORMarshal()(nil)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, bs, val)\n\t})\n\n\tt.Run(\"set cbor marshal error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetCBORMarshal(func(_ any) ([]byte, error) {\n\t\t\treturn nil, errors.New(\"invalid struct\")\n\t\t})\n\n\t\tval, err := client.CBORMarshal()(nil)\n\t\trequire.Nil(t, val)\n\t\trequire.Equal(t, errors.New(\"invalid struct\"), err)\n\t})\n\n\tt.Run(\"set xml unmarshal\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetXMLUnmarshal(func(_ []byte, _ any) error {\n\t\t\t\treturn errors.New(\"empty xml\")\n\t\t\t})\n\n\t\terr := client.XMLUnmarshal()(nil, nil)\n\t\trequire.Equal(t, errors.New(\"empty xml\"), err)\n\t})\n\n\tt.Run(\"set xml unmarshal error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetXMLUnmarshal(func(_ []byte, _ any) error {\n\t\t\t\treturn errors.New(\"empty xml\")\n\t\t\t})\n\n\t\terr := client.XMLUnmarshal()(nil, nil)\n\t\trequire.Equal(t, errors.New(\"empty xml\"), err)\n\t})\n}\n\nfunc Test_Client_SetBaseURL(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New().SetBaseURL(\"http://example.com\")\n\n\trequire.Equal(t, \"http://example.com\", client.BaseURL())\n}\n\nfunc Test_Client_Invalid_URL(t *testing.T) {\n\tt.Parallel()\n\n\tapp, dial, start := createHelperServer(t)\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tgo start()\n\n\t_, err := New().SetDial(dial).\n\t\tR().\n\t\tGet(\"http//example\")\n\n\trequire.ErrorIs(t, err, ErrURLFormat)\n}\n\nfunc Test_Client_Unsupported_Protocol(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := New().\n\t\tR().\n\t\tGet(\"ftp://example.com\")\n\n\trequire.ErrorIs(t, err, ErrURLFormat)\n}\n\nfunc Test_Client_ConcurrencyRequests(t *testing.T) {\n\tt.Parallel()\n\n\tapp, dial, start := createHelperServer(t)\n\tapp.All(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname() + \" \" + c.Method())\n\t})\n\tgo start()\n\n\tclient := New().SetDial(dial)\n\n\twg := sync.WaitGroup{}\n\tfor range 5 {\n\t\tfor _, method := range []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"} {\n\t\t\tm := method\n\t\t\twg.Go(func() {\n\t\t\t\tresp, err := client.Custom(\"http://example.com\", m)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, \"example.com \"+m, utils.UnsafeString(resp.RawResponse.Body()))\n\t\t\t})\n\t\t}\n\t}\n\n\twg.Wait()\n}\n\nfunc Test_Get(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(c.Hostname())\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global get function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tresp, err := Get(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"0.0.0.0\", utils.UnsafeString(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"client get\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tresp, err := New().Get(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"0.0.0.0\", utils.UnsafeString(resp.RawResponse.Body()))\n\t})\n}\n\nfunc Test_Head(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Head(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(c.Hostname())\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global head function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tresp, err := Head(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"7\", resp.Header(fiber.HeaderContentLength))\n\t\trequire.Empty(t, utils.UnsafeString(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"client head\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tresp, err := New().Head(\"http://\" + addr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"7\", resp.Header(fiber.HeaderContentLength))\n\t\trequire.Empty(t, utils.UnsafeString(resp.RawResponse.Body()))\n\t})\n}\n\nfunc Test_Post(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Status(fiber.StatusCreated).\n\t\t\t\t\tSendString(c.FormValue(\"foo\"))\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global post function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := Post(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusCreated, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"client post\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := New().Post(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusCreated, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Put(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Put(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(c.FormValue(\"foo\"))\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global put function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := Put(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"client put\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := New().Put(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Delete(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Delete(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Status(fiber.StatusNoContent).\n\t\t\t\t\tSendString(\"deleted\")\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global delete function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tfor range 5 {\n\t\t\tresp, err := Delete(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode())\n\t\t\trequire.Empty(t, resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"client delete\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := New().Delete(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode())\n\t\t\trequire.Empty(t, resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Options(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Options(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tc.Set(fiber.HeaderAllow, \"GET, POST, PUT, DELETE, PATCH\")\n\t\t\t\treturn c.Status(fiber.StatusNoContent).SendString(\"\")\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global options function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := Options(\"http://\" + addr)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"GET, POST, PUT, DELETE, PATCH\", resp.Header(fiber.HeaderAllow))\n\t\t\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode())\n\t\t\trequire.Empty(t, resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"client options\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := New().Options(\"http://\" + addr)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"GET, POST, PUT, DELETE, PATCH\", resp.Header(fiber.HeaderAllow))\n\t\t\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode())\n\t\t\trequire.Empty(t, resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Patch(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Patch(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(c.FormValue(\"foo\"))\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"global patch function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tfor range 5 {\n\t\t\tresp, err := Patch(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"client patch\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := New().Patch(\"http://\"+addr, Config{\n\t\t\t\tFormData: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, \"bar\", resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Client_UserAgent(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() (*fiber.App, string) {\n\t\tapp, addr := startTestServerWithPort(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Send(c.Request().Header.UserAgent())\n\t\t\t})\n\t\t})\n\n\t\treturn app, addr\n\t}\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tresp, err := Get(\"http://\" + addr)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, defaultUserAgent, resp.String())\n\t\t}\n\t})\n\n\tt.Run(\"custom\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, addr := setupApp()\n\t\tdefer func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t}()\n\n\t\tfor range 5 {\n\t\t\tc := New().\n\t\t\t\tSetUserAgent(\"ua\")\n\n\t\t\tresp, err := c.Get(\"http://\" + addr)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\t\trequire.Equal(t, \"ua\", resp.String())\n\t\t}\n\t})\n}\n\nfunc Test_Client_Header(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.AddHeader(\"foo\", \"bar\").AddHeader(\"foo\", \"fiber\")\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n\n\tt.Run(\"set header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.AddHeader(\"foo\", \"bar\").SetHeader(\"foo\", \"fiber\")\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\t})\n\n\tt.Run(\"add headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetHeader(\"foo\", \"bar\").\n\t\t\tAddHeaders(map[string][]string{\n\t\t\t\t\"foo\": {\"fiber\", \"buaa\"},\n\t\t\t\t\"bar\": {\"foo\"},\n\t\t\t})\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 3)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t\trequire.Equal(t, \"buaa\", res[2])\n\n\t\tres = req.Header(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetHeader(\"foo\", \"bar\").\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\n\t\tres = req.Header(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set header case-insensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetHeader(\"foo\", \"bar\").\n\t\t\tAddHeader(\"FOO\", \"fiber\")\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n}\n\nfunc Test_Client_Header_With_Server(t *testing.T) {\n\thandler := func(c fiber.Ctx) error {\n\t\tfor key, value := range c.Request().Header.All() {\n\t\t\tif k := string(key); k == \"K1\" || k == \"K2\" {\n\t\t\t\t_, _ = c.Write(key)   //nolint:errcheck // It is fine to ignore the error here\n\t\t\t\t_, _ = c.Write(value) //nolint:errcheck // It is fine to ignore the error here\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\twrapAgent := func(c *Client) {\n\t\tc.SetHeader(\"k1\", \"v1\").\n\t\t\tAddHeader(\"k1\", \"v11\").\n\t\t\tAddHeaders(map[string][]string{\n\t\t\t\t\"k1\": {\"v22\", \"v33\"},\n\t\t\t}).\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\t\"k2\": \"v2\",\n\t\t\t}).\n\t\t\tAddHeader(\"k2\", \"v22\")\n\t}\n\n\ttestClient(t, handler, wrapAgent, \"K1v1K1v11K1v22K1v33K2v2K2v22\")\n}\n\nfunc Test_Client_Cookie(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetCookie(\"foo\", \"bar\")\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\n\t\treq.SetCookie(\"foo\", \"bar1\")\n\t\trequire.Equal(t, \"bar1\", req.Cookie(\"foo\"))\n\t})\n\n\tt.Run(\"set cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\n\t\treq.SetCookies(map[string]string{\n\t\t\t\"foo\": \"bar1\",\n\t\t})\n\t\trequire.Equal(t, \"bar1\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\t})\n\n\tt.Run(\"set cookies with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype args struct {\n\t\t\tCookieString string `cookie:\"string\"`\n\t\t\tCookieInt    int    `cookie:\"int\"`\n\t\t}\n\n\t\treq := New().SetCookiesWithStruct(&args{\n\t\t\tCookieInt:    5,\n\t\t\tCookieString: \"foo\",\n\t\t})\n\n\t\trequire.Equal(t, \"5\", req.Cookie(\"int\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"string\"))\n\t})\n\n\tt.Run(\"del cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\n\t\treq.DelCookies(\"foo\")\n\t\trequire.Empty(t, req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\t})\n}\n\nfunc Test_Client_Cookie_With_Server(t *testing.T) {\n\tt.Parallel()\n\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\n\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\") + c.Cookies(\"k3\") + c.Cookies(\"k4\"))\n\t}\n\n\twrapAgent := func(c *Client) {\n\t\tc.SetCookie(\"k1\", \"v1\").\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"k2\": \"v2\",\n\t\t\t\t\"k3\": \"v3\",\n\t\t\t\t\"k4\": \"v4\",\n\t\t\t}).DelCookies(\"k4\")\n\t}\n\n\ttestClient(t, handler, wrapAgent, \"v1v2v3\")\n}\n\nfunc Test_Client_CookieJar(t *testing.T) {\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\n\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\") + c.Cookies(\"k3\"))\n\t}\n\n\tjar := AcquireCookieJar()\n\tdefer ReleaseCookieJar(jar)\n\n\tjar.SetKeyValue(\"example.com\", \"k1\", \"v1\")\n\tjar.SetKeyValue(\"example.com\", \"k2\", \"v2\")\n\tjar.SetKeyValue(\"example\", \"k3\", \"v3\")\n\n\twrapAgent := func(c *Client) {\n\t\tc.SetCookieJar(jar)\n\t}\n\ttestClient(t, handler, wrapAgent, \"v1v2\")\n}\n\nfunc Test_Client_CookieJar_Response(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"without expiration\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thandler := func(c fiber.Ctx) error {\n\t\t\tc.Cookie(&fiber.Cookie{\n\t\t\t\tName:  \"k4\",\n\t\t\t\tValue: \"v4\",\n\t\t\t})\n\t\t\treturn c.SendString(\n\t\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\") + c.Cookies(\"k3\"))\n\t\t}\n\n\t\tjar := AcquireCookieJar()\n\t\tdefer ReleaseCookieJar(jar)\n\n\t\tjar.SetKeyValue(\"example.com\", \"k1\", \"v1\")\n\t\tjar.SetKeyValue(\"example.com\", \"k2\", \"v2\")\n\t\tjar.SetKeyValue(\"example\", \"k3\", \"v3\")\n\n\t\twrapAgent := func(c *Client) {\n\t\t\tc.SetCookieJar(jar)\n\t\t}\n\t\ttestClient(t, handler, wrapAgent, \"v1v2\")\n\n\t\trequire.Len(t, jar.getCookiesByHost(\"example.com\"), 3)\n\t})\n\n\tt.Run(\"with expiration\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thandler := func(c fiber.Ctx) error {\n\t\t\tc.Cookie(&fiber.Cookie{\n\t\t\t\tName:    \"k4\",\n\t\t\t\tValue:   \"v4\",\n\t\t\t\tExpires: time.Now().Add(1 * time.Nanosecond),\n\t\t\t})\n\t\t\treturn c.SendString(\n\t\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\") + c.Cookies(\"k3\"))\n\t\t}\n\n\t\tjar := AcquireCookieJar()\n\t\tdefer ReleaseCookieJar(jar)\n\n\t\tjar.SetKeyValue(\"example.com\", \"k1\", \"v1\")\n\t\tjar.SetKeyValue(\"example.com\", \"k2\", \"v2\")\n\t\tjar.SetKeyValue(\"example\", \"k3\", \"v3\")\n\n\t\twrapAgent := func(c *Client) {\n\t\t\tc.SetCookieJar(jar)\n\t\t}\n\t\ttestClient(t, handler, wrapAgent, \"v1v2\")\n\n\t\trequire.Len(t, jar.getCookiesByHost(\"example.com\"), 2)\n\t})\n\n\tt.Run(\"override cookie value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thandler := func(c fiber.Ctx) error {\n\t\t\tc.Cookie(&fiber.Cookie{\n\t\t\t\tName:  \"k1\",\n\t\t\t\tValue: \"v2\",\n\t\t\t})\n\t\t\treturn c.SendString(\n\t\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\"))\n\t\t}\n\n\t\tjar := AcquireCookieJar()\n\t\tdefer ReleaseCookieJar(jar)\n\n\t\tjar.SetKeyValue(\"example.com\", \"k1\", \"v1\")\n\t\tjar.SetKeyValue(\"example.com\", \"k2\", \"v2\")\n\n\t\twrapAgent := func(c *Client) {\n\t\t\tc.SetCookieJar(jar)\n\t\t}\n\t\ttestClient(t, handler, wrapAgent, \"v1v2\")\n\n\t\tfor _, cookie := range jar.getCookiesByHost(\"example.com\") {\n\t\t\tif string(cookie.Key()) == \"k1\" {\n\t\t\t\trequire.Equal(t, \"v2\", string(cookie.Value()))\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"different domain\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thandler := func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(c.Cookies(\"k1\"))\n\t\t}\n\n\t\tjar := AcquireCookieJar()\n\t\tdefer ReleaseCookieJar(jar)\n\n\t\tjar.SetKeyValue(\"example.com\", \"k1\", \"v1\")\n\n\t\twrapAgent := func(c *Client) {\n\t\t\tc.SetCookieJar(jar)\n\t\t}\n\t\ttestClient(t, handler, wrapAgent, \"v1\")\n\n\t\trequire.Len(t, jar.getCookiesByHost(\"example.com\"), 1)\n\t\trequire.Empty(t, jar.getCookiesByHost(\"example\"))\n\t})\n}\n\nfunc Test_Client_Referer(t *testing.T) {\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.Send(c.Request().Header.Referer())\n\t}\n\n\twrapAgent := func(c *Client) {\n\t\tc.SetReferer(\"http://referer.com\")\n\t}\n\n\ttestClient(t, handler, wrapAgent, \"http://referer.com\")\n}\n\nfunc Test_Client_QueryParam(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.AddParam(\"foo\", \"bar\").AddParam(\"foo\", \"fiber\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n\n\tt.Run(\"set param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.AddParam(\"foo\", \"bar\").SetParam(\"foo\", \"fiber\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\t})\n\n\tt.Run(\"add params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tAddParams(map[string][]string{\n\t\t\t\t\"foo\": {\"fiber\", \"buaa\"},\n\t\t\t\t\"bar\": {\"foo\"},\n\t\t\t})\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 3)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t\trequire.Equal(t, \"buaa\", res[2])\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tSetParams(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set params with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype args struct {\n\t\t\tTString   string\n\t\t\tTSlice    []string\n\t\t\tTIntSlice []int `param:\"int_slice\"`\n\t\t\tTInt      int\n\t\t\tTFloat    float64\n\t\t\tTBool     bool\n\t\t}\n\n\t\tp := New()\n\t\tp.SetParamsWithStruct(&args{\n\t\t\tTInt:      5,\n\t\t\tTString:   \"string\",\n\t\t\tTFloat:    3.1,\n\t\t\tTBool:     true,\n\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\tTIntSlice: []int{1, 2},\n\t\t})\n\n\t\trequire.Empty(t, p.Param(\"unexport\"))\n\n\t\trequire.Len(t, p.Param(\"TInt\"), 1)\n\t\trequire.Equal(t, \"5\", p.Param(\"TInt\")[0])\n\n\t\trequire.Len(t, p.Param(\"TString\"), 1)\n\t\trequire.Equal(t, \"string\", p.Param(\"TString\")[0])\n\n\t\trequire.Len(t, p.Param(\"TFloat\"), 1)\n\t\trequire.Equal(t, \"3.1\", p.Param(\"TFloat\")[0])\n\n\t\trequire.Len(t, p.Param(\"TBool\"), 1)\n\n\t\ttslice := p.Param(\"TSlice\")\n\t\trequire.Len(t, tslice, 2)\n\t\trequire.Equal(t, \"foo\", tslice[0])\n\t\trequire.Equal(t, \"bar\", tslice[1])\n\n\t\ttint := p.Param(\"TSlice\")\n\t\trequire.Len(t, tint, 2)\n\t\trequire.Equal(t, \"foo\", tint[0])\n\t\trequire.Equal(t, \"bar\", tint[1])\n\t})\n\n\tt.Run(\"del params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tSetParams(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t}).DelParams(\"foo\", \"bar\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Empty(t, res)\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Empty(t, res)\n\t})\n}\n\nfunc Test_Client_QueryParam_With_Server(t *testing.T) {\n\thandler := func(c fiber.Ctx) error {\n\t\t_, _ = c.WriteString(c.Query(\"k1\")) //nolint:errcheck // It is fine to ignore the error here\n\t\t_, _ = c.WriteString(c.Query(\"k2\")) //nolint:errcheck // It is fine to ignore the error here\n\n\t\treturn nil\n\t}\n\n\twrapAgent := func(c *Client) {\n\t\tc.SetParam(\"k1\", \"v1\").\n\t\t\tAddParam(\"k2\", \"v2\")\n\t}\n\n\ttestClient(t, handler, wrapAgent, \"v1v2\")\n}\n\nfunc Test_Client_PathParam(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set path param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetPathParam(\"foo\", \"bar\")\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\n\t\treq.SetPathParam(\"foo\", \"bar1\")\n\t\trequire.Equal(t, \"bar1\", req.PathParam(\"foo\"))\n\t})\n\n\tt.Run(\"set path params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\n\t\treq.SetPathParams(map[string]string{\n\t\t\t\"foo\": \"bar1\",\n\t\t})\n\t\trequire.Equal(t, \"bar1\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\t})\n\n\tt.Run(\"set path params with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype args struct {\n\t\t\tCookieString string `path:\"string\"`\n\t\t\tCookieInt    int    `path:\"int\"`\n\t\t}\n\n\t\treq := New().SetPathParamsWithStruct(&args{\n\t\t\tCookieInt:    5,\n\t\t\tCookieString: \"foo\",\n\t\t})\n\n\t\trequire.Equal(t, \"5\", req.PathParam(\"int\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"string\"))\n\t})\n\n\tt.Run(\"del path params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := New().\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\n\t\treq.DelPathParams(\"foo\")\n\t\trequire.Empty(t, req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\t})\n}\n\nfunc Test_Client_PathParam_With_Server(t *testing.T) {\n\tapp, dial, start := createHelperServer(t)\n\n\tapp.Get(\"/:test\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Params(\"test\"))\n\t})\n\n\tgo start()\n\n\tresp, err := New().SetDial(dial).\n\t\tSetPathParam(\"path\", \"test\").\n\t\tGet(\"http://example.com/:path\")\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"test\", resp.String())\n}\n\nfunc Test_Client_TLS(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tln = tls.NewListener(ln, serverTLSConf)\n\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"tls\")\n\t})\n\n\tgo func() {\n\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t}))\n\t}()\n\ttime.Sleep(1 * time.Second)\n\n\tclient := New()\n\tresp, err := client.SetTLSConfig(clientTLSConf).Get(\"https://\" + ln.Addr().String())\n\n\trequire.NoError(t, err)\n\tcfg := client.TLSConfig()\n\trequire.Same(t, clientTLSConf, cfg)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"tls\", resp.String())\n}\n\nfunc Test_Client_TLS_Error(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\tclientTLSConf.MaxVersion = tls.VersionTLS12\n\tserverTLSConf.MinVersion = tls.VersionTLS13\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tln = tls.NewListener(ln, serverTLSConf)\n\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"tls\")\n\t})\n\n\tgo func() {\n\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t}))\n\t}()\n\ttime.Sleep(1 * time.Second)\n\n\tclient := New()\n\tresp, err := client.SetTLSConfig(clientTLSConf).Get(\"https://\" + ln.Addr().String())\n\n\trequire.Error(t, err)\n\tcfg := client.TLSConfig()\n\trequire.Same(t, clientTLSConf, cfg)\n\trequire.Nil(t, resp)\n}\n\nfunc Test_Client_TLS_Empty_TLSConfig(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tln = tls.NewListener(ln, serverTLSConf)\n\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"tls\")\n\t})\n\n\tgo func() {\n\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t}))\n\t}()\n\ttime.Sleep(1 * time.Second)\n\n\tclient := New()\n\tresp, err := client.Get(\"https://\" + ln.Addr().String())\n\n\trequire.Error(t, err)\n\trequire.NotEqual(t, clientTLSConf, client.TLSConfig())\n\trequire.Nil(t, resp)\n}\n\nfunc Test_Client_SetCertificates(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, _, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tclient := New().SetCertificates(serverTLSConf.Certificates...)\n\trequire.Len(t, client.TLSConfig().Certificates, 1)\n}\n\nfunc Test_Client_SetRootCertificate(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New().SetRootCertificate(\"../.github/testdata/ssl.pem\")\n\trequire.NotNil(t, client.TLSConfig().RootCAs)\n}\n\nfunc Test_Client_SetRootCertificateFromString(t *testing.T) {\n\tt.Parallel()\n\n\tfile, err := os.Open(\"../.github/testdata/ssl.pem\")\n\tdefer func() { require.NoError(t, file.Close()) }()\n\trequire.NoError(t, err)\n\n\tpem, err := io.ReadAll(file)\n\trequire.NoError(t, err)\n\n\tclient := New().SetRootCertificateFromString(string(pem))\n\trequire.NotNil(t, client.TLSConfig().RootCAs)\n}\n\nfunc Test_Client_R(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\treq := client.R()\n\n\trequire.Equal(t, \"Request\", reflect.TypeOf(req).Elem().Name())\n\trequire.Equal(t, client, req.Client())\n}\n\nfunc Test_Replace(t *testing.T) {\n\tapp, dial, start := createHelperServer(t)\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(string(c.Request().Header.Peek(\"k1\")))\n\t})\n\n\tgo start()\n\n\tC().SetDial(dial)\n\tresp, err := Get(\"http://example.com\")\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Empty(t, resp.String())\n\n\tr := New().SetDial(dial).SetHeader(\"k1\", \"v1\")\n\tclean := Replace(r)\n\tresp, err = Get(\"http://example.com\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"v1\", resp.String())\n\n\tclean()\n\n\tC().SetDial(dial)\n\tresp, err = Get(\"http://example.com\")\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Empty(t, resp.String())\n\n\tC().SetDial(nil)\n}\n\nfunc Test_Set_Config_To_Request(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set ctx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype ctxKey struct{}\n\t\tvar key ctxKey = struct{}{}\n\n\t\tctx := context.Background()\n\t\tctx = context.WithValue(ctx, key, \"v1\")\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Ctx: ctx})\n\n\t\trequire.Equal(t, \"v1\", req.Context().Value(key))\n\t})\n\n\tt.Run(\"set useragent\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{UserAgent: \"agent\"})\n\n\t\trequire.Equal(t, \"agent\", req.UserAgent())\n\t})\n\n\tt.Run(\"set referer\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Referer: \"referer\"})\n\n\t\trequire.Equal(t, \"referer\", req.Referer())\n\t})\n\n\tt.Run(\"set header\", func(t *testing.T) {\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Header: map[string]string{\n\t\t\t\"k1\": \"v1\",\n\t\t}})\n\n\t\trequire.Equal(t, \"v1\", req.Header(\"k1\")[0])\n\t})\n\n\tt.Run(\"set params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Param: map[string]string{\n\t\t\t\"k1\": \"v1\",\n\t\t}})\n\n\t\trequire.Equal(t, \"v1\", req.Param(\"k1\")[0])\n\t})\n\n\tt.Run(\"set cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Cookie: map[string]string{\n\t\t\t\"k1\": \"v1\",\n\t\t}})\n\n\t\trequire.Equal(t, \"v1\", req.Cookie(\"k1\"))\n\t})\n\n\tt.Run(\"set pathparam\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{PathParam: map[string]string{\n\t\t\t\"k1\": \"v1\",\n\t\t}})\n\n\t\trequire.Equal(t, \"v1\", req.PathParam(\"k1\"))\n\t})\n\n\tt.Run(\"set timeout\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Timeout: 1 * time.Second})\n\n\t\trequire.Equal(t, 1*time.Second, req.Timeout())\n\t})\n\n\tt.Run(\"set maxredirects\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{MaxRedirects: 1})\n\n\t\trequire.Equal(t, 1, req.MaxRedirects())\n\t})\n\n\tt.Run(\"set body string\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Body: \"test\"})\n\n\t\tbody, ok := req.body.([]byte)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"test\", string(body))\n\t})\n\n\tt.Run(\"set body byte\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Body: []byte(\"test\")})\n\n\t\trequire.Equal(t, []byte(\"test\"), req.body)\n\t})\n\n\tt.Run(\"set body json\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\ttype payload struct {\n\t\t\tFoo string `json:\"foo\"`\n\t\t}\n\n\t\tsetConfigToRequest(req, Config{Body: payload{Foo: \"bar\"}})\n\n\t\tpayloadBody, ok := req.body.(payload)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, payload{Foo: \"bar\"}, payloadBody)\n\t})\n\n\tt.Run(\"set body map\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{Body: map[string]string{\n\t\t\t\"foo\": \"bar\",\n\t\t}})\n\n\t\trequire.Equal(t, map[string]string{\n\t\t\t\"foo\": \"bar\",\n\t\t}, req.body)\n\t})\n\n\tt.Run(\"set file\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\n\t\tsetConfigToRequest(req, Config{File: []*File{\n\t\t\t{\n\t\t\t\tname: \"test\",\n\t\t\t\tpath: \"path\",\n\t\t\t},\n\t\t}})\n\n\t\trequire.Equal(t, \"path\", req.File(\"test\").path)\n\t})\n}\n\nfunc Test_Client_SetProxyURL(t *testing.T) {\n\tt.Parallel()\n\n\tapp, dial, start := createHelperServer(t)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Get(\"isProxy\"))\n\t})\n\n\tgo start()\n\n\tfasthttpClient := &fasthttp.Client{\n\t\tDial:                     dial,\n\t\tNoDefaultUserAgentHeader: true,\n\t\tDisablePathNormalizing:   true,\n\t}\n\n\t// Create a simple proxy server\n\tproxyServer := fiber.New()\n\n\tproxyServer.Use(\"*\", func(c fiber.Ctx) error {\n\t\treq := fasthttp.AcquireRequest()\n\t\tresp := fasthttp.AcquireResponse()\n\n\t\treq.SetRequestURI(c.BaseURL())\n\t\treq.Header.SetMethod(fasthttp.MethodGet)\n\n\t\tfor key, value := range c.Request().Header.All() {\n\t\t\treq.Header.AddBytesKV(key, value)\n\t\t}\n\n\t\treq.Header.Set(\"isProxy\", \"true\")\n\n\t\tif err := fasthttpClient.Do(req, resp); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Status(resp.StatusCode())\n\t\tc.RequestCtx().SetBody(resp.Body())\n\n\t\treturn nil\n\t})\n\n\taddrChan := make(chan string)\n\tgo func() {\n\t\tassert.NoError(t, proxyServer.Listen(\":0\", fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t}))\n\t}()\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\ttime.Sleep(1 * time.Second)\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := New()\n\t\terr := client.SetProxyURL(<-addrChan)\n\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := client.Get(\"http://localhost:3000\")\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, 200, resp.StatusCode())\n\t\trequire.Equal(t, \"true\", string(resp.Body()))\n\t})\n\n\tt.Run(\"error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\n\t\terr := client.SetProxyURL(\":this is not a proxy\")\n\t\trequire.NoError(t, err)\n\n\t\t_, err = client.Get(\"http://localhost:3000\")\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc Test_Client_SetRetryConfig(t *testing.T) {\n\tt.Parallel()\n\tretryConfig := &retry.Config{\n\t\tInitialInterval: 1 * time.Second,\n\t\tMaxRetryCount:   3,\n\t}\n\n\tcore, client, req := newCore(), New(), AcquireRequest()\n\treq.SetURL(\"http://exampleretry.com\")\n\tclient.SetRetryConfig(retryConfig)\n\t_, err := core.execute(context.Background(), client, req)\n\n\trequire.Error(t, err)\n\trequire.Equal(t, retryConfig.InitialInterval, client.RetryConfig().InitialInterval)\n\trequire.Equal(t, retryConfig.MaxRetryCount, client.RetryConfig().MaxRetryCount)\n}\n\nfunc Benchmark_Client_Request(b *testing.B) {\n\tapp, dial, start := createHelperServer(b)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello world\")\n\t})\n\n\tgo start()\n\n\tclient := New().SetDial(dial)\n\n\tb.ReportAllocs()\n\n\tvar err error\n\tvar resp *Response\n\tfor b.Loop() {\n\t\tresp, err = client.Get(\"http://example.com\")\n\t\tresp.Close()\n\t}\n\trequire.NoError(b, err)\n}\n\nfunc Benchmark_Client_Request_Parallel(b *testing.B) {\n\tapp, dial, start := createHelperServer(b)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello world\")\n\t})\n\n\tgo start()\n\n\tclient := New().SetDial(dial)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tvar err error\n\t\tvar resp *Response\n\t\tfor pb.Next() {\n\t\t\tresp, err = client.Get(\"http://example.com\")\n\t\t\tresp.Close()\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n}\n\nfunc Benchmark_Client_Request_Send_ContextCancel(b *testing.B) {\n\tapp, ln, start := createHelperServer(b)\n\n\tstartedCh := make(chan struct{})\n\terrCh := make(chan error)\n\trespCh := make(chan *Response)\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tstartedCh <- struct{}{}\n\t\ttime.Sleep(time.Millisecond) // let cancel be called\n\t\treturn c.Status(fiber.StatusOK).SendString(\"post\")\n\t})\n\n\tgo start()\n\n\tclient := New().SetDial(ln)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\treq := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetURL(\"http://example.com\").\n\t\t\tSetMethod(fiber.MethodPost).\n\t\t\tSetContext(ctx)\n\n\t\tgo func(r *Request) {\n\t\t\tdefer ReleaseRequest(r)\n\n\t\t\tresp, err := r.Send()\n\n\t\t\trespCh <- resp\n\t\t\terrCh <- err\n\t\t}(req)\n\n\t\t<-startedCh // request is made, we can cancel the context now\n\t\tcancel()\n\n\t\trequire.Nil(b, <-respCh)\n\t\trequire.ErrorIs(b, <-errCh, ErrTimeoutOrCancel)\n\t}\n}\n\nfunc Test_Client_StreamResponseBody(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"default value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\trequire.False(t, client.StreamResponseBody())\n\t})\n\n\tt.Run(\"enable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\tresult := client.SetStreamResponseBody(true)\n\t\trequire.True(t, client.StreamResponseBody())\n\t\trequire.Equal(t, client, result)\n\t})\n\n\tt.Run(\"disable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\tclient.SetStreamResponseBody(true)\n\t\trequire.True(t, client.StreamResponseBody())\n\t\tclient.SetStreamResponseBody(false)\n\t\trequire.False(t, client.StreamResponseBody())\n\t})\n\n\tt.Run(\"with host client\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{}\n\t\tclient := NewWithHostClient(hostClient)\n\t\tclient.SetStreamResponseBody(true)\n\t\trequire.True(t, client.StreamResponseBody())\n\t\trequire.True(t, hostClient.StreamResponseBody)\n\t})\n\n\tt.Run(\"with lb client\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\t\tlbClient := &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\thostClient,\n\t\t\t},\n\t\t}\n\t\tclient := NewWithLBClient(lbClient)\n\t\tclient.SetStreamResponseBody(true)\n\t\trequire.True(t, client.StreamResponseBody())\n\t\trequire.True(t, hostClient.StreamResponseBody)\n\t})\n}\n"
  },
  {
    "path": "client/cookiejar.go",
    "content": "// The code was originally taken from https://github.com/valyala/fasthttp/pull/526.\npackage client\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nvar cookieJarPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &CookieJar{}\n\t},\n}\n\n// AcquireCookieJar returns an empty CookieJar object from the pool.\nfunc AcquireCookieJar() *CookieJar {\n\tjar, ok := cookieJarPool.Get().(*CookieJar)\n\tif !ok {\n\t\tpanic(errCookieJarTypeAssertion)\n\t}\n\n\treturn jar\n}\n\n// ReleaseCookieJar returns a CookieJar object to the pool.\nfunc ReleaseCookieJar(c *CookieJar) {\n\tc.Release()\n\tcookieJarPool.Put(c)\n}\n\n// CookieJar manages cookie storage for the client. It stores cookies keyed by host.\ntype CookieJar struct {\n\thostCookies map[string][]*fasthttp.Cookie\n\tmu          sync.Mutex\n}\n\n// Get returns all cookies stored for a given URI. If there are no cookies for the\n// provided host, the returned slice will be nil.\n//\n// The CookieJar keeps its own copies of cookies, so it is safe to release the returned\n// cookies after use.\nfunc (cj *CookieJar) Get(uri *fasthttp.URI) []*fasthttp.Cookie {\n\tif uri == nil {\n\t\treturn nil\n\t}\n\n\tsecure := bytes.Equal(uri.Scheme(), httpsScheme)\n\treturn cj.getByHostAndPath(uri.Host(), uri.Path(), secure)\n}\n\n// getByHostAndPath returns cookies stored for a specific host and path.\nfunc (cj *CookieJar) getByHostAndPath(host, path []byte, secure bool) []*fasthttp.Cookie {\n\tif cj.hostCookies == nil {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\terr     error\n\t\thostStr = utils.UnsafeString(host)\n\t)\n\n\t// port must not be included.\n\thostStr, _, err = net.SplitHostPort(hostStr)\n\tif err != nil {\n\t\thostStr = utils.UnsafeString(host)\n\t}\n\treturn cj.cookiesForRequest(hostStr, path, secure)\n}\n\n// getCookiesByHost returns cookies stored for a specific host, removing any that have expired.\nfunc (cj *CookieJar) getCookiesByHost(host string) []*fasthttp.Cookie {\n\tcj.mu.Lock()\n\tdefer cj.mu.Unlock()\n\n\tnow := time.Now()\n\tcookies := cj.hostCookies[host]\n\n\tkept := cookies[:0]\n\tfor _, c := range cookies {\n\t\t// Remove expired cookies.\n\t\tif !c.Expire().Equal(fasthttp.CookieExpireUnlimited) && c.Expire().Before(now) {\n\t\t\tfasthttp.ReleaseCookie(c)\n\t\t\tcontinue\n\t\t}\n\t\tkept = append(kept, c)\n\t}\n\tcj.hostCookies[host] = kept\n\n\treturn kept\n}\n\n// cookiesForRequest returns cookies that match the given host, path and security settings.\n//\n//nolint:revive // secure is required to filter Secure cookies based on scheme\nfunc (cj *CookieJar) cookiesForRequest(host string, path []byte, secure bool) []*fasthttp.Cookie {\n\tcj.mu.Lock()\n\tdefer cj.mu.Unlock()\n\n\tnow := time.Now()\n\tvar matched []*fasthttp.Cookie\n\n\tfor domain, cookies := range cj.hostCookies {\n\t\tif !domainMatch(host, domain) {\n\t\t\tcontinue\n\t\t}\n\n\t\tkept := cookies[:0]\n\t\tfor _, c := range cookies {\n\t\t\tif !c.Expire().Equal(fasthttp.CookieExpireUnlimited) && c.Expire().Before(now) {\n\t\t\t\tfasthttp.ReleaseCookie(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkept = append(kept, c)\n\n\t\t\tif !pathMatch(path, c.Path()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif c.Secure() && !secure {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnc := fasthttp.AcquireCookie()\n\t\t\tnc.CopyTo(c)\n\t\t\tmatched = append(matched, nc)\n\t\t}\n\t\tcj.hostCookies[domain] = kept\n\t}\n\n\treturn matched\n}\n\n// Set stores the given cookies for the specified URI host. If a cookie key already exists,\n// it will be replaced by the new cookie value.\n//\n// CookieJar stores copies of the provided cookies, so they may be safely released after use.\nfunc (cj *CookieJar) Set(uri *fasthttp.URI, cookies ...*fasthttp.Cookie) {\n\tif uri == nil {\n\t\treturn\n\t}\n\tcj.SetByHost(uri.Host(), cookies...)\n}\n\n// SetByHost stores the given cookies for the specified host. If a cookie key already exists,\n// it will be replaced by the new cookie value.\n//\n// CookieJar stores copies of the provided cookies, so they may be safely released after use.\nfunc (cj *CookieJar) SetByHost(host []byte, cookies ...*fasthttp.Cookie) {\n\thostStr := utils.UnsafeString(host)\n\tif h, _, err := net.SplitHostPort(hostStr); err == nil {\n\t\thostStr = h\n\t}\n\thostStr = utilsstrings.ToLower(hostStr)\n\thostKey := utils.CopyString(hostStr)\n\n\tcj.mu.Lock()\n\tdefer cj.mu.Unlock()\n\n\tif cj.hostCookies == nil {\n\t\tcj.hostCookies = make(map[string][]*fasthttp.Cookie)\n\t}\n\n\tfor _, cookie := range cookies {\n\t\tdomain := utils.TrimLeft(cookie.Domain(), '.')\n\t\tutilsbytes.UnsafeToLower(domain)\n\t\tkey := hostKey\n\t\tif len(domain) == 0 {\n\t\t\tcookie.SetDomain(hostStr)\n\t\t} else {\n\t\t\tkey = utils.CopyString(utils.UnsafeString(domain))\n\t\t\tcookie.SetDomainBytes(domain)\n\t\t}\n\n\t\thostCookies := cj.hostCookies[key]\n\n\t\texisting := searchCookieByKeyAndPath(cookie.Key(), cookie.Path(), hostCookies)\n\t\tif existing == nil {\n\t\t\texisting = fasthttp.AcquireCookie()\n\t\t\thostCookies = append(hostCookies, existing)\n\t\t}\n\t\texisting.CopyTo(cookie)\n\t\tcj.hostCookies[key] = hostCookies\n\t}\n}\n\n// SetKeyValue sets a cookie for the specified host with the given key and value.\n//\n// This function helps prevent extra allocations by avoiding duplication of repeated cookies.\nfunc (cj *CookieJar) SetKeyValue(host, key, value string) {\n\tc := fasthttp.AcquireCookie()\n\tc.SetKey(key)\n\tc.SetValue(value)\n\n\tcj.SetByHost(utils.UnsafeBytes(host), c)\n}\n\n// SetKeyValueBytes sets a cookie for the specified host using byte slices for the key and value.\n//\n// This function helps prevent extra allocations by avoiding duplication of repeated cookies.\nfunc (cj *CookieJar) SetKeyValueBytes(host string, key, value []byte) {\n\tc := fasthttp.AcquireCookie()\n\tc.SetKeyBytes(key)\n\tc.SetValueBytes(value)\n\n\tcj.SetByHost(utils.UnsafeBytes(host), c)\n}\n\n// dumpCookiesToReq writes the stored cookies to the given request.\nfunc (cj *CookieJar) dumpCookiesToReq(req *fasthttp.Request) {\n\turi := req.URI()\n\tsecure := bytes.Equal(uri.Scheme(), httpsScheme)\n\tcookies := cj.getByHostAndPath(uri.Host(), uri.Path(), secure)\n\tfor _, cookie := range cookies {\n\t\treq.Header.SetCookieBytesKV(cookie.Key(), cookie.Value())\n\t\tfasthttp.ReleaseCookie(cookie)\n\t}\n}\n\n// parseCookiesFromResp parses the cookies from the response and stores them for the specified host and path.\nfunc (cj *CookieJar) parseCookiesFromResp(host, _ []byte, resp *fasthttp.Response) {\n\thostStr := utils.UnsafeString(host)\n\tif h, _, err := net.SplitHostPort(hostStr); err == nil {\n\t\thostStr = h\n\t}\n\thostStr = utilsstrings.ToLower(hostStr)\n\thostKey := utils.CopyString(hostStr)\n\n\tcj.mu.Lock()\n\tdefer cj.mu.Unlock()\n\n\tif cj.hostCookies == nil {\n\t\tcj.hostCookies = make(map[string][]*fasthttp.Cookie)\n\t}\n\n\tnow := time.Now()\n\tfor _, value := range resp.Header.Cookies() {\n\t\ttmp := fasthttp.AcquireCookie()\n\t\t_ = tmp.ParseBytes(value) //nolint:errcheck // ignore error\n\n\t\tdomainBytes := utils.TrimLeft(tmp.Domain(), '.')\n\t\tutilsbytes.UnsafeToLower(domainBytes)\n\t\tkey := hostKey\n\t\tif len(domainBytes) == 0 {\n\t\t\ttmp.SetDomain(hostStr)\n\t\t} else {\n\t\t\tkey = utils.CopyString(utils.UnsafeString(domainBytes))\n\t\t\ttmp.SetDomainBytes(domainBytes)\n\t\t}\n\n\t\tcookies := cj.hostCookies[key]\n\t\tc := searchCookieByKeyAndPath(tmp.Key(), tmp.Path(), cookies)\n\t\tif c == nil {\n\t\t\tc = fasthttp.AcquireCookie()\n\t\t\tcookies = append(cookies, c)\n\t\t}\n\n\t\tc.CopyTo(tmp)\n\t\tif c.Expire().Equal(fasthttp.CookieExpireUnlimited) || c.Expire().After(now) {\n\t\t\tcj.hostCookies[key] = cookies\n\t\t} else {\n\t\t\tkept := cookies[:0]\n\t\t\tfor _, v := range cookies {\n\t\t\t\tif v != c {\n\t\t\t\t\tkept = append(kept, v)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcj.hostCookies[key] = kept\n\t\t\tfasthttp.ReleaseCookie(c)\n\t\t}\n\t\tfasthttp.ReleaseCookie(tmp)\n\t}\n}\n\n// Release releases all stored cookies. After this, the CookieJar is empty.\nfunc (cj *CookieJar) Release() {\n\t// FOLLOW-UP performance optimization:\n\t// Currently, a race condition is found because the reset method modifies a value\n\t// that is not a copy but a reference. A solution would be to make a copy.\n\t// for _, v := range cj.hostCookies {\n\t//\t  for _, c := range v {\n\t//\t\tfasthttp.ReleaseCookie(c)\n\t//\t  }\n\t// }\n\tcj.hostCookies = nil\n}\n\n// searchCookieByKeyAndPath looks up a cookie by its key and path from the provided slice of cookies.\nfunc searchCookieByKeyAndPath(key, path []byte, cookies []*fasthttp.Cookie) *fasthttp.Cookie {\n\tfor _, c := range cookies {\n\t\tif bytes.Equal(key, c.Key()) {\n\t\t\tif pathMatch(path, c.Path()) {\n\t\t\t\treturn c\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// pathMatch determines whether the request path matches the cookie path\n// according to RFC 6265 section 5.1.4.\nfunc pathMatch(reqPath, cookiePath []byte) bool {\n\tif len(reqPath) == 0 {\n\t\treqPath = []byte(\"/\")\n\t}\n\tif len(cookiePath) == 0 {\n\t\tcookiePath = []byte(\"/\")\n\t}\n\tif bytes.Equal(reqPath, cookiePath) {\n\t\treturn true\n\t}\n\tif !bytes.HasPrefix(reqPath, cookiePath) {\n\t\treturn false\n\t}\n\tif cookiePath[len(cookiePath)-1] == '/' {\n\t\treturn true\n\t}\n\treturn len(reqPath) > len(cookiePath) && reqPath[len(cookiePath)] == '/'\n}\n\n// domainMatch reports whether host domain-matches the given cookie domain.\nfunc domainMatch(host, domain string) bool {\n\thost = utilsstrings.UnsafeToLower(host)\n\n\tif host == domain {\n\t\treturn true\n\t}\n\treturn strings.HasSuffix(host, \".\"+domain)\n}\n"
  },
  {
    "path": "client/cookiejar_test.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc checkKeyValue(t *testing.T, cj *CookieJar, cookie *fasthttp.Cookie, uri *fasthttp.URI, n int) {\n\tt.Helper()\n\n\tcs := cj.Get(uri)\n\trequire.GreaterOrEqual(t, len(cs), n)\n\n\tc := cs[n-1]\n\trequire.NotNil(t, c)\n\n\trequire.Equal(t, string(c.Key()), string(cookie.Key()))\n\trequire.Equal(t, string(c.Value()), string(cookie.Value()))\n}\n\nfunc Test_CookieJarGet(t *testing.T) {\n\tt.Parallel()\n\n\turl := []byte(\"http://fasthttp.com/\")\n\turl1 := []byte(\"http://fasthttp.com/make/\")\n\turl11 := []byte(\"http://fasthttp.com/hola\")\n\turl2 := []byte(\"http://fasthttp.com/make/fasthttp\")\n\turl3 := []byte(\"http://fasthttp.com/make/fasthttp/great\")\n\tcj := &CookieJar{}\n\n\tc1 := &fasthttp.Cookie{}\n\tc1.SetKey(\"k\")\n\tc1.SetValue(\"v\")\n\tc1.SetPath(\"/make/\")\n\n\tc2 := &fasthttp.Cookie{}\n\tc2.SetKey(\"kk\")\n\tc2.SetValue(\"vv\")\n\tc2.SetPath(\"/make/fasthttp\")\n\n\tc3 := &fasthttp.Cookie{}\n\tc3.SetKey(\"kkk\")\n\tc3.SetValue(\"vvv\")\n\tc3.SetPath(\"/make/fasthttp/great\")\n\n\turi := fasthttp.AcquireURI()\n\trequire.NoError(t, uri.Parse(nil, url))\n\n\turi1 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri1.Parse(nil, url1))\n\n\turi11 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri11.Parse(nil, url11))\n\n\turi2 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri2.Parse(nil, url2))\n\n\turi3 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri3.Parse(nil, url3))\n\n\tcj.Set(uri1, c1, c2, c3)\n\n\tcookies := cj.Get(uri1)\n\trequire.Len(t, cookies, 1)\n\tfor _, cookie := range cookies {\n\t\trequire.True(t, bytes.HasPrefix(uri1.Path(), cookie.Path()))\n\t}\n\n\tcookies = cj.Get(uri11)\n\trequire.Empty(t, cookies)\n\n\tcookies = cj.Get(uri2)\n\trequire.Len(t, cookies, 2)\n\tfor _, cookie := range cookies {\n\t\trequire.True(t, bytes.HasPrefix(uri2.Path(), cookie.Path()))\n\t}\n\n\tcookies = cj.Get(uri3)\n\trequire.Len(t, cookies, 3)\n\tfor _, cookie := range cookies {\n\t\trequire.True(t, bytes.HasPrefix(uri3.Path(), cookie.Path()))\n\t}\n\n\tcookies = cj.Get(uri)\n\trequire.Empty(t, cookies)\n}\n\nfunc Test_CookieJarGetExpired(t *testing.T) {\n\tt.Parallel()\n\n\turl1 := []byte(\"http://fasthttp.com/make/\")\n\turi1 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri1.Parse(nil, url1))\n\n\tc1 := &fasthttp.Cookie{}\n\tc1.SetKey(\"k\")\n\tc1.SetValue(\"v\")\n\tc1.SetExpire(time.Now().Add(-time.Hour))\n\n\tcj := &CookieJar{}\n\tcj.Set(uri1, c1)\n\n\tcookies := cj.Get(uri1)\n\trequire.Empty(t, cookies)\n}\n\nfunc Test_CookieJarSet(t *testing.T) {\n\tt.Parallel()\n\n\turl := []byte(\"http://fasthttp.com/hello/world\")\n\tcj := &CookieJar{}\n\n\tcookie := &fasthttp.Cookie{}\n\tcookie.SetKey(\"k\")\n\tcookie.SetValue(\"v\")\n\n\turi := fasthttp.AcquireURI()\n\trequire.NoError(t, uri.Parse(nil, url))\n\n\tcj.Set(uri, cookie)\n\tcheckKeyValue(t, cj, cookie, uri, 1)\n}\n\nfunc Test_CookieJarSetRepeatedCookieKeys(t *testing.T) {\n\tt.Parallel()\n\thost := \"fast.http\"\n\tcj := &CookieJar{}\n\n\turi := fasthttp.AcquireURI()\n\turi.SetHost(host)\n\n\tcookie := &fasthttp.Cookie{}\n\tcookie.SetKey(\"k\")\n\tcookie.SetValue(\"v\")\n\n\tcookie2 := &fasthttp.Cookie{}\n\tcookie2.SetKey(\"k\")\n\tcookie2.SetValue(\"v2\")\n\n\tcookie3 := &fasthttp.Cookie{}\n\tcookie3.SetKey(\"key\")\n\tcookie3.SetValue(\"value\")\n\n\tcj.Set(uri, cookie, cookie2, cookie3)\n\n\tcookies := cj.Get(uri)\n\trequire.Len(t, cookies, 2)\n\trequire.Equal(t, cookies[0].String(), cookie2.String())\n\trequire.True(t, bytes.Equal(cookies[0].Value(), cookie2.Value()))\n}\n\nfunc Test_CookieJarSetKeyValue(t *testing.T) {\n\tt.Parallel()\n\n\thost := \"fast.http\"\n\tcj := &CookieJar{}\n\n\turi := fasthttp.AcquireURI()\n\turi.SetHost(host)\n\n\tcj.SetKeyValue(host, \"k\", \"v\")\n\tcj.SetKeyValue(host, \"key\", \"value\")\n\tcj.SetKeyValue(host, \"k\", \"vv\")\n\tcj.SetKeyValue(host, \"key\", \"value2\")\n\n\tcookies := cj.Get(uri)\n\trequire.Len(t, cookies, 2)\n}\n\nfunc Test_CookieJarGetFromResponse(t *testing.T) {\n\tt.Parallel()\n\n\tres := fasthttp.AcquireResponse()\n\thost := []byte(\"fast.http\")\n\turi := fasthttp.AcquireURI()\n\turi.SetHostBytes(host)\n\n\tc := &fasthttp.Cookie{}\n\tc.SetKey(\"key\")\n\tc.SetValue(\"val\")\n\n\tc2 := &fasthttp.Cookie{}\n\tc2.SetKey(\"k\")\n\tc2.SetValue(\"v\")\n\n\tc3 := &fasthttp.Cookie{}\n\tc3.SetKey(\"kk\")\n\tc3.SetValue(\"vv\")\n\n\tres.Header.SetStatusCode(200)\n\tres.Header.SetCookie(c)\n\tres.Header.SetCookie(c2)\n\tres.Header.SetCookie(c3)\n\n\tcj := &CookieJar{}\n\tcj.parseCookiesFromResp(host, nil, res)\n\n\tcookies := cj.Get(uri)\n\trequire.Len(t, cookies, 3)\n\tvalues := map[string]string{\"key\": \"val\", \"k\": \"v\", \"kk\": \"vv\"}\n\tfor _, c := range cookies {\n\t\tk := string(c.Key())\n\t\tv, ok := values[k]\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, v, string(c.Value()))\n\t\tdelete(values, k)\n\t}\n\trequire.Empty(t, values)\n}\n\nfunc Test_CookieJar_HostPort(t *testing.T) {\n\tt.Parallel()\n\n\tjar := &CookieJar{}\n\turiSet := fasthttp.AcquireURI()\n\trequire.NoError(t, uriSet.Parse(nil, []byte(\"http://fasthttp.com:80/path\")))\n\n\tc := &fasthttp.Cookie{}\n\tc.SetKey(\"k\")\n\tc.SetValue(\"v\")\n\tjar.Set(uriSet, c)\n\n\t// retrieve using a different port to ensure port is ignored\n\turiGet := fasthttp.AcquireURI()\n\trequire.NoError(t, uriGet.Parse(nil, []byte(\"http://fasthttp.com:8080/path\")))\n\n\tcookies := jar.Get(uriGet)\n\trequire.Len(t, cookies, 1)\n\trequire.Equal(t, \"k\", string(cookies[0].Key()))\n\trequire.Equal(t, \"v\", string(cookies[0].Value()))\n\trequire.Equal(t, \"fasthttp.com\", string(cookies[0].Domain()))\n}\n\nfunc Test_CookieJar_Domain(t *testing.T) {\n\tt.Parallel()\n\n\tjar := &CookieJar{}\n\n\turi := fasthttp.AcquireURI()\n\trequire.NoError(t, uri.Parse(nil, []byte(\"http://sub.example.com/\")))\n\n\tc := &fasthttp.Cookie{}\n\tc.SetKey(\"k\")\n\tc.SetValue(\"v\")\n\tc.SetDomain(\"example.com\")\n\n\tjar.Set(uri, c)\n\n\turi2 := fasthttp.AcquireURI()\n\trequire.NoError(t, uri2.Parse(nil, []byte(\"http://other.example.com/\")))\n\n\tcookies := jar.Get(uri2)\n\trequire.Len(t, cookies, 1)\n\trequire.Equal(t, \"k\", string(cookies[0].Key()))\n\trequire.Equal(t, \"v\", string(cookies[0].Value()))\n}\n\nfunc Test_CookieJar_Secure(t *testing.T) {\n\tt.Parallel()\n\n\tjar := &CookieJar{}\n\n\turiHTTP := fasthttp.AcquireURI()\n\trequire.NoError(t, uriHTTP.Parse(nil, []byte(\"http://example.com/\")))\n\n\tc := &fasthttp.Cookie{}\n\tc.SetKey(\"k\")\n\tc.SetValue(\"v\")\n\tc.SetSecure(true)\n\n\tjar.Set(uriHTTP, c)\n\n\tcookies := jar.Get(uriHTTP)\n\trequire.Empty(t, cookies)\n\n\turiHTTPS := fasthttp.AcquireURI()\n\trequire.NoError(t, uriHTTPS.Parse(nil, []byte(\"https://example.com/\")))\n\n\tcookies = jar.Get(uriHTTPS)\n\trequire.Len(t, cookies, 1)\n\trequire.Equal(t, \"k\", string(cookies[0].Key()))\n\trequire.Equal(t, \"v\", string(cookies[0].Value()))\n}\n\nfunc Test_CookieJar_PathMatch(t *testing.T) {\n\tt.Parallel()\n\n\tjar := &CookieJar{}\n\n\tsetURI := fasthttp.AcquireURI()\n\trequire.NoError(t, setURI.Parse(nil, []byte(\"http://example.com/api\")))\n\n\tc := &fasthttp.Cookie{}\n\tc.SetKey(\"k\")\n\tc.SetValue(\"v\")\n\tc.SetPath(\"/api\")\n\n\tjar.Set(setURI, c)\n\n\turiExact := fasthttp.AcquireURI()\n\trequire.NoError(t, uriExact.Parse(nil, []byte(\"http://example.com/api\")))\n\trequire.Len(t, jar.Get(uriExact), 1)\n\n\turiChild := fasthttp.AcquireURI()\n\trequire.NoError(t, uriChild.Parse(nil, []byte(\"http://example.com/api/v1\")))\n\trequire.Len(t, jar.Get(uriChild), 1)\n\n\turiNoMatch := fasthttp.AcquireURI()\n\trequire.NoError(t, uriNoMatch.Parse(nil, []byte(\"http://example.com/apiv1\")))\n\trequire.Empty(t, jar.Get(uriNoMatch))\n}\n"
  },
  {
    "path": "client/core.go",
    "content": "// Core pipeline scaffolds request execution for Fiber's HTTP client, including\n// hook invocation, retry orchestration, and timeout management around fasthttp\n// transports.\npackage client\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/addon/retry\"\n)\n\nconst boundary = \"FiberFormBoundary\"\n\n// RequestHook is a function invoked before the request is sent.\n// It receives a Client and a Request, allowing you to modify the Request or Client data.\ntype RequestHook func(*Client, *Request) error\n\n// ResponseHook is a function invoked after a response is received.\n// It receives a Client, Response, and Request, allowing you to modify the Response data\n// or perform actions based on the response.\ntype ResponseHook func(*Client, *Response, *Request) error\n\n// RetryConfig is an alias for the `retry.Config` type from the `addon/retry` package.\ntype RetryConfig = retry.Config\n\n// addMissingPort appends the appropriate port number to the given address if it doesn't have one.\n// If isTLS is true, it uses port 443; otherwise, it uses port 80.\nfunc addMissingPort(addr string, isTLS bool) string { //revive:disable-line:flag-parameter\n\tif strings.IndexByte(addr, ':') != -1 {\n\t\treturn addr\n\t}\n\tport := 80\n\tif isTLS {\n\t\tport = 443\n\t}\n\treturn net.JoinHostPort(addr, strconv.Itoa(port))\n}\n\n// core stores middleware and plugin definitions and defines the request execution process.\ntype core struct {\n\tclient *Client\n\treq    *Request\n\tctx    context.Context //nolint:containedctx // Context is needed here.\n}\n\n// getRetryConfig returns a copy of the client's retry configuration.\nfunc (c *core) getRetryConfig() *RetryConfig {\n\tc.client.mu.RLock()\n\tdefer c.client.mu.RUnlock()\n\n\tcfg := c.client.RetryConfig()\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\treturn &RetryConfig{\n\t\tInitialInterval: cfg.InitialInterval,\n\t\tMaxBackoffTime:  cfg.MaxBackoffTime,\n\t\tMultiplier:      cfg.Multiplier,\n\t\tMaxRetryCount:   cfg.MaxRetryCount,\n\t}\n}\n\n// execFunc is the core logic to send the request and receive the response.\n// It leverages the fasthttp client, optionally with retries or redirects.\nfunc (c *core) execFunc() (*Response, error) {\n\t// do not close, these will be returned to the pool\n\terrChan := acquireErrChan()\n\trespChan := acquireResponseChan()\n\n\tcfg := c.getRetryConfig()\n\tgo func() {\n\t\t// retain both channels until they are drained\n\t\tdefer releaseErrChan(errChan)\n\t\tdefer releaseResponseChan(respChan)\n\n\t\treqv := fasthttp.AcquireRequest()\n\t\tdefer fasthttp.ReleaseRequest(reqv)\n\n\t\trespv := fasthttp.AcquireResponse()\n\t\tdefer func() {\n\t\t\tif respv != nil {\n\t\t\t\tfasthttp.ReleaseResponse(respv)\n\t\t\t}\n\t\t}()\n\n\t\tc.req.RawRequest.CopyTo(reqv)\n\t\tif bodyStream := c.req.RawRequest.BodyStream(); bodyStream != nil {\n\t\t\treqv.SetBodyStream(bodyStream, c.req.RawRequest.Header.ContentLength())\n\t\t}\n\n\t\tvar err error\n\t\tif cfg != nil {\n\t\t\t// Use an exponential backoff retry strategy.\n\t\t\terr = retry.NewExponentialBackoff(*cfg).Retry(func() error {\n\t\t\t\tif c.req.maxRedirects > 0 && (string(reqv.Header.Method()) == fiber.MethodGet || string(reqv.Header.Method()) == fiber.MethodHead) {\n\t\t\t\t\treturn c.client.DoRedirects(reqv, respv, c.req.maxRedirects)\n\t\t\t\t}\n\t\t\t\treturn c.client.Do(reqv, respv)\n\t\t\t})\n\t\t} else {\n\t\t\tif c.req.maxRedirects > 0 && (string(reqv.Header.Method()) == fiber.MethodGet || string(reqv.Header.Method()) == fiber.MethodHead) {\n\t\t\t\terr = c.client.DoRedirects(reqv, respv, c.req.maxRedirects)\n\t\t\t} else {\n\t\t\t\terr = c.client.Do(reqv, respv)\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\terrChan <- err\n\t\t\treturn\n\t\t}\n\n\t\tresp := AcquireResponse()\n\t\tresp.setClient(c.client)\n\t\tresp.setRequest(c.req)\n\n\t\t// Swap the fasthttp response with the Fiber response's RawResponse field.\n\t\t// This is required, as (*fasthttp.Response).CopyTo() explicitly does not\n\t\t// copy body streams.\n\t\t//\n\t\t// See: https://github.com/valyala/fasthttp/blob/v1.69.0/http.go#L909-L923\n\t\t//\n\t\t// The defer statement above ensures that the original RawResponse\n\t\t// (now stored in respv) will be properly released.\n\t\tresp.RawResponse, respv = respv, resp.RawResponse\n\t\trespChan <- resp\n\t}()\n\n\tselect {\n\tcase err := <-errChan:\n\t\treturn nil, err\n\tcase resp := <-respChan:\n\t\treturn resp, nil\n\tcase <-c.ctx.Done():\n\t\tgo func() { // drain the channels and release the response\n\t\t\tselect {\n\t\t\tcase resp := <-respChan:\n\t\t\t\tReleaseResponse(resp)\n\t\t\tcase <-errChan:\n\t\t\t}\n\t\t}()\n\t\treturn nil, ErrTimeoutOrCancel\n\t}\n}\n\n// preHooks runs all request hooks before sending the request.\nfunc (c *core) preHooks() error {\n\tc.client.mu.Lock()\n\tdefer c.client.mu.Unlock()\n\n\tfor _, f := range c.client.userRequestHooks {\n\t\tif err := f(c.client, c.req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, f := range c.client.builtinRequestHooks {\n\t\tif err := f(c.client, c.req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// afterHooks runs all response hooks after receiving the response.\nfunc (c *core) afterHooks(resp *Response) error {\n\tc.client.mu.Lock()\n\tdefer c.client.mu.Unlock()\n\n\tfor _, f := range c.client.builtinResponseHooks {\n\t\tif err := f(c.client, resp, c.req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, f := range c.client.userResponseHooks {\n\t\tif err := f(c.client, resp, c.req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// timeout applies the configured timeout to the request, if any.\nfunc (c *core) timeout() context.CancelFunc {\n\tvar cancel context.CancelFunc\n\n\tif c.req.timeout > 0 {\n\t\tc.ctx, cancel = context.WithTimeout(c.ctx, c.req.timeout)\n\t} else if c.client.timeout > 0 {\n\t\tc.ctx, cancel = context.WithTimeout(c.ctx, c.client.timeout)\n\t}\n\n\treturn cancel\n}\n\n// execute runs all hooks, applies timeouts, sends the request, and runs response hooks.\nfunc (c *core) execute(ctx context.Context, client *Client, req *Request) (*Response, error) {\n\t// Store references locally.\n\tc.ctx = ctx\n\tc.client = client\n\tc.req = req\n\n\t// Execute pre request hooks (user-defined and built-in).\n\tif err := c.preHooks(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Apply timeout if specified.\n\tcancel := c.timeout()\n\tif cancel != nil {\n\t\tdefer cancel()\n\t}\n\n\t// Perform the actual HTTP request.\n\tresp, err := c.execFunc()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Execute after response hooks (built-in and then user-defined).\n\tif err := c.afterHooks(resp); err != nil {\n\t\tresp.Close()\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\nvar responseChanPool = &sync.Pool{\n\tNew: func() any {\n\t\treturn make(chan *Response)\n\t},\n}\n\n// acquireResponseChan returns an empty, non-closed *Response channel from the pool.\n// The returned channel may be returned to the pool with releaseResponseChan\nfunc acquireResponseChan() chan *Response {\n\tch, ok := responseChanPool.Get().(chan *Response)\n\tif !ok {\n\t\tpanic(errResponseChanTypeAssertion)\n\t}\n\treturn ch\n}\n\n// releaseResponseChan returns the *Response channel to the pool.\n// It's the caller's responsibility to ensure that:\n// - the channel is not closed\n// - the channel is drained before returning it\n// - the channel is not reused after returning it\nfunc releaseResponseChan(ch chan *Response) {\n\tresponseChanPool.Put(ch)\n}\n\nvar errChanPool = &sync.Pool{\n\tNew: func() any {\n\t\treturn make(chan error)\n\t},\n}\n\n// acquireErrChan returns an empty, non-closed error channel from the pool.\n// The returned channel may be returned to the pool with releaseErrChan\nfunc acquireErrChan() chan error {\n\tch, ok := errChanPool.Get().(chan error)\n\tif !ok {\n\t\tpanic(errChanErrorTypeAssertion)\n\t}\n\treturn ch\n}\n\n// releaseErrChan returns the error channel to the pool.\n// It's caller's responsibility to ensure that:\n// - the channel is not closed\n// - the channel is drained before returning it\n// - the channel is not reused after returning it\nfunc releaseErrChan(ch chan error) {\n\terrChanPool.Put(ch)\n}\n\n// newCore returns a new core object.\nfunc newCore() *core {\n\treturn &core{}\n}\n\nvar (\n\tErrTimeoutOrCancel      = errors.New(\"timeout or cancel\")\n\tErrURLFormat            = errors.New(\"the URL is incorrect\")\n\tErrNotSupportSchema     = errors.New(\"protocol not supported; only http or https are allowed\")\n\tErrFileNoName           = errors.New(\"the file should have a name\")\n\tErrBodyType             = errors.New(\"the body type should be []byte\")\n\tErrNotSupportSaveMethod = errors.New(\"only file paths and io.Writer are supported\")\n\tErrBodyTypeNotSupported = errors.New(\"the body type is not supported\")\n)\n"
  },
  {
    "path": "client/core_test.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nfunc Test_AddMissing_Port(t *testing.T) {\n\tt.Parallel()\n\n\ttype args struct {\n\t\taddr  string\n\t\tisTLS bool\n\t}\n\ttests := []struct {\n\t\tname string\n\t\twant string\n\t\targs args\n\t}{\n\t\t{\n\t\t\tname: \"do anything\",\n\t\t\targs: args{\n\t\t\t\taddr: \"example.com:1234\",\n\t\t\t},\n\t\t\twant: \"example.com:1234\",\n\t\t},\n\t\t{\n\t\t\tname: \"add 80 port\",\n\t\t\targs: args{\n\t\t\t\taddr: \"example.com\",\n\t\t\t},\n\t\t\twant: \"example.com:80\",\n\t\t},\n\t\t{\n\t\t\tname: \"add 443 port\",\n\t\t\targs: args{\n\t\t\t\taddr:  \"example.com\",\n\t\t\t\tisTLS: true,\n\t\t\t},\n\t\t\twant: \"example.com:443\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.want, addMissingPort(tt.args.addr, tt.args.isTLS))\n\t\t})\n\t}\n}\n\nfunc Test_Exec_Func(t *testing.T) {\n\tt.Parallel()\n\tln := fasthttputil.NewInmemoryListener()\n\tapp := fiber.New()\n\n\tapp.Get(\"/normal\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tapp.Get(\"/return-error\", func(_ fiber.Ctx) error {\n\t\treturn errors.New(\"the request is error\")\n\t})\n\n\tapp.Get(\"/redirect\", func(c fiber.Ctx) error {\n\t\treturn c.Redirect().Status(fiber.StatusFound).To(\"/normal\")\n\t})\n\n\tapp.Get(\"/hang-up\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(time.Second)\n\t\treturn c.SendString(c.Hostname() + \" hang up\")\n\t})\n\n\tgo func() {\n\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}))\n\t}()\n\n\ttime.Sleep(300 * time.Millisecond)\n\n\tt.Run(\"normal request\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tcore.ctx = context.Background()\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/normal\")\n\n\t\tresp, err := core.execFunc()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, resp.RawResponse.StatusCode())\n\t\trequire.Equal(t, \"example.com\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"follow redirect with retry config\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tcore.ctx = context.Background()\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\tclient.SetRetryConfig(&RetryConfig{MaxRetryCount: 1})\n\t\tclient.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\t\treq.SetMaxRedirects(1)\n\t\treq.RawRequest.Header.SetMethod(fiber.MethodGet)\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/redirect\")\n\n\t\tresp, err := core.execFunc()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, resp.RawResponse.StatusCode())\n\t\trequire.Equal(t, \"example.com\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"the request return an error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tcore.ctx = context.Background()\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/return-error\")\n\n\t\tresp, err := core.execFunc()\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 500, resp.RawResponse.StatusCode())\n\t\trequire.Equal(t, \"the request is error\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"the request timeout\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tcore.ctx = ctx\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/hang-up\")\n\n\t\t_, err := core.execFunc()\n\n\t\trequire.Equal(t, ErrTimeoutOrCancel, err)\n\t})\n\n\tt.Run(\"cancel drains errChan\", func(t *testing.T) {\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\n\t\tcore.ctx = ctx\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/drain-err\")\n\n\t\tblockingTransport := newBlockingErrTransport(errors.New(\"upstream failure\"))\n\t\tclient.transport = blockingTransport\n\t\tdefer blockingTransport.release()\n\n\t\ttype execResult struct {\n\t\t\tresp *Response\n\t\t\terr  error\n\t\t}\n\n\t\tresultCh := make(chan execResult, 1)\n\t\tgo func() {\n\t\t\tresp, err := core.execFunc()\n\t\t\tresultCh <- execResult{resp: resp, err: err}\n\t\t}()\n\n\t\tselect {\n\t\tcase <-blockingTransport.called:\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"transport Do was not invoked\")\n\t\t}\n\n\t\tcancel()\n\n\t\tvar result execResult\n\t\tselect {\n\t\tcase result = <-resultCh:\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"execFunc did not return\")\n\t\t}\n\n\t\trequire.Nil(t, result.resp)\n\t\trequire.ErrorIs(t, result.err, ErrTimeoutOrCancel)\n\n\t\tblockingTransport.release()\n\n\t\tselect {\n\t\tcase <-blockingTransport.finished:\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"transport Do did not finish\")\n\t\t}\n\t})\n}\n\nfunc Test_Execute(t *testing.T) {\n\tt.Parallel()\n\tln := fasthttputil.NewInmemoryListener()\n\tapp := fiber.New()\n\n\tapp.Get(\"/normal\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tapp.Get(\"/return-error\", func(_ fiber.Ctx) error {\n\t\treturn errors.New(\"the request is error\")\n\t})\n\n\tapp.Get(\"/hang-up\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(time.Second)\n\t\treturn c.SendString(c.Hostname() + \" hang up\")\n\t})\n\n\tgo func() {\n\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}))\n\t}()\n\n\tt.Run(\"add user request hooks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tclient.AddRequestHook(func(_ *Client, _ *Request) error {\n\t\t\trequire.Equal(t, \"http://example.com\", req.URL())\n\t\t\treturn nil\n\t\t})\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com\")\n\n\t\tresp, err := core.execute(context.Background(), client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Not Found\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"add user response hooks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tclient.AddResponseHook(func(_ *Client, _ *Response, req *Request) error {\n\t\t\trequire.Equal(t, \"http://example.com\", req.URL())\n\t\t\treturn nil\n\t\t})\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com\")\n\n\t\tresp, err := core.execute(context.Background(), client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Not Found\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"no timeout\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com/hang-up\")\n\n\t\tresp, err := core.execute(context.Background(), client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"example.com hang up\", string(resp.RawResponse.Body()))\n\t})\n\n\tt.Run(\"client timeout\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tclient.SetTimeout(500 * time.Millisecond)\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com/hang-up\")\n\n\t\t_, err := core.execute(context.Background(), client, req)\n\t\trequire.Equal(t, ErrTimeoutOrCancel, err)\n\t})\n\n\tt.Run(\"request timeout\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com/hang-up\").\n\t\t\tSetTimeout(300 * time.Millisecond)\n\n\t\t_, err := core.execute(context.Background(), client, req)\n\t\trequire.Equal(t, ErrTimeoutOrCancel, err)\n\t})\n\n\tt.Run(\"request timeout has higher level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tclient.SetTimeout(30 * time.Millisecond)\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\t\treq.SetURL(\"http://example.com/hang-up\").\n\t\t\tSetTimeout(3000 * time.Millisecond)\n\n\t\tresp, err := core.execute(context.Background(), client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"example.com hang up\", string(resp.RawResponse.Body()))\n\t})\n}\n\ntype blockingErrTransport struct {\n\terr error\n\n\tcalled   chan struct{}\n\tunblock  chan struct{}\n\tfinished chan struct{}\n\n\tcalledOnce   sync.Once\n\treleaseOnce  sync.Once\n\tfinishedOnce sync.Once\n}\n\nfunc newBlockingErrTransport(err error) *blockingErrTransport {\n\treturn &blockingErrTransport{\n\t\terr:      err,\n\t\tcalled:   make(chan struct{}),\n\t\tunblock:  make(chan struct{}),\n\t\tfinished: make(chan struct{}),\n\t}\n}\n\nfunc (b *blockingErrTransport) Do(_ *fasthttp.Request, _ *fasthttp.Response) error {\n\tb.calledOnce.Do(func() { close(b.called) })\n\t<-b.unblock\n\tb.finishedOnce.Do(func() { close(b.finished) })\n\treturn b.err\n}\n\nfunc (b *blockingErrTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, _ time.Duration) error {\n\treturn b.Do(req, resp)\n}\n\nfunc (b *blockingErrTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, _ time.Time) error {\n\treturn b.Do(req, resp)\n}\n\nfunc (b *blockingErrTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, _ int) error {\n\treturn b.Do(req, resp)\n}\n\nfunc (*blockingErrTransport) CloseIdleConnections() {\n}\n\nfunc (*blockingErrTransport) TLSConfig() *tls.Config {\n\treturn nil\n}\n\nfunc (*blockingErrTransport) SetTLSConfig(_ *tls.Config) {\n}\n\nfunc (*blockingErrTransport) SetDial(_ fasthttp.DialFunc) {\n}\n\nfunc (*blockingErrTransport) Client() any {\n\treturn nil\n}\n\nfunc (*blockingErrTransport) StreamResponseBody() bool {\n\treturn false\n}\n\nfunc (*blockingErrTransport) SetStreamResponseBody(_ bool) {\n}\n\nfunc (b *blockingErrTransport) release() {\n\tb.releaseOnce.Do(func() { close(b.unblock) })\n}\n\nfunc Test_Core_RequestBodyStream(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"request with body stream is properly copied\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Post(\"/echo\", func(c fiber.Ctx) error {\n\t\t\tbody := c.Body()\n\t\t\treturn c.Send(body)\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tgo func() {\n\t\t\terr := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}()\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := New().SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\n\t\t// Create a request with a body stream using SetRawBody which properly sets the body\n\t\tstreamContent := \"this is streamed body content\"\n\t\treq := AcquireRequest().SetClient(client)\n\t\treq.SetURL(\"http://example.com/echo\")\n\t\treq.SetMethod(fiber.MethodPost)\n\t\treq.SetRawBody([]byte(streamContent))\n\n\t\tresp, err := req.Send()\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\trequire.Equal(t, streamContent, string(resp.Body()))\n\t})\n\n\tt.Run(\"request body stream with content length\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresultCh := make(chan struct {\n\t\t\tbody   string\n\t\t\tlength int\n\t\t}, 1)\n\t\tapp := fiber.New()\n\t\tapp.Post(\"/check-length\", func(c fiber.Ctx) error {\n\t\t\tresultCh <- struct {\n\t\t\t\tbody   string\n\t\t\t\tlength int\n\t\t\t}{\n\t\t\t\tbody:   string(c.Body()),\n\t\t\t\tlength: c.Request().Header.ContentLength(),\n\t\t\t}\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tgo func() {\n\t\t\terr := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}()\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tclient := New().SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\n\t\tstreamContent := \"body with known length\"\n\t\treq := AcquireRequest().SetClient(client)\n\t\treq.SetURL(\"http://example.com/check-length\")\n\t\treq.SetMethod(fiber.MethodPost)\n\t\treq.SetRawBody([]byte(streamContent))\n\n\t\tresp, err := req.Send()\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\tresult := <-resultCh\n\t\trequire.Equal(t, streamContent, result.body)\n\t\trequire.Equal(t, len(streamContent), result.length)\n\t})\n\n\tt.Run(\"raw body stream survives CopyTo\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconst streamContent = \"streaming raw request body\"\n\n\t\tresultCh := make(chan struct {\n\t\t\tbody   string\n\t\t\tlength int\n\t\t}, 1)\n\n\t\tapp := fiber.New()\n\t\tapp.Post(\"/copy-to-body-stream\", func(c fiber.Ctx) error {\n\t\t\tbody := string(c.Body())\n\t\t\tresultCh <- struct {\n\t\t\t\tbody   string\n\t\t\t\tlength int\n\t\t\t}{\n\t\t\t\tbody:   body,\n\t\t\t\tlength: c.Request().Header.ContentLength(),\n\t\t\t}\n\t\t\treturn c.SendString(body)\n\t\t})\n\n\t\tln := fasthttputil.NewInmemoryListener()\n\t\tgo func() {\n\t\t\terr := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}()\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, app.Shutdown())\n\t\t})\n\n\t\tcore, client, req := newCore(), New(), AcquireRequest()\n\t\tcore.ctx = context.Background()\n\t\tcore.client = client\n\t\tcore.req = req\n\n\t\tclient.SetDial(func(_ string) (net.Conn, error) {\n\t\t\treturn ln.Dial()\n\t\t})\n\n\t\treq.RawRequest.SetRequestURI(\"http://example.com/copy-to-body-stream\")\n\t\treq.RawRequest.Header.SetMethod(fiber.MethodPost)\n\t\treq.RawRequest.SetBodyStream(bytes.NewBufferString(streamContent), len(streamContent))\n\n\t\tresp, err := core.execFunc()\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\tresult := <-resultCh\n\t\trequire.Equal(t, streamContent, string(resp.Body()))\n\t\trequire.Equal(t, streamContent, result.body)\n\t\trequire.Equal(t, len(streamContent), result.length)\n\t})\n}\n"
  },
  {
    "path": "client/errors.go",
    "content": "package client\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\terrResponseChanTypeAssertion = errors.New(\"failed to type-assert to *Response\")\n\terrChanErrorTypeAssertion    = errors.New(\"failed to type-assert to chan error\")\n\terrRequestTypeAssertion      = errors.New(\"failed to type-assert to *Request\")\n\terrFileTypeAssertion         = errors.New(\"failed to type-assert to *File\")\n\terrCookieJarTypeAssertion    = errors.New(\"failed to type-assert to *CookieJar\")\n\terrSyncPoolBuffer            = errors.New(\"failed to retrieve buffer from a sync.Pool\")\n)\n"
  },
  {
    "path": "client/helper_test.go",
    "content": "package client\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n)\n\ntype testServer struct {\n\tapp *fiber.App\n\tch  chan struct{}\n\tln  *fasthttputil.InmemoryListener\n\ttb  testing.TB\n}\n\nfunc startTestServer(tb testing.TB, beforeStarting func(app *fiber.App)) *testServer {\n\ttb.Helper()\n\n\tln := fasthttputil.NewInmemoryListener()\n\tapp := fiber.New(fiber.Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\n\tif beforeStarting != nil {\n\t\tbeforeStarting(app)\n\t}\n\n\tch := make(chan struct{})\n\tgo func() {\n\t\terr := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})\n\t\tassert.NoError(tb, err)\n\n\t\tclose(ch)\n\t}()\n\n\treturn &testServer{\n\t\tapp: app,\n\t\tch:  ch,\n\t\tln:  ln,\n\t\ttb:  tb,\n\t}\n}\n\nfunc (ts *testServer) stop() {\n\tts.tb.Helper()\n\n\tif err := ts.app.Shutdown(); err != nil {\n\t\tts.tb.Fatal(err)\n\t}\n\n\tselect {\n\tcase <-ts.ch:\n\tcase <-time.After(time.Second):\n\t\tts.tb.Fatalf(\"timeout when waiting for server close\")\n\t}\n}\n\nfunc (ts *testServer) dial() func(addr string) (net.Conn, error) {\n\tts.tb.Helper()\n\n\treturn func(_ string) (net.Conn, error) {\n\t\treturn ts.ln.Dial()\n\t}\n}\n\nfunc createHelperServer(tb testing.TB) (app *fiber.App, dial func(addr string) (net.Conn, error), start func()) { //nolint:nonamedreturns // gocritic unnamedResult requires explicit result identifiers for helper components\n\ttb.Helper()\n\n\tln := fasthttputil.NewInmemoryListener()\n\n\tapp = fiber.New()\n\n\tdial = func(_ string) (net.Conn, error) {\n\t\treturn ln.Dial()\n\t}\n\tstart = func() {\n\t\trequire.NoError(tb, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}))\n\t}\n\n\treturn app, dial, start\n}\n\nfunc testRequest(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Request), excepted string, count ...int) {\n\tt.Helper()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Get(\"/\", handler)\n\tgo start()\n\n\tc := 1\n\tif len(count) > 0 {\n\t\tc = count[0]\n\t}\n\n\tclient := New().SetDial(ln)\n\n\tfor i := 0; i < c; i++ {\n\t\treq := AcquireRequest().SetClient(client)\n\t\twrapAgent(req)\n\n\t\tresp, err := req.Get(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, excepted, resp.String())\n\t\tresp.Close()\n\t}\n}\n\nfunc testRequestFail(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Request), excepted error, count ...int) {\n\tt.Helper()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Get(\"/\", handler)\n\tgo start()\n\n\tc := 1\n\tif len(count) > 0 {\n\t\tc = count[0]\n\t}\n\n\tclient := New().SetDial(ln)\n\n\tfor i := 0; i < c; i++ {\n\t\treq := AcquireRequest().SetClient(client)\n\t\twrapAgent(req)\n\n\t\t_, err := req.Get(\"http://example.com\")\n\n\t\trequire.Equal(t, excepted.Error(), err.Error())\n\t}\n}\n\nfunc testClient(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Client), excepted string, count ...int) { //nolint:unparam // maybe needed\n\tt.Helper()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Get(\"/\", handler)\n\tgo start()\n\n\tc := 1\n\tif len(count) > 0 {\n\t\tc = count[0]\n\t}\n\n\tfor i := 0; i < c; i++ {\n\t\tclient := New().SetDial(ln)\n\t\twrapAgent(client)\n\n\t\tresp, err := client.Get(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, excepted, resp.String())\n\t\tresp.Close()\n\t}\n}\n"
  },
  {
    "path": "client/hooks.go",
    "content": "package client\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nvar protocolCheck = regexp.MustCompile(`^https?://.*$`)\n\nvar fileBufPool = sync.Pool{\n\tNew: func() any {\n\t\tb := make([]byte, 1<<20) // 1MB buffer\n\t\treturn &b\n\t},\n}\n\nconst (\n\theaderAccept      = \"Accept\"\n\tapplicationJSON   = \"application/json\"\n\tapplicationCBOR   = \"application/cbor\"\n\tapplicationXML    = \"application/xml\"\n\tapplicationForm   = \"application/x-www-form-urlencoded\"\n\tmultipartFormData = \"multipart/form-data\"\n\n\tletterBytes = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n)\n\n// unsafeRandString returns a random string of length n.\n// An error is returned if the random source fails.\nfunc unsafeRandString(n int) (string, error) {\n\tinputLength := byte(len(letterBytes))\n\n\t// Compute the largest multiple of inputLength ≤ 256 to avoid modulo bias.\n\t// Any byte ≥ max will be rejected and re‑read.\n\tmaxLength := byte(256 - (256 % int(inputLength)))\n\n\tout := make([]byte, n)\n\tbuf := make([]byte, n)\n\n\t// Read n raw bytes in one shot\n\tif _, err := rand.Read(buf); err != nil {\n\t\treturn \"\", fmt.Errorf(\"rand.Read failed: %w\", err)\n\t}\n\n\tfor i, b := range buf {\n\t\t// Reject values ≥ maxLength\n\t\tfor b >= maxLength {\n\t\t\tif _, err := rand.Read(buf[i : i+1]); err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"rand.Read failed: %w\", err)\n\t\t\t}\n\t\t\tb = buf[i]\n\t\t}\n\t\tout[i] = letterBytes[b%inputLength]\n\t}\n\n\treturn utils.UnsafeString(out), nil\n}\n\n// parserRequestURL sets options for the hostclient and normalizes the URL.\n// It merges the baseURL with the request URI if needed and applies query and path parameters.\nfunc parserRequestURL(c *Client, req *Request) error {\n\t// Split URL into path and query parts using Cut (avoids allocation)\n\turi, queryPart, _ := strings.Cut(req.url, \"?\")\n\n\t// If the URL doesn't start with http/https, prepend the baseURL.\n\tif !protocolCheck.MatchString(uri) {\n\t\turi = c.baseURL + uri\n\t\tif !protocolCheck.MatchString(uri) {\n\t\t\treturn ErrURLFormat\n\t\t}\n\t}\n\n\t// Set path parameters from the request and client.\n\tfor key, val := range req.path.All() {\n\t\turi = strings.ReplaceAll(uri, \":\"+key, val)\n\t}\n\tfor key, val := range c.path.All() {\n\t\turi = strings.ReplaceAll(uri, \":\"+key, val)\n\t}\n\n\t// Set the URI in the raw request.\n\tdisablePathNormalizing := c.DisablePathNormalizing() || req.DisablePathNormalizing()\n\treq.RawRequest.SetRequestURI(uri)\n\treq.RawRequest.URI().DisablePathNormalizing = disablePathNormalizing\n\tif disablePathNormalizing {\n\t\treq.RawRequest.URI().SetPathBytes(req.RawRequest.URI().PathOriginal())\n\t}\n\n\t// Merge query parameters (split query from fragment using Cut).\n\tqueryOnly, hashPart, _ := strings.Cut(queryPart, \"#\")\n\targs := fasthttp.AcquireArgs()\n\tdefer fasthttp.ReleaseArgs(args)\n\n\targs.Parse(queryOnly)\n\n\tfor key, value := range c.params.All() {\n\t\targs.AddBytesKV(key, value)\n\t}\n\tfor key, value := range req.params.All() {\n\t\targs.AddBytesKV(key, value)\n\t}\n\n\treq.RawRequest.URI().SetQueryStringBytes(utils.CopyBytes(args.QueryString()))\n\treq.RawRequest.URI().SetHash(hashPart)\n\n\treturn nil\n}\n\n// parserRequestHeader merges client and request headers, and sets headers automatically based on the request data.\n// It also sets the User-Agent and Referer headers, and applies any cookies from the cookie jar.\nfunc parserRequestHeader(c *Client, req *Request) error {\n\t// Set HTTP method.\n\treq.RawRequest.Header.SetMethod(req.Method())\n\n\t// Merge headers from the client.\n\tfor key, value := range c.header.All() {\n\t\treq.RawRequest.Header.AddBytesKV(key, value)\n\t}\n\n\t// Merge headers from the request.\n\tfor key, value := range req.header.All() {\n\t\treq.RawRequest.Header.AddBytesKV(key, value)\n\t}\n\n\t// Set Content-Type and Accept headers based on the request body type.\n\tswitch req.bodyType {\n\tcase jsonBody:\n\t\treq.RawRequest.Header.SetContentType(applicationJSON)\n\t\treq.RawRequest.Header.Set(headerAccept, applicationJSON)\n\tcase xmlBody:\n\t\treq.RawRequest.Header.SetContentType(applicationXML)\n\tcase cborBody:\n\t\treq.RawRequest.Header.SetContentType(applicationCBOR)\n\tcase formBody:\n\t\treq.RawRequest.Header.SetContentType(applicationForm)\n\tcase filesBody:\n\t\treq.RawRequest.Header.SetContentType(multipartFormData)\n\t\t// If boundary is default, append a random string to it.\n\t\tif req.boundary == boundary {\n\t\t\trandStr, err := unsafeRandString(16)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"boundary generation: %w\", err)\n\t\t\t}\n\t\t\treq.boundary += randStr\n\t\t}\n\t\treq.RawRequest.Header.SetMultipartFormBoundary(req.boundary)\n\tdefault:\n\t\t// noBody or rawBody do not require special handling here.\n\t}\n\n\t// Set User-Agent header.\n\treq.RawRequest.Header.SetUserAgent(defaultUserAgent)\n\tif c.userAgent != \"\" {\n\t\treq.RawRequest.Header.SetUserAgent(c.userAgent)\n\t}\n\tif req.userAgent != \"\" {\n\t\treq.RawRequest.Header.SetUserAgent(req.userAgent)\n\t}\n\n\t// Set Referer header.\n\treq.RawRequest.Header.SetReferer(c.referer)\n\tif req.referer != \"\" {\n\t\treq.RawRequest.Header.SetReferer(req.referer)\n\t}\n\n\t// Set cookies from the cookie jar if available.\n\tif c.cookieJar != nil {\n\t\tc.cookieJar.dumpCookiesToReq(req.RawRequest)\n\t}\n\n\t// Set cookies from the client.\n\tfor key, val := range c.cookies.All() {\n\t\treq.RawRequest.Header.SetCookie(key, val)\n\t}\n\n\t// Set cookies from the request.\n\tfor key, val := range req.cookies.All() {\n\t\treq.RawRequest.Header.SetCookie(key, val)\n\t}\n\n\treturn nil\n}\n\n// parserRequestBody serializes the request body based on its type and sets it into the RawRequest.\nfunc parserRequestBody(c *Client, req *Request) error {\n\tswitch req.bodyType {\n\tcase jsonBody:\n\t\tbody, err := c.jsonMarshal(req.body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.RawRequest.SetBody(body)\n\tcase xmlBody:\n\t\tbody, err := c.xmlMarshal(req.body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.RawRequest.SetBody(body)\n\tcase cborBody:\n\t\tbody, err := c.cborMarshal(req.body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treq.RawRequest.SetBody(body)\n\tcase formBody:\n\t\treq.RawRequest.SetBody(req.formData.QueryString())\n\tcase filesBody:\n\t\treturn parserRequestBodyFile(req)\n\tcase rawBody:\n\t\tif body, ok := req.body.([]byte); ok { //nolint:revive // ignore simplicity\n\t\t\treq.RawRequest.SetBody(body)\n\t\t} else {\n\t\t\treturn ErrBodyType\n\t\t}\n\tcase noBody:\n\t\t// No body to set.\n\t\treturn nil\n\tdefault:\n\t\treturn ErrBodyTypeNotSupported\n\t}\n\treturn nil\n}\n\n// parserRequestBodyFile handles the case where the request contains files to be uploaded.\nfunc parserRequestBodyFile(req *Request) error {\n\tmw := multipart.NewWriter(req.RawRequest.BodyWriter())\n\terr := mw.SetBoundary(req.boundary)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"set boundary error: %w\", err)\n\t}\n\tdefer func() {\n\t\te := mw.Close()\n\t\tif e != nil {\n\t\t\t// Close errors are typically ignored.\n\t\t\treturn\n\t\t}\n\t}()\n\n\t// Add form data.\n\tfor key, value := range req.formData.All() {\n\t\terr = mw.WriteField(utils.UnsafeString(key), utils.UnsafeString(value))\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write formdata error: %w\", err)\n\t}\n\n\t// Add files.\n\tfileBuf, ok := fileBufPool.Get().(*[]byte)\n\tif !ok {\n\t\treturn errSyncPoolBuffer\n\t}\n\n\tdefer fileBufPool.Put(fileBuf)\n\n\tfor i, f := range req.files {\n\t\tif f.name == \"\" && f.path == \"\" {\n\t\t\treturn ErrFileNoName\n\t\t}\n\n\t\t// Set the file name if not provided.\n\t\tif f.name == \"\" && f.path != \"\" {\n\t\t\tf.path = filepath.Clean(f.path)\n\t\t\tf.name = filepath.Base(f.path)\n\t\t}\n\n\t\t// Set the field name if not provided.\n\t\tif f.fieldName == \"\" {\n\t\t\tf.fieldName = \"file\" + strconv.Itoa(i+1)\n\t\t}\n\n\t\tif err := addFormFile(mw, f, fileBuf); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc addFormFile(mw *multipart.Writer, f *File, fileBuf *[]byte) error {\n\t// If reader is not set, open the file.\n\tif f.reader == nil {\n\t\tvar err error\n\t\tf.reader, err = os.Open(f.path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"open file error: %w\", err)\n\t\t}\n\t}\n\n\t// Ensure the file reader is always closed after copying.\n\tdefer f.reader.Close() //nolint:errcheck // not needed\n\n\t// Create form file and copy the content.\n\tw, err := mw.CreateFormFile(f.fieldName, f.name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create file error: %w\", err)\n\t}\n\n\tif _, err := io.CopyBuffer(w, f.reader, *fileBuf); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy file data: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// parserResponseCookie parses the Set-Cookie headers from the response and stores them.\nfunc parserResponseCookie(c *Client, resp *Response, req *Request) error {\n\tvar err error\n\tfor key, value := range resp.RawResponse.Header.Cookies() {\n\t\tcookie := fasthttp.AcquireCookie()\n\t\tif err = cookie.ParseBytes(value); err != nil {\n\t\t\tfasthttp.ReleaseCookie(cookie)\n\t\t\tbreak\n\t\t}\n\t\tcookie.SetKeyBytes(key)\n\t\tresp.cookie = append(resp.cookie, cookie)\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Store cookies in the cookie jar if available.\n\tif c.cookieJar != nil {\n\t\tc.cookieJar.parseCookiesFromResp(req.RawRequest.URI().Host(), req.RawRequest.URI().Path(), resp.RawResponse)\n\t}\n\n\treturn nil\n}\n\n// logger is a response hook that logs request and response data if debug mode is enabled.\nfunc logger(c *Client, resp *Response, req *Request) error {\n\tif !c.debug {\n\t\treturn nil\n\t}\n\n\tc.logger.Debugf(\"%s\\n\", req.RawRequest.String())\n\tc.logger.Debugf(\"%s\\n\", resp.RawResponse.String())\n\n\treturn nil\n}\n"
  },
  {
    "path": "client/hooks_test.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_Rand_String(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname string\n\t\targs int\n\t}{\n\t\t{\n\t\t\tname: \"test generate\",\n\t\t\targs: 16,\n\t\t},\n\t\t{\n\t\t\tname: \"test generate smaller string\",\n\t\t\targs: 8,\n\t\t},\n\t\t{\n\t\t\tname: \"test generate larger string\",\n\t\t\targs: 32,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := unsafeRandString(tt.args)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, got, tt.args)\n\t\t})\n\t}\n\n\tt.Run(\"valid characters\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tgot, err := unsafeRandString(32)\n\t\trequire.NoError(t, err)\n\t\tfor i := 0; i < len(got); i++ {\n\t\t\trequire.Contains(t, letterBytes, string(got[i]))\n\t\t}\n\t})\n}\n\nfunc Test_Parser_Request_URL(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"client baseurl should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetBaseURL(\"http://example.com/api\")\n\t\treq := AcquireRequest().SetURL(\"\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"request url should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().SetURL(\"http://example.com/api\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"the request url will override baseurl with protocol\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetBaseURL(\"http://example.com/api\")\n\t\treq := AcquireRequest().SetURL(\"http://example.com/api/v1\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api/v1\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"the request url should be append after baseurl without protocol\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetBaseURL(\"http://example.com/api\")\n\t\treq := AcquireRequest().SetURL(\"/v1\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api/v1\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"the url is error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetBaseURL(\"example.com/api\")\n\t\treq := AcquireRequest().SetURL(\"/v1\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.Equal(t, ErrURLFormat, err)\n\t})\n\n\tt.Run(\"the path param from client\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetBaseURL(\"http://example.com/api/:id\").\n\t\t\tSetPathParam(\"id\", \"5\")\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api/5\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"the path param from request\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetBaseURL(\"http://example.com/api/:id/:name\").\n\t\t\tSetPathParam(\"id\", \"5\")\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"/{key}\").\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"name\": \"fiber\",\n\t\t\t\t\"key\":  \"val\",\n\t\t\t}).\n\t\t\tDelPathParams(\"key\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api/5/fiber/%7Bkey%7D\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"the path param from request and client\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetBaseURL(\"http://example.com/api/:id/:name\").\n\t\t\tSetPathParam(\"id\", \"5\")\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"/:key\").\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"name\": \"fiber\",\n\t\t\t\t\"key\":  \"val\",\n\t\t\t\t\"id\":   \"12\",\n\t\t\t})\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"http://example.com/api/12/fiber/val\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"query params from client should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetParam(\"foo\", \"bar\")\n\t\treq := AcquireRequest().SetURL(\"http://example.com/api/v1\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"foo=bar\"), req.RawRequest.URI().QueryString())\n\t})\n\n\tt.Run(\"query params from request should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"http://example.com/api/v1\").\n\t\t\tSetParam(\"bar\", \"foo\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"bar=foo\"), req.RawRequest.URI().QueryString())\n\t})\n\n\tt.Run(\"query params should be merged\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetParam(\"bar\", \"foo1\")\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"http://example.com/api/v1?bar=foo2\").\n\t\t\tSetParam(\"bar\", \"foo\")\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\n\t\tvalues, err := url.ParseQuery(string(req.RawRequest.URI().QueryString()))\n\t\trequire.NoError(t, err)\n\n\t\tflag1, flag2, flag3 := false, false, false\n\t\tfor _, v := range values[\"bar\"] {\n\t\t\tswitch v {\n\t\t\tcase \"foo1\":\n\t\t\t\tflag1 = true\n\t\t\tcase \"foo2\":\n\t\t\t\tflag2 = true\n\t\t\tcase \"foo\":\n\t\t\t\tflag3 = true\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unexpected query param value: %s\", v)\n\t\t\t}\n\t\t}\n\t\trequire.True(t, flag1)\n\t\trequire.True(t, flag2)\n\t\trequire.True(t, flag3)\n\t})\n\n\tt.Run(\"request disable path normalizing should be respected\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"https://example.my.host/other.host%2Fpath%2Fto%2Fdata%23123\").\n\t\t\tSetDisablePathNormalizing(true)\n\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"https://example.my.host/other.host%2Fpath%2Fto%2Fdata%23123\", req.RawRequest.URI().String())\n\t})\n\n\tt.Run(\"client disable path normalizing should be respected\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetDisablePathNormalizing(true)\n\t\treq := AcquireRequest().\n\t\t\tSetURL(\"https://example.my.host/other.host%2Fpath%2Fto%2Fdata%23123\")\n\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\terr := parserRequestURL(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"https://example.my.host/other.host%2Fpath%2Fto%2Fdata%23123\", req.RawRequest.URI().String())\n\t})\n}\n\nfunc Test_Parser_Request_Header(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"client header should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\tfiber.HeaderContentType: \"application/json\",\n\t\t\t})\n\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"application/json\"), req.RawRequest.Header.ContentType())\n\t})\n\n\tt.Run(\"request header should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\n\t\treq := AcquireRequest().\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\tfiber.HeaderContentType: \"application/json, utf-8\",\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"application/json, utf-8\"), req.RawRequest.Header.ContentType())\n\t})\n\n\tt.Run(\"request header should override client header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetHeader(fiber.HeaderContentType, \"application/xml\")\n\n\t\treq := AcquireRequest().\n\t\t\tSetHeader(fiber.HeaderContentType, \"application/json, utf-8\")\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"application/json, utf-8\"), req.RawRequest.Header.ContentType())\n\t})\n\n\tt.Run(\"auto set json header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype jsonData struct {\n\t\t\tName string `json:\"name\"`\n\t\t}\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetJSON(jsonData{\n\t\t\t\tName: \"foo\",\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(applicationJSON), req.RawRequest.Header.ContentType()) //nolint:testifylint // test\n\t})\n\n\tt.Run(\"auto set xml header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype xmlData struct {\n\t\t\tXMLName xml.Name `xml:\"body\"`\n\t\t\tName    string   `xml:\"name\"`\n\t\t}\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetXML(xmlData{\n\t\t\t\tName: \"foo\",\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(applicationXML), req.RawRequest.Header.ContentType())\n\t})\n\n\tt.Run(\"auto set form data header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\"foo\":  \"bar\",\n\t\t\t\t\"ball\": \"circle and square\",\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, applicationForm, string(req.RawRequest.Header.ContentType()))\n\t})\n\n\tt.Run(\"auto set file header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tAddFileWithReader(\"hello\", io.NopCloser(strings.NewReader(\"world\"))).\n\t\t\tSetFormData(\"foo\", \"bar\")\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(req.RawRequest.Header.MultipartFormBoundary()), \"FiberFormBoundary\")\n\t\trequire.Contains(t, string(req.RawRequest.Header.ContentType()), multipartFormData)\n\t})\n\n\tt.Run(\"ua should have default value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"fiber\"), req.RawRequest.Header.UserAgent())\n\t})\n\n\tt.Run(\"ua in client should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetUserAgent(\"foo\")\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"foo\"), req.RawRequest.Header.UserAgent())\n\t})\n\n\tt.Run(\"ua in request should have higher level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetUserAgent(\"foo\")\n\t\treq := AcquireRequest().SetUserAgent(\"bar\")\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"bar\"), req.RawRequest.Header.UserAgent())\n\t})\n\n\tt.Run(\"referer in client should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetReferer(\"https://example.com\")\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"https://example.com\"), req.RawRequest.Header.Referer())\n\t})\n\n\tt.Run(\"referer in request should have higher level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().SetReferer(\"http://example.com\")\n\t\treq := AcquireRequest().SetReferer(\"https://example.com\")\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"https://example.com\"), req.RawRequest.Header.Referer())\n\t})\n\n\tt.Run(\"client cookie should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New().\n\t\t\tSetCookie(\"foo\", \"bar\").\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"bar\":  \"foo\",\n\t\t\t\t\"bar1\": \"foo1\",\n\t\t\t}).\n\t\t\tDelCookies(\"bar1\")\n\n\t\treq := AcquireRequest()\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"bar\", string(req.RawRequest.Header.Cookie(\"foo\")))\n\t\trequire.Equal(t, \"foo\", string(req.RawRequest.Header.Cookie(\"bar\")))\n\t\trequire.Empty(t, string(req.RawRequest.Header.Cookie(\"bar1\")))\n\t})\n\n\tt.Run(\"request cookie should be set\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype cookies struct {\n\t\t\tFoo string `cookie:\"foo\"`\n\t\t\tBar int    `cookie:\"bar\"`\n\t\t}\n\n\t\tclient := New()\n\n\t\treq := AcquireRequest().\n\t\t\tSetCookiesWithStruct(&cookies{\n\t\t\t\tFoo: \"bar\",\n\t\t\t\tBar: 67,\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"bar\", string(req.RawRequest.Header.Cookie(\"foo\")))\n\t\trequire.Equal(t, \"67\", string(req.RawRequest.Header.Cookie(\"bar\")))\n\t\trequire.Empty(t, string(req.RawRequest.Header.Cookie(\"bar1\")))\n\t})\n\n\tt.Run(\"request cookie will override client cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype cookies struct {\n\t\t\tFoo string `cookie:\"foo\"`\n\t\t\tBar int    `cookie:\"bar\"`\n\t\t}\n\n\t\tclient := New().\n\t\t\tSetCookie(\"foo\", \"bar\").\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"bar\":  \"foo\",\n\t\t\t\t\"bar1\": \"foo1\",\n\t\t\t})\n\n\t\treq := AcquireRequest().\n\t\t\tSetCookiesWithStruct(&cookies{\n\t\t\t\tFoo: \"bar\",\n\t\t\t\tBar: 67,\n\t\t\t})\n\n\t\terr := parserRequestHeader(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"bar\", string(req.RawRequest.Header.Cookie(\"foo\")))\n\t\trequire.Equal(t, \"67\", string(req.RawRequest.Header.Cookie(\"bar\")))\n\t\trequire.Equal(t, \"foo1\", string(req.RawRequest.Header.Cookie(\"bar1\")))\n\t})\n}\n\nfunc Test_Parser_Request_Body(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"json body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype jsonData struct {\n\t\t\tName string `json:\"name\"`\n\t\t}\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetJSON(jsonData{\n\t\t\t\tName: \"foo\",\n\t\t\t})\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"{\\\"name\\\":\\\"foo\\\"}\"), req.RawRequest.Body()) //nolint:testifylint // test\n\t})\n\n\tt.Run(\"xml body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype xmlData struct {\n\t\t\tXMLName xml.Name `xml:\"body\"`\n\t\t\tName    string   `xml:\"name\"`\n\t\t}\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetXML(xmlData{\n\t\t\t\tName: \"foo\",\n\t\t\t})\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"<body><name>foo</name></body>\"), req.RawRequest.Body())\n\t})\n\n\tt.Run(\"CBOR body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype cborData struct {\n\t\t\tName string `cbor:\"name\"`\n\t\t\tAge  int    `cbor:\"age\"`\n\t\t}\n\n\t\tdata := cborData{\n\t\t\tName: \"foo\",\n\t\t\tAge:  12,\n\t\t}\n\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetCBOR(data)\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\n\t\tencoded, err := cbor.Marshal(data)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, encoded, req.RawRequest.Body())\n\t})\n\n\tt.Run(\"form data body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\"ball\": \"circle and square\",\n\t\t\t})\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"ball=circle+and+square\", string(req.RawRequest.Body()))\n\t})\n\n\tt.Run(\"form data body error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\"\": \"\",\n\t\t\t})\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"file body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tAddFileWithReader(\"hello\", io.NopCloser(strings.NewReader(\"world\")))\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(req.RawRequest.Body()), \"--FiberFormBoundary\")\n\t\trequire.Contains(t, string(req.RawRequest.Body()), \"world\")\n\t})\n\n\tt.Run(\"file body open error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\tmissingPath := filepath.Join(t.TempDir(), \"missing.txt\")\n\n\t\treq := AcquireRequest().AddFile(missingPath)\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.ErrorContains(t, err, \"open file error\")\n\t})\n\n\tt.Run(\"file body missing path and name\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\tfile := AcquireFile(SetFileReader(io.NopCloser(strings.NewReader(\"world\"))))\n\n\t\treq := AcquireRequest().AddFiles(file)\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.ErrorIs(t, err, ErrFileNoName)\n\t})\n\n\tt.Run(\"file and form data\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tAddFileWithReader(\"hello\", io.NopCloser(strings.NewReader(\"world\"))).\n\t\t\tSetFormData(\"foo\", \"bar\")\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(req.RawRequest.Body()), \"--FiberFormBoundary\")\n\t\trequire.Contains(t, string(req.RawRequest.Body()), \"world\")\n\t\trequire.Contains(t, string(req.RawRequest.Body()), \"bar\")\n\t})\n\n\tt.Run(\"raw body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetRawBody([]byte(\"hello world\"))\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"hello world\"), req.RawRequest.Body())\n\t})\n\n\tt.Run(\"raw body error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := New()\n\t\treq := AcquireRequest().\n\t\t\tSetRawBody([]byte(\"hello world\"))\n\n\t\treq.body = nil\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.ErrorIs(t, err, ErrBodyType)\n\t})\n\n\tt.Run(\"unsupported body type\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := New()\n\t\treq := AcquireRequest()\n\n\t\treq.bodyType = 999 // some invalid type\n\n\t\terr := parserRequestBody(client, req)\n\t\trequire.ErrorIs(t, err, ErrBodyTypeNotSupported)\n\t})\n}\n\ntype dummyLogger struct {\n\tbuf *bytes.Buffer\n}\n\nfunc (*dummyLogger) Trace(_ ...any) {}\n\nfunc (*dummyLogger) Debug(_ ...any) {}\n\nfunc (*dummyLogger) Info(_ ...any) {}\n\nfunc (*dummyLogger) Warn(_ ...any) {}\n\nfunc (*dummyLogger) Error(_ ...any) {}\n\nfunc (*dummyLogger) Fatal(_ ...any) {}\n\nfunc (*dummyLogger) Panic(_ ...any) {}\n\nfunc (*dummyLogger) Tracef(_ string, _ ...any) {}\n\nfunc (l *dummyLogger) Debugf(format string, v ...any) {\n\tfmt.Fprintf(l.buf, format, v...)\n}\n\nfunc (*dummyLogger) Infof(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Warnf(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Errorf(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Fatalf(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Panicf(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Tracew(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Debugw(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Infow(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Warnw(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Errorw(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Fatalw(_ string, _ ...any) {}\n\nfunc (*dummyLogger) Panicw(_ string, _ ...any) {}\n\nfunc Test_Client_Logger_Debug(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"response\")\n\t})\n\n\taddrChan := make(chan string)\n\tgo func() {\n\t\tassert.NoError(t, app.Listen(\":0\", fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t}))\n\t}()\n\n\tdefer func(app *fiber.App) {\n\t\trequire.NoError(t, app.Shutdown())\n\t}(app)\n\n\tvar buf bytes.Buffer\n\tlogger := &dummyLogger{buf: &buf}\n\n\tclient := New()\n\tclient.Debug().SetLogger(logger)\n\n\taddr := <-addrChan\n\tresp, err := client.Get(\"http://\" + addr)\n\trequire.NoError(t, err)\n\tdefer resp.Close()\n\n\trequire.NoError(t, err)\n\trequire.Contains(t, buf.String(), \"Host: \"+addr)\n\trequire.Contains(t, buf.String(), \"Content-Length: 8\")\n}\n\nfunc Test_Client_Logger_DisableDebug(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"response\")\n\t})\n\n\taddrChan := make(chan string)\n\tgo func() {\n\t\tassert.NoError(t, app.Listen(\":0\", fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\t\taddrChan <- addr.String()\n\t\t\t},\n\t\t}))\n\t}()\n\n\tdefer func(app *fiber.App) {\n\t\trequire.NoError(t, app.Shutdown())\n\t}(app)\n\n\tvar buf bytes.Buffer\n\tlogger := &dummyLogger{buf: &buf}\n\n\tclient := New()\n\tclient.DisableDebug().SetLogger(logger)\n\n\taddr := <-addrChan\n\tresp, err := client.Get(\"http://\" + addr)\n\trequire.NoError(t, err)\n\tdefer resp.Close()\n\n\trequire.NoError(t, err)\n\trequire.Empty(t, buf.String())\n}\n\nfunc Benchmark_Parser_Request_Body_File(b *testing.B) {\n\tb.Helper()\n\n\tconst (\n\t\tfileCount = 3\n\t\tfileSize  = 32 << 10 // 32KB payload per file\n\t)\n\n\tformValues := map[string]string{\n\t\t\"username\": \"fiber\",\n\t\t\"api_key\":  \"d5942ef5\",\n\t}\n\n\tfileContents := make([][]byte, fileCount)\n\tfor i := range fileContents {\n\t\tfileContents[i] = bytes.Repeat([]byte{byte('a' + i)}, fileSize)\n\t}\n\n\tb.ReportAllocs()\n\n\tfor i := 0; i < b.N; i++ {\n\t\tvar totalBytes int64\n\t\tfor _, c := range fileContents {\n\t\t\ttotalBytes += int64(len(c))\n\t\t}\n\t\tb.SetBytes(totalBytes)\n\t\treq := newBenchmarkRequest(formValues, fileContents)\n\t\tif err := parserRequestBodyFile(req); err != nil {\n\t\t\tb.Fatalf(\"parserRequestBodyFile: %v\", err)\n\t\t}\n\t\treleaseBenchmarkRequest(req)\n\t}\n}\n\nfunc newBenchmarkRequest(formValues map[string]string, fileContents [][]byte) *Request {\n\treq := &Request{\n\t\tboundary:   \"FiberBenchmarkBoundary\",\n\t\tformData:   FormData{Args: fasthttp.AcquireArgs()},\n\t\tRawRequest: fasthttp.AcquireRequest(),\n\t\tfiles:      make([]*File, len(fileContents)),\n\t}\n\n\treq.RawRequest.Header.SetContentType(\"multipart/form-data; boundary=\" + req.boundary)\n\n\tfor key, value := range formValues {\n\t\treq.formData.Set(key, value)\n\t}\n\n\tfor i, content := range fileContents {\n\t\treq.files[i] = AcquireFile(\n\t\t\tSetFileName(fmt.Sprintf(\"file-%d.bin\", i)),\n\t\t\tSetFileFieldName(fmt.Sprintf(\"file%d\", i)),\n\t\t\tSetFileReader(io.NopCloser(bytes.NewReader(content))),\n\t\t)\n\t}\n\n\treturn req\n}\n\nfunc releaseBenchmarkRequest(req *Request) {\n\tfasthttp.ReleaseRequest(req.RawRequest)\n\tfasthttp.ReleaseArgs(req.formData.Args)\n\tfor _, f := range req.files {\n\t\tReleaseFile(f)\n\t}\n}\n"
  },
  {
    "path": "client/request.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"iter\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// WithStruct is implemented by types that allow data to be stored from a struct via reflection.\ntype WithStruct interface {\n\tAdd(name, obj string)\n\tDel(name string)\n}\n\n// bodyType defines the type of request body.\ntype bodyType int\n\n// Enumeration of request body types.\nconst (\n\tnoBody bodyType = iota\n\tjsonBody\n\txmlBody\n\tformBody\n\tfilesBody\n\trawBody\n\tcborBody\n)\n\nvar ErrClientNil = errors.New(\"client cannot be nil\")\n\n// Request contains all data related to an HTTP request.\ntype Request struct {\n\tctx context.Context //nolint:containedctx // Context is needed to be stored in the request.\n\n\tbody    any\n\theader  Header\n\tparams  QueryParam\n\tcookies Cookie\n\tpath    PathParam\n\n\tclient *Client\n\n\tformData FormData\n\n\tRawRequest *fasthttp.Request\n\turl        string\n\tmethod     string\n\tuserAgent  string\n\tboundary   string\n\treferer    string\n\tfiles      []*File\n\n\ttimeout      time.Duration\n\tmaxRedirects int\n\n\tbodyType bodyType\n\n\tdisablePathNormalizing bool\n}\n\n// Method returns the HTTP method set in the Request.\nfunc (r *Request) Method() string {\n\treturn r.method\n}\n\n// SetMethod sets the HTTP method for the Request.\n// It is recommended to use the specialized methods (e.g., Get, Post) instead.\nfunc (r *Request) SetMethod(method string) *Request {\n\tr.method = method\n\treturn r\n}\n\n// URL returns the URL set in the Request.\nfunc (r *Request) URL() string {\n\treturn r.url\n}\n\n// SetURL sets the URL for the Request.\nfunc (r *Request) SetURL(url string) *Request {\n\tr.url = url\n\treturn r\n}\n\n// Client returns the Client instance associated with this Request.\nfunc (r *Request) Client() *Client {\n\treturn r.client\n}\n\n// SetClient sets the Client instance for the Request.\nfunc (r *Request) SetClient(c *Client) *Request {\n\tif c == nil {\n\t\tpanic(ErrClientNil)\n\t}\n\n\tr.client = c\n\treturn r\n}\n\n// Context returns the context associated with the Request.\n// If not set, a background context is returned.\nfunc (r *Request) Context() context.Context {\n\tif r.ctx == nil {\n\t\treturn context.Background()\n\t}\n\treturn r.ctx\n}\n\n// SetContext sets the context for the Request, allowing request cancellation if ctx is done.\n// See https://blog.golang.org/context article and the \"context\" package documentation.\nfunc (r *Request) SetContext(ctx context.Context) *Request {\n\tr.ctx = ctx\n\treturn r\n}\n\n// Header returns all values associated with the given header key.\nfunc (r *Request) Header(key string) []string {\n\treturn r.header.PeekMultiple(key)\n}\n\ntype pair struct {\n\tk []string\n\tv []string\n}\n\n// Len implements sort.Interface and reports the number of tracked keys.\nfunc (p *pair) Len() int {\n\treturn len(p.k)\n}\n\n// Swap implements sort.Interface and swaps the entries at the provided indices.\nfunc (p *pair) Swap(i, j int) {\n\tp.k[i], p.k[j] = p.k[j], p.k[i]\n\tp.v[i], p.v[j] = p.v[j], p.v[i]\n}\n\n// Less implements sort.Interface and orders entries lexicographically by key.\nfunc (p *pair) Less(i, j int) bool {\n\treturn p.k[i] < p.k[j]\n}\n\n// Headers returns an iterator over all headers in the Request.\n// Use maps.Collect() to gather them into a map if needed.\n//\n// The returned values are only valid until the request object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Request) Headers() iter.Seq2[string, []string] {\n\treturn func(yield func(string, []string) bool) {\n\t\tpeekKeys := r.header.PeekKeys()\n\n\t\t// Copy keys to immutable strings to decouple from fasthttp's internal buffers.\n\t\tkeys := make([]string, len(peekKeys))\n\t\tfor i, key := range peekKeys {\n\t\t\tkeys[i] = utils.UnsafeString(key)\n\t\t}\n\n\t\tfor _, key := range keys {\n\t\t\tvals := r.header.PeekAll(key)\n\t\t\tvalsStr := make([]string, len(vals))\n\t\t\tfor i, v := range vals {\n\t\t\t\tvalsStr[i] = utils.UnsafeString(v)\n\t\t\t}\n\n\t\t\tif !yield(key, valsStr) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// AddHeader adds a single header field and value to the Request.\nfunc (r *Request) AddHeader(key, val string) *Request {\n\tr.header.Add(key, val)\n\treturn r\n}\n\n// SetHeader sets a single header field and value in the Request, overriding any previously set value.\nfunc (r *Request) SetHeader(key, val string) *Request {\n\tr.header.Del(key)\n\tr.header.Set(key, val)\n\treturn r\n}\n\n// AddHeaders adds multiple header fields and values at once.\nfunc (r *Request) AddHeaders(h map[string][]string) *Request {\n\tr.header.AddHeaders(h)\n\treturn r\n}\n\n// SetHeaders sets multiple header fields and values at once, overriding previously set values.\nfunc (r *Request) SetHeaders(h map[string]string) *Request {\n\tr.header.SetHeaders(h)\n\treturn r\n}\n\n// Param returns all values associated with the given query parameter.\nfunc (r *Request) Param(key string) []string {\n\ttmp := r.params.PeekMulti(key)\n\tres := make([]string, 0, len(tmp))\n\tfor _, v := range tmp {\n\t\tres = append(res, utils.UnsafeString(v))\n\t}\n\treturn res\n}\n\n// Params returns an iterator over all query parameters in the Request.\n// Use maps.Collect() to gather them into a map if needed.\n//\n// The returned values are only valid until the request object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Request) Params() iter.Seq2[string, []string] {\n\treturn func(yield func(string, []string) bool) {\n\t\tvals := r.params.Len()\n\t\tif vals == 0 {\n\t\t\treturn\n\t\t}\n\t\tprealloc := make([]string, 2*vals)\n\t\tp := pair{\n\t\t\tk: prealloc[:0:vals],\n\t\t\tv: prealloc[vals : vals : 2*vals],\n\t\t}\n\t\tfor k, v := range r.params.All() {\n\t\t\tp.k = append(p.k, utils.UnsafeString(k))\n\t\t\tp.v = append(p.v, utils.UnsafeString(v))\n\t\t}\n\t\tsort.Sort(&p)\n\n\t\tj := 0\n\t\tfor i := range vals {\n\t\t\tif i == vals-1 || p.k[i] != p.k[i+1] {\n\t\t\t\tif !yield(p.k[i], p.v[j:i+1]) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tj = i + 1\n\t\t\t}\n\t\t}\n\t}\n}\n\n// AddParam adds a single query parameter and value to the Request.\nfunc (r *Request) AddParam(key, val string) *Request {\n\tr.params.Add(key, val)\n\treturn r\n}\n\n// SetParam sets a single query parameter and value in the Request, overriding any previously set value.\nfunc (r *Request) SetParam(key, val string) *Request {\n\tr.params.Set(key, val)\n\treturn r\n}\n\n// AddParams adds multiple query parameters and their values at once.\nfunc (r *Request) AddParams(m map[string][]string) *Request {\n\tr.params.AddParams(m)\n\treturn r\n}\n\n// SetParams sets multiple query parameters and their values at once, overriding previously set values.\nfunc (r *Request) SetParams(m map[string]string) *Request {\n\tr.params.SetParams(m)\n\treturn r\n}\n\n// SetParamsWithStruct sets multiple query parameters from a struct, overriding previously set values.\nfunc (r *Request) SetParamsWithStruct(v any) *Request {\n\tr.params.SetParamsWithStruct(v)\n\treturn r\n}\n\n// DelParams deletes one or more query parameters.\nfunc (r *Request) DelParams(key ...string) *Request {\n\tfor _, v := range key {\n\t\tr.params.Del(v)\n\t}\n\treturn r\n}\n\n// UserAgent returns the User-Agent header set in the Request.\nfunc (r *Request) UserAgent() string {\n\treturn r.userAgent\n}\n\n// SetUserAgent sets the User-Agent header, overriding any previously set value.\nfunc (r *Request) SetUserAgent(ua string) *Request {\n\tr.userAgent = ua\n\treturn r\n}\n\n// Boundary returns the multipart boundary used by the Request.\nfunc (r *Request) Boundary() string {\n\treturn r.boundary\n}\n\n// SetBoundary sets the multipart boundary.\nfunc (r *Request) SetBoundary(b string) *Request {\n\tr.boundary = b\n\treturn r\n}\n\n// Referer returns the Referer header set in the Request.\nfunc (r *Request) Referer() string {\n\treturn r.referer\n}\n\n// SetReferer sets the Referer header, overriding any previously set value.\nfunc (r *Request) SetReferer(referer string) *Request {\n\tr.referer = referer\n\treturn r\n}\n\n// Cookie returns the value of a named cookie.\n// If the cookie does not exist, an empty string is returned.\nfunc (r *Request) Cookie(key string) string {\n\tif val, ok := r.cookies[key]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n\n// Cookies returns an iterator over all cookies.\n// Use maps.Collect() to gather them into a map if needed.\nfunc (r *Request) Cookies() iter.Seq2[string, string] {\n\treturn r.cookies.All()\n}\n\n// SetCookie sets a single cookie, overriding any previously set value.\nfunc (r *Request) SetCookie(key, val string) *Request {\n\tr.cookies.SetCookie(key, val)\n\treturn r\n}\n\n// SetCookies sets multiple cookies at once, overriding previously set values.\nfunc (r *Request) SetCookies(m map[string]string) *Request {\n\tr.cookies.SetCookies(m)\n\treturn r\n}\n\n// SetCookiesWithStruct sets multiple cookies from a struct, overriding previously set values.\nfunc (r *Request) SetCookiesWithStruct(v any) *Request {\n\tr.cookies.SetCookiesWithStruct(v)\n\treturn r\n}\n\n// DelCookies deletes one or more cookies.\nfunc (r *Request) DelCookies(key ...string) *Request {\n\tr.cookies.DelCookies(key...)\n\treturn r\n}\n\n// PathParam returns the value of a named path parameter.\n// If the parameter does not exist, an empty string is returned.\nfunc (r *Request) PathParam(key string) string {\n\tif val, ok := r.path[key]; ok {\n\t\treturn val\n\t}\n\treturn \"\"\n}\n\n// PathParams returns an iterator over all path parameters.\n// Use maps.Collect() to gather them into a map if needed.\nfunc (r *Request) PathParams() iter.Seq2[string, string] {\n\treturn r.path.All()\n}\n\n// SetPathParam sets a single path parameter and value, overriding any previously set value.\nfunc (r *Request) SetPathParam(key, val string) *Request {\n\tr.path.SetParam(key, val)\n\treturn r\n}\n\n// SetPathParams sets multiple path parameters and values at once, overriding previously set values.\nfunc (r *Request) SetPathParams(m map[string]string) *Request {\n\tr.path.SetParams(m)\n\treturn r\n}\n\n// SetPathParamsWithStruct sets multiple path parameters from a struct, overriding previously set values.\nfunc (r *Request) SetPathParamsWithStruct(v any) *Request {\n\tr.path.SetParamsWithStruct(v)\n\treturn r\n}\n\n// DelPathParams deletes one or more path parameters.\nfunc (r *Request) DelPathParams(key ...string) *Request {\n\tr.path.DelParams(key...)\n\treturn r\n}\n\n// ResetPathParams deletes all path parameters.\nfunc (r *Request) ResetPathParams() *Request {\n\tr.path.Reset()\n\treturn r\n}\n\n// SetJSON sets the request body to a JSON-encoded value.\nfunc (r *Request) SetJSON(v any) *Request {\n\tr.body = v\n\tr.bodyType = jsonBody\n\treturn r\n}\n\n// SetXML sets the request body to an XML-encoded value.\nfunc (r *Request) SetXML(v any) *Request {\n\tr.body = v\n\tr.bodyType = xmlBody\n\treturn r\n}\n\n// SetCBOR sets the request body to a CBOR-encoded value.\nfunc (r *Request) SetCBOR(v any) *Request {\n\tr.body = v\n\tr.bodyType = cborBody\n\treturn r\n}\n\n// SetRawBody sets the request body to raw bytes.\nfunc (r *Request) SetRawBody(v []byte) *Request {\n\tr.body = v\n\tr.bodyType = rawBody\n\treturn r\n}\n\n// resetBody clears the existing body. If the current body type is filesBody and\n// the new type is formBody, the formBody setting is ignored to preserve files.\nfunc (r *Request) resetBody(t bodyType) {\n\tr.body = nil\n\n\t// If bodyType is filesBody and we attempt to set formBody, ignore the change.\n\tif r.bodyType == filesBody && t == formBody {\n\t\treturn\n\t}\n\tr.bodyType = t\n}\n\n// FormData returns all values associated with a form field.\nfunc (r *Request) FormData(key string) []string {\n\ttmp := r.formData.PeekMulti(key)\n\tres := make([]string, 0, len(tmp))\n\tfor _, v := range tmp {\n\t\tres = append(res, utils.UnsafeString(v))\n\t}\n\treturn res\n}\n\n// AllFormData returns an iterator over all form fields.\n// Use maps.Collect() to gather them into a map if needed.\n//\n// The returned values are only valid until the request object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Request) AllFormData() iter.Seq2[string, []string] {\n\treturn func(yield func(string, []string) bool) {\n\t\tvals := r.formData.Len()\n\t\tif vals == 0 {\n\t\t\treturn\n\t\t}\n\t\tprealloc := make([]string, 2*vals)\n\t\tp := pair{\n\t\t\tk: prealloc[:0:vals],\n\t\t\tv: prealloc[vals : vals : 2*vals],\n\t\t}\n\t\tfor k, v := range r.formData.All() {\n\t\t\tp.k = append(p.k, utils.UnsafeString(k))\n\t\t\tp.v = append(p.v, utils.UnsafeString(v))\n\t\t}\n\t\tsort.Sort(&p)\n\n\t\tj := 0\n\t\tfor i := range vals {\n\t\t\tif i == vals-1 || p.k[i] != p.k[i+1] {\n\t\t\t\tif !yield(p.k[i], p.v[j:i+1]) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tj = i + 1\n\t\t\t}\n\t\t}\n\t}\n}\n\n// AddFormData adds a single form field and value to the Request.\nfunc (r *Request) AddFormData(key, val string) *Request {\n\tr.formData.Add(key, val)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// SetFormData sets a single form field and value, overriding any previously set value.\nfunc (r *Request) SetFormData(key, val string) *Request {\n\tr.formData.Set(key, val)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// AddFormDataWithMap adds multiple form fields and values to the Request.\nfunc (r *Request) AddFormDataWithMap(m map[string][]string) *Request {\n\tr.formData.AddWithMap(m)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// SetFormDataWithMap sets multiple form fields and values at once, overriding previously set values.\nfunc (r *Request) SetFormDataWithMap(m map[string]string) *Request {\n\tr.formData.SetWithMap(m)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// SetFormDataWithStruct sets multiple form fields from a struct, overriding previously set values.\nfunc (r *Request) SetFormDataWithStruct(v any) *Request {\n\tr.formData.SetWithStruct(v)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// DelFormData deletes one or more form fields.\nfunc (r *Request) DelFormData(key ...string) *Request {\n\tr.formData.DelData(key...)\n\tr.resetBody(formBody)\n\treturn r\n}\n\n// File returns the file associated with the given name.\n// If no name was provided during addition, it attempts to match by the file's base name.\nfunc (r *Request) File(name string) *File {\n\tfor _, v := range r.files {\n\t\tswitch v.name {\n\t\tcase \"\":\n\t\t\tif filepath.Base(v.path) == name {\n\t\t\t\treturn v\n\t\t\t}\n\t\tcase name:\n\t\t\treturn v\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn nil\n}\n\n// Files returns all files added to the Request.\n//\n// The returned values are only valid until the request object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Request) Files() []*File {\n\treturn r.files\n}\n\n// FileByPath returns the file associated with the given file path.\nfunc (r *Request) FileByPath(path string) *File {\n\tfor _, v := range r.files {\n\t\tif v.path == path {\n\t\t\treturn v\n\t\t}\n\t}\n\treturn nil\n}\n\n// AddFile adds a single file by its path.\nfunc (r *Request) AddFile(path string) *Request {\n\tr.files = append(r.files, AcquireFile(SetFilePath(path)))\n\tr.resetBody(filesBody)\n\treturn r\n}\n\n// AddFileWithReader adds a file using an io.ReadCloser.\nfunc (r *Request) AddFileWithReader(name string, reader io.ReadCloser) *Request {\n\tr.files = append(r.files, AcquireFile(SetFileName(name), SetFileReader(reader)))\n\tr.resetBody(filesBody)\n\treturn r\n}\n\n// AddFiles adds multiple files at once.\nfunc (r *Request) AddFiles(files ...*File) *Request {\n\tr.files = append(r.files, files...)\n\tr.resetBody(filesBody)\n\treturn r\n}\n\n// Timeout returns the timeout duration set in the Request.\nfunc (r *Request) Timeout() time.Duration {\n\treturn r.timeout\n}\n\n// SetTimeout sets the timeout for the Request, overriding any previously set value.\nfunc (r *Request) SetTimeout(t time.Duration) *Request {\n\tr.timeout = t\n\treturn r\n}\n\n// MaxRedirects returns the maximum number of redirects configured for the Request.\nfunc (r *Request) MaxRedirects() int {\n\treturn r.maxRedirects\n}\n\n// SetMaxRedirects sets the maximum number of redirects, overriding any previously set value.\nfunc (r *Request) SetMaxRedirects(count int) *Request {\n\tr.maxRedirects = count\n\treturn r\n}\n\n// DisablePathNormalizing reports whether path normalizing is disabled for the Request.\nfunc (r *Request) DisablePathNormalizing() bool {\n\treturn r.disablePathNormalizing\n}\n\n// SetDisablePathNormalizing configures the Request to disable or enable path normalizing.\nfunc (r *Request) SetDisablePathNormalizing(disable bool) *Request {\n\tr.disablePathNormalizing = disable\n\tr.RawRequest.URI().DisablePathNormalizing = disable\n\treturn r\n}\n\n// checkClient ensures that a Client is set. If none is set, it defaults to the global defaultClient.\nfunc (r *Request) checkClient() {\n\tif r.client == nil {\n\t\tr.SetClient(defaultClient)\n\t}\n}\n\n// Get sends a GET request to the given URL.\nfunc (r *Request) Get(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodGet).Send()\n}\n\n// Post sends a POST request to the given URL.\nfunc (r *Request) Post(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodPost).Send()\n}\n\n// Head sends a HEAD request to the given URL.\nfunc (r *Request) Head(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodHead).Send()\n}\n\n// Put sends a PUT request to the given URL.\nfunc (r *Request) Put(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodPut).Send()\n}\n\n// Delete sends a DELETE request to the given URL.\nfunc (r *Request) Delete(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodDelete).Send()\n}\n\n// Options sends an OPTIONS request to the given URL.\nfunc (r *Request) Options(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodOptions).Send()\n}\n\n// Patch sends a PATCH request to the given URL.\nfunc (r *Request) Patch(url string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(fiber.MethodPatch).Send()\n}\n\n// Custom sends a request with a custom HTTP method to the given URL.\nfunc (r *Request) Custom(url, method string) (*Response, error) {\n\treturn r.SetURL(url).SetMethod(method).Send()\n}\n\n// Send executes the Request.\nfunc (r *Request) Send() (*Response, error) {\n\tr.checkClient()\n\treturn newCore().execute(r.Context(), r.Client(), r)\n}\n\n// Reset clears the Request object, returning it to its default state.\n// Used by ReleaseRequest to recycle the object.\nfunc (r *Request) Reset() {\n\tr.url = \"\"\n\tr.method = fiber.MethodGet\n\tr.userAgent = \"\"\n\tr.referer = \"\"\n\tr.ctx = nil\n\tr.body = nil\n\tr.timeout = 0\n\tr.maxRedirects = 0\n\tr.bodyType = noBody\n\tr.boundary = boundary\n\tr.disablePathNormalizing = false\n\n\tfor len(r.files) != 0 {\n\t\tt := r.files[0]\n\t\tr.files = r.files[1:]\n\t\tReleaseFile(t)\n\t}\n\n\tr.formData.Reset()\n\tr.path.Reset()\n\tr.cookies.Reset()\n\tr.header.Reset()\n\tr.params.Reset()\n\tr.RawRequest.Reset()\n}\n\n// Header wraps fasthttp.RequestHeader, storing headers for both client and request.\ntype Header struct {\n\t*fasthttp.RequestHeader\n}\n\n// PeekMultiple returns multiple values of a header field with the same key.\nfunc (h *Header) PeekMultiple(key string) []string {\n\tvar res []string\n\tbyteKey := []byte(key)\n\tfor k, value := range h.All() {\n\t\tif bytes.EqualFold(k, byteKey) {\n\t\t\tres = append(res, utils.UnsafeString(value))\n\t\t}\n\t}\n\treturn res\n}\n\n// AddHeaders adds multiple headers from a map.\nfunc (h *Header) AddHeaders(r map[string][]string) {\n\tfor k, v := range r {\n\t\tfor _, vv := range v {\n\t\t\th.Add(k, vv)\n\t\t}\n\t}\n}\n\n// SetHeaders sets multiple headers from a map, overriding previously set values.\nfunc (h *Header) SetHeaders(r map[string]string) {\n\tfor k, v := range r {\n\t\th.Del(k)\n\t\th.Set(k, v)\n\t}\n}\n\n// QueryParam wraps fasthttp.Args for query parameters.\ntype QueryParam struct {\n\t*fasthttp.Args\n}\n\n// Keys returns all keys from the query parameters.\nfunc (p *QueryParam) Keys() []string {\n\tkeys := make([]string, 0, p.Len())\n\tfor key := range p.All() {\n\t\tkeys = append(keys, utils.UnsafeString(key))\n\t}\n\treturn slices.Compact(keys)\n}\n\n// AddParams adds multiple parameters from a map.\nfunc (p *QueryParam) AddParams(r map[string][]string) {\n\tfor k, v := range r {\n\t\tfor _, vv := range v {\n\t\t\tp.Add(k, vv)\n\t\t}\n\t}\n}\n\n// SetParams sets multiple parameters from a map, overriding previously set values.\nfunc (p *QueryParam) SetParams(r map[string]string) {\n\tfor k, v := range r {\n\t\tp.Set(k, v)\n\t}\n}\n\n// SetParamsWithStruct sets multiple parameters from a struct.\n// Nested structs are not currently supported.\nfunc (p *QueryParam) SetParamsWithStruct(v any) {\n\tSetValWithStruct(p, \"param\", v)\n}\n\n// Cookie is a map used to store cookies.\ntype Cookie map[string]string\n\n// Add adds a cookie key-value pair.\nfunc (c Cookie) Add(key, val string) {\n\tc[key] = val\n}\n\n// Del deletes a cookie by key.\nfunc (c Cookie) Del(key string) {\n\tdelete(c, key)\n}\n\n// SetCookie sets a single cookie value.\nfunc (c Cookie) SetCookie(key, val string) {\n\tc[key] = val\n}\n\n// SetCookies sets multiple cookies from a map.\nfunc (c Cookie) SetCookies(m map[string]string) {\n\tmaps.Copy(c, m)\n}\n\n// SetCookiesWithStruct sets cookies from a struct.\n// Nested structs are not currently supported.\nfunc (c Cookie) SetCookiesWithStruct(v any) {\n\tSetValWithStruct(c, \"cookie\", v)\n}\n\n// DelCookies deletes multiple cookies by keys.\nfunc (c Cookie) DelCookies(key ...string) {\n\tfor _, v := range key {\n\t\tc.Del(v)\n\t}\n}\n\n// All returns an iterator over cookie key-value pairs.\n//\n// The returned key and value should not be retained after the iteration loop.\nfunc (c Cookie) All() iter.Seq2[string, string] {\n\treturn maps.All(c)\n}\n\n// Reset clears the Cookie map.\nfunc (c Cookie) Reset() {\n\tclear(c)\n}\n\n// PathParam is a map used to store path parameters.\ntype PathParam map[string]string\n\n// Add adds a path parameter key-value pair.\nfunc (p PathParam) Add(key, val string) {\n\tp[key] = val\n}\n\n// Del deletes a path parameter by key.\nfunc (p PathParam) Del(key string) {\n\tdelete(p, key)\n}\n\n// SetParam sets a single path parameter.\nfunc (p PathParam) SetParam(key, val string) {\n\tp[key] = val\n}\n\n// SetParams sets multiple path parameters from a map.\nfunc (p PathParam) SetParams(m map[string]string) {\n\tmaps.Copy(p, m)\n}\n\n// SetParamsWithStruct sets multiple path parameters from a struct.\n// Nested structs are not currently supported.\nfunc (p PathParam) SetParamsWithStruct(v any) {\n\tSetValWithStruct(p, \"path\", v)\n}\n\n// DelParams deletes multiple path parameters.\nfunc (p PathParam) DelParams(key ...string) {\n\tfor _, v := range key {\n\t\tp.Del(v)\n\t}\n}\n\n// All returns an iterator over path parameter key-value pairs.\n//\n// The returned key and value should not be retained after the iteration loop.\nfunc (p PathParam) All() iter.Seq2[string, string] {\n\treturn maps.All(p)\n}\n\n// Reset clears the PathParam map.\nfunc (p PathParam) Reset() {\n\tclear(p)\n}\n\n// FormData wraps fasthttp.Args for URL-encoded bodies and form data.\ntype FormData struct {\n\t*fasthttp.Args\n}\n\n// Keys returns all keys from the form data.\nfunc (f *FormData) Keys() []string {\n\tkeys := make([]string, 0, f.Len())\n\tfor key := range f.All() {\n\t\tkeys = append(keys, utils.UnsafeString(key))\n\t}\n\treturn slices.Compact(keys)\n}\n\n// Add adds a single form field.\nfunc (f *FormData) Add(key, val string) {\n\tf.Args.Add(key, val)\n}\n\n// Set sets a single form field, overriding previously set values.\nfunc (f *FormData) Set(key, val string) {\n\tf.Args.Set(key, val)\n}\n\n// AddWithMap adds multiple form fields from a map.\nfunc (f *FormData) AddWithMap(m map[string][]string) {\n\tfor k, v := range m {\n\t\tfor _, vv := range v {\n\t\t\tf.Add(k, vv)\n\t\t}\n\t}\n}\n\n// SetWithMap sets multiple form fields from a map, overriding previously set values.\nfunc (f *FormData) SetWithMap(m map[string]string) {\n\tfor k, v := range m {\n\t\tf.Set(k, v)\n\t}\n}\n\n// SetWithStruct sets multiple form fields from a struct.\n// Nested structs are not currently supported.\nfunc (f *FormData) SetWithStruct(v any) {\n\tSetValWithStruct(f, \"form\", v)\n}\n\n// DelData deletes multiple form fields.\nfunc (f *FormData) DelData(key ...string) {\n\tfor _, v := range key {\n\t\tf.Del(v)\n\t}\n}\n\n// Reset clears the FormData object.\nfunc (f *FormData) Reset() {\n\tf.Args.Reset()\n}\n\n// File represents a file to be sent with the request.\ntype File struct {\n\treader    io.ReadCloser\n\tname      string\n\tfieldName string\n\tpath      string\n}\n\n// SetName sets the file's name.\nfunc (f *File) SetName(n string) {\n\tf.name = n\n}\n\n// SetFieldName sets the key associated with the file in the body.\nfunc (f *File) SetFieldName(n string) {\n\tf.fieldName = n\n}\n\n// SetPath sets the file's path.\nfunc (f *File) SetPath(p string) {\n\tf.path = p\n}\n\n// SetReader sets the file's reader, which will be closed in the parserBody hook.\nfunc (f *File) SetReader(r io.ReadCloser) {\n\tf.reader = r\n}\n\n// Reset clears the File object.\nfunc (f *File) Reset() {\n\tf.name = \"\"\n\tf.fieldName = \"\"\n\tf.path = \"\"\n\tf.reader = nil\n}\n\nvar requestPool = &sync.Pool{\n\tNew: func() any {\n\t\treturn &Request{\n\t\t\theader:     Header{RequestHeader: &fasthttp.RequestHeader{}},\n\t\t\tparams:     QueryParam{Args: fasthttp.AcquireArgs()},\n\t\t\tcookies:    Cookie{},\n\t\t\tpath:       PathParam{},\n\t\t\tboundary:   boundary,\n\t\t\tformData:   FormData{Args: fasthttp.AcquireArgs()},\n\t\t\tfiles:      make([]*File, 0),\n\t\t\tRawRequest: fasthttp.AcquireRequest(),\n\t\t}\n\t},\n}\n\n// AcquireRequest returns a new (pooled) Request object.\nfunc AcquireRequest() *Request {\n\treq, ok := requestPool.Get().(*Request)\n\tif !ok {\n\t\tpanic(errRequestTypeAssertion)\n\t}\n\treturn req\n}\n\n// ReleaseRequest returns the Request object to the pool.\n// Do not use the released Request afterward to avoid data races.\nfunc ReleaseRequest(req *Request) {\n\treq.Reset()\n\trequestPool.Put(req)\n}\n\nvar filePool sync.Pool\n\n// SetFileFunc defines a function that modifies a File object.\ntype SetFileFunc func(f *File)\n\n// SetFileName sets the file name.\nfunc SetFileName(n string) SetFileFunc {\n\treturn func(f *File) {\n\t\tf.SetName(n)\n\t}\n}\n\n// SetFileFieldName sets the file's field name.\nfunc SetFileFieldName(p string) SetFileFunc {\n\treturn func(f *File) {\n\t\tf.SetFieldName(p)\n\t}\n}\n\n// SetFilePath sets the file path.\nfunc SetFilePath(p string) SetFileFunc {\n\treturn func(f *File) {\n\t\tf.SetPath(p)\n\t}\n}\n\n// SetFileReader sets the file's reader.\nfunc SetFileReader(r io.ReadCloser) SetFileFunc {\n\treturn func(f *File) {\n\t\tf.SetReader(r)\n\t}\n}\n\n// AcquireFile returns a (pooled) File object and applies the provided SetFileFunc functions to it.\nfunc AcquireFile(setter ...SetFileFunc) *File {\n\tfv := filePool.Get()\n\tif fv != nil {\n\t\tf, ok := fv.(*File)\n\t\tif !ok {\n\t\t\tpanic(errFileTypeAssertion)\n\t\t}\n\t\tfor _, v := range setter {\n\t\t\tv(f)\n\t\t}\n\t\treturn f\n\t}\n\tf := &File{}\n\tfor _, v := range setter {\n\t\tv(f)\n\t}\n\treturn f\n}\n\n// ReleaseFile returns the File object to the pool.\n// Do not use the released File afterward to avoid data races.\nfunc ReleaseFile(f *File) {\n\tf.Reset()\n\tfilePool.Put(f)\n}\n\n// SetValWithStruct sets values using a struct. The struct's fields are examined via reflection.\n// `p` is a type that implements WithStruct. `tagName` defines the struct tag to look for.\n// `v` is the struct containing data.\n//\n// Fields in `v` should be string, int, int8, int16, int32, int64, uint,\n// uint8, uint16, uint32, uint64, float32, float64, complex64,\n// complex128 or bool. Arrays or slices are inserted sequentially with the\n// same key. Other types are ignored.\nfunc SetValWithStruct(p WithStruct, tagName string, v any) {\n\tvalueOfV := reflect.ValueOf(v)\n\ttypeOfV := reflect.TypeOf(v)\n\n\t// The value should be a struct or a pointer to a struct.\n\tif typeOfV.Kind() == reflect.Pointer && typeOfV.Elem().Kind() == reflect.Struct {\n\t\tvalueOfV = valueOfV.Elem()\n\t\ttypeOfV = typeOfV.Elem()\n\t} else if typeOfV.Kind() != reflect.Struct {\n\t\treturn\n\t}\n\n\t// A helper function to set values.\n\tvar setVal func(name string, val reflect.Value)\n\tsetVal = func(name string, val reflect.Value) {\n\t\tswitch val.Kind() {\n\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\t\tp.Add(name, strconv.Itoa(int(val.Int())))\n\t\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\t\tp.Add(name, strconv.FormatUint(val.Uint(), 10))\n\t\tcase reflect.Float32, reflect.Float64:\n\t\t\tp.Add(name, strconv.FormatFloat(val.Float(), 'f', -1, 64))\n\t\tcase reflect.Complex64, reflect.Complex128:\n\t\t\tp.Add(name, strconv.FormatComplex(val.Complex(), 'f', -1, 128))\n\t\tcase reflect.Bool:\n\t\t\tif val.Bool() {\n\t\t\t\tp.Add(name, \"true\")\n\t\t\t} else {\n\t\t\t\tp.Add(name, \"false\")\n\t\t\t}\n\t\tcase reflect.String:\n\t\t\tp.Add(name, val.String())\n\t\tcase reflect.Slice, reflect.Array:\n\t\t\tfor i := 0; i < val.Len(); i++ {\n\t\t\t\tsetVal(name, val.Index(i))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor i := 0; i < typeOfV.NumField(); i++ {\n\t\tfield := typeOfV.Field(i)\n\t\tif !field.IsExported() {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := field.Tag.Get(tagName)\n\t\tif name == \"\" {\n\t\t\tname = field.Name\n\t\t}\n\t\tval := valueOfV.Field(i)\n\t\t// To cover slice and array, we delete the val then add it.\n\t\tp.Del(name)\n\t\tsetVal(name, val)\n\t}\n}\n"
  },
  {
    "path": "client/request_bench_test.go",
    "content": "package client\n\nimport (\n\t\"runtime\"\n\t\"runtime/metrics\"\n\t\"strconv\"\n\t\"testing\"\n)\n\n// BenchmarkRequestHeapScan measures how much heap memory the GC needs to scan\n// when a batch of requests is created and released.\nfunc BenchmarkRequestHeapScan(b *testing.B) {\n\tsamples := []metrics.Sample{\n\t\t{Name: \"/gc/scan/heap:bytes\"},\n\t\t{Name: \"/gc/scan/total:bytes\"},\n\t}\n\n\tb.ReportAllocs()\n\tb.StopTimer()\n\tb.ResetTimer()\n\n\tconst batchSize = 512\n\tvar totalScanHeap, totalScanAll uint64\n\tfor i := 0; i < b.N; i++ {\n\t\treqs := make([]*Request, batchSize)\n\t\t// revive:disable-next-line:call-to-gc // Ensure consistent heap state before measuring scan metrics\n\t\truntime.GC()\n\t\tmetrics.Read(samples)\n\t\tstartScanHeap := samples[0].Value.Uint64()\n\t\tstartScanAll := samples[1].Value.Uint64()\n\n\t\tb.StartTimer()\n\t\tfor j := range reqs {\n\t\t\treq := AcquireRequest()\n\t\t\treq.SetHeader(\"X-Benchmark\", \"value\")\n\t\t\treq.SetCookie(\"session\", strconv.Itoa(j))\n\t\t\treq.SetPathParam(\"id\", strconv.Itoa(j))\n\t\t\treq.SetParam(\"page\", strconv.Itoa(j))\n\t\t\treqs[j] = req\n\t\t}\n\t\tb.StopTimer()\n\n\t\t// revive:disable-next-line:call-to-gc // Force GC to capture post-benchmark scan metrics\n\t\truntime.GC()\n\t\tmetrics.Read(samples)\n\t\ttotalScanHeap += samples[0].Value.Uint64() - startScanHeap\n\t\ttotalScanAll += samples[1].Value.Uint64() - startScanAll\n\n\t\tfor _, req := range reqs {\n\t\t\tReleaseRequest(req)\n\t\t}\n\t}\n\n\tif b.N > 0 {\n\t\tb.ReportMetric(float64(totalScanHeap)/float64(b.N), \"scan-bytes-heap/op\")\n\t\tb.ReportMetric(float64(totalScanAll)/float64(b.N), \"scan-bytes-total/op\")\n\t}\n}\n"
  },
  {
    "path": "client/request_test.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"maps\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n)\n\nfunc Test_Request_Method(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\treq.SetMethod(\"GET\")\n\trequire.Equal(t, \"GET\", req.Method())\n\n\treq.SetMethod(\"POST\")\n\trequire.Equal(t, \"POST\", req.Method())\n\n\treq.SetMethod(\"PUT\")\n\trequire.Equal(t, \"PUT\", req.Method())\n\n\treq.SetMethod(\"DELETE\")\n\trequire.Equal(t, \"DELETE\", req.Method())\n\n\treq.SetMethod(\"PATCH\")\n\trequire.Equal(t, \"PATCH\", req.Method())\n\n\treq.SetMethod(\"OPTIONS\")\n\trequire.Equal(t, \"OPTIONS\", req.Method())\n\n\treq.SetMethod(\"HEAD\")\n\trequire.Equal(t, \"HEAD\", req.Method())\n\n\treq.SetMethod(\"TRACE\")\n\trequire.Equal(t, \"TRACE\", req.Method())\n\n\treq.SetMethod(\"CUSTOM\")\n\trequire.Equal(t, \"CUSTOM\", req.Method())\n}\n\nfunc Test_Request_URL(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\n\treq.SetURL(\"http://example.com/normal\")\n\trequire.Equal(t, \"http://example.com/normal\", req.URL())\n\n\treq.SetURL(\"https://example.com/normal\")\n\trequire.Equal(t, \"https://example.com/normal\", req.URL())\n}\n\nfunc Test_Request_Client(t *testing.T) {\n\tt.Parallel()\n\n\tclient := New()\n\treq := AcquireRequest()\n\n\treq.SetClient(client)\n\trequire.Equal(t, client, req.Client())\n}\n\nfunc Test_Request_Context(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\tctx := req.Context()\n\ttype ctxKey struct{}\n\tvar key ctxKey = struct{}{}\n\n\trequire.Nil(t, ctx.Value(key))\n\n\tctx = context.WithValue(ctx, key, \"string\")\n\treq.SetContext(ctx)\n\tctx = req.Context()\n\n\tv, ok := ctx.Value(key).(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, \"string\", v)\n}\n\nfunc Test_Request_Header(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.AddHeader(\"foo\", \"bar\").AddHeader(\"foo\", \"fiber\")\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n\n\tt.Run(\"set header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.AddHeader(\"foo\", \"bar\").SetHeader(\"foo\", \"fiber\")\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\t})\n\n\tt.Run(\"add headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.SetHeader(\"foo\", \"bar\").\n\t\t\tAddHeaders(map[string][]string{\n\t\t\t\t\"foo\": {\"fiber\", \"buaa\"},\n\t\t\t\t\"bar\": {\"foo\"},\n\t\t\t})\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 3)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t\trequire.Equal(t, \"buaa\", res[2])\n\n\t\tres = req.Header(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.SetHeader(\"foo\", \"bar\").\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\n\t\tres := req.Header(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\n\t\tres = req.Header(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n}\n\nfunc Test_Request_Headers(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\treq.AddHeaders(map[string][]string{\n\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\"bar\": {\"foo\"},\n\t})\n\n\theaders := maps.Collect(req.Headers())\n\n\trequire.Contains(t, headers[\"Foo\"], \"fiber\")\n\trequire.Contains(t, headers[\"Foo\"], \"bar\")\n\trequire.Contains(t, headers[\"Bar\"], \"foo\")\n\n\trequire.Len(t, headers, 2)\n}\n\nfunc Benchmark_Request_Headers(b *testing.B) {\n\treq := AcquireRequest()\n\treq.AddHeaders(map[string][]string{\n\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\"bar\": {\"foo\"},\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.Headers() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_QueryParam(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.AddParam(\"foo\", \"bar\").AddParam(\"foo\", \"fiber\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n\n\tt.Run(\"set param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.AddParam(\"foo\", \"bar\").SetParam(\"foo\", \"fiber\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\t})\n\n\tt.Run(\"add params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tAddParams(map[string][]string{\n\t\t\t\t\"foo\": {\"fiber\", \"buaa\"},\n\t\t\t\t\"bar\": {\"foo\"},\n\t\t\t})\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 3)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t\trequire.Equal(t, \"buaa\", res[2])\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tSetParams(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set params with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype args struct {\n\t\t\tTString   string\n\t\t\tTSlice    []string\n\t\t\tTIntSlice []int `param:\"int_slice\"`\n\t\t\tTInt      int\n\t\t\tTFloat    float64\n\t\t\tTBool     bool\n\t\t}\n\n\t\tp := AcquireRequest()\n\t\tp.SetParamsWithStruct(&args{\n\t\t\tTInt:      5,\n\t\t\tTString:   \"string\",\n\t\t\tTFloat:    3.1,\n\t\t\tTBool:     true,\n\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\tTIntSlice: []int{1, 2},\n\t\t})\n\n\t\trequire.Empty(t, p.Param(\"unexport\"))\n\n\t\trequire.Len(t, p.Param(\"TInt\"), 1)\n\t\trequire.Equal(t, \"5\", p.Param(\"TInt\")[0])\n\n\t\trequire.Len(t, p.Param(\"TString\"), 1)\n\t\trequire.Equal(t, \"string\", p.Param(\"TString\")[0])\n\n\t\trequire.Len(t, p.Param(\"TFloat\"), 1)\n\t\trequire.Equal(t, \"3.1\", p.Param(\"TFloat\")[0])\n\n\t\trequire.Len(t, p.Param(\"TBool\"), 1)\n\n\t\ttslice := p.Param(\"TSlice\")\n\t\trequire.Len(t, tslice, 2)\n\t\trequire.Equal(t, \"foo\", tslice[0])\n\t\trequire.Equal(t, \"bar\", tslice[1])\n\n\t\ttint := p.Param(\"TSlice\")\n\t\trequire.Len(t, tint, 2)\n\t\trequire.Equal(t, \"foo\", tint[0])\n\t\trequire.Equal(t, \"bar\", tint[1])\n\t})\n\n\tt.Run(\"del params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tSetParams(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t}).DelParams(\"foo\", \"bar\")\n\n\t\tres := req.Param(\"foo\")\n\t\trequire.Empty(t, res)\n\n\t\tres = req.Param(\"bar\")\n\t\trequire.Empty(t, res)\n\t})\n}\n\nfunc Test_Request_Params(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty iterator\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treq := AcquireRequest()\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\tcalled := false\n\t\treq.Params()(func(_ string, _ []string) bool {\n\t\t\tcalled = true\n\t\t\treturn true\n\t\t})\n\n\t\trequire.False(t, called)\n\t})\n\n\tt.Run(\"populated iterator\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treq := AcquireRequest()\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\treq.AddParams(map[string][]string{\n\t\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\t\"bar\": {\"foo\"},\n\t\t})\n\n\t\tpathParams := maps.Collect(req.Params())\n\n\t\trequire.Contains(t, pathParams[\"foo\"], \"bar\")\n\t\trequire.Contains(t, pathParams[\"foo\"], \"fiber\")\n\t\trequire.Contains(t, pathParams[\"bar\"], \"foo\")\n\n\t\trequire.Len(t, pathParams, 2)\n\t})\n}\n\nfunc Benchmark_Request_Params(b *testing.B) {\n\treq := AcquireRequest()\n\treq.AddParams(map[string][]string{\n\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\"bar\": {\"foo\"},\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.Params() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_UA(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest().SetUserAgent(\"fiber\")\n\trequire.Equal(t, \"fiber\", req.UserAgent())\n\n\treq.SetUserAgent(\"foo\")\n\trequire.Equal(t, \"foo\", req.UserAgent())\n}\n\nfunc Test_Request_Referer(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest().SetReferer(\"http://example.com\")\n\trequire.Equal(t, \"http://example.com\", req.Referer())\n\n\treq.SetReferer(\"https://example.com\")\n\trequire.Equal(t, \"https://example.com\", req.Referer())\n}\n\nfunc Test_Request_Cookie(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetCookie(\"foo\", \"bar\")\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\n\t\treq.SetCookie(\"foo\", \"bar1\")\n\t\trequire.Equal(t, \"bar1\", req.Cookie(\"foo\"))\n\t})\n\n\tt.Run(\"set cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\n\t\treq.SetCookies(map[string]string{\n\t\t\t\"foo\": \"bar1\",\n\t\t})\n\t\trequire.Equal(t, \"bar1\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\t})\n\n\tt.Run(\"set cookies with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype args struct {\n\t\t\tCookieString string `cookie:\"string\"`\n\t\t\tCookieInt    int    `cookie:\"int\"`\n\t\t}\n\n\t\treq := AcquireRequest().SetCookiesWithStruct(&args{\n\t\t\tCookieInt:    5,\n\t\t\tCookieString: \"foo\",\n\t\t})\n\n\t\trequire.Equal(t, \"5\", req.Cookie(\"int\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"string\"))\n\t})\n\n\tt.Run(\"del cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\n\t\treq.DelCookies(\"foo\")\n\t\trequire.Empty(t, req.Cookie(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.Cookie(\"bar\"))\n\t})\n}\n\nfunc Test_Request_Cookies(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\treq.SetCookies(map[string]string{\n\t\t\"foo\": \"bar\",\n\t\t\"bar\": \"foo\",\n\t})\n\n\tcookies := maps.Collect(req.Cookies())\n\n\trequire.Equal(t, \"bar\", cookies[\"foo\"])\n\trequire.Equal(t, \"foo\", cookies[\"bar\"])\n\n\trequire.NotPanics(t, func() {\n\t\tfor _, v := range req.Cookies() {\n\t\t\tif v == \"bar\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t})\n\n\trequire.Len(t, cookies, 2)\n}\n\nfunc Benchmark_Request_Cookies(b *testing.B) {\n\treq := AcquireRequest()\n\treq.SetCookies(map[string]string{\n\t\t\"foo\": \"bar\",\n\t\t\"bar\": \"foo\",\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.Cookies() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_PathParam(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"set path param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetPathParam(\"foo\", \"bar\")\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\n\t\treq.SetPathParam(\"foo\", \"bar1\")\n\t\trequire.Equal(t, \"bar1\", req.PathParam(\"foo\"))\n\t})\n\n\tt.Run(\"set path params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\n\t\treq.SetPathParams(map[string]string{\n\t\t\t\"foo\": \"bar1\",\n\t\t})\n\t\trequire.Equal(t, \"bar1\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\t})\n\n\tt.Run(\"set path params with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype args struct {\n\t\t\tCookieString string `path:\"string\"`\n\t\t\tCookieInt    int    `path:\"int\"`\n\t\t}\n\n\t\treq := AcquireRequest().SetPathParamsWithStruct(&args{\n\t\t\tCookieInt:    5,\n\t\t\tCookieString: \"foo\",\n\t\t})\n\n\t\trequire.Equal(t, \"5\", req.PathParam(\"int\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"string\"))\n\t})\n\n\tt.Run(\"del path params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\n\t\treq.DelPathParams(\"foo\")\n\t\trequire.Empty(t, req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\t})\n\n\tt.Run(\"clear path params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tSetPathParams(map[string]string{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\t\trequire.Equal(t, \"bar\", req.PathParam(\"foo\"))\n\t\trequire.Equal(t, \"foo\", req.PathParam(\"bar\"))\n\n\t\treq.ResetPathParams()\n\t\trequire.Empty(t, req.PathParam(\"foo\"))\n\t\trequire.Empty(t, req.PathParam(\"bar\"))\n\t})\n}\n\nfunc Test_Request_PathParams(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\treq.SetPathParams(map[string]string{\n\t\t\"foo\": \"bar\",\n\t\t\"bar\": \"foo\",\n\t})\n\n\tpathParams := maps.Collect(req.PathParams())\n\n\trequire.Equal(t, \"bar\", pathParams[\"foo\"])\n\trequire.Equal(t, \"foo\", pathParams[\"bar\"])\n\n\trequire.Len(t, pathParams, 2)\n\n\trequire.NotPanics(t, func() {\n\t\tfor _, v := range req.PathParams() {\n\t\t\tif v == \"bar\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc Benchmark_Request_PathParams(b *testing.B) {\n\treq := AcquireRequest()\n\treq.SetPathParams(map[string]string{\n\t\t\"foo\": \"bar\",\n\t\t\"bar\": \"foo\",\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.PathParams() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_FormData(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add form data\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\tdefer ReleaseRequest(req)\n\t\treq.AddFormData(\"foo\", \"bar\").AddFormData(\"foo\", \"fiber\")\n\n\t\tres := req.FormData(\"foo\")\n\t\trequire.Len(t, res, 2)\n\t\trequire.Equal(t, \"bar\", res[0])\n\t\trequire.Equal(t, \"fiber\", res[1])\n\t})\n\n\tt.Run(\"set param\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\tdefer ReleaseRequest(req)\n\t\treq.AddFormData(\"foo\", \"bar\").SetFormData(\"foo\", \"fiber\")\n\n\t\tres := req.FormData(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\t})\n\n\tt.Run(\"add params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\tdefer ReleaseRequest(req)\n\t\treq.SetFormData(\"foo\", \"bar\").\n\t\t\tAddFormDataWithMap(map[string][]string{\n\t\t\t\t\"foo\": {\"fiber\", \"buaa\"},\n\t\t\t\t\"bar\": {\"foo\"},\n\t\t\t})\n\n\t\tres := req.FormData(\"foo\")\n\t\trequire.Len(t, res, 3)\n\t\trequire.Contains(t, res, \"bar\")\n\t\trequire.Contains(t, res, \"buaa\")\n\t\trequire.Contains(t, res, \"fiber\")\n\n\t\tres = req.FormData(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\tdefer ReleaseRequest(req)\n\t\treq.SetFormData(\"foo\", \"bar\").\n\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t})\n\n\t\tres := req.FormData(\"foo\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"fiber\", res[0])\n\n\t\tres = req.FormData(\"bar\")\n\t\trequire.Len(t, res, 1)\n\t\trequire.Equal(t, \"foo\", res[0])\n\t})\n\n\tt.Run(\"set params with struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype args struct {\n\t\t\tTString   string\n\t\t\tTSlice    []string\n\t\t\tTIntSlice []int `form:\"int_slice\"`\n\t\t\tTInt      int\n\t\t\tTFloat    float64\n\t\t\tTBool     bool\n\t\t}\n\n\t\tp := AcquireRequest()\n\t\tdefer ReleaseRequest(p)\n\t\tp.SetFormDataWithStruct(&args{\n\t\t\tTInt:      5,\n\t\t\tTString:   \"string\",\n\t\t\tTFloat:    3.1,\n\t\t\tTBool:     true,\n\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\tTIntSlice: []int{1, 2},\n\t\t})\n\n\t\trequire.Empty(t, p.FormData(\"unexport\"))\n\n\t\trequire.Len(t, p.FormData(\"TInt\"), 1)\n\t\trequire.Equal(t, \"5\", p.FormData(\"TInt\")[0])\n\n\t\trequire.Len(t, p.FormData(\"TString\"), 1)\n\t\trequire.Equal(t, \"string\", p.FormData(\"TString\")[0])\n\n\t\trequire.Len(t, p.FormData(\"TFloat\"), 1)\n\t\trequire.Equal(t, \"3.1\", p.FormData(\"TFloat\")[0])\n\n\t\trequire.Len(t, p.FormData(\"TBool\"), 1)\n\n\t\ttslice := p.FormData(\"TSlice\")\n\t\trequire.Len(t, tslice, 2)\n\t\trequire.Contains(t, tslice, \"bar\")\n\t\trequire.Contains(t, tslice, \"foo\")\n\n\t\ttint := p.FormData(\"TSlice\")\n\t\trequire.Len(t, tint, 2)\n\t\trequire.Contains(t, tint, \"bar\")\n\t\trequire.Contains(t, tint, \"foo\")\n\t})\n\n\tt.Run(\"del params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest()\n\t\tdefer ReleaseRequest(req)\n\t\treq.SetFormData(\"foo\", \"bar\").\n\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\"foo\": \"fiber\",\n\t\t\t\t\"bar\": \"foo\",\n\t\t\t}).DelFormData(\"foo\", \"bar\")\n\n\t\tres := req.FormData(\"foo\")\n\t\trequire.Empty(t, res)\n\n\t\tres = req.FormData(\"bar\")\n\t\trequire.Empty(t, res)\n\t})\n}\n\nfunc Test_Request_File(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"add file\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tAddFile(\"../.github/index.html\").\n\t\t\tAddFiles(AcquireFile(SetFileName(\"tmp.txt\")))\n\n\t\trequire.Equal(t, \"../.github/index.html\", req.File(\"index.html\").path)\n\t\trequire.Equal(t, \"../.github/index.html\", req.FileByPath(\"../.github/index.html\").path)\n\t\trequire.Equal(t, \"tmp.txt\", req.File(\"tmp.txt\").name)\n\t\trequire.Nil(t, req.File(\"tmp2.txt\"))\n\t\trequire.Nil(t, req.FileByPath(\"tmp2.txt\"))\n\t})\n\n\tt.Run(\"add file by reader\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tAddFileWithReader(\"tmp.txt\", io.NopCloser(strings.NewReader(\"world\")))\n\n\t\trequire.Equal(t, \"tmp.txt\", req.File(\"tmp.txt\").name)\n\n\t\tcontent, err := io.ReadAll(req.File(\"tmp.txt\").reader)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"world\", string(content))\n\t})\n\n\tt.Run(\"add files\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := AcquireRequest().\n\t\t\tAddFiles(AcquireFile(SetFileName(\"tmp.txt\")), AcquireFile(SetFileName(\"foo.txt\")))\n\n\t\trequire.Equal(t, \"tmp.txt\", req.File(\"tmp.txt\").name)\n\t\trequire.Equal(t, \"foo.txt\", req.File(\"foo.txt\").name)\n\t})\n}\n\nfunc Test_Request_Files(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest()\n\treq.AddFile(\"../.github/index.html\")\n\treq.AddFiles(AcquireFile(SetFileName(\"tmp.txt\")))\n\n\tfiles := req.Files()\n\n\trequire.Equal(t, \"../.github/index.html\", files[0].path)\n\trequire.Nil(t, files[0].reader)\n\n\trequire.Equal(t, \"tmp.txt\", files[1].name)\n\trequire.Nil(t, files[1].reader)\n\n\trequire.Len(t, files, 2)\n}\n\nfunc Benchmark_Request_Files(b *testing.B) {\n\treq := AcquireRequest()\n\treq.AddFile(\"../.github/index.html\")\n\treq.AddFiles(AcquireFile(SetFileName(\"tmp.txt\")))\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.Files() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_Timeout(t *testing.T) {\n\tt.Parallel()\n\n\treq := AcquireRequest().SetTimeout(5 * time.Second)\n\n\trequire.Equal(t, 5*time.Second, req.Timeout())\n}\n\nfunc Test_Request_Invalid_URL(t *testing.T) {\n\tt.Parallel()\n\n\tresp, err := AcquireRequest().\n\t\tGet(\"http://example.com\\r\\n\\r\\nGET /\\r\\n\\r\\n\")\n\n\trequire.Equal(t, ErrURLFormat, err)\n\trequire.Equal(t, (*Response)(nil), resp)\n}\n\nfunc Test_Request_Unsupported_Protocol(t *testing.T) {\n\tt.Parallel()\n\n\tresp, err := AcquireRequest().\n\t\tGet(\"ftp://example.com\")\n\trequire.Equal(t, ErrURLFormat, err)\n\trequire.Equal(t, (*Response)(nil), resp)\n}\n\nfunc Test_Request_Get(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\treq := AcquireRequest().SetClient(client)\n\n\t\tresp, err := req.Get(\"http://example.com\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"example.com\", resp.String())\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Post(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusCreated).\n\t\t\tSendString(c.FormValue(\"foo\"))\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetFormData(\"foo\", \"bar\").\n\t\t\tPost(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusCreated, resp.StatusCode())\n\t\trequire.Equal(t, \"bar\", resp.String())\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Head(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Head(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tHead(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Empty(t, resp.String())\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Put(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Put(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.FormValue(\"foo\"))\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetFormData(\"foo\", \"bar\").\n\t\t\tPut(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"bar\", resp.String())\n\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Delete(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\n\tapp.Delete(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusNoContent).\n\t\t\tSendString(\"deleted\")\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tDelete(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode())\n\t\trequire.Empty(t, resp.String())\n\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Options(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\n\tapp.Options(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusOK).\n\t\t\tSendString(\"options\")\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tOptions(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"options\", resp.String())\n\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Send(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusOK).\n\t\t\tSendString(\"post\")\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetURL(\"http://example.com\").\n\t\t\tSetMethod(fiber.MethodPost).\n\t\t\tSend()\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"post\", resp.String())\n\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Patch(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\n\tapp.Patch(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.FormValue(\"foo\"))\n\t})\n\n\tgo start()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tclient := New().SetDial(ln)\n\n\tfor range 5 {\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetFormData(\"foo\", \"bar\").\n\t\t\tPatch(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"bar\", resp.String())\n\n\t\tresp.Close()\n\t}\n}\n\nfunc Test_Request_Header_With_Server(t *testing.T) {\n\tt.Parallel()\n\thandler := func(c fiber.Ctx) error {\n\t\tfor key, value := range c.Request().Header.All() {\n\t\t\tif k := string(key); k == \"K1\" || k == \"K2\" {\n\t\t\t\t_, err := c.Write(key)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\t_, err = c.Write(value)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\twrapAgent := func(r *Request) {\n\t\tr.SetHeader(\"k1\", \"v1\").\n\t\t\tAddHeader(\"k1\", \"v11\").\n\t\t\tAddHeaders(map[string][]string{\n\t\t\t\t\"k1\": {\"v22\", \"v33\"},\n\t\t\t}).\n\t\t\tSetHeaders(map[string]string{\n\t\t\t\t\"k2\": \"v2\",\n\t\t\t}).\n\t\t\tAddHeader(\"k2\", \"v22\")\n\t}\n\n\ttestRequest(t, handler, wrapAgent, \"K1v1K1v11K1v22K1v33K2v2K2v22\")\n}\n\nfunc Test_Request_UserAgent_With_Server(t *testing.T) {\n\tt.Parallel()\n\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.Send(c.Request().Header.UserAgent())\n\t}\n\n\tt.Run(\"default\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t, handler, func(_ *Request) {}, defaultUserAgent, 5)\n\t})\n\n\tt.Run(\"custom\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t, handler, func(agent *Request) {\n\t\t\tagent.SetUserAgent(\"ua\")\n\t\t}, \"ua\", 5)\n\t})\n}\n\nfunc Test_Request_Cookie_With_Server(t *testing.T) {\n\tt.Parallel()\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\n\t\t\tc.Cookies(\"k1\") + c.Cookies(\"k2\") + c.Cookies(\"k3\") + c.Cookies(\"k4\"))\n\t}\n\n\twrapAgent := func(req *Request) {\n\t\treq.SetCookie(\"k1\", \"v1\").\n\t\t\tSetCookies(map[string]string{\n\t\t\t\t\"k2\": \"v2\",\n\t\t\t\t\"k3\": \"v3\",\n\t\t\t\t\"k4\": \"v4\",\n\t\t\t}).DelCookies(\"k4\")\n\t}\n\n\ttestRequest(t, handler, wrapAgent, \"v1v2v3\")\n}\n\nfunc Test_Request_Referer_With_Server(t *testing.T) {\n\tt.Parallel()\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.Send(c.Request().Header.Referer())\n\t}\n\n\twrapAgent := func(req *Request) {\n\t\treq.SetReferer(\"http://referer.com\")\n\t}\n\n\ttestRequest(t, handler, wrapAgent, \"http://referer.com\")\n}\n\nfunc Test_Request_QueryString_With_Server(t *testing.T) {\n\tt.Parallel()\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.Send(c.Request().URI().QueryString())\n\t}\n\n\twrapAgent := func(req *Request) {\n\t\treq.SetParam(\"foo\", \"bar\").\n\t\t\tSetParams(map[string]string{\n\t\t\t\t\"bar\": \"baz\",\n\t\t\t})\n\t}\n\n\ttestRequest(t, handler, wrapAgent, \"foo=bar&bar=baz\")\n}\n\nfunc checkFormFile(t *testing.T, fh *multipart.FileHeader, filename string) {\n\tt.Helper()\n\n\tbasename := filepath.Base(filename)\n\trequire.Equal(t, fh.Filename, basename)\n\n\tb1, err := os.ReadFile(filepath.Clean(filename))\n\trequire.NoError(t, err)\n\n\tb2 := make([]byte, fh.Size)\n\tf, err := fh.Open()\n\trequire.NoError(t, err)\n\tdefer func() { require.NoError(t, f.Close()) }()\n\t_, err = f.Read(b2)\n\trequire.NoError(t, err)\n\trequire.Equal(t, b1, b2)\n}\n\nfunc Test_Request_Body_With_Server(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"json body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\trequire.Equal(t, \"application/json\", string(c.Request().Header.ContentType()))\n\t\t\t\treturn c.SendString(string(c.Request().Body()))\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\tagent.SetJSON(map[string]string{\n\t\t\t\t\t\"success\": \"hello\",\n\t\t\t\t})\n\t\t\t},\n\t\t\t\"{\\\"success\\\":\\\"hello\\\"}\",\n\t\t)\n\t})\n\n\tt.Run(\"xml body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\trequire.Equal(t, \"application/xml\", string(c.Request().Header.ContentType()))\n\t\t\t\treturn c.SendString(string(c.Request().Body()))\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\ttype args struct {\n\t\t\t\t\tContent string `xml:\"content\"`\n\t\t\t\t}\n\t\t\t\tagent.SetXML(args{\n\t\t\t\t\tContent: \"hello\",\n\t\t\t\t})\n\t\t\t},\n\t\t\t\"<args><content>hello</content></args>\",\n\t\t)\n\t})\n\n\tt.Run(\"cbor body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\trequire.Equal(t, \"application/cbor\", string(c.Request().Header.ContentType()))\n\t\t\t\treturn c.SendString(string(c.Request().Body()))\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\ttype args struct {\n\t\t\t\t\tContent string `cbor:\"content\"`\n\t\t\t\t}\n\t\t\t\tagent.SetCBOR(args{\n\t\t\t\t\tContent: \"hello\",\n\t\t\t\t})\n\t\t\t},\n\t\t\t\"\\xa1gcontentehello\",\n\t\t)\n\t})\n\n\tt.Run(\"formdata\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\trequire.Equal(t, fiber.MIMEApplicationForm, string(c.Request().Header.ContentType()))\n\t\t\t\treturn c.Send([]byte(\"foo=\" + c.FormValue(\"foo\") + \"&bar=\" + c.FormValue(\"bar\") + \"&fiber=\" + c.FormValue(\"fiber\")))\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\tagent.SetFormData(\"foo\", \"bar\").\n\t\t\t\t\tSetFormDataWithMap(map[string]string{\n\t\t\t\t\t\t\"bar\":   \"baz\",\n\t\t\t\t\t\t\"fiber\": \"fast\",\n\t\t\t\t\t})\n\t\t\t},\n\t\t\t\"foo=bar&bar=baz&fiber=fast\")\n\t})\n\n\tt.Run(\"multipart form\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, ln, start := createHelperServer(t)\n\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\trequire.Equal(t, \"multipart/form-data; boundary=myBoundary\", c.Get(fiber.HeaderContentType))\n\n\t\t\tmf, err := c.MultipartForm()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"bar\", mf.Value[\"foo\"][0])\n\n\t\t\treturn c.Send(c.Request().Body())\n\t\t})\n\n\t\tgo start()\n\n\t\tclient := New().SetDial(ln)\n\n\t\treq := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetBoundary(\"myBoundary\").\n\t\t\tSetFormData(\"foo\", \"bar\").\n\t\t\tAddFiles(AcquireFile(\n\t\t\t\tSetFileName(\"hello.txt\"),\n\t\t\t\tSetFileFieldName(\"foo\"),\n\t\t\t\tSetFileReader(io.NopCloser(strings.NewReader(\"world\"))),\n\t\t\t))\n\n\t\trequire.Equal(t, \"myBoundary\", req.Boundary())\n\n\t\tresp, err := req.Post(\"http://example.com\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\n\t\tform, err := multipart.NewReader(bytes.NewReader(resp.Body()), \"myBoundary\").ReadForm(1024 * 1024)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"bar\", form.Value[\"foo\"][0])\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"multipart form send file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, ln, start := createHelperServer(t)\n\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\trequire.Equal(t, \"multipart/form-data; boundary=myBoundary\", c.Get(fiber.HeaderContentType))\n\n\t\t\tfh1, err := c.FormFile(\"field1\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"name\", fh1.Filename)\n\t\t\tbuf := make([]byte, fh1.Size)\n\t\t\tf, err := fh1.Open()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer func() { require.NoError(t, f.Close()) }()\n\t\t\t_, err = f.Read(buf)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"form file\", string(buf))\n\n\t\t\tfh2, err := c.FormFile(\"file2\")\n\t\t\trequire.NoError(t, err)\n\t\t\tcheckFormFile(t, fh2, \"../.github/testdata/index.html\")\n\n\t\t\tfh3, err := c.FormFile(\"file3\")\n\t\t\trequire.NoError(t, err)\n\t\t\tcheckFormFile(t, fh3, \"../.github/testdata/index.tmpl\")\n\n\t\t\treturn c.SendString(\"multipart form files\")\n\t\t})\n\n\t\tgo start()\n\n\t\tclient := New().SetDial(ln)\n\n\t\tfor range 5 {\n\t\t\treq := AcquireRequest().\n\t\t\t\tSetClient(client).\n\t\t\t\tAddFiles(\n\t\t\t\t\tAcquireFile(\n\t\t\t\t\t\tSetFileFieldName(\"field1\"),\n\t\t\t\t\t\tSetFileName(\"name\"),\n\t\t\t\t\t\tSetFileReader(io.NopCloser(bytes.NewReader([]byte(\"form file\")))),\n\t\t\t\t\t),\n\t\t\t\t).\n\t\t\t\tAddFile(\"../.github/testdata/index.html\").\n\t\t\t\tAddFile(\"../.github/testdata/index.tmpl\").\n\t\t\t\tSetBoundary(\"myBoundary\")\n\n\t\t\tresp, err := req.Post(\"http://example.com\")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"multipart form files\", resp.String())\n\n\t\t\tresp.Close()\n\t\t}\n\t})\n\n\tt.Run(\"multipart random boundary\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp, ln, start := createHelperServer(t)\n\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\treg := regexp.MustCompile(`multipart/form-data; boundary=[\\-\\w]{33}`)\n\t\t\trequire.True(t, reg.MatchString(c.Get(fiber.HeaderContentType)))\n\n\t\t\treturn c.Send(c.Request().Body())\n\t\t})\n\n\t\tgo start()\n\n\t\tclient := New().SetDial(ln)\n\n\t\treq := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetFormData(\"foo\", \"bar\").\n\t\t\tAddFiles(AcquireFile(\n\t\t\t\tSetFileName(\"hello.txt\"),\n\t\t\t\tSetFileFieldName(\"foo\"),\n\t\t\t\tSetFileReader(io.NopCloser(strings.NewReader(\"world\"))),\n\t\t\t))\n\n\t\tresp, err := req.Post(\"http://example.com\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t})\n\n\tt.Run(\"raw body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequest(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(string(c.Request().Body()))\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\tagent.SetRawBody([]byte(\"hello\"))\n\t\t\t},\n\t\t\t\"hello\",\n\t\t)\n\t})\n}\n\nfunc Test_Request_AllFormData(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty iterator\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treq := AcquireRequest()\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\tcalled := false\n\t\treq.AllFormData()(func(_ string, _ []string) bool {\n\t\t\tcalled = true\n\t\t\treturn true\n\t\t})\n\n\t\trequire.False(t, called)\n\t})\n\n\tt.Run(\"populated iterator\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treq := AcquireRequest()\n\t\tt.Cleanup(func() {\n\t\t\tReleaseRequest(req)\n\t\t})\n\n\t\treq.AddFormDataWithMap(map[string][]string{\n\t\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\t\"bar\": {\"foo\"},\n\t\t})\n\n\t\tpathParams := maps.Collect(req.AllFormData())\n\n\t\trequire.Contains(t, pathParams[\"foo\"], \"bar\")\n\t\trequire.Contains(t, pathParams[\"foo\"], \"fiber\")\n\t\trequire.Contains(t, pathParams[\"bar\"], \"foo\")\n\n\t\trequire.Len(t, pathParams, 2)\n\t})\n}\n\nfunc Benchmark_Request_AllFormData(b *testing.B) {\n\treq := AcquireRequest()\n\treq.AddFormDataWithMap(map[string][]string{\n\t\t\"foo\": {\"bar\", \"fiber\"},\n\t\t\"bar\": {\"foo\"},\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range req.AllFormData() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Request_Error_Body_With_Server(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"json error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequestFail(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"\")\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\tagent.SetJSON(complex(1, 1))\n\t\t\t},\n\t\t\terrors.New(\"json: unsupported type: complex128\"),\n\t\t)\n\t})\n\n\tt.Run(\"xml error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRequestFail(t,\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"\")\n\t\t\t},\n\t\t\tfunc(agent *Request) {\n\t\t\t\tagent.SetXML(complex(1, 1))\n\t\t\t},\n\t\t\terrors.New(\"xml: unsupported type: complex128\"),\n\t\t)\n\t})\n\n\tt.Run(\"form body with invalid boundary\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := AcquireRequest().\n\t\t\tSetBoundary(\"*\").\n\t\t\tAddFileWithReader(\"t.txt\", io.NopCloser(strings.NewReader(\"world\"))).\n\t\t\tGet(\"http://example.com\")\n\t\trequire.Equal(t, \"set boundary error: mime: invalid boundary character\", err.Error())\n\t})\n\n\tt.Run(\"open non exist file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := AcquireRequest().\n\t\t\tAddFile(\"non-exist-file!\").\n\t\t\tGet(\"http://example.com\")\n\t\trequire.Contains(t, err.Error(), \"open non-exist-file!\")\n\t})\n}\n\nfunc Test_Request_Timeout_With_Server(t *testing.T) {\n\tt.Parallel()\n\n\tapp, ln, start := createHelperServer(t)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(time.Millisecond * 200)\n\t\treturn c.SendString(\"timeout\")\n\t})\n\tgo start()\n\n\tclient := New().SetDial(ln)\n\n\t_, err := AcquireRequest().\n\t\tSetClient(client).\n\t\tSetTimeout(50 * time.Millisecond).\n\t\tGet(\"http://example.com\")\n\n\trequire.Equal(t, ErrTimeoutOrCancel, err)\n}\n\nfunc Test_Request_MaxRedirects(t *testing.T) {\n\tt.Parallel()\n\n\tln := fasthttputil.NewInmemoryListener()\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tif c.Request().URI().QueryArgs().Has(\"foo\") {\n\t\t\treturn c.Redirect().To(\"/foo\")\n\t\t}\n\t\treturn c.Redirect().To(\"/\")\n\t})\n\tapp.Get(\"/foo\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"redirect\")\n\t})\n\n\tgo func() { assert.NoError(t, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})) }()\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := New().SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetMaxRedirects(1).\n\t\t\tGet(\"http://example.com?foo\")\n\t\tbody := resp.String()\n\t\tcode := resp.StatusCode()\n\n\t\trequire.Equal(t, 200, code)\n\t\trequire.Equal(t, \"redirect\", body)\n\t\trequire.NoError(t, err)\n\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := New().SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetMaxRedirects(1).\n\t\t\tGet(\"http://example.com\")\n\n\t\trequire.Nil(t, resp)\n\t\trequire.Equal(t, \"too many redirects detected when doing the request\", err.Error())\n\t})\n\n\tt.Run(\"MaxRedirects\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclient := New().SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })\n\n\t\treq := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tSetMaxRedirects(3)\n\n\t\trequire.Equal(t, 3, req.MaxRedirects())\n\t})\n}\n\nfunc Test_SetValWithStruct(t *testing.T) {\n\tt.Parallel()\n\n\t// test SetValWithStruct via QueryParam struct.\n\ttype args struct {\n\t\tTString   string\n\t\tTSlice    []string\n\t\tTIntSlice []int `param:\"int_slice\"`\n\t\tunexport  int\n\t\tTInt      int\n\t\tTUint     uint\n\t\tTFloat    float64\n\t\tTComplex  complex128\n\t\tTBool     bool\n\t}\n\n\tt.Run(\"the struct should be applied\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\n\t\tSetValWithStruct(p, \"param\", args{\n\t\t\tunexport:  5,\n\t\t\tTInt:      5,\n\t\t\tTUint:     5,\n\t\t\tTString:   \"string\",\n\t\t\tTFloat:    3.1,\n\t\t\tTComplex:  3 + 4i,\n\t\t\tTBool:     false,\n\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\tTIntSlice: []int{0, 1, 2},\n\t\t})\n\n\t\trequire.Empty(t, string(p.Peek(\"unexport\")))\n\t\trequire.Equal(t, []byte(\"5\"), p.Peek(\"TInt\"))\n\t\trequire.Equal(t, []byte(\"5\"), p.Peek(\"TUint\"))\n\t\trequire.Equal(t, []byte(\"string\"), p.Peek(\"TString\"))\n\t\trequire.Equal(t, []byte(\"3.1\"), p.Peek(\"TFloat\"))\n\t\trequire.Equal(t, []byte(\"(3+4i)\"), p.Peek(\"TComplex\"))\n\t\trequire.Equal(t, []byte(\"false\"), p.Peek(\"TBool\"))\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"foo\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"bar\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"0\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"1\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"2\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\t})\n\n\tt.Run(\"the pointer of a struct should be applied\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\n\t\tSetValWithStruct(p, \"param\", &args{\n\t\t\tTInt:      5,\n\t\t\tTString:   \"string\",\n\t\t\tTFloat:    3.1,\n\t\t\tTBool:     true,\n\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\tTIntSlice: []int{1, 2},\n\t\t})\n\n\t\trequire.Equal(t, []byte(\"5\"), p.Peek(\"TInt\"))\n\t\trequire.Equal(t, []byte(\"string\"), p.Peek(\"TString\"))\n\t\trequire.Equal(t, []byte(\"3.1\"), p.Peek(\"TFloat\"))\n\t\trequire.Equal(t, \"true\", string(p.Peek(\"TBool\")))\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"foo\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"bar\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"1\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(t, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"2\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\t})\n\n\tt.Run(\"error type should ignore\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\t\tSetValWithStruct(p, \"param\", 5)\n\t\trequire.Equal(t, 0, p.Len())\n\t})\n}\n\nfunc Benchmark_SetValWithStruct(b *testing.B) {\n\t// test SetValWithStruct via QueryParam struct.\n\ttype args struct {\n\t\tTString   string\n\t\tTSlice    []string\n\t\tTIntSlice []int `param:\"int_slice\"`\n\t\tunexport  int\n\t\tTInt      int\n\t\tTUint     uint\n\t\tTFloat    float64\n\t\tTComplex  complex128\n\t\tTBool     bool\n\t}\n\n\tb.Run(\"the struct should be applied\", func(b *testing.B) {\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\n\t\tb.ReportAllocs()\n\n\t\tfor b.Loop() {\n\t\t\tSetValWithStruct(p, \"param\", args{\n\t\t\t\tunexport:  5,\n\t\t\t\tTInt:      5,\n\t\t\t\tTUint:     5,\n\t\t\t\tTString:   \"string\",\n\t\t\t\tTFloat:    3.1,\n\t\t\t\tTComplex:  3 + 4i,\n\t\t\t\tTBool:     false,\n\t\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\t\tTIntSlice: []int{0, 1, 2},\n\t\t\t})\n\t\t}\n\n\t\trequire.Empty(b, string(p.Peek(\"unexport\")))\n\t\trequire.Equal(b, []byte(\"5\"), p.Peek(\"TInt\"))\n\t\trequire.Equal(b, []byte(\"5\"), p.Peek(\"TUint\"))\n\t\trequire.Equal(b, []byte(\"string\"), p.Peek(\"TString\"))\n\t\trequire.Equal(b, []byte(\"3.1\"), p.Peek(\"TFloat\"))\n\t\trequire.Equal(b, []byte(\"(3+4i)\"), p.Peek(\"TComplex\"))\n\t\trequire.Equal(b, []byte(\"false\"), p.Peek(\"TBool\"))\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"foo\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"bar\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"0\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"1\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"2\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\t})\n\n\tb.Run(\"the pointer of a struct should be applied\", func(b *testing.B) {\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\n\t\tb.ReportAllocs()\n\n\t\tfor b.Loop() {\n\t\t\tSetValWithStruct(p, \"param\", &args{\n\t\t\t\tTInt:      5,\n\t\t\t\tTString:   \"string\",\n\t\t\t\tTFloat:    3.1,\n\t\t\t\tTBool:     true,\n\t\t\t\tTSlice:    []string{\"foo\", \"bar\"},\n\t\t\t\tTIntSlice: []int{1, 2},\n\t\t\t})\n\t\t}\n\n\t\trequire.Equal(b, []byte(\"5\"), p.Peek(\"TInt\"))\n\t\trequire.Equal(b, []byte(\"string\"), p.Peek(\"TString\"))\n\t\trequire.Equal(b, []byte(\"3.1\"), p.Peek(\"TFloat\"))\n\t\trequire.Equal(b, \"true\", string(p.Peek(\"TBool\")))\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"foo\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"TSlice\") {\n\t\t\t\tif string(v) == \"bar\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"1\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\n\t\trequire.True(b, func() bool {\n\t\t\tfor _, v := range p.PeekMulti(\"int_slice\") {\n\t\t\t\tif string(v) == \"2\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}())\n\t})\n\n\tb.Run(\"error type should ignore\", func(b *testing.B) {\n\t\tp := &QueryParam{\n\t\t\tArgs: fasthttp.AcquireArgs(),\n\t\t}\n\n\t\tb.ReportAllocs()\n\n\t\tfor b.Loop() {\n\t\t\tSetValWithStruct(p, \"param\", 5)\n\t\t}\n\n\t\trequire.Equal(b, 0, p.Len())\n\t})\n}\n"
  },
  {
    "path": "client/response.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"iter\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Response represents the result of a request. It provides access to the response data.\ntype Response struct {\n\tclient  *Client\n\trequest *Request\n\n\tRawResponse *fasthttp.Response\n\tcookie      []*fasthttp.Cookie\n}\n\n// setClient sets the client instance in the response. The client object is used by core functionalities.\nfunc (r *Response) setClient(c *Client) {\n\tr.client = c\n}\n\n// setRequest sets the request object in the response. The request is released when Response.Close is called.\nfunc (r *Response) setRequest(req *Request) {\n\tr.request = req\n}\n\n// Status returns the HTTP status message of the executed request.\nfunc (r *Response) Status() string {\n\treturn string(r.RawResponse.Header.StatusMessage())\n}\n\n// StatusCode returns the HTTP status code of the executed request.\nfunc (r *Response) StatusCode() int {\n\treturn r.RawResponse.StatusCode()\n}\n\n// Protocol returns the HTTP protocol used for the request.\nfunc (r *Response) Protocol() string {\n\treturn string(r.RawResponse.Header.Protocol())\n}\n\n// Header returns the value of the specified response header field.\nfunc (r *Response) Header(key string) string {\n\treturn utils.UnsafeString(r.RawResponse.Header.Peek(key))\n}\n\n// Headers returns all headers in the response using an iterator.\n// Use maps.Collect() to gather them into a map if needed.\n//\n// The returned values are valid only until the response object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Response) Headers() iter.Seq2[string, []string] {\n\treturn func(yield func(string, []string) bool) {\n\t\tkeys := r.RawResponse.Header.PeekKeys()\n\t\tfor _, key := range keys {\n\t\t\tvals := r.RawResponse.Header.PeekAll(utils.UnsafeString(key))\n\t\t\tvalsStr := make([]string, len(vals))\n\t\t\tfor i, v := range vals {\n\t\t\t\tvalsStr[i] = utils.UnsafeString(v)\n\t\t\t}\n\n\t\t\tif !yield(utils.UnsafeString(key), valsStr) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Cookies returns all cookies set by the response.\n//\n// The returned values are valid only until the response object is released.\n// Do not store references to returned values; make copies instead.\nfunc (r *Response) Cookies() []*fasthttp.Cookie {\n\treturn r.cookie\n}\n\n// Body returns the HTTP response body as a byte slice.\nfunc (r *Response) Body() []byte {\n\treturn r.RawResponse.Body()\n}\n\n// BodyStream returns the response body as a stream reader.\n// Note: When using BodyStream(), the response body is not copied to memory,\n// so calling Body() afterwards may return an empty slice.\nfunc (r *Response) BodyStream() io.Reader {\n\tif stream := r.RawResponse.BodyStream(); stream != nil {\n\t\treturn stream\n\t}\n\t// If streaming is not enabled, return a bytes.Reader from the regular body\n\treturn bytes.NewReader(r.RawResponse.Body())\n}\n\n// IsStreaming returns true if the response body is being streamed.\nfunc (r *Response) IsStreaming() bool {\n\treturn r.RawResponse.BodyStream() != nil\n}\n\n// String returns the response body as a trimmed string.\nfunc (r *Response) String() string {\n\treturn utils.TrimSpace(string(r.Body()))\n}\n\n// JSON unmarshal the response body into the given interface{} using JSON.\nfunc (r *Response) JSON(v any) error {\n\tif r.client == nil {\n\t\treturn ErrClientNil\n\t}\n\n\treturn r.client.jsonUnmarshal(r.Body(), v)\n}\n\n// CBOR unmarshal the response body into the given interface{} using CBOR.\nfunc (r *Response) CBOR(v any) error {\n\tif r.client == nil {\n\t\treturn ErrClientNil\n\t}\n\n\treturn r.client.cborUnmarshal(r.Body(), v)\n}\n\n// XML unmarshal the response body into the given interface{} using XML.\nfunc (r *Response) XML(v any) error {\n\tif r.client == nil {\n\t\treturn ErrClientNil\n\t}\n\n\treturn r.client.xmlUnmarshal(r.Body(), v)\n}\n\n// Save writes the response body to a file or io.Writer.\n// If a string path is provided, it creates directories if needed, then writes to a file.\n// If an io.Writer is provided, it writes directly to it.\n// When streaming is enabled, the body is read directly from the stream.\nfunc (r *Response) Save(v any) error {\n\tswitch p := v.(type) {\n\tcase string:\n\t\tfile := filepath.Clean(p)\n\t\tdir := filepath.Dir(file)\n\n\t\t// Create directory if it doesn't exist\n\t\tif _, err := os.Stat(dir); err != nil {\n\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\treturn fmt.Errorf(\"failed to check directory: %w\", err)\n\t\t\t}\n\n\t\t\tif err = os.MkdirAll(dir, 0o750); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create directory: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Create and write to file\n\t\toutFile, err := os.Create(file)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create file: %w\", err)\n\t\t}\n\t\tdefer func() { _ = outFile.Close() }() //nolint:errcheck // not needed\n\n\t\t// Use BodyStream() which handles both streaming and non-streaming cases\n\t\tif _, err = io.Copy(outFile, r.BodyStream()); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write response body to file: %w\", err)\n\t\t}\n\n\t\treturn nil\n\n\tcase io.Writer:\n\t\t// Use BodyStream() which handles both streaming and non-streaming cases\n\t\tif _, err := io.Copy(p, r.BodyStream()); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write response body to writer: %w\", err)\n\t\t}\n\t\t// Close the writer if it implements io.WriteCloser\n\t\tif pc, ok := p.(io.WriteCloser); ok {\n\t\t\t_ = pc.Close() //nolint:errcheck // not needed\n\t\t}\n\n\t\treturn nil\n\n\tdefault:\n\t\treturn ErrNotSupportSaveMethod\n\t}\n}\n\n// Reset clears the Response object, making it ready for reuse.\nfunc (r *Response) Reset() {\n\tr.client = nil\n\tr.request = nil\n\n\tfor len(r.cookie) != 0 {\n\t\tt := r.cookie[0]\n\t\tr.cookie = r.cookie[1:]\n\t\tfasthttp.ReleaseCookie(t)\n\t}\n\n\tr.RawResponse.Reset()\n}\n\n// Close releases both the Request and Response objects back to their pools.\n// After calling Close, do not use these objects.\nfunc (r *Response) Close() {\n\tif r.request != nil {\n\t\ttmp := r.request\n\t\tr.request = nil\n\t\tReleaseRequest(tmp)\n\t}\n\tReleaseResponse(r)\n}\n\nvar responsePool = &sync.Pool{\n\tNew: func() any {\n\t\treturn &Response{\n\t\t\tcookie:      []*fasthttp.Cookie{},\n\t\t\tRawResponse: fasthttp.AcquireResponse(),\n\t\t}\n\t},\n}\n\n// AcquireResponse returns a new (pooled) Response object.\n// When done, release it with ReleaseResponse to reduce GC load.\nfunc AcquireResponse() *Response {\n\tresp, ok := responsePool.Get().(*Response)\n\tif !ok {\n\t\tpanic(\"unexpected type from responsePool.Get()\")\n\t}\n\treturn resp\n}\n\n// ReleaseResponse returns the Response object to the pool.\n// Do not use the released Response afterward to avoid data races.\nfunc ReleaseResponse(resp *Response) {\n\tresp.Reset()\n\tresponsePool.Put(resp)\n}\n"
  },
  {
    "path": "client/response_test.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3/internal/tlstest\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nfunc Test_Response_Status(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() *testServer {\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"foo\")\n\t\t\t})\n\t\t\tapp.Get(\"/fail\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(407)\n\t\t\t})\n\t\t})\n\n\t\treturn server\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"OK\", resp.Status())\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example/fail\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Proxy Authentication Required\", resp.Status())\n\t\tresp.Close()\n\t})\n}\n\nfunc Test_Response_Status_Code(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() *testServer {\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"foo\")\n\t\t\t})\n\t\t\tapp.Get(\"/fail\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(407)\n\t\t\t})\n\t\t})\n\n\t\treturn server\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, resp.StatusCode())\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example/fail\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 407, resp.StatusCode())\n\t\tresp.Close()\n\t})\n}\n\nfunc Test_Response_Protocol(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"http\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"foo\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"HTTP/1.1\", resp.Protocol())\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"https\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs()\n\t\trequire.NoError(t, err)\n\n\t\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\t\trequire.NoError(t, err)\n\n\t\tln = tls.NewListener(ln, serverTLSConf)\n\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(c.Scheme())\n\t\t})\n\n\t\tgo func() {\n\t\t\tassert.NoError(t, app.Listener(ln, fiber.ListenConfig{\n\t\t\t\tDisableStartupMessage: true,\n\t\t\t}))\n\t\t}()\n\n\t\tclient := New()\n\t\tresp, err := client.SetTLSConfig(clientTLSConf).Get(\"https://\" + ln.Addr().String())\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, clientTLSConf, client.TLSConfig())\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\t\trequire.Equal(t, \"https\", resp.String())\n\t\trequire.Equal(t, \"HTTP/1.1\", resp.Protocol())\n\n\t\tresp.Close()\n\t})\n}\n\nfunc Test_Response_Header(t *testing.T) {\n\tt.Parallel()\n\n\tserver := startTestServer(t, func(app *fiber.App) {\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Add(\"foo\", \"bar\")\n\t\t\treturn c.SendString(\"helo world\")\n\t\t})\n\t})\n\tdefer server.stop()\n\n\tclient := New().SetDial(server.dial())\n\n\tresp, err := AcquireRequest().\n\t\tSetClient(client).\n\t\tGet(\"http://example.com\")\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"bar\", resp.Header(\"foo\"))\n\tresp.Close()\n}\n\nfunc Test_Response_Headers(t *testing.T) {\n\tt.Parallel()\n\n\tserver := startTestServer(t, func(app *fiber.App) {\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Add(\"foo\", \"bar\")\n\t\t\tc.Response().Header.Add(\"foo\", \"bar2\")\n\t\t\tc.Response().Header.Add(\"foo2\", \"bar\")\n\n\t\t\treturn c.SendString(\"hello world\")\n\t\t})\n\t})\n\tdefer server.stop()\n\n\tclient := New().SetDial(server.dial())\n\n\tresp, err := AcquireRequest().\n\t\tSetClient(client).\n\t\tGet(\"http://example.com\")\n\n\trequire.NoError(t, err)\n\n\theaders := make(map[string][]string)\n\tfor k, v := range resp.Headers() {\n\t\theaders[k] = append(headers[k], v...)\n\t}\n\n\trequire.Equal(t, \"hello world\", resp.String())\n\n\trequire.Contains(t, headers[\"Foo\"], \"bar\")\n\trequire.Contains(t, headers[\"Foo\"], \"bar2\")\n\trequire.Contains(t, headers[\"Foo2\"], \"bar\")\n\n\trequire.Len(t, headers, 5) // Foo + Foo2 + Date + Content-Length + Content-Type\n\n\tresp.Close()\n}\n\nfunc Benchmark_Headers(b *testing.B) {\n\tserver := startTestServer(\n\t\tb,\n\t\tfunc(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tc.Response().Header.Add(\"foo\", \"bar\")\n\t\t\t\tc.Response().Header.Add(\"foo\", \"bar2\")\n\t\t\t\tc.Response().Header.Add(\"foo\", \"bar3\")\n\n\t\t\t\tc.Response().Header.Add(\"foo2\", \"bar\")\n\t\t\t\tc.Response().Header.Add(\"foo2\", \"bar2\")\n\t\t\t\tc.Response().Header.Add(\"foo2\", \"bar3\")\n\n\t\t\t\treturn c.SendString(\"helo world\")\n\t\t\t})\n\t\t},\n\t)\n\n\tclient := New().SetDial(server.dial())\n\n\tresp, err := AcquireRequest().\n\t\tSetClient(client).\n\t\tGet(\"http://example.com\")\n\trequire.NoError(b, err)\n\n\tb.Cleanup(func() {\n\t\tresp.Close()\n\t\tserver.stop()\n\t})\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tfor k, v := range resp.Headers() {\n\t\t\t_ = k\n\t\t\t_ = v\n\t\t}\n\t}\n}\n\nfunc Test_Response_Cookie(t *testing.T) {\n\tt.Parallel()\n\n\tserver := startTestServer(t, func(app *fiber.App) {\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Cookie(&fiber.Cookie{\n\t\t\t\tName:  \"foo\",\n\t\t\t\tValue: \"bar\",\n\t\t\t})\n\t\t\treturn c.SendString(\"helo world\")\n\t\t})\n\t})\n\tdefer server.stop()\n\n\tclient := New().SetDial(server.dial())\n\n\tresp, err := AcquireRequest().\n\t\tSetClient(client).\n\t\tGet(\"http://example.com\")\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"bar\", string(resp.Cookies()[0].Value()))\n\tresp.Close()\n}\n\nfunc Test_Response_Body(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() *testServer {\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"hello world\")\n\t\t\t})\n\n\t\t\tapp.Get(\"/json\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"{\\\"status\\\":\\\"success\\\"}\")\n\t\t\t})\n\n\t\t\tapp.Get(\"/xml\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"<status><name>success</name></status>\")\n\t\t\t})\n\n\t\t\tapp.Get(\"/cbor\", func(c fiber.Ctx) error {\n\t\t\t\ttype cborData struct {\n\t\t\t\t\tName string `cbor:\"name\"`\n\t\t\t\t\tAge  int    `cbor:\"age\"`\n\t\t\t\t}\n\n\t\t\t\treturn c.CBOR(cborData{\n\t\t\t\t\tName: \"foo\",\n\t\t\t\t\tAge:  12,\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\treturn server\n\t}\n\n\tt.Run(\"raw body\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, []byte(\"hello world\"), resp.Body())\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"string body\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com\")\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"hello world\", resp.String())\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"json body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype body struct {\n\t\t\tStatus string `json:\"status\"`\n\t\t}\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/json\")\n\n\t\trequire.NoError(t, err)\n\n\t\ttmp := &body{}\n\t\terr = resp.JSON(tmp)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"success\", tmp.Status)\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"xml body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype body struct {\n\t\t\tName   xml.Name `xml:\"status\"`\n\t\t\tStatus string   `xml:\"name\"`\n\t\t}\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/xml\")\n\n\t\trequire.NoError(t, err)\n\n\t\ttmp := &body{}\n\t\terr = resp.XML(tmp)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"success\", tmp.Status)\n\t\tresp.Close()\n\t})\n\n\tt.Run(\"cbor body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttype cborData struct {\n\t\t\tName string `cbor:\"name\"`\n\t\t\tAge  int    `cbor:\"age\"`\n\t\t}\n\n\t\tdata := cborData{\n\t\t\tName: \"foo\",\n\t\t\tAge:  12,\n\t\t}\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/cbor\")\n\n\t\trequire.NoError(t, err)\n\n\t\ttmp := &cborData{}\n\t\terr = resp.CBOR(tmp)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, data, *tmp)\n\t\tresp.Close()\n\t})\n}\n\nfunc Test_Response_DecodeHelpers_ClientNilSafety(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"client nil returns exported error without panic\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype payload struct {\n\t\t\tStatus string `json:\"status\" xml:\"status\" cbor:\"status\"`\n\t\t}\n\n\t\tt.Run(\"json\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.RawResponse.SetBodyString(`{\"status\":\"success\"}`)\n\n\t\t\tdecoded := payload{}\n\t\t\trequire.NotPanics(t, func() {\n\t\t\t\terr := resp.JSON(&decoded)\n\t\t\t\trequire.ErrorIs(t, err, ErrClientNil)\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"xml\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.RawResponse.SetBodyString(`<payload><status>success</status></payload>`)\n\n\t\t\tdecoded := payload{}\n\t\t\trequire.NotPanics(t, func() {\n\t\t\t\terr := resp.XML(&decoded)\n\t\t\t\trequire.ErrorIs(t, err, ErrClientNil)\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"cbor\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.RawResponse.SetBodyString(\"not-cbor\")\n\n\t\t\tdecoded := payload{}\n\t\t\trequire.NotPanics(t, func() {\n\t\t\t\terr := resp.CBOR(&decoded)\n\t\t\t\trequire.ErrorIs(t, err, ErrClientNil)\n\t\t\t})\n\t\t})\n\t})\n\n\tt.Run(\"decode helpers still work with client\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype payload struct {\n\t\t\tStatus string `json:\"status\" xml:\"status\" cbor:\"status\"`\n\t\t}\n\n\t\tt.Run(\"json\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.setClient(New())\n\t\t\tresp.RawResponse.SetBodyString(`{\"status\":\"success\"}`)\n\n\t\t\tdecoded := payload{}\n\t\t\terr := resp.JSON(&decoded)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"success\", decoded.Status)\n\t\t})\n\n\t\tt.Run(\"xml\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.setClient(New())\n\t\t\tresp.RawResponse.SetBodyString(`<payload><status>success</status></payload>`)\n\n\t\t\tdecoded := payload{}\n\t\t\terr := resp.XML(&decoded)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"success\", decoded.Status)\n\t\t})\n\n\t\tt.Run(\"cbor\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tclient := New()\n\t\t\tresp := AcquireResponse()\n\t\t\tt.Cleanup(func() {\n\t\t\t\tReleaseResponse(resp)\n\t\t\t})\n\t\t\tresp.setClient(client)\n\n\t\t\tbody, err := client.cborMarshal(payload{Status: \"success\"})\n\t\t\trequire.NoError(t, err)\n\t\t\tresp.RawResponse.SetBody(body)\n\n\t\t\tdecoded := payload{}\n\t\t\terr = resp.CBOR(&decoded)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"success\", decoded.Status)\n\t\t})\n\t})\n}\n\nfunc Test_Response_Save(t *testing.T) {\n\tt.Parallel()\n\n\tsetupApp := func() *testServer {\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/json\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"{\\\"status\\\":\\\"success\\\"}\")\n\t\t\t})\n\t\t})\n\n\t\treturn server\n\t}\n\n\tt.Run(\"file path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/json\")\n\n\t\trequire.NoError(t, err)\n\n\t\terr = resp.Save(\"./test/tmp.json\")\n\t\trequire.NoError(t, err)\n\t\tdefer func() {\n\t\t\t_, statErr := os.Stat(\"./test/tmp.json\")\n\t\t\trequire.NoError(t, statErr)\n\n\t\t\tstatErr = os.RemoveAll(\"./test\")\n\t\t\trequire.NoError(t, statErr)\n\t\t}()\n\n\t\tfile, err := os.Open(\"./test/tmp.json\")\n\t\trequire.NoError(t, err)\n\t\tdefer func(file *os.File) {\n\t\t\tcloseErr := file.Close()\n\t\t\trequire.NoError(t, closeErr)\n\t\t}(file)\n\n\t\tdata, err := io.ReadAll(file)\n\t\trequire.NoError(t, err)\n\t\trequire.JSONEq(t, \"{\\\"status\\\":\\\"success\\\"}\", string(data))\n\t})\n\n\tt.Run(\"io.Writer\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/json\")\n\n\t\trequire.NoError(t, err)\n\n\t\tbuf := &bytes.Buffer{}\n\n\t\terr = resp.Save(buf)\n\t\trequire.NoError(t, err)\n\t\trequire.JSONEq(t, \"{\\\"status\\\":\\\"success\\\"}\", buf.String())\n\t})\n\n\tt.Run(\"io.Copy error when saving to file is surfaced\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresp := AcquireResponse()\n\t\tdefer ReleaseResponse(resp)\n\n\t\tresp.RawResponse.SetBodyStream(&errorReader{err: errors.New(\"copy failure\")}, -1)\n\n\t\ttarget := filepath.Join(t.TempDir(), \"out.txt\")\n\t\terr := resp.Save(target)\n\t\trequire.ErrorContains(t, err, \"failed to write response body to file: copy failure\")\n\t})\n\n\tt.Run(\"io.Copy error when saving to writer is surfaced\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresp := AcquireResponse()\n\t\tdefer ReleaseResponse(resp)\n\n\t\tresp.RawResponse.SetBodyStream(bytes.NewBufferString(\"data\"), len(\"data\"))\n\n\t\terr := resp.Save(&errorWriter{err: errors.New(\"sink closed\")})\n\t\trequire.ErrorContains(t, err, \"failed to write response body to writer: sink closed\")\n\t})\n\n\tt.Run(\"error type\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := setupApp()\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := AcquireRequest().\n\t\t\tSetClient(client).\n\t\t\tGet(\"http://example.com/json\")\n\n\t\trequire.NoError(t, err)\n\n\t\terr = resp.Save(nil)\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc Test_Response_BodyStream(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"basic streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/stream\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStream(bytes.NewReader([]byte(\"streaming data\")))\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial()).SetStreamResponseBody(true)\n\n\t\tresp, err := client.Get(\"http://example.com/stream\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\t\tbodyStream := resp.BodyStream()\n\t\trequire.NotNil(t, bodyStream)\n\t\tdata, err := io.ReadAll(bodyStream)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"streaming data\", string(data))\n\t})\n\n\tt.Run(\"large response streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/large\", func(c fiber.Ctx) error {\n\t\t\t\tdata := make([]byte, 1024)\n\t\t\t\tfor i := range data {\n\t\t\t\t\tdata[i] = byte('A' + i%26)\n\t\t\t\t}\n\t\t\t\treturn c.SendStream(bytes.NewReader(data))\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial()).SetStreamResponseBody(true)\n\t\tresp, err := client.Get(\"http://example.com/large\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\t\tbodyStream := resp.BodyStream()\n\t\trequire.NotNil(t, bodyStream)\n\t\tbuffer := make([]byte, 256)\n\t\tvar totalRead []byte\n\t\tfor {\n\t\t\tn, err := bodyStream.Read(buffer)\n\t\t\tif n > 0 {\n\t\t\t\ttotalRead = append(totalRead, buffer[:n]...)\n\t\t\t}\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t\trequire.Len(t, totalRead, 1024)\n\t})\n}\n\nfunc Test_Response_BodyStream_Fallback(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"non-streaming response fallback to bytes.Reader\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/regular\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"regular response body\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\t\tclient := New().SetDial(server.dial())\n\t\tresp, err := client.Get(\"http://example.com/regular\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\t\trequire.False(t, resp.IsStreaming())\n\t\tbodyStream := resp.BodyStream()\n\t\trequire.NotNil(t, bodyStream)\n\t\tdata, err := io.ReadAll(bodyStream)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"regular response body\", string(data))\n\t})\n}\n\nfunc Test_Response_IsStreaming(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"streaming disabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/regular\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"regular content\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\t\tclient := New().SetDial(server.dial())\n\t\tresp, err := client.Get(\"http://example.com/regular\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\t\trequire.False(t, resp.IsStreaming())\n\t})\n\n\tt.Run(\"bodystream always works regardless of streaming state\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"test content\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\t// Test with streaming enabled\n\t\tclient1 := New().SetDial(server.dial()).SetStreamResponseBody(true)\n\t\tresp1, err := client1.Get(\"http://example.com/test\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp1.Close()\n\t\tbodyStream1 := resp1.BodyStream()\n\t\trequire.NotNil(t, bodyStream1)\n\t\tdata1, err := io.ReadAll(bodyStream1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"test content\", string(data1))\n\n\t\t// Test with streaming disabled\n\t\tclient2 := New().SetDial(server.dial()).SetStreamResponseBody(false)\n\t\tresp2, err := client2.Get(\"http://example.com/test\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp2.Close()\n\t\trequire.False(t, resp2.IsStreaming())\n\t\tbodyStream2 := resp2.BodyStream()\n\t\trequire.NotNil(t, bodyStream2)\n\t\tdata2, err := io.ReadAll(bodyStream2)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"test content\", string(data2))\n\t})\n}\n\nfunc Test_Response_Save_Streaming(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"save streaming response to file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/stream\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStream(bytes.NewReader([]byte(\"streaming file content\")))\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial()).SetStreamResponseBody(true)\n\n\t\tresp, err := client.Get(\"http://example.com/stream\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\ttestFile := filepath.Join(t.TempDir(), \"stream_test.txt\")\n\t\terr = resp.Save(testFile)\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := os.ReadFile(testFile) //nolint:gosec // test file is created in a temp directory\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"streaming file content\", string(data))\n\t})\n\n\tt.Run(\"save streaming response to io.Writer\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/stream\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStream(bytes.NewReader([]byte(\"streaming writer content\")))\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial()).SetStreamResponseBody(true)\n\n\t\tresp, err := client.Get(\"http://example.com/stream\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\tvar buf bytes.Buffer\n\t\terr = resp.Save(&buf)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"streaming writer content\", buf.String())\n\t})\n\n\tt.Run(\"save non-streaming response to file using BodyStream\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/regular\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"regular file content\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := client.Get(\"http://example.com/regular\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\ttestFile := filepath.Join(t.TempDir(), \"regular_test.txt\")\n\t\terr = resp.Save(testFile)\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := os.ReadFile(testFile) //nolint:gosec // test file is created in a temp directory\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"regular file content\", string(data))\n\t})\n\n\tt.Run(\"save to io.WriteCloser closes writer\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := startTestServer(t, func(app *fiber.App) {\n\t\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"test content\")\n\t\t\t})\n\t\t})\n\t\tdefer server.stop()\n\n\t\tclient := New().SetDial(server.dial())\n\n\t\tresp, err := client.Get(\"http://example.com/test\")\n\t\trequire.NoError(t, err)\n\t\tdefer resp.Close()\n\n\t\t// Create a mock WriteCloser to verify Close is called\n\t\tmockWriter := &mockWriteCloser{}\n\t\terr = resp.Save(mockWriter)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, mockWriter.closed, \"Save() should close io.WriteCloser\")\n\t\trequire.Equal(t, \"test content\", mockWriter.buf.String())\n\t})\n}\n\n// mockWriteCloser is a helper to verify that Save() closes io.WriteCloser\ntype mockWriteCloser struct {\n\tbuf    bytes.Buffer\n\tclosed bool\n}\n\ntype errorReader struct {\n\terr error\n}\n\nfunc (m *errorReader) Read(_ []byte) (int, error) {\n\treturn 0, m.err\n}\n\ntype errorWriter struct {\n\terr error\n}\n\nfunc (m *errorWriter) Write(_ []byte) (int, error) {\n\treturn 0, m.err\n}\n\nfunc (m *mockWriteCloser) Write(p []byte) (int, error) {\n\treturn m.buf.Write(p) //nolint:wrapcheck // propagate buffer write error directly for test helper\n}\n\nfunc (m *mockWriteCloser) Close() error {\n\tm.closed = true\n\treturn nil\n}\n"
  },
  {
    "path": "client/transport.go",
    "content": "// Transport adapters unify fasthttp clients behind a shared interface so the\n// Fiber client can coordinate behavior like redirects, TLS overrides, and\n// dial customizations regardless of the underlying transport type.\npackage client\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"time\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// defaultRedirectLimit mirrors fasthttp's default when callers supply a negative redirect cap.\nconst defaultRedirectLimit = 16\n\nvar (\n\t// Pre-allocated byte slice for http/https scheme comparison\n\thttpScheme  = []byte(\"http\")\n\thttpsScheme = []byte(\"https\")\n)\n\n// httpClientTransport unifies the operations exposed by the Fiber client across\n// the fasthttp.Client, fasthttp.HostClient, and fasthttp.LBClient adapters so\n// helper logic can treat the concrete transports uniformly.\ntype httpClientTransport interface {\n\tDo(req *fasthttp.Request, resp *fasthttp.Response) error\n\tDoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error\n\tDoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error\n\tDoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error\n\tCloseIdleConnections()\n\tTLSConfig() *tls.Config\n\tSetTLSConfig(config *tls.Config)\n\tSetDial(dial fasthttp.DialFunc)\n\tClient() any\n\tStreamResponseBody() bool\n\tSetStreamResponseBody(enable bool)\n}\n\n// standardClientTransport adapts fasthttp.Client to the httpClientTransport\n// interface used by Fiber's client helpers.\ntype standardClientTransport struct {\n\tclient *fasthttp.Client\n}\n\nfunc newStandardClientTransport(client *fasthttp.Client) *standardClientTransport {\n\treturn &standardClientTransport{client: client}\n}\n\nfunc (s *standardClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {\n\treturn s.client.Do(req, resp)\n}\n\nfunc (s *standardClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {\n\treturn s.client.DoTimeout(req, resp, timeout)\n}\n\nfunc (s *standardClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {\n\treturn s.client.DoDeadline(req, resp, deadline)\n}\n\nfunc (s *standardClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {\n\treturn s.client.DoRedirects(req, resp, maxRedirects)\n}\n\nfunc (s *standardClientTransport) CloseIdleConnections() {\n\ts.client.CloseIdleConnections()\n}\n\nfunc (s *standardClientTransport) TLSConfig() *tls.Config {\n\treturn s.client.TLSConfig\n}\n\nfunc (s *standardClientTransport) SetTLSConfig(config *tls.Config) {\n\ts.client.TLSConfig = config\n}\n\nfunc (s *standardClientTransport) SetDial(dial fasthttp.DialFunc) {\n\ts.client.Dial = dial\n}\n\nfunc (s *standardClientTransport) Client() any {\n\treturn s.client\n}\n\nfunc (s *standardClientTransport) StreamResponseBody() bool {\n\treturn s.client.StreamResponseBody\n}\n\nfunc (s *standardClientTransport) SetStreamResponseBody(enable bool) {\n\ts.client.StreamResponseBody = enable\n}\n\n// hostClientTransport adapts fasthttp.HostClient to the httpClientTransport\n// interface used by Fiber's client helpers.\ntype hostClientTransport struct {\n\tclient *fasthttp.HostClient\n}\n\nfunc newHostClientTransport(client *fasthttp.HostClient) *hostClientTransport {\n\treturn &hostClientTransport{client: client}\n}\n\nfunc (h *hostClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {\n\treturn h.client.Do(req, resp)\n}\n\nfunc (h *hostClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {\n\treturn h.client.DoTimeout(req, resp, timeout)\n}\n\nfunc (h *hostClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {\n\treturn h.client.DoDeadline(req, resp, deadline)\n}\n\nfunc (h *hostClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {\n\treturn h.client.DoRedirects(req, resp, maxRedirects)\n}\n\nfunc (h *hostClientTransport) CloseIdleConnections() {\n\th.client.CloseIdleConnections()\n}\n\nfunc (h *hostClientTransport) TLSConfig() *tls.Config {\n\treturn h.client.TLSConfig\n}\n\nfunc (h *hostClientTransport) SetTLSConfig(config *tls.Config) {\n\th.client.TLSConfig = config\n}\n\nfunc (h *hostClientTransport) SetDial(dial fasthttp.DialFunc) {\n\th.client.Dial = dial\n}\n\nfunc (h *hostClientTransport) Client() any {\n\treturn h.client\n}\n\nfunc (h *hostClientTransport) StreamResponseBody() bool {\n\treturn h.client.StreamResponseBody\n}\n\nfunc (h *hostClientTransport) SetStreamResponseBody(enable bool) {\n\th.client.StreamResponseBody = enable\n}\n\n// lbClientTransport adapts fasthttp.LBClient to the httpClientTransport\n// interface used by Fiber's client helpers.\ntype lbClientTransport struct {\n\tclient *fasthttp.LBClient\n}\n\nfunc newLBClientTransport(client *fasthttp.LBClient) *lbClientTransport {\n\treturn &lbClientTransport{client: client}\n}\n\nfunc (l *lbClientTransport) Do(req *fasthttp.Request, resp *fasthttp.Response) error {\n\treturn l.client.Do(req, resp)\n}\n\nfunc (l *lbClientTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {\n\treturn l.client.DoTimeout(req, resp, timeout)\n}\n\nfunc (l *lbClientTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {\n\treturn l.client.DoDeadline(req, resp, deadline)\n}\n\n// DoRedirects proxies redirect handling through doRedirectsWithClient so the\n// load-balanced transport mirrors fasthttp.Client semantics despite\n// fasthttp.LBClient not exposing DoRedirects directly.\nfunc (l *lbClientTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int) error {\n\treturn doRedirectsWithClient(req, resp, maxRedirects, l.client)\n}\n\nfunc (l *lbClientTransport) CloseIdleConnections() {\n\tforEachHostClient(l.client, func(hc *fasthttp.HostClient) {\n\t\thc.CloseIdleConnections()\n\t})\n}\n\nfunc (l *lbClientTransport) TLSConfig() *tls.Config {\n\tif len(l.client.Clients) == 0 {\n\t\treturn nil\n\t}\n\treturn extractTLSConfig(l.client.Clients)\n}\n\nfunc (l *lbClientTransport) SetTLSConfig(config *tls.Config) {\n\tforEachHostClient(l.client, func(hc *fasthttp.HostClient) {\n\t\thc.TLSConfig = config\n\t})\n}\n\nfunc (l *lbClientTransport) SetDial(dial fasthttp.DialFunc) {\n\tforEachHostClient(l.client, func(hc *fasthttp.HostClient) {\n\t\thc.Dial = dial\n\t})\n}\n\nfunc (l *lbClientTransport) Client() any {\n\treturn l.client\n}\n\nfunc (l *lbClientTransport) StreamResponseBody() bool {\n\tif len(l.client.Clients) == 0 {\n\t\treturn false\n\t}\n\t// Return the StreamResponseBody setting from the first HostClient\n\tvar streamEnabled bool\n\tfor _, c := range l.client.Clients {\n\t\tif walkBalancingClientWithBreak(c, func(hc *fasthttp.HostClient) bool {\n\t\t\tstreamEnabled = hc.StreamResponseBody\n\t\t\treturn true\n\t\t}) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn streamEnabled\n}\n\nfunc (l *lbClientTransport) SetStreamResponseBody(enable bool) {\n\tforEachHostClient(l.client, func(hc *fasthttp.HostClient) {\n\t\thc.StreamResponseBody = enable\n\t})\n}\n\n// forEachHostClient applies fn to every host client reachable from the provided\n// load balancer by recursively following nested balancers and wrapper types.\nfunc forEachHostClient(lb *fasthttp.LBClient, fn func(*fasthttp.HostClient)) {\n\tfor _, c := range lb.Clients {\n\t\twalkBalancingClient(c, fn)\n\t}\n}\n\n// walkBalancingClient traverses balancing clients recursively, invoking fn for\n// every host client discovered beneath the current node.\nfunc walkBalancingClient(client any, fn func(*fasthttp.HostClient)) {\n\twalkBalancingClientWithBreak(client, func(hc *fasthttp.HostClient) bool {\n\t\tfn(hc)\n\t\treturn false\n\t})\n}\n\n// extractTLSConfig returns the first TLS configuration discovered while walking\n// the provided balancing clients so cached settings flow through nested load\n// balancers without redundant traversal.\nfunc extractTLSConfig(clients []fasthttp.BalancingClient) *tls.Config {\n\tvar cfg *tls.Config\n\tfor _, c := range clients {\n\t\tif walkBalancingClientWithBreak(c, func(hc *fasthttp.HostClient) bool {\n\t\t\tif hc.TLSConfig != nil {\n\t\t\t\tcfg = hc.TLSConfig\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t}) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn cfg\n}\n\n// walkBalancingClientWithBreak traverses balancing clients recursively and\n// invokes fn for each host client until fn signals success, enabling early\n// termination once a match is found.\nfunc walkBalancingClientWithBreak(client any, fn func(*fasthttp.HostClient) bool) bool {\n\tswitch c := client.(type) {\n\tcase *fasthttp.HostClient:\n\t\treturn fn(c)\n\tcase *fasthttp.LBClient:\n\t\tfor _, nestedClient := range c.Clients {\n\t\t\tif walkBalancingClientWithBreak(nestedClient, fn) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\tcase interface{ LBClient() *fasthttp.LBClient }:\n\t\tif nested := c.LBClient(); nested != nil {\n\t\t\tif walkBalancingClientWithBreak(nested, fn) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// redirectClient describes the minimal Do-capable surface needed by\n// doRedirectsWithClient so transports that do not expose DoRedirects (such as\n// fasthttp.LBClient) can participate in redirect handling.\ntype redirectClient interface {\n\tDo(req *fasthttp.Request, resp *fasthttp.Response) error\n}\n\n// doRedirectsWithClient mirrors fasthttp's redirect loop for transports that do\n// not expose DoRedirects (e.g. fasthttp.LBClient). The helper always issues the\n// initial request, respects zero redirect limits, falls back to the default cap\n// for negative values, and validates redirect targets before following them.\nfunc doRedirectsWithClient(req *fasthttp.Request, resp *fasthttp.Response, maxRedirects int, client redirectClient) error {\n\tcurrentURL := req.URI().String()\n\tredirects := 0\n\tsingleRequestOnly := maxRedirects <= 0\n\n\tif maxRedirects < 0 {\n\t\tmaxRedirects = defaultRedirectLimit\n\t\tsingleRequestOnly = false\n\t}\n\n\tfor {\n\t\treq.SetRequestURI(currentURL)\n\n\t\tif err := client.Do(req, resp); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tstatusCode := resp.Header.StatusCode()\n\t\tif !fasthttp.StatusCodeIsRedirect(statusCode) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif singleRequestOnly {\n\t\t\treturn nil\n\t\t}\n\n\t\tredirects++\n\t\tif redirects > maxRedirects {\n\t\t\treturn fasthttp.ErrTooManyRedirects\n\t\t}\n\n\t\tlocation := resp.Header.Peek(\"Location\")\n\t\tif len(location) == 0 {\n\t\t\treturn fasthttp.ErrMissingLocation\n\t\t}\n\n\t\tnextURL, err := composeRedirectURL(currentURL, location, req.DisableRedirectPathNormalizing)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcurrentURL = nextURL\n\n\t\tif req.Header.IsPost() && (statusCode == fasthttp.StatusMovedPermanently || statusCode == fasthttp.StatusFound || statusCode == fasthttp.StatusSeeOther) {\n\t\t\treq.Header.SetMethod(fasthttp.MethodGet)\n\t\t\treq.SetBody(nil)\n\t\t\treq.Header.Del(fasthttp.HeaderContentType)\n\t\t\treq.Header.Del(fasthttp.HeaderContentLength)\n\t\t}\n\t}\n}\n\n// composeRedirectURL resolves a redirect target relative to the current request\n// URL while rejecting suspicious payloads (e.g. control characters) and\n// restricting schemes to HTTP/S so caller-provided Location headers cannot\n// trigger arbitrary transports.\nfunc composeRedirectURL(base string, location []byte, disablePathNormalizing bool) (string, error) {\n\tfor _, b := range location {\n\t\tif b < 0x20 || b == 0x7f {\n\t\t\treturn \"\", fasthttp.ErrorInvalidURI\n\t\t}\n\t}\n\n\turi := fasthttp.AcquireURI()\n\tdefer fasthttp.ReleaseURI(uri)\n\n\turi.Update(base)\n\turi.UpdateBytes(location)\n\turi.DisablePathNormalizing = disablePathNormalizing\n\n\tif scheme := uri.Scheme(); len(scheme) > 0 && !bytes.EqualFold(scheme, httpScheme) && !bytes.EqualFold(scheme, httpsScheme) {\n\t\treturn \"\", fasthttp.ErrorInvalidURI\n\t}\n\n\tif len(uri.Scheme()) > 0 && len(uri.Host()) == 0 {\n\t\treturn \"\", fasthttp.ErrorInvalidURI\n\t}\n\n\treturn uri.String(), nil\n}\n"
  },
  {
    "path": "client/transport_test.go",
    "content": "package client\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\ntype stubBalancingClient struct{}\n\nfunc (stubBalancingClient) DoDeadline(*fasthttp.Request, *fasthttp.Response, time.Time) error {\n\treturn nil\n}\nfunc (stubBalancingClient) PendingRequests() int { return 0 }\n\ntype lbBalancingClient struct {\n\tclient *fasthttp.LBClient\n}\n\nfunc (l *lbBalancingClient) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {\n\tif l.client == nil {\n\t\treturn nil\n\t}\n\treturn l.client.DoDeadline(req, resp, deadline)\n}\n\nfunc (*lbBalancingClient) PendingRequests() int { return 0 }\n\nfunc (l *lbBalancingClient) LBClient() *fasthttp.LBClient { return l.client }\n\ntype stubRedirectCall struct {\n\terr      error\n\tstatus   *int\n\tlocation *string\n}\n\nfunc ptrInt(v int) *int { return &v }\n\nfunc ptrString(v string) *string { return &v }\n\ntype stubRedirectClient struct {\n\tcalls     []stubRedirectCall\n\tcallCount int\n}\n\nfunc (s *stubRedirectClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error {\n\t_ = req\n\ts.callCount++\n\tif len(s.calls) == 0 {\n\t\tresp.Reset()\n\t\tresp.Header.SetStatusCode(fasthttp.StatusOK)\n\t\treturn nil\n\t}\n\n\tcall := s.calls[0]\n\ts.calls = s.calls[1:]\n\n\tresp.Reset()\n\tif call.status != nil {\n\t\tresp.Header.SetStatusCode(*call.status)\n\t}\n\tif call.location != nil {\n\t\tresp.Header.Set(\"Location\", *call.location)\n\t}\n\treturn call.err\n}\n\nfunc (s *stubRedirectClient) CallCount() int { return s.callCount }\n\nfunc TestStandardClientTransportCoverage(t *testing.T) {\n\tt.Parallel()\n\n\tvar dialCount atomic.Int32\n\tclient := &fasthttp.Client{}\n\tclient.Dial = func(addr string) (net.Conn, error) {\n\t\t_ = addr\n\t\tdialCount.Add(1)\n\t\treturn nil, errors.New(\"dial error\")\n\t}\n\n\ttransport := newStandardClientTransport(client)\n\n\treq := fasthttp.AcquireRequest()\n\tresp := fasthttp.AcquireResponse()\n\tdefer fasthttp.ReleaseRequest(req)\n\tdefer fasthttp.ReleaseResponse(resp)\n\n\treq.SetRequestURI(\"http://example.com/\")\n\trequire.Error(t, transport.Do(req, resp))\n\n\treq.SetRequestURI(\"http://example.com/\")\n\trequire.Error(t, transport.DoTimeout(req, resp, time.Millisecond))\n\n\treq.SetRequestURI(\"http://example.com/\")\n\trequire.Error(t, transport.DoDeadline(req, resp, time.Now().Add(time.Second)))\n\n\ttransport.CloseIdleConnections()\n\n\tunderlying, ok := transport.Client().(*fasthttp.Client)\n\trequire.True(t, ok)\n\trequire.Same(t, client, underlying)\n\n\trequire.Equal(t, int32(3), dialCount.Load())\n\n\tclientTLS := &tls.Config{ServerName: \"standard\", MinVersion: tls.VersionTLS12}\n\tclient.TLSConfig = clientTLS\n\n\tcfg := transport.TLSConfig()\n\trequire.Same(t, clientTLS, cfg)\n\n\toverride := &tls.Config{ServerName: \"override\", MinVersion: tls.VersionTLS13}\n\ttransport.SetTLSConfig(override)\n\trequire.Equal(t, override, client.TLSConfig)\n}\n\nfunc TestHostClientTransportClientAccessor(t *testing.T) {\n\tt.Parallel()\n\n\thost := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\ttransport := newHostClientTransport(host)\n\n\tcurrent, ok := transport.Client().(*fasthttp.HostClient)\n\trequire.True(t, ok)\n\trequire.Same(t, host, current)\n\n\thostTLS := &tls.Config{ServerName: \"host\", MinVersion: tls.VersionTLS12}\n\thost.TLSConfig = hostTLS\n\n\tcfg := transport.TLSConfig()\n\trequire.Same(t, hostTLS, cfg)\n\n\toverride := &tls.Config{ServerName: \"host-override\", MinVersion: tls.VersionTLS13}\n\ttransport.SetTLSConfig(override)\n\trequire.Equal(t, override, host.TLSConfig)\n}\n\nfunc TestLBClientTransportAccessorsAndOverrides(t *testing.T) {\n\tt.Parallel()\n\n\thostWithoutOverrides := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\tnestedDialHost := &fasthttp.HostClient{Addr: \"example.org:80\"}\n\tnestedTLSHost := &fasthttp.HostClient{Addr: \"example.net:80\", TLSConfig: &tls.Config{ServerName: \"example\", MinVersion: tls.VersionTLS12}}\n\tmultiLevelHost := &fasthttp.HostClient{Addr: \"example.edu:80\"}\n\n\tnestedDialHost.Dial = func(addr string) (net.Conn, error) {\n\t\t_ = addr\n\t\treturn nil, errors.New(\"original dial\")\n\t}\n\n\tmultiLevelHost.Dial = func(addr string) (net.Conn, error) {\n\t\t_ = addr\n\t\treturn nil, errors.New(\"multi-level dial\")\n\t}\n\n\tnestedDialLB := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{nestedDialHost}}}\n\tnestedTLSLB := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{nestedTLSHost}}}\n\tmultiLevelLeaf := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{multiLevelHost}}}\n\tmultiLevelWrapper := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{multiLevelLeaf}}}\n\n\tlb := &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{\n\t\tstubBalancingClient{},\n\t\thostWithoutOverrides,\n\t\tnestedDialLB,\n\t\tnestedTLSLB,\n\t\tmultiLevelWrapper,\n\t}}\n\n\ttransport := newLBClientTransport(lb)\n\trequire.Same(t, lb, transport.Client())\n\tcfg := transport.TLSConfig()\n\trequire.Same(t, nestedTLSHost.TLSConfig, cfg)\n\n\toverrideTLS := &tls.Config{ServerName: \"override\", MinVersion: tls.VersionTLS12}\n\ttransport.SetTLSConfig(overrideTLS)\n\trequire.Equal(t, overrideTLS, hostWithoutOverrides.TLSConfig)\n\trequire.Equal(t, overrideTLS, nestedDialHost.TLSConfig)\n\trequire.Equal(t, overrideTLS, nestedTLSHost.TLSConfig)\n\trequire.Equal(t, overrideTLS, multiLevelHost.TLSConfig)\n\tcfg = transport.TLSConfig()\n\trequire.Same(t, overrideTLS, cfg)\n\tcfg.ServerName = \"mutated\"\n\trequire.Equal(t, \"mutated\", transport.TLSConfig().ServerName)\n\n\toverrideDialCalled := atomic.Bool{}\n\toverrideDial := func(addr string) (net.Conn, error) {\n\t\t_ = addr\n\t\toverrideDialCalled.Store(true)\n\t\treturn nil, errors.New(\"override dial\")\n\t}\n\ttransport.SetDial(overrideDial)\n\toverrideDialCalled.Store(false)\n\t_, err := hostWithoutOverrides.Dial(\"example.com:80\")\n\trequire.Error(t, err)\n\trequire.True(t, overrideDialCalled.Load())\n\n\toverrideDialCalled.Store(false)\n\t_, err = nestedDialHost.Dial(\"example.org:80\")\n\trequire.Error(t, err)\n\trequire.True(t, overrideDialCalled.Load())\n\n\toverrideDialCalled.Store(false)\n\t_, err = multiLevelHost.Dial(\"example.edu:80\")\n\trequire.Error(t, err)\n\trequire.True(t, overrideDialCalled.Load())\n}\n\nfunc TestExtractTLSConfigVariations(t *testing.T) {\n\tt.Parallel()\n\n\trequire.Nil(t, extractTLSConfig(nil))\n\trequire.Nil(t, extractTLSConfig([]fasthttp.BalancingClient{stubBalancingClient{}}))\n\n\thost := &fasthttp.HostClient{TLSConfig: &tls.Config{ServerName: \"configured\", MinVersion: tls.VersionTLS12}}\n\trequire.Equal(t, host.TLSConfig, extractTLSConfig([]fasthttp.BalancingClient{host}))\n\n\tnested := &fasthttp.HostClient{TLSConfig: &tls.Config{ServerName: \"nested\", MinVersion: tls.VersionTLS12}}\n\tnestedLB := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{nested}}}\n\trequire.Equal(t, nested.TLSConfig, extractTLSConfig([]fasthttp.BalancingClient{nestedLB}))\n\n\tmultiLayerLB := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{nestedLB}}}\n\trequire.Equal(t, nested.TLSConfig, extractTLSConfig([]fasthttp.BalancingClient{multiLayerLB}))\n}\n\nfunc TestWalkBalancingClientWithBreak(t *testing.T) {\n\tt.Parallel()\n\n\thost := &fasthttp.HostClient{}\n\trequire.True(t, walkBalancingClientWithBreak(host, func(*fasthttp.HostClient) bool { return true }))\n\n\trequire.False(t, walkBalancingClientWithBreak(stubBalancingClient{}, func(*fasthttp.HostClient) bool {\n\t\tt.Fatal(\"unexpected call\")\n\t\treturn false\n\t}))\n\n\tnested := &fasthttp.HostClient{}\n\tnestedLB := &lbBalancingClient{client: &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{nested}}}\n\trequire.True(t, walkBalancingClientWithBreak(nestedLB, func(*fasthttp.HostClient) bool { return true }))\n\n\tdirectNestedHost := &fasthttp.HostClient{}\n\tdirectNestedLB := &fasthttp.LBClient{Clients: []fasthttp.BalancingClient{directNestedHost}}\n\trequire.True(t, walkBalancingClientWithBreak(directNestedLB, func(hc *fasthttp.HostClient) bool {\n\t\trequire.Same(t, directNestedHost, hc)\n\t\treturn true\n\t}))\n}\n\nfunc TestDoRedirectsWithClientBranches(t *testing.T) {\n\tt.Parallel()\n\n\treq := fasthttp.AcquireRequest()\n\tresp := fasthttp.AcquireResponse()\n\tdefer fasthttp.ReleaseRequest(req)\n\tdefer fasthttp.ReleaseResponse(resp)\n\n\treq.SetRequestURI(\"http://example.com/start\")\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.Header.SetContentType(\"application/json\")\n\treq.SetBodyString(\"payload\")\n\n\tclient := &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusMovedPermanently), location: ptrString(\"/redirect\")}, {status: ptrInt(fasthttp.StatusOK)}}}\n\trequire.NoError(t, doRedirectsWithClient(req, resp, -1, client))\n\trequire.Equal(t, fasthttp.MethodGet, string(req.Header.Method()))\n\trequire.Equal(t, \"http://example.com/redirect\", req.URI().String())\n\trequire.Empty(t, req.Body())\n\trequire.Empty(t, req.Header.ContentType())\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.Header.SetContentType(\"application/json\")\n\treq.SetBodyString(\"payload\")\n\n\tseeOtherClient := &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusSeeOther), location: ptrString(\"/see-other\")}, {status: ptrInt(fasthttp.StatusOK)}}}\n\trequire.NoError(t, doRedirectsWithClient(req, resp, -1, seeOtherClient))\n\trequire.Equal(t, fasthttp.MethodGet, string(req.Header.Method()))\n\trequire.Equal(t, \"http://example.com/see-other\", req.URI().String())\n\trequire.Empty(t, req.Body())\n\trequire.Empty(t, req.Header.ContentType())\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.SetRequestURI(\"http://example.com/again\")\n\treq.SetBodyString(\"payload\")\n\n\tsingleCall := &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusFound), location: ptrString(\"/ignored\")}}}\n\trequire.NoError(t, doRedirectsWithClient(req, resp, 0, singleCall))\n\trequire.Equal(t, fasthttp.StatusFound, resp.StatusCode())\n\trequire.Equal(t, fasthttp.MethodPost, string(req.Header.Method()))\n\trequire.Equal(t, \"http://example.com/again\", req.URI().String())\n\trequire.Equal(t, \"payload\", string(req.Body()))\n\trequire.Equal(t, 1, singleCall.CallCount())\n\trequire.Equal(t, fasthttp.StatusFound, resp.Header.StatusCode())\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.SetRequestURI(\"http://example.com/start\")\n\n\tclient = &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusFound)}}}\n\trequire.ErrorIs(t, doRedirectsWithClient(req, resp, 1, client), fasthttp.ErrMissingLocation)\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.SetRequestURI(\"http://example.com/start\")\n\n\tclient = &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusMovedPermanently), location: ptrString(\"ftp://example.com\")}}}\n\trequire.ErrorIs(t, doRedirectsWithClient(req, resp, 1, client), fasthttp.ErrorInvalidURI)\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.SetRequestURI(\"http://example.com/start\")\n\n\tclient = &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusFound), location: ptrString(\"/bad\\x00path\")}}}\n\trequire.ErrorIs(t, doRedirectsWithClient(req, resp, 1, client), fasthttp.ErrorInvalidURI)\n\n\tresp.Reset()\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\treq.SetRequestURI(\"http://example.com/start\")\n\n\tclient = &stubRedirectClient{calls: []stubRedirectCall{{status: ptrInt(fasthttp.StatusMovedPermanently), location: ptrString(\"/loop\")}, {status: ptrInt(fasthttp.StatusFound), location: ptrString(\"/final\")}, {status: ptrInt(fasthttp.StatusOK)}}}\n\trequire.ErrorIs(t, doRedirectsWithClient(req, resp, 1, client), fasthttp.ErrTooManyRedirects)\n}\n\nfunc TestDoRedirectsWithClientDefaultLimit(t *testing.T) {\n\tt.Parallel()\n\n\treq := fasthttp.AcquireRequest()\n\tresp := fasthttp.AcquireResponse()\n\tdefer fasthttp.ReleaseRequest(req)\n\tdefer fasthttp.ReleaseResponse(resp)\n\n\treq.SetRequestURI(\"http://example.com/start\")\n\treq.Header.SetMethod(fasthttp.MethodPost)\n\n\tcalls := make([]stubRedirectCall, 0, defaultRedirectLimit+1)\n\tfor i := 0; i < defaultRedirectLimit+1; i++ {\n\t\tcalls = append(calls, stubRedirectCall{status: ptrInt(fasthttp.StatusFound), location: ptrString(\"/loop\")})\n\t}\n\n\tclient := &stubRedirectClient{calls: calls}\n\terr := doRedirectsWithClient(req, resp, -1, client)\n\trequire.ErrorIs(t, err, fasthttp.ErrTooManyRedirects)\n\trequire.Equal(t, defaultRedirectLimit+1, client.CallCount())\n}\n\nfunc Test_StandardClientTransport_StreamResponseBody(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"default value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttransport := newStandardClientTransport(&fasthttp.Client{})\n\t\trequire.False(t, transport.StreamResponseBody())\n\t})\n\n\tt.Run(\"enable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := &fasthttp.Client{}\n\t\ttransport := newStandardClientTransport(client)\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\trequire.True(t, client.StreamResponseBody)\n\t})\n\n\tt.Run(\"disable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tclient := &fasthttp.Client{}\n\t\ttransport := newStandardClientTransport(client)\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\ttransport.SetStreamResponseBody(false)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t\trequire.False(t, client.StreamResponseBody)\n\t})\n}\n\nfunc Test_HostClientTransport_StreamResponseBody(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"default value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{}\n\t\ttransport := newHostClientTransport(hostClient)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t})\n\n\tt.Run(\"enable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{}\n\t\ttransport := newHostClientTransport(hostClient)\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\trequire.True(t, hostClient.StreamResponseBody)\n\t})\n\n\tt.Run(\"disable streaming\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{}\n\t\ttransport := newHostClientTransport(hostClient)\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\ttransport.SetStreamResponseBody(false)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t\trequire.False(t, hostClient.StreamResponseBody)\n\t})\n}\n\nfunc Test_LBClientTransport_StreamResponseBody(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty clients\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tlbClient := &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{},\n\t\t}\n\t\ttransport := newLBClientTransport(lbClient)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t})\n\n\tt.Run(\"single host client\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient := &fasthttp.HostClient{Addr: \"example.com:80\"}\n\t\tlbClient := &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{hostClient},\n\t\t}\n\t\ttransport := newLBClientTransport(lbClient)\n\n\t\t// Test default\n\t\trequire.False(t, transport.StreamResponseBody())\n\n\t\t// Enable streaming\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\trequire.True(t, hostClient.StreamResponseBody)\n\n\t\t// Disable streaming\n\t\ttransport.SetStreamResponseBody(false)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t\trequire.False(t, hostClient.StreamResponseBody)\n\t})\n\n\tt.Run(\"multiple host clients\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thostClient1 := &fasthttp.HostClient{Addr: \"example1.com:80\"}\n\t\thostClient2 := &fasthttp.HostClient{Addr: \"example2.com:80\"}\n\t\tlbClient := &fasthttp.LBClient{\n\t\t\tClients: []fasthttp.BalancingClient{hostClient1, hostClient2},\n\t\t}\n\t\ttransport := newLBClientTransport(lbClient)\n\n\t\t// Enable streaming on all clients\n\t\ttransport.SetStreamResponseBody(true)\n\t\trequire.True(t, transport.StreamResponseBody())\n\t\trequire.True(t, hostClient1.StreamResponseBody)\n\t\trequire.True(t, hostClient2.StreamResponseBody)\n\n\t\t// Disable streaming on all clients\n\t\ttransport.SetStreamResponseBody(false)\n\t\trequire.False(t, transport.StreamResponseBody())\n\t\trequire.False(t, hostClient1.StreamResponseBody)\n\t\trequire.False(t, hostClient2.StreamResponseBody)\n\t})\n}\n\nfunc Test_httpClientTransport_Interface(t *testing.T) {\n\tt.Parallel()\n\n\ttransports := []struct {\n\t\ttransport httpClientTransport\n\t\tname      string\n\t}{\n\t\t{\n\t\t\tname:      \"standardClientTransport\",\n\t\t\ttransport: newStandardClientTransport(&fasthttp.Client{}),\n\t\t},\n\t\t{\n\t\t\tname:      \"hostClientTransport\",\n\t\t\ttransport: newHostClientTransport(&fasthttp.HostClient{}),\n\t\t},\n\t\t{\n\t\t\tname: \"lbClientTransport\",\n\t\t\ttransport: newLBClientTransport(&fasthttp.LBClient{\n\t\t\t\tClients: []fasthttp.BalancingClient{\n\t\t\t\t\t&fasthttp.HostClient{Addr: \"example.com:80\"},\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t}\n\n\tfor _, tt := range transports {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttransport := tt.transport\n\t\t\trequire.NotNil(t, transport.Client())\n\t\t\tinitialStream := transport.StreamResponseBody()\n\t\t\ttransport.SetStreamResponseBody(!initialStream)\n\t\t\trequire.Equal(t, !initialStream, transport.StreamResponseBody())\n\t\t\ttransport.SetStreamResponseBody(initialStream)\n\t\t\trequire.Equal(t, initialStream, transport.StreamResponseBody())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "color.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\n// Colors is a struct to define custom colors for Fiber app and middlewares.\ntype Colors struct {\n\t// Black color.\n\t//\n\t// Optional. Default: \"\\u001b[90m\"\n\tBlack string\n\n\t// Red color.\n\t//\n\t// Optional. Default: \"\\u001b[91m\"\n\tRed string\n\n\t// Green color.\n\t//\n\t// Optional. Default: \"\\u001b[92m\"\n\tGreen string\n\n\t// Yellow color.\n\t//\n\t// Optional. Default: \"\\u001b[93m\"\n\tYellow string\n\n\t// Blue color.\n\t//\n\t// Optional. Default: \"\\u001b[94m\"\n\tBlue string\n\n\t// Magenta color.\n\t//\n\t// Optional. Default: \"\\u001b[95m\"\n\tMagenta string\n\n\t// Cyan color.\n\t//\n\t// Optional. Default: \"\\u001b[96m\"\n\tCyan string\n\n\t// White color.\n\t//\n\t// Optional. Default: \"\\u001b[97m\"\n\tWhite string\n\n\t// Reset color.\n\t//\n\t// Optional. Default: \"\\u001b[0m\"\n\tReset string\n}\n\n// DefaultColors Default color codes\nvar DefaultColors = Colors{\n\tBlack:   \"\\u001b[90m\",\n\tRed:     \"\\u001b[91m\",\n\tGreen:   \"\\u001b[92m\",\n\tYellow:  \"\\u001b[93m\",\n\tBlue:    \"\\u001b[94m\",\n\tMagenta: \"\\u001b[95m\",\n\tCyan:    \"\\u001b[96m\",\n\tWhite:   \"\\u001b[97m\",\n\tReset:   \"\\u001b[0m\",\n}\n\n// defaultColors is a function to override default colors to config\nfunc defaultColors(colors *Colors) Colors {\n\tif colors == nil {\n\t\treturn DefaultColors\n\t}\n\n\tcfg := *colors\n\n\tif cfg.Black == \"\" {\n\t\tcfg.Black = DefaultColors.Black\n\t}\n\n\tif cfg.Red == \"\" {\n\t\tcfg.Red = DefaultColors.Red\n\t}\n\n\tif cfg.Green == \"\" {\n\t\tcfg.Green = DefaultColors.Green\n\t}\n\n\tif cfg.Yellow == \"\" {\n\t\tcfg.Yellow = DefaultColors.Yellow\n\t}\n\n\tif cfg.Blue == \"\" {\n\t\tcfg.Blue = DefaultColors.Blue\n\t}\n\n\tif cfg.Magenta == \"\" {\n\t\tcfg.Magenta = DefaultColors.Magenta\n\t}\n\n\tif cfg.Cyan == \"\" {\n\t\tcfg.Cyan = DefaultColors.Cyan\n\t}\n\n\tif cfg.White == \"\" {\n\t\tcfg.White = DefaultColors.White\n\t}\n\n\tif cfg.Reset == \"\" {\n\t\tcfg.Reset = DefaultColors.Reset\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "constants.go",
    "content": "package fiber\n\n// HTTP methods were copied from net/http.\nconst (\n\tMethodGet     = \"GET\"     // RFC 7231, 4.3.1\n\tMethodHead    = \"HEAD\"    // RFC 7231, 4.3.2\n\tMethodPost    = \"POST\"    // RFC 7231, 4.3.3\n\tMethodPut     = \"PUT\"     // RFC 7231, 4.3.4\n\tMethodPatch   = \"PATCH\"   // RFC 5789\n\tMethodDelete  = \"DELETE\"  // RFC 7231, 4.3.5\n\tMethodConnect = \"CONNECT\" // RFC 7231, 4.3.6\n\tMethodOptions = \"OPTIONS\" // RFC 7231, 4.3.7\n\tMethodTrace   = \"TRACE\"   // RFC 7231, 4.3.8\n\tmethodUse     = \"USE\"\n)\n\n// MIME types that are commonly used\nconst (\n\tMIMETextXML               = \"text/xml\"\n\tMIMETextHTML              = \"text/html\"\n\tMIMETextPlain             = \"text/plain\"\n\tMIMETextJavaScript        = \"text/javascript\"\n\tMIMETextCSS               = \"text/css\"\n\tMIMEApplicationXML        = \"application/xml\"\n\tMIMEApplicationJSON       = \"application/json\"\n\tMIMEApplicationJavaScript = \"application/javascript\"\n\tMIMEApplicationCBOR       = \"application/cbor\"\n\tMIMEApplicationForm       = \"application/x-www-form-urlencoded\"\n\tMIMEOctetStream           = \"application/octet-stream\"\n\tMIMEMultipartForm         = \"multipart/form-data\"\n\tMIMEApplicationMsgPack    = \"application/vnd.msgpack\"\n\n\tMIMETextXMLCharsetUTF8         = \"text/xml; charset=utf-8\"\n\tMIMETextHTMLCharsetUTF8        = \"text/html; charset=utf-8\"\n\tMIMETextPlainCharsetUTF8       = \"text/plain; charset=utf-8\"\n\tMIMETextJavaScriptCharsetUTF8  = \"text/javascript; charset=utf-8\"\n\tMIMETextCSSCharsetUTF8         = \"text/css; charset=utf-8\"\n\tMIMEApplicationXMLCharsetUTF8  = \"application/xml; charset=utf-8\"\n\tMIMEApplicationJSONCharsetUTF8 = \"application/json; charset=utf-8\"\n)\n\n// HTTP status codes were copied from net/http with the following updates:\n// - Rename StatusNonAuthoritativeInfo to StatusNonAuthoritativeInformation\n// - Add StatusSwitchProxy (306)\n// NOTE: Keep this list in sync with statusMessage\nconst (\n\tStatusContinue           = 100 // RFC 9110, 15.2.1\n\tStatusSwitchingProtocols = 101 // RFC 9110, 15.2.2\n\tStatusProcessing         = 102 // RFC 2518, 10.1\n\tStatusEarlyHints         = 103 // RFC 8297\n\n\tStatusOK                          = 200 // RFC 9110, 15.3.1\n\tStatusCreated                     = 201 // RFC 9110, 15.3.2\n\tStatusAccepted                    = 202 // RFC 9110, 15.3.3\n\tStatusNonAuthoritativeInformation = 203 // RFC 9110, 15.3.4\n\tStatusNoContent                   = 204 // RFC 9110, 15.3.5\n\tStatusResetContent                = 205 // RFC 9110, 15.3.6\n\tStatusPartialContent              = 206 // RFC 9110, 15.3.7\n\tStatusMultiStatus                 = 207 // RFC 4918, 11.1\n\tStatusAlreadyReported             = 208 // RFC 5842, 7.1\n\tStatusIMUsed                      = 226 // RFC 3229, 10.4.1\n\n\tStatusMultipleChoices   = 300 // RFC 9110, 15.4.1\n\tStatusMovedPermanently  = 301 // RFC 9110, 15.4.2\n\tStatusFound             = 302 // RFC 9110, 15.4.3\n\tStatusSeeOther          = 303 // RFC 9110, 15.4.4\n\tStatusNotModified       = 304 // RFC 9110, 15.4.5\n\tStatusUseProxy          = 305 // RFC 9110, 15.4.6\n\tStatusSwitchProxy       = 306 // RFC 9110, 15.4.7 (Unused)\n\tStatusTemporaryRedirect = 307 // RFC 9110, 15.4.8\n\tStatusPermanentRedirect = 308 // RFC 9110, 15.4.9\n\n\tStatusBadRequest                   = 400 // RFC 9110, 15.5.1\n\tStatusUnauthorized                 = 401 // RFC 9110, 15.5.2\n\tStatusPaymentRequired              = 402 // RFC 9110, 15.5.3\n\tStatusForbidden                    = 403 // RFC 9110, 15.5.4\n\tStatusNotFound                     = 404 // RFC 9110, 15.5.5\n\tStatusMethodNotAllowed             = 405 // RFC 9110, 15.5.6\n\tStatusNotAcceptable                = 406 // RFC 9110, 15.5.7\n\tStatusProxyAuthRequired            = 407 // RFC 9110, 15.5.8\n\tStatusRequestTimeout               = 408 // RFC 9110, 15.5.9\n\tStatusConflict                     = 409 // RFC 9110, 15.5.10\n\tStatusGone                         = 410 // RFC 9110, 15.5.11\n\tStatusLengthRequired               = 411 // RFC 9110, 15.5.12\n\tStatusPreconditionFailed           = 412 // RFC 9110, 15.5.13\n\tStatusRequestEntityTooLarge        = 413 // RFC 9110, 15.5.14\n\tStatusRequestURITooLong            = 414 // RFC 9110, 15.5.15\n\tStatusUnsupportedMediaType         = 415 // RFC 9110, 15.5.16\n\tStatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17\n\tStatusExpectationFailed            = 417 // RFC 9110, 15.5.18\n\tStatusTeapot                       = 418 // RFC 9110, 15.5.19 (Unused)\n\tStatusMisdirectedRequest           = 421 // RFC 9110, 15.5.20\n\tStatusUnprocessableEntity          = 422 // RFC 9110, 15.5.21\n\tStatusLocked                       = 423 // RFC 4918, 11.3\n\tStatusFailedDependency             = 424 // RFC 4918, 11.4\n\tStatusTooEarly                     = 425 // RFC 8470, 5.2.\n\tStatusUpgradeRequired              = 426 // RFC 9110, 15.5.22\n\tStatusPreconditionRequired         = 428 // RFC 6585, 3\n\tStatusTooManyRequests              = 429 // RFC 6585, 4\n\tStatusRequestHeaderFieldsTooLarge  = 431 // RFC 6585, 5\n\tStatusUnavailableForLegalReasons   = 451 // RFC 7725, 3\n\n\tStatusInternalServerError           = 500 // RFC 9110, 15.6.1\n\tStatusNotImplemented                = 501 // RFC 9110, 15.6.2\n\tStatusBadGateway                    = 502 // RFC 9110, 15.6.3\n\tStatusServiceUnavailable            = 503 // RFC 9110, 15.6.4\n\tStatusGatewayTimeout                = 504 // RFC 9110, 15.6.5\n\tStatusHTTPVersionNotSupported       = 505 // RFC 9110, 15.6.6\n\tStatusVariantAlsoNegotiates         = 506 // RFC 2295, 8.1\n\tStatusInsufficientStorage           = 507 // RFC 4918, 11.5\n\tStatusLoopDetected                  = 508 // RFC 5842, 7.2\n\tStatusNotExtended                   = 510 // RFC 2774, 7\n\tStatusNetworkAuthenticationRequired = 511 // RFC 6585, 6\n)\n\n// Errors\nvar (\n\tErrBadRequest                   = NewError(StatusBadRequest)                   // 400\n\tErrUnauthorized                 = NewError(StatusUnauthorized)                 // 401\n\tErrPaymentRequired              = NewError(StatusPaymentRequired)              // 402\n\tErrForbidden                    = NewError(StatusForbidden)                    // 403\n\tErrNotFound                     = NewError(StatusNotFound)                     // 404\n\tErrMethodNotAllowed             = NewError(StatusMethodNotAllowed)             // 405\n\tErrNotAcceptable                = NewError(StatusNotAcceptable)                // 406\n\tErrProxyAuthRequired            = NewError(StatusProxyAuthRequired)            // 407\n\tErrRequestTimeout               = NewError(StatusRequestTimeout)               // 408\n\tErrConflict                     = NewError(StatusConflict)                     // 409\n\tErrGone                         = NewError(StatusGone)                         // 410\n\tErrLengthRequired               = NewError(StatusLengthRequired)               // 411\n\tErrPreconditionFailed           = NewError(StatusPreconditionFailed)           // 412\n\tErrRequestEntityTooLarge        = NewError(StatusRequestEntityTooLarge)        // 413\n\tErrRequestURITooLong            = NewError(StatusRequestURITooLong)            // 414\n\tErrUnsupportedMediaType         = NewError(StatusUnsupportedMediaType)         // 415\n\tErrRequestedRangeNotSatisfiable = NewError(StatusRequestedRangeNotSatisfiable) // 416\n\tErrExpectationFailed            = NewError(StatusExpectationFailed)            // 417\n\tErrTeapot                       = NewError(StatusTeapot)                       // 418\n\tErrMisdirectedRequest           = NewError(StatusMisdirectedRequest)           // 421\n\tErrUnprocessableEntity          = NewError(StatusUnprocessableEntity)          // 422\n\tErrLocked                       = NewError(StatusLocked)                       // 423\n\tErrFailedDependency             = NewError(StatusFailedDependency)             // 424\n\tErrTooEarly                     = NewError(StatusTooEarly)                     // 425\n\tErrUpgradeRequired              = NewError(StatusUpgradeRequired)              // 426\n\tErrPreconditionRequired         = NewError(StatusPreconditionRequired)         // 428\n\tErrTooManyRequests              = NewError(StatusTooManyRequests)              // 429\n\tErrRequestHeaderFieldsTooLarge  = NewError(StatusRequestHeaderFieldsTooLarge)  // 431\n\tErrUnavailableForLegalReasons   = NewError(StatusUnavailableForLegalReasons)   // 451\n\n\tErrInternalServerError           = NewError(StatusInternalServerError)           // 500\n\tErrNotImplemented                = NewError(StatusNotImplemented)                // 501\n\tErrBadGateway                    = NewError(StatusBadGateway)                    // 502\n\tErrServiceUnavailable            = NewError(StatusServiceUnavailable)            // 503\n\tErrGatewayTimeout                = NewError(StatusGatewayTimeout)                // 504\n\tErrHTTPVersionNotSupported       = NewError(StatusHTTPVersionNotSupported)       // 505\n\tErrVariantAlsoNegotiates         = NewError(StatusVariantAlsoNegotiates)         // 506\n\tErrInsufficientStorage           = NewError(StatusInsufficientStorage)           // 507\n\tErrLoopDetected                  = NewError(StatusLoopDetected)                  // 508\n\tErrNotExtended                   = NewError(StatusNotExtended)                   // 510\n\tErrNetworkAuthenticationRequired = NewError(StatusNetworkAuthenticationRequired) // 511\n)\n\n// HTTP Headers were copied from net/http.\nconst (\n\tHeaderAuthorization                      = \"Authorization\"\n\tHeaderProxyAuthenticate                  = \"Proxy-Authenticate\"\n\tHeaderProxyAuthorization                 = \"Proxy-Authorization\"\n\tHeaderWWWAuthenticate                    = \"WWW-Authenticate\"\n\tHeaderAge                                = \"Age\"\n\tHeaderCacheControl                       = \"Cache-Control\"\n\tHeaderClearSiteData                      = \"Clear-Site-Data\"\n\tHeaderExpires                            = \"Expires\"\n\tHeaderPragma                             = \"Pragma\"\n\tHeaderWarning                            = \"Warning\"\n\tHeaderAcceptCH                           = \"Accept-CH\"\n\tHeaderAcceptCHLifetime                   = \"Accept-CH-Lifetime\"\n\tHeaderContentDPR                         = \"Content-DPR\"\n\tHeaderDPR                                = \"DPR\"\n\tHeaderEarlyData                          = \"Early-Data\"\n\tHeaderSaveData                           = \"Save-Data\"\n\tHeaderViewportWidth                      = \"Viewport-Width\"\n\tHeaderWidth                              = \"Width\"\n\tHeaderETag                               = \"ETag\"\n\tHeaderIfMatch                            = \"If-Match\"\n\tHeaderIfModifiedSince                    = \"If-Modified-Since\"\n\tHeaderIfNoneMatch                        = \"If-None-Match\"\n\tHeaderIfUnmodifiedSince                  = \"If-Unmodified-Since\"\n\tHeaderLastModified                       = \"Last-Modified\"\n\tHeaderVary                               = \"Vary\"\n\tHeaderConnection                         = \"Connection\"\n\tHeaderKeepAlive                          = \"Keep-Alive\"\n\tHeaderAccept                             = \"Accept\"\n\tHeaderAcceptCharset                      = \"Accept-Charset\"\n\tHeaderAcceptEncoding                     = \"Accept-Encoding\"\n\tHeaderAcceptLanguage                     = \"Accept-Language\"\n\tHeaderCookie                             = \"Cookie\"\n\tHeaderExpect                             = \"Expect\"\n\tHeaderMaxForwards                        = \"Max-Forwards\"\n\tHeaderSetCookie                          = \"Set-Cookie\"\n\tHeaderAccessControlAllowCredentials      = \"Access-Control-Allow-Credentials\"\n\tHeaderAccessControlAllowHeaders          = \"Access-Control-Allow-Headers\"\n\tHeaderAccessControlAllowMethods          = \"Access-Control-Allow-Methods\"\n\tHeaderAccessControlAllowOrigin           = \"Access-Control-Allow-Origin\"\n\tHeaderAccessControlExposeHeaders         = \"Access-Control-Expose-Headers\"\n\tHeaderAccessControlMaxAge                = \"Access-Control-Max-Age\"\n\tHeaderAccessControlRequestHeaders        = \"Access-Control-Request-Headers\"\n\tHeaderAccessControlRequestMethod         = \"Access-Control-Request-Method\"\n\tHeaderOrigin                             = \"Origin\"\n\tHeaderTimingAllowOrigin                  = \"Timing-Allow-Origin\"\n\tHeaderXPermittedCrossDomainPolicies      = \"X-Permitted-Cross-Domain-Policies\"\n\tHeaderDNT                                = \"DNT\"\n\tHeaderTk                                 = \"Tk\"\n\tHeaderContentDisposition                 = \"Content-Disposition\"\n\tHeaderContentEncoding                    = \"Content-Encoding\"\n\tHeaderContentLanguage                    = \"Content-Language\"\n\tHeaderContentLength                      = \"Content-Length\"\n\tHeaderContentLocation                    = \"Content-Location\"\n\tHeaderContentType                        = \"Content-Type\"\n\tHeaderForwarded                          = \"Forwarded\"\n\tHeaderVia                                = \"Via\"\n\tHeaderXForwardedFor                      = \"X-Forwarded-For\"\n\tHeaderXForwardedHost                     = \"X-Forwarded-Host\"\n\tHeaderXForwardedProto                    = \"X-Forwarded-Proto\"\n\tHeaderXForwardedProtocol                 = \"X-Forwarded-Protocol\"\n\tHeaderXForwardedSsl                      = \"X-Forwarded-Ssl\"\n\tHeaderXUrlScheme                         = \"X-Url-Scheme\"\n\tHeaderLocation                           = \"Location\"\n\tHeaderFrom                               = \"From\"\n\tHeaderHost                               = \"Host\"\n\tHeaderReferer                            = \"Referer\"\n\tHeaderReferrerPolicy                     = \"Referrer-Policy\"\n\tHeaderUserAgent                          = \"User-Agent\"\n\tHeaderAllow                              = \"Allow\"\n\tHeaderServer                             = \"Server\"\n\tHeaderAcceptRanges                       = \"Accept-Ranges\"\n\tHeaderContentRange                       = \"Content-Range\"\n\tHeaderIfRange                            = \"If-Range\"\n\tHeaderRange                              = \"Range\"\n\tHeaderContentSecurityPolicy              = \"Content-Security-Policy\"\n\tHeaderContentSecurityPolicyReportOnly    = \"Content-Security-Policy-Report-Only\"\n\tHeaderCrossOriginResourcePolicy          = \"Cross-Origin-Resource-Policy\"\n\tHeaderExpectCT                           = \"Expect-CT\"\n\tHeaderPermissionsPolicy                  = \"Permissions-Policy\"\n\tHeaderPublicKeyPins                      = \"Public-Key-Pins\"\n\tHeaderPublicKeyPinsReportOnly            = \"Public-Key-Pins-Report-Only\"\n\tHeaderStrictTransportSecurity            = \"Strict-Transport-Security\"\n\tHeaderUpgradeInsecureRequests            = \"Upgrade-Insecure-Requests\"\n\tHeaderXContentTypeOptions                = \"X-Content-Type-Options\"\n\tHeaderXDownloadOptions                   = \"X-Download-Options\"\n\tHeaderXFrameOptions                      = \"X-Frame-Options\"\n\tHeaderXPoweredBy                         = \"X-Powered-By\"\n\tHeaderXXSSProtection                     = \"X-XSS-Protection\"\n\tHeaderLastEventID                        = \"Last-Event-ID\"\n\tHeaderNEL                                = \"NEL\"\n\tHeaderPingFrom                           = \"Ping-From\"\n\tHeaderPingTo                             = \"Ping-To\"\n\tHeaderReportTo                           = \"Report-To\"\n\tHeaderTE                                 = \"TE\"\n\tHeaderTrailer                            = \"Trailer\"\n\tHeaderTransferEncoding                   = \"Transfer-Encoding\"\n\tHeaderSecFetchSite                       = \"Sec-Fetch-Site\"\n\tHeaderSecWebSocketAccept                 = \"Sec-WebSocket-Accept\"\n\tHeaderSecWebSocketExtensions             = \"Sec-WebSocket-Extensions\"\n\tHeaderSecWebSocketKey                    = \"Sec-WebSocket-Key\"\n\tHeaderSecWebSocketProtocol               = \"Sec-WebSocket-Protocol\"\n\tHeaderSecWebSocketVersion                = \"Sec-WebSocket-Version\"\n\tHeaderAcceptPatch                        = \"Accept-Patch\"\n\tHeaderAcceptPushPolicy                   = \"Accept-Push-Policy\"\n\tHeaderAcceptSignature                    = \"Accept-Signature\"\n\tHeaderAltSvc                             = \"Alt-Svc\"\n\tHeaderDate                               = \"Date\"\n\tHeaderIndex                              = \"Index\"\n\tHeaderLargeAllocation                    = \"Large-Allocation\"\n\tHeaderLink                               = \"Link\"\n\tHeaderPushPolicy                         = \"Push-Policy\"\n\tHeaderRetryAfter                         = \"Retry-After\"\n\tHeaderServerTiming                       = \"Server-Timing\"\n\tHeaderSignature                          = \"Signature\"\n\tHeaderSignedHeaders                      = \"Signed-Headers\"\n\tHeaderSourceMap                          = \"SourceMap\"\n\tHeaderUpgrade                            = \"Upgrade\"\n\tHeaderXDNSPrefetchControl                = \"X-DNS-Prefetch-Control\"\n\tHeaderXPingback                          = \"X-Pingback\"\n\tHeaderXRequestID                         = \"X-Request-ID\"\n\tHeaderXRequestedWith                     = \"X-Requested-With\"\n\tHeaderXResponseTime                      = \"X-Response-Time\"\n\tHeaderXRobotsTag                         = \"X-Robots-Tag\"\n\tHeaderXUACompatible                      = \"X-UA-Compatible\"\n\tHeaderAccessControlAllowPrivateNetwork   = \"Access-Control-Allow-Private-Network\"\n\tHeaderAccessControlRequestPrivateNetwork = \"Access-Control-Request-Private-Network\"\n)\n\n// Network types that are commonly used\nconst (\n\tNetworkTCP  = \"tcp\"\n\tNetworkTCP4 = \"tcp4\"\n\tNetworkTCP6 = \"tcp6\"\n\tNetworkUnix = \"unix\"\n)\n\n// Compression types\nconst (\n\tStrGzip     = \"gzip\"\n\tStrCompress = \"compress\"\n\tStrIdentity = \"identity\"\n\tStrBr       = \"br\"\n\tStrDeflate  = \"deflate\"\n\tStrBrotli   = \"brotli\"\n\tStrZstd     = \"zstd\"\n)\n\n// Cookie SameSite\n// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7\nconst (\n\tCookieSameSiteDisabled   = \"disabled\" // not in RFC, just control \"SameSite\" attribute will not be set.\n\tCookieSameSiteLaxMode    = \"Lax\"\n\tCookieSameSiteStrictMode = \"Strict\"\n\tCookieSameSiteNoneMode   = \"None\"\n)\n\n// Route Constraints\nconst (\n\tConstraintInt             = \"int\"\n\tConstraintBool            = \"bool\"\n\tConstraintFloat           = \"float\"\n\tConstraintAlpha           = \"alpha\"\n\tConstraintGUID            = \"guid\"\n\tConstraintMinLen          = \"minLen\"\n\tConstraintMaxLen          = \"maxLen\"\n\tConstraintLen             = \"len\"\n\tConstraintBetweenLen      = \"betweenLen\"\n\tConstraintMinLenLower     = \"minlen\"\n\tConstraintMaxLenLower     = \"maxlen\"\n\tConstraintBetweenLenLower = \"betweenlen\"\n\tConstraintMin             = \"min\"\n\tConstraintMax             = \"max\"\n\tConstraintRange           = \"range\"\n\tConstraintDatetime        = \"datetime\"\n\tConstraintRegex           = \"regex\"\n)\n"
  },
  {
    "path": "ctx.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"mime/multipart\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nconst (\n\tschemeHTTP  = \"http\"\n\tschemeHTTPS = \"https\"\n)\n\nconst (\n\t// maxParams defines the maximum number of parameters per route.\n\tmaxParams         = 30\n\tmaxDetectionPaths = 3\n)\n\nvar (\n\t_ io.Writer       = (*DefaultCtx)(nil) // Compile-time check\n\t_ context.Context = (*DefaultCtx)(nil) // Compile-time check\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\n// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx\nconst (\n\tuserContextKey contextKey = iota // __local_user_context__\n)\n\n// DefaultCtx is the default implementation of the Ctx interface\n// generation tool `go install github.com/vburenin/ifacemaker@f30b6f9bdbed4b5c4804ec9ba4a04a999525c202`\n// https://github.com/vburenin/ifacemaker/blob/f30b6f9bdbed4b5c4804ec9ba4a04a999525c202/ifacemaker.go#L14-L31\n//\n//go:generate ifacemaker --file ctx.go --file req.go --file res.go --struct DefaultCtx --iface Ctx --pkg fiber --promoted --output ctx_interface_gen.go --not-exported true --iface-comment \"Ctx represents the Context which hold the HTTP request and response.\\nIt has methods for the request query string, parameters, body, HTTP headers and so on.\"\ntype DefaultCtx struct {\n\thandlerCtx       CustomCtx            // Active custom context implementation, if any\n\tDefaultReq                            // Default request api\n\tDefaultRes                            // Default response api\n\tapp              *App                 // Reference to *App\n\troute            *Route               // Reference to *Route\n\tfasthttp         *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx\n\tbind             *Bind                // Default bind reference\n\tredirect         *Redirect            // Default redirect reference\n\tviewBindMap      Map                  // Default view map to bind template engine\n\tvalues           [maxParams]string    // Route parameter values\n\tbaseURI          string               // HTTP base uri\n\tpathOriginal     string               // Original HTTP path\n\tflashMessages    redirectionMsgs      // Flash messages\n\tpath             []byte               // HTTP path with the modifications by the configuration\n\tdetectionPath    []byte               // Route detection path\n\ttreePathHash     int                  // Hash of the path for the search in the tree\n\tindexRoute       int                  // Index of the current route\n\tindexHandler     int                  // Index of the current handler\n\tmethodInt        int                  // HTTP method INT equivalent\n\tabandoned        atomic.Bool          // If true, ctx won't be pooled until ForceRelease is called\n\tmatched          bool                 // Non use route matched\n\tskipNonUseRoutes bool                 // Skip non-use routes while iterating middleware\n}\n\n// TLSHandler hosts the callback hooks Fiber invokes while negotiating TLS\n// connections, including optional client certificate lookups.\ntype TLSHandler struct {\n\tclientHelloInfo *tls.ClientHelloInfo\n}\n\n// GetClientInfo Callback function to set ClientHelloInfo\n// Must comply with the method structure of https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/tls/common.go;l=554-563\n// Since we overlay the method of the TLS config in the listener method\nfunc (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\tt.clientHelloInfo = info\n\treturn nil, nil //nolint:nilnil // Not returning anything useful here is probably fine\n}\n\n// Views is the interface that wraps the Render function.\ntype Views interface {\n\tLoad() error\n\tRender(out io.Writer, name string, binding any, layout ...string) error\n}\n\n// App returns the *App reference to the instance of the Fiber application\nfunc (c *DefaultCtx) App() *App {\n\treturn c.app\n}\n\n// BaseURL returns (protocol + host + base path).\nfunc (c *DefaultCtx) BaseURL() string {\n\t// TODO: Could be improved: 53.8 ns/op  32 B/op  1 allocs/op\n\t// Should work like https://codeigniter.com/user_guide/helpers/url_helper.html\n\tif c.baseURI != \"\" {\n\t\treturn c.baseURI\n\t}\n\tc.baseURI = c.Scheme() + \"://\" + c.Host()\n\treturn c.baseURI\n}\n\n// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n// a cancellation signal, and other values across API boundaries.\nfunc (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx {\n\treturn c.fasthttp\n}\n\n// Context returns a context implementation that was set by\n// user earlier or returns a non-nil, empty context, if it was not set earlier.\nfunc (c *DefaultCtx) Context() context.Context {\n\tif c.fasthttp == nil {\n\t\treturn context.Background()\n\t}\n\tif ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {\n\t\treturn ctx\n\t}\n\tctx := context.Background()\n\tc.SetContext(ctx)\n\treturn ctx\n}\n\n// SetContext sets a context implementation by user.\nfunc (c *DefaultCtx) SetContext(ctx context.Context) {\n\tif c.fasthttp == nil {\n\t\treturn\n\t}\n\tc.fasthttp.SetUserValue(userContextKey, ctx)\n}\n\n// Deadline returns the time when work done on behalf of this context\n// should be canceled. Deadline returns ok==false when no deadline is\n// set. Successive calls to Deadline return the same results.\n//\n// Due to current limitations in how fasthttp works, Deadline operates as a nop.\n// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\nfunc (*DefaultCtx) Deadline() (time.Time, bool) {\n\treturn time.Time{}, false\n}\n\n// Done returns a channel that's closed when work done on behalf of this\n// context should be canceled. Done may return nil if this context can\n// never be canceled. Successive calls to Done return the same value.\n// The close of the Done channel may happen asynchronously,\n// after the cancel function returns.\n//\n// Due to current limitations in how fasthttp works, Done operates as a nop.\n// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\nfunc (*DefaultCtx) Done() <-chan struct{} {\n\treturn nil\n}\n\n// Err mirrors context.Err, returning nil until cancellation and then the terminal error value.\n//\n// Due to current limitations in how fasthttp works, Err operates as a nop.\n// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\nfunc (*DefaultCtx) Err() error {\n\treturn nil\n}\n\n// Request return the *fasthttp.Request object\n// This allows you to use all fasthttp request methods\n// https://godoc.org/github.com/valyala/fasthttp#Request\n// Returns nil if the context has been released.\nfunc (c *DefaultCtx) Request() *fasthttp.Request {\n\tif c.fasthttp == nil {\n\t\treturn nil\n\t}\n\treturn &c.fasthttp.Request\n}\n\n// Response return the *fasthttp.Response object\n// This allows you to use all fasthttp response methods\n// https://godoc.org/github.com/valyala/fasthttp#Response\n// Returns nil if the context has been released.\nfunc (c *DefaultCtx) Response() *fasthttp.Response {\n\tif c.fasthttp == nil {\n\t\treturn nil\n\t}\n\treturn &c.fasthttp.Response\n}\n\n// Get returns the HTTP request header specified by field.\n// Field names are case-insensitive\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (c *DefaultCtx) Get(key string, defaultValue ...string) string {\n\treturn c.DefaultReq.Get(key, defaultValue...)\n}\n\n// GetHeaders returns the HTTP request headers.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (c *DefaultCtx) GetHeaders() map[string][]string {\n\treturn c.DefaultReq.GetHeaders()\n}\n\n// GetReqHeaders returns the HTTP request headers.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (c *DefaultCtx) GetReqHeaders() map[string][]string {\n\treturn c.DefaultReq.GetHeaders()\n}\n\n// GetRespHeader returns the HTTP response header specified by field.\n// Field names are case-insensitive\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (c *DefaultCtx) GetRespHeader(key string, defaultValue ...string) string {\n\treturn c.DefaultRes.Get(key, defaultValue...)\n}\n\n// GetRespHeaders returns the HTTP response headers.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (c *DefaultCtx) GetRespHeaders() map[string][]string {\n\treturn c.DefaultRes.GetHeaders()\n}\n\n// ClientHelloInfo return CHI from context\nfunc (c *DefaultCtx) ClientHelloInfo() *tls.ClientHelloInfo {\n\tif c.app.tlsHandler != nil {\n\t\treturn c.app.tlsHandler.clientHelloInfo\n\t}\n\n\treturn nil\n}\n\n// Next executes the next method in the stack that matches the current route.\nfunc (c *DefaultCtx) Next() error {\n\t// Increment handler index\n\tc.indexHandler++\n\n\t// Did we execute all route handlers?\n\tif c.indexHandler < len(c.route.Handlers) {\n\t\tif c.handlerCtx != nil {\n\t\t\treturn c.route.Handlers[c.indexHandler](c.handlerCtx)\n\t\t}\n\t\treturn c.route.Handlers[c.indexHandler](c)\n\t}\n\n\tif c.handlerCtx != nil {\n\t\t_, err := c.app.nextCustom(c.handlerCtx)\n\t\treturn err\n\t}\n\t_, err := c.app.next(c)\n\treturn err\n}\n\n// RestartRouting instead of going to the next handler. This may be useful after\n// changing the request path. Note that handlers might be executed again.\nfunc (c *DefaultCtx) RestartRouting() error {\n\tc.indexRoute = -1\n\tif c.handlerCtx != nil {\n\t\t_, err := c.app.nextCustom(c.handlerCtx)\n\t\treturn err\n\t}\n\t_, err := c.app.next(c)\n\treturn err\n}\n\nfunc (c *DefaultCtx) setHandlerCtx(ctx CustomCtx) {\n\tif ctx == nil {\n\t\tc.handlerCtx = nil\n\t\treturn\n\t}\n\tif defaultCtx, ok := ctx.(*DefaultCtx); ok && defaultCtx == c {\n\t\tc.handlerCtx = nil\n\t\treturn\n\t}\n\tc.handlerCtx = ctx\n}\n\n// OriginalURL contains the original request URL.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (c *DefaultCtx) OriginalURL() string {\n\treturn c.app.toString(c.fasthttp.Request.Header.RequestURI())\n}\n\n// Path returns the path part of the request URL.\n// Optionally, you could override the path.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (c *DefaultCtx) Path(override ...string) string {\n\tif len(override) != 0 && string(c.path) != override[0] {\n\t\t// Set new path to context\n\t\tc.pathOriginal = override[0]\n\n\t\t// Set new path to request context\n\t\tc.fasthttp.Request.URI().SetPath(c.pathOriginal)\n\t\t// Prettify path\n\t\tc.configDependentPaths()\n\t}\n\treturn c.app.toString(c.path)\n}\n\n// RequestID returns the request identifier from the response header or request header.\nfunc (c *DefaultCtx) RequestID() string {\n\tif requestID := c.GetRespHeader(HeaderXRequestID); requestID != \"\" {\n\t\treturn requestID\n\t}\n\treturn c.Get(HeaderXRequestID)\n}\n\n// Req returns a convenience type whose API is limited to operations\n// on the incoming request.\nfunc (c *DefaultCtx) Req() Req {\n\treturn &c.DefaultReq\n}\n\n// Res returns a convenience type whose API is limited to operations\n// on the outgoing response.\nfunc (c *DefaultCtx) Res() Res {\n\treturn &c.DefaultRes\n}\n\n// Redirect returns the Redirect reference.\n// Use Redirect().Status() to set custom redirection status code.\n// If status is not specified, status defaults to 303 See Other.\n// You can use Redirect().To(), Redirect().Route() and Redirect().Back() for redirection.\nfunc (c *DefaultCtx) Redirect() *Redirect {\n\tif c.redirect == nil {\n\t\tc.redirect = AcquireRedirect()\n\t\tc.redirect.c = c\n\t}\n\n\treturn c.redirect\n}\n\n// ViewBind Add vars to default view var map binding to template engine.\n// Variables are read by the Render method and may be overwritten.\nfunc (c *DefaultCtx) ViewBind(vars Map) error {\n\t// init viewBindMap - lazy map\n\tif c.viewBindMap == nil {\n\t\tc.viewBindMap = make(Map, len(vars))\n\t}\n\tmaps.Copy(c.viewBindMap, vars)\n\treturn nil\n}\n\n// Route returns the matched Route struct.\nfunc (c *DefaultCtx) Route() *Route {\n\tif c.route == nil {\n\t\t// Fallback for fasthttp error handler\n\t\treturn &Route{\n\t\t\tpath:     c.pathOriginal,\n\t\t\tPath:     c.pathOriginal,\n\t\t\tMethod:   c.Method(),\n\t\t\tHandlers: make([]Handler, 0),\n\t\t\tParams:   make([]string, 0),\n\t\t}\n\t}\n\treturn c.route\n}\n\n// FullPath returns the matched route path, including any group prefixes.\nfunc (c *DefaultCtx) FullPath() string {\n\treturn c.Route().Path\n}\n\n// Matched returns true if the current request path was matched by the router.\nfunc (c *DefaultCtx) Matched() bool {\n\treturn c.getMatched()\n}\n\n// IsMiddleware returns true if the current request handler was registered as middleware.\nfunc (c *DefaultCtx) IsMiddleware() bool {\n\tif c.route == nil {\n\t\treturn false\n\t}\n\tif c.route.use {\n\t\treturn true\n\t}\n\t// For route-level middleware, there will be a next handler in the chain\n\treturn c.indexHandler+1 < len(c.route.Handlers)\n}\n\n// HasBody returns true if the request declares a body via Content-Length, Transfer-Encoding, or already buffered payload data.\nfunc (c *DefaultCtx) HasBody() bool {\n\thdr := &c.fasthttp.Request.Header\n\n\t//nolint:revive // switch is exhaustive for all ContentLength() cases\n\tswitch cl := hdr.ContentLength(); {\n\tcase cl > 0:\n\t\treturn true\n\tcase cl == -1:\n\t\t// fasthttp reports -1 for Transfer-Encoding: chunked bodies.\n\t\treturn true\n\tcase cl == 0:\n\t\tif hasTransferEncodingBody(hdr) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn len(c.fasthttp.Request.Body()) > 0\n}\n\n// OverrideParam overwrites a route parameter value by name.\n// If the parameter name does not exist in the route, this method does nothing.\nfunc (c *DefaultCtx) OverrideParam(name, value string) {\n\t// If no route is matched, there are no parameters to update\n\tif !c.Matched() {\n\t\treturn\n\t}\n\n\t// Normalize wildcard (*) and plus (+) tokens to their internal\n\t// representations (*1, +1) used by the router.\n\tif name == \"*\" || name == \"+\" {\n\t\tname += \"1\"\n\t}\n\n\tif c.app.config.CaseSensitive {\n\t\tfor i, param := range c.route.Params {\n\t\t\tif param == name {\n\t\t\t\tc.values[i] = value\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\tnameBytes := utils.UnsafeBytes(name)\n\tfor i, param := range c.route.Params {\n\t\tif utils.EqualFold(utils.UnsafeBytes(param), nameBytes) {\n\t\t\tc.values[i] = value\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc hasTransferEncodingBody(hdr *fasthttp.RequestHeader) bool {\n\tteBytes := hdr.Peek(HeaderTransferEncoding)\n\tvar te string\n\n\tif len(teBytes) > 0 {\n\t\tte = utils.UnsafeString(teBytes)\n\t} else {\n\t\tfor key, value := range hdr.All() {\n\t\t\tif !utils.EqualFold(utils.UnsafeString(key), HeaderTransferEncoding) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tte = utils.UnsafeString(value)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif te == \"\" {\n\t\treturn false\n\t}\n\n\thasEncoding := false\n\tfor raw := range strings.SplitSeq(te, \",\") {\n\t\ttoken := utils.TrimSpace(raw)\n\t\tif token == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif idx := strings.IndexByte(token, ';'); idx >= 0 {\n\t\t\ttoken = utils.TrimSpace(token[:idx])\n\t\t}\n\t\tif token == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif utils.EqualFold(token, \"identity\") {\n\t\t\tcontinue\n\t\t}\n\t\thasEncoding = true\n\t}\n\n\treturn hasEncoding\n}\n\n// IsWebSocket returns true if the request includes a WebSocket upgrade handshake.\nfunc (c *DefaultCtx) IsWebSocket() bool {\n\tconn := c.fasthttp.Request.Header.Peek(HeaderConnection)\n\tvar isUpgrade bool\n\tfor v := range strings.SplitSeq(utils.UnsafeString(conn), \",\") {\n\t\tif utils.EqualFold(utils.TrimSpace(v), \"upgrade\") {\n\t\t\tisUpgrade = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !isUpgrade {\n\t\treturn false\n\t}\n\treturn utils.EqualFold(c.fasthttp.Request.Header.Peek(HeaderUpgrade), websocketBytes)\n}\n\n// IsPreflight returns true if the request is a CORS preflight.\nfunc (c *DefaultCtx) IsPreflight() bool {\n\tif c.Method() != MethodOptions {\n\t\treturn false\n\t}\n\thdr := &c.fasthttp.Request.Header\n\tif len(hdr.Peek(HeaderAccessControlRequestMethod)) == 0 {\n\t\treturn false\n\t}\n\treturn len(hdr.Peek(HeaderOrigin)) > 0\n}\n\n// SaveFile saves any multipart file to disk.\nfunc (*DefaultCtx) SaveFile(fileheader *multipart.FileHeader, path string) error {\n\treturn fasthttp.SaveMultipartFile(fileheader, path)\n}\n\n// SaveFileToStorage saves any multipart file to an external storage system.\nfunc (c *DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error {\n\tfile, err := fileheader.Open()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open: %w\", err)\n\t}\n\tdefer file.Close() //nolint:errcheck // not needed\n\n\tmaxUploadSize := c.app.config.BodyLimit\n\tif maxUploadSize <= 0 {\n\t\tmaxUploadSize = DefaultBodyLimit\n\t}\n\n\tif fileheader.Size > 0 && fileheader.Size > int64(maxUploadSize) {\n\t\treturn fmt.Errorf(\"failed to read: %w\", fasthttp.ErrBodyTooLarge)\n\t}\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tlimitedReader := io.LimitReader(file, int64(maxUploadSize)+1)\n\tif _, err = buf.ReadFrom(limitedReader); err != nil {\n\t\treturn fmt.Errorf(\"failed to read: %w\", err)\n\t}\n\n\tif buf.Len() > maxUploadSize {\n\t\treturn fmt.Errorf(\"failed to read: %w\", fasthttp.ErrBodyTooLarge)\n\t}\n\n\tdata := append([]byte(nil), buf.Bytes()...)\n\n\tif err := storage.SetWithContext(c.Context(), path, data, 0); err != nil {\n\t\treturn fmt.Errorf(\"failed to store: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Secure returns whether a secure connection was established.\nfunc (c *DefaultCtx) Secure() bool {\n\treturn c.Protocol() == schemeHTTPS\n}\n\n// Status sets the HTTP status for the response.\n// This method is chainable.\nfunc (c *DefaultCtx) Status(status int) Ctx {\n\tc.fasthttp.Response.SetStatusCode(status)\n\treturn c\n}\n\n// String returns unique string representation of the ctx.\n//\n// The returned value may be useful for logging.\nfunc (c *DefaultCtx) String() string {\n\t// Get buffer from pool\n\tbuf := bytebufferpool.Get()\n\n\t// Start with the ID, converting it to a hex string without fmt.Sprintf\n\tbuf.WriteByte('#')\n\t// Convert ID to hexadecimal\n\tid := strconv.FormatUint(c.fasthttp.ID(), 16)\n\t// Pad with leading zeros to ensure 16 characters\n\tfor i := 0; i < (16 - len(id)); i++ {\n\t\tbuf.WriteByte('0')\n\t}\n\tbuf.WriteString(id)\n\tbuf.WriteString(\" - \")\n\n\t// Add local and remote addresses directly\n\tbuf.WriteString(c.fasthttp.LocalAddr().String())\n\tbuf.WriteString(\" <-> \")\n\tbuf.WriteString(c.fasthttp.RemoteAddr().String())\n\tbuf.WriteString(\" - \")\n\n\t// Add method and URI\n\tbuf.Write(c.fasthttp.Request.Header.Method())\n\tbuf.WriteByte(' ')\n\tbuf.Write(c.fasthttp.URI().FullURI())\n\n\t// Allocate string\n\tstr := buf.String()\n\n\t// Reset buffer\n\tbuf.Reset()\n\tbytebufferpool.Put(buf)\n\n\treturn str\n}\n\n// Value makes it possible to retrieve values (Locals) under keys scoped to the request\n// and therefore available to all following routes that match the request. If the context\n// has been released and c.fasthttp is nil (for example, after ReleaseCtx), Value returns nil.\nfunc (c *DefaultCtx) Value(key any) any {\n\tif c.fasthttp == nil {\n\t\treturn nil\n\t}\n\treturn c.fasthttp.UserValue(key)\n}\n\nvar (\n\t// xmlHTTPRequestBytes is precomputed for XHR detection\n\txmlHTTPRequestBytes = []byte(\"xmlhttprequest\")\n\t// websocketBytes is precomputed for WebSocket upgrade detection\n\twebsocketBytes = []byte(\"websocket\")\n)\n\n// XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest,\n// indicating that the request was issued by a client library (such as jQuery).\nfunc (c *DefaultCtx) XHR() bool {\n\treturn utils.EqualFold(c.fasthttp.Request.Header.Peek(HeaderXRequestedWith), xmlHTTPRequestBytes)\n}\n\n// configDependentPaths set paths for route recognition and prepared paths for the user,\n// here the features for caseSensitive, decoded paths, strict paths are evaluated\nfunc (c *DefaultCtx) configDependentPaths() {\n\tc.path = append(c.path[:0], c.pathOriginal...)\n\t// If UnescapePath enabled, we decode the path and save it for the framework user\n\tif c.app.config.UnescapePath {\n\t\tc.path = fasthttp.AppendUnquotedArg(c.path[:0], c.path)\n\t}\n\n\t// another path is specified which is for routing recognition only\n\t// use the path that was changed by the previous configuration flags\n\tc.detectionPath = append(c.detectionPath[:0], c.path...)\n\t// If CaseSensitive is disabled, we lowercase the original path\n\tif !c.app.config.CaseSensitive {\n\t\tc.detectionPath = utilsbytes.UnsafeToLower(c.detectionPath)\n\t}\n\t// If StrictRouting is disabled, we strip all trailing slashes\n\tif !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' {\n\t\tc.detectionPath = utils.TrimRight(c.detectionPath, '/')\n\t}\n\n\t// Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed,\n\t// since the first three characters area select a list of routes\n\tc.treePathHash = 0\n\tif len(c.detectionPath) >= maxDetectionPaths {\n\t\tc.treePathHash = int(c.detectionPath[0])<<16 |\n\t\t\tint(c.detectionPath[1])<<8 |\n\t\t\tint(c.detectionPath[2])\n\t}\n}\n\n// Reset is a method to reset context fields by given request when to use server handlers.\nfunc (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {\n\t// Reset route and handler index\n\tc.indexRoute = -1\n\tc.indexHandler = 0\n\t// Reset matched flag\n\tc.matched = false\n\tc.skipNonUseRoutes = false\n\t// Set paths\n\tc.pathOriginal = c.app.toString(fctx.URI().PathOriginal())\n\t// Set method\n\tc.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method()))\n\t// Attach *fasthttp.RequestCtx to ctx\n\tc.fasthttp = fctx\n\t// reset base uri\n\tc.baseURI = \"\"\n\t// Prettify path\n\tc.configDependentPaths()\n\n\tc.DefaultReq.c = c\n\tc.DefaultRes.c = c\n\tc.fasthttp.SetUserValue(userContextKey, nil)\n}\n\n// release is a method to reset context fields when to use ReleaseCtx()\nfunc (c *DefaultCtx) release() {\n\tc.route = nil\n\tc.fasthttp = nil\n\tif c.bind != nil {\n\t\tReleaseBind(c.bind)\n\t\tc.bind = nil\n\t}\n\tc.flashMessages = c.flashMessages[:0]\n\t// Clear viewBindMap by deleting all keys (reuse underlying map if possible)\n\tclear(c.viewBindMap)\n\tif c.redirect != nil {\n\t\tReleaseRedirect(c.redirect)\n\t\tc.redirect = nil\n\t}\n\tc.skipNonUseRoutes = false\n\t// performance: no need for using c.abandoned.Store(false) here, as it is always set to false when it was true in ForceRelease\n\tc.handlerCtx = nil\n\tc.DefaultReq.release()\n\tc.DefaultRes.release()\n}\n\n// Abandon marks this context as abandoned. An abandoned context will not be\n// returned to the pool when ReleaseCtx is called.\n//\n// This is used by the timeout middleware to return immediately while the\n// handler goroutine continues using the context safely.\n//\n// Only call ForceRelease after Abandon if you can guarantee no other goroutine\n// (including Fiber's requestHandler and ErrorHandler) will touch the context.\n// The timeout middleware intentionally does NOT call ForceRelease to avoid\n// races, which means timed-out requests leak their contexts until a safe\n// reclamation strategy exists.\nfunc (c *DefaultCtx) Abandon() {\n\tc.abandoned.Store(true)\n}\n\n// IsAbandoned returns true if Abandon() was called on this context.\nfunc (c *DefaultCtx) IsAbandoned() bool {\n\treturn c.abandoned.Load()\n}\n\n// ForceRelease releases an abandoned context back to the pool.\n// This MUST only be called after all goroutines (including requestHandler and\n// ErrorHandler) have completely finished using this context. Calling it while\n// any goroutine is still running causes races.\nfunc (c *DefaultCtx) ForceRelease() {\n\tc.abandoned.Store(false)\n\tc.app.ReleaseCtx(c)\n}\n\nfunc (c *DefaultCtx) renderExtensions(bind any) {\n\tif bindMap, ok := bind.(Map); ok {\n\t\t// Bind view map\n\t\tfor key, value := range c.viewBindMap {\n\t\t\tif _, ok := bindMap[key]; !ok {\n\t\t\t\tbindMap[key] = value\n\t\t\t}\n\t\t}\n\n\t\t// Check if the PassLocalsToViews option is enabled (by default it is disabled)\n\t\tif c.app.config.PassLocalsToViews {\n\t\t\t// Loop through each local and set it in the map\n\t\t\tc.fasthttp.VisitUserValues(func(key []byte, val any) {\n\t\t\t\t// check if bindMap doesn't contain the key\n\t\t\t\tif _, ok := bindMap[c.app.toString(key)]; !ok {\n\t\t\t\t\t// Set the key and value in the bindMap\n\t\t\t\t\tbindMap[c.app.toString(key)] = val\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(c.app.mountFields.appListKeys) == 0 {\n\t\tc.app.generateAppListKeys()\n\t}\n}\n\n// Bind You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method.\n// It gives custom binding support, detailed binding options and more.\n// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser\nfunc (c *DefaultCtx) Bind() *Bind {\n\tif c.bind == nil {\n\t\tc.bind = AcquireBind()\n\t}\n\tc.bind.ctx = c\n\treturn c.bind\n}\n\n// Methods to use with next stack.\nfunc (c *DefaultCtx) getMethodInt() int {\n\treturn c.methodInt\n}\n\nfunc (c *DefaultCtx) getIndexRoute() int {\n\treturn c.indexRoute\n}\n\nfunc (c *DefaultCtx) getTreePathHash() int {\n\treturn c.treePathHash\n}\n\nfunc (c *DefaultCtx) getDetectionPath() string {\n\treturn c.app.toString(c.detectionPath)\n}\n\nfunc (c *DefaultCtx) getValues() *[maxParams]string {\n\treturn &c.values\n}\n\nfunc (c *DefaultCtx) getMatched() bool {\n\treturn c.matched\n}\n\nfunc (c *DefaultCtx) getSkipNonUseRoutes() bool {\n\treturn c.skipNonUseRoutes\n}\n\nfunc (c *DefaultCtx) setIndexHandler(handler int) {\n\tc.indexHandler = handler\n}\n\nfunc (c *DefaultCtx) setIndexRoute(route int) {\n\tc.indexRoute = route\n}\n\nfunc (c *DefaultCtx) setMatched(matched bool) {\n\tc.matched = matched\n}\n\nfunc (c *DefaultCtx) setSkipNonUseRoutes(skip bool) {\n\tc.skipNonUseRoutes = skip\n}\n\nfunc (c *DefaultCtx) setRoute(route *Route) {\n\tc.route = route\n}\n\nfunc (c *DefaultCtx) getPathOriginal() string {\n\treturn c.pathOriginal\n}\n"
  },
  {
    "path": "ctx_interface.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"github.com/valyala/fasthttp\"\n)\n\n// CustomCtx extends Ctx with the additional methods required by Fiber's\n// internals and middleware helpers.\ntype CustomCtx interface {\n\tCtx\n\n\t// Reset is a method to reset context fields by given request when to use server handlers.\n\tReset(fctx *fasthttp.RequestCtx)\n\n\t// release is called before returning the context to the pool.\n\trelease()\n\n\t// Abandon marks the context as abandoned. An abandoned context will not be\n\t// returned to the pool when ReleaseCtx is called. This is used by the timeout\n\t// middleware to return immediately while the handler goroutine continues.\n\t// The cleanup goroutine must call ForceRelease when the handler finishes.\n\tAbandon()\n\n\t// IsAbandoned returns true if the context has been abandoned.\n\tIsAbandoned() bool\n\n\t// ForceRelease releases an abandoned context back to the pool.\n\t// Must only be called after the handler goroutine has completely finished.\n\tForceRelease()\n\n\t// Methods to use with next stack.\n\tgetMethodInt() int\n\tgetIndexRoute() int\n\tgetTreePathHash() int\n\tgetDetectionPath() string\n\tgetPathOriginal() string\n\tgetValues() *[maxParams]string\n\tgetMatched() bool\n\tgetSkipNonUseRoutes() bool\n\tsetIndexHandler(handler int)\n\tsetIndexRoute(route int)\n\tsetMatched(matched bool)\n\tsetSkipNonUseRoutes(skip bool)\n\tsetRoute(route *Route)\n}\n\n// NewDefaultCtx constructs the default context implementation bound to the\n// provided application.\nfunc NewDefaultCtx(app *App) *DefaultCtx {\n\t// return ctx\n\tctx := &DefaultCtx{\n\t\t// Set app reference\n\t\tapp: app,\n\t}\n\tctx.DefaultReq.c = ctx\n\tctx.DefaultRes.c = ctx\n\n\treturn ctx\n}\n\n// AcquireCtx retrieves a new Ctx from the pool.\nfunc (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) CustomCtx {\n\tctx, ok := app.pool.Get().(CustomCtx)\n\n\tif !ok {\n\t\tpanic(errCustomCtxTypeAssertion)\n\t}\n\n\tif app.hasCustomCtx {\n\t\tif setter, ok := ctx.(interface{ setHandlerCtx(CustomCtx) }); ok {\n\t\t\tsetter.setHandlerCtx(ctx)\n\t\t}\n\t}\n\n\tctx.Reset(fctx)\n\n\treturn ctx\n}\n\n// ReleaseCtx releases the ctx back into the pool.\n// If the context was abandoned (e.g., by timeout middleware), this is a no-op.\n// Call ForceRelease only when you can guarantee no goroutines (including the\n// requestHandler and ErrorHandler) still touch the context; the timeout\n// middleware intentionally leaves abandoned contexts unreleased to avoid races.\nfunc (app *App) ReleaseCtx(c CustomCtx) {\n\tif c.IsAbandoned() {\n\t\treturn\n\t}\n\tc.release()\n\tapp.pool.Put(c)\n}\n"
  },
  {
    "path": "ctx_interface_gen.go",
    "content": "// Code generated by ifacemaker; DO NOT EDIT.\n\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"time\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Ctx represents the Context which hold the HTTP request and response.\n// It has methods for the request query string, parameters, body, HTTP headers and so on.\ntype Ctx interface {\n\t// App returns the *App reference to the instance of the Fiber application\n\tApp() *App\n\t// BaseURL returns (protocol + host + base path).\n\tBaseURL() string\n\t// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n\t// a cancellation signal, and other values across API boundaries.\n\tRequestCtx() *fasthttp.RequestCtx\n\t// Context returns a context implementation that was set by\n\t// user earlier or returns a non-nil, empty context, if it was not set earlier.\n\tContext() context.Context\n\t// SetContext sets a context implementation by user.\n\tSetContext(ctx context.Context)\n\t// Deadline returns the time when work done on behalf of this context\n\t// should be canceled. Deadline returns ok==false when no deadline is\n\t// set. Successive calls to Deadline return the same results.\n\t//\n\t// Due to current limitations in how fasthttp works, Deadline operates as a nop.\n\t// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\n\tDeadline() (time.Time, bool)\n\t// Done returns a channel that's closed when work done on behalf of this\n\t// context should be canceled. Done may return nil if this context can\n\t// never be canceled. Successive calls to Done return the same value.\n\t// The close of the Done channel may happen asynchronously,\n\t// after the cancel function returns.\n\t//\n\t// Due to current limitations in how fasthttp works, Done operates as a nop.\n\t// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\n\tDone() <-chan struct{}\n\t// Err mirrors context.Err, returning nil until cancellation and then the terminal error value.\n\t//\n\t// Due to current limitations in how fasthttp works, Err operates as a nop.\n\t// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945\n\tErr() error\n\t// Request return the *fasthttp.Request object\n\t// This allows you to use all fasthttp request methods\n\t// https://godoc.org/github.com/valyala/fasthttp#Request\n\t// Returns nil if the context has been released.\n\tRequest() *fasthttp.Request\n\t// Response return the *fasthttp.Response object\n\t// This allows you to use all fasthttp response methods\n\t// https://godoc.org/github.com/valyala/fasthttp#Response\n\t// Returns nil if the context has been released.\n\tResponse() *fasthttp.Response\n\t// Get returns the HTTP request header specified by field.\n\t// Field names are case-insensitive\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGet(key string, defaultValue ...string) string\n\t// GetHeaders returns the HTTP request headers.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetHeaders() map[string][]string\n\t// GetReqHeaders returns the HTTP request headers.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetReqHeaders() map[string][]string\n\t// GetRespHeader returns the HTTP response header specified by field.\n\t// Field names are case-insensitive\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetRespHeader(key string, defaultValue ...string) string\n\t// GetRespHeaders returns the HTTP response headers.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetRespHeaders() map[string][]string\n\t// ClientHelloInfo return CHI from context\n\tClientHelloInfo() *tls.ClientHelloInfo\n\t// Next executes the next method in the stack that matches the current route.\n\tNext() error\n\t// RestartRouting instead of going to the next handler. This may be useful after\n\t// changing the request path. Note that handlers might be executed again.\n\tRestartRouting() error\n\tsetHandlerCtx(ctx CustomCtx)\n\t// OriginalURL contains the original request URL.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tOriginalURL() string\n\t// Path returns the path part of the request URL.\n\t// Optionally, you could override the path.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tPath(override ...string) string\n\t// RequestID returns the request identifier from the response header or request header.\n\tRequestID() string\n\t// Req returns a convenience type whose API is limited to operations\n\t// on the incoming request.\n\tReq() Req\n\t// Res returns a convenience type whose API is limited to operations\n\t// on the outgoing response.\n\tRes() Res\n\t// Redirect returns the Redirect reference.\n\t// Use Redirect().Status() to set custom redirection status code.\n\t// If status is not specified, status defaults to 303 See Other.\n\t// You can use Redirect().To(), Redirect().Route() and Redirect().Back() for redirection.\n\tRedirect() *Redirect\n\t// ViewBind Add vars to default view var map binding to template engine.\n\t// Variables are read by the Render method and may be overwritten.\n\tViewBind(vars Map) error\n\t// Route returns the matched Route struct.\n\tRoute() *Route\n\t// FullPath returns the matched route path, including any group prefixes.\n\tFullPath() string\n\t// Matched returns true if the current request path was matched by the router.\n\tMatched() bool\n\t// IsMiddleware returns true if the current request handler was registered as middleware.\n\tIsMiddleware() bool\n\t// HasBody returns true if the request declares a body via Content-Length, Transfer-Encoding, or already buffered payload data.\n\tHasBody() bool\n\t// OverrideParam overwrites a route parameter value by name.\n\t// If the parameter name does not exist in the route, this method does nothing.\n\tOverrideParam(name, value string)\n\t// IsWebSocket returns true if the request includes a WebSocket upgrade handshake.\n\tIsWebSocket() bool\n\t// IsPreflight returns true if the request is a CORS preflight.\n\tIsPreflight() bool\n\t// SaveFile saves any multipart file to disk.\n\tSaveFile(fileheader *multipart.FileHeader, path string) error\n\t// SaveFileToStorage saves any multipart file to an external storage system.\n\tSaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error\n\t// Secure returns whether a secure connection was established.\n\tSecure() bool\n\t// Status sets the HTTP status for the response.\n\t// This method is chainable.\n\tStatus(status int) Ctx\n\t// String returns unique string representation of the ctx.\n\t//\n\t// The returned value may be useful for logging.\n\tString() string\n\t// Value makes it possible to retrieve values (Locals) under keys scoped to the request\n\t// and therefore available to all following routes that match the request. If the context\n\t// has been released and c.fasthttp is nil (for example, after ReleaseCtx), Value returns nil.\n\tValue(key any) any\n\t// XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest,\n\t// indicating that the request was issued by a client library (such as jQuery).\n\tXHR() bool\n\t// configDependentPaths set paths for route recognition and prepared paths for the user,\n\t// here the features for caseSensitive, decoded paths, strict paths are evaluated\n\tconfigDependentPaths()\n\t// Reset is a method to reset context fields by given request when to use server handlers.\n\tReset(fctx *fasthttp.RequestCtx)\n\t// release is a method to reset context fields when to use ReleaseCtx()\n\trelease()\n\t// Abandon marks this context as abandoned. An abandoned context will not be\n\t// returned to the pool when ReleaseCtx is called.\n\t//\n\t// This is used by the timeout middleware to return immediately while the\n\t// handler goroutine continues using the context safely.\n\t//\n\t// Only call ForceRelease after Abandon if you can guarantee no other goroutine\n\t// (including Fiber's requestHandler and ErrorHandler) will touch the context.\n\t// The timeout middleware intentionally does NOT call ForceRelease to avoid\n\t// races, which means timed-out requests leak their contexts until a safe\n\t// reclamation strategy exists.\n\tAbandon()\n\t// IsAbandoned returns true if Abandon() was called on this context.\n\tIsAbandoned() bool\n\t// ForceRelease releases an abandoned context back to the pool.\n\t// This MUST only be called after all goroutines (including requestHandler and\n\t// ErrorHandler) have completely finished using this context. Calling it while\n\t// any goroutine is still running causes races.\n\tForceRelease()\n\trenderExtensions(bind any)\n\t// Bind You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method.\n\t// It gives custom binding support, detailed binding options and more.\n\t// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser\n\tBind() *Bind\n\t// Methods to use with next stack.\n\tgetMethodInt() int\n\tgetIndexRoute() int\n\tgetTreePathHash() int\n\tgetDetectionPath() string\n\tgetValues() *[maxParams]string\n\tgetMatched() bool\n\tgetSkipNonUseRoutes() bool\n\tsetIndexHandler(handler int)\n\tsetIndexRoute(route int)\n\tsetMatched(matched bool)\n\tsetSkipNonUseRoutes(skip bool)\n\tsetRoute(route *Route)\n\tgetPathOriginal() string\n\t// FullURL returns the full request URL (protocol + host + original URL).\n\tFullURL() string\n\t// UserAgent returns the User-Agent request header.\n\tUserAgent() string\n\t// Referer returns the Referer request header.\n\tReferer() string\n\t// AcceptLanguage returns the Accept-Language request header.\n\tAcceptLanguage() string\n\t// AcceptEncoding returns the Accept-Encoding request header.\n\tAcceptEncoding() string\n\t// HasHeader reports whether the request includes a header with the given key.\n\tHasHeader(key string) bool\n\t// MediaType returns the MIME type from the Content-Type header without parameters.\n\tMediaType() string\n\t// Charset returns the charset parameter from the Content-Type header.\n\tCharset() string\n\t// IsJSON reports whether the Content-Type header is JSON.\n\tIsJSON() bool\n\t// IsForm reports whether the Content-Type header is form-encoded.\n\tIsForm() bool\n\t// IsMultipart reports whether the Content-Type header is multipart form data.\n\tIsMultipart() bool\n\t// AcceptsJSON reports whether the Accept header allows JSON.\n\tAcceptsJSON() bool\n\t// AcceptsHTML reports whether the Accept header allows HTML.\n\tAcceptsHTML() bool\n\t// AcceptsXML reports whether the Accept header allows XML.\n\tAcceptsXML() bool\n\t// AcceptsEventStream reports whether the Accept header allows text/event-stream.\n\tAcceptsEventStream() bool\n\t// Accepts checks if the specified extensions or content types are acceptable.\n\tAccepts(offers ...string) string\n\t// AcceptsCharsets checks if the specified charset is acceptable.\n\tAcceptsCharsets(offers ...string) string\n\t// AcceptsEncodings checks if the specified encoding is acceptable.\n\tAcceptsEncodings(offers ...string) string\n\t// AcceptsLanguages checks if the specified language is acceptable using\n\t// RFC 4647 Basic Filtering.\n\tAcceptsLanguages(offers ...string) string\n\t// AcceptsLanguagesExtended checks if the specified language is acceptable using\n\t// RFC 4647 Extended Filtering.\n\tAcceptsLanguagesExtended(offers ...string) string\n\t// BodyRaw contains the raw body submitted in a POST request.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tBodyRaw() []byte\n\t//nolint:nonamedreturns // gocritic unnamedResult prefers naming decoded body, decode count, and error\n\ttryDecodeBodyInOrder(originalBody *[]byte, encodings []string) (body []byte, decodesRealized uint8, err error)\n\t// Body contains the raw body submitted in a POST request.\n\t// This method will decompress the body if the 'Content-Encoding' header is provided.\n\t// It returns the original (or decompressed) body data which is valid only within the handler.\n\t// Don't store direct references to the returned data.\n\t// If you need to keep the body's data later, make a copy or use the Immutable option.\n\tBody() []byte\n\t// Cookies are used for getting a cookie value by key.\n\t// Defaults to the empty string \"\" if the cookie doesn't exist.\n\t// If a default value is given, it will return that value if the cookie doesn't exist.\n\t// The returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tCookies(key string, defaultValue ...string) string\n\t// FormFile returns the first file by key from a MultipartForm.\n\tFormFile(key string) (*multipart.FileHeader, error)\n\t// FormValue returns the first value by key from a MultipartForm.\n\t// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.\n\t// Defaults to the empty string \"\" if the form value doesn't exist.\n\t// If a default value is given, it will return that value if the form value does not exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tFormValue(key string, defaultValue ...string) string\n\t// Fresh returns true when the response is still “fresh” in the client's cache,\n\t// otherwise false is returned to indicate that the client cache is now stale\n\t// and the full response should be sent.\n\t// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end\n\t// reload request, this module will return false to make handling these requests transparent.\n\t// https://github.com/jshttp/fresh/blob/master/index.js#L33\n\tFresh() bool\n\t// Host contains the host derived from the X-Forwarded-Host or Host HTTP header.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// In a network context, `Host` refers to the combination of a hostname and potentially a port number used for connecting,\n\t// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.\n\t// Example: URL: https://example.com:8080 -> Host: example.com:8080\n\t// Make copies or use the Immutable setting instead.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tHost() string\n\t// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Example: URL: https://example.com:8080 -> Hostname: example.com\n\t// Make copies or use the Immutable setting instead.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tHostname() string\n\t// Port returns the remote port of the request.\n\tPort() string\n\t// IP returns the remote IP address of the request.\n\t// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tIP() string\n\t// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear.\n\t// When IP validation is enabled, any invalid IPs will be omitted.\n\textractIPsFromHeader(header string) []string\n\t// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled.\n\t// currently, it will return the first valid IP address in header.\n\t// when IP validation is disabled, it will simply return the value of the header without any inspection.\n\t// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string.\n\textractIPFromHeader(header string) string\n\t// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header.\n\t// When IP validation is enabled, only valid IPs are returned.\n\tIPs() []string\n\t// Is returns the matching content type,\n\t// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter\n\tIs(extension string) bool\n\t// Locals makes it possible to pass any values under keys scoped to the request\n\t// and therefore available to all following routes that match the request.\n\t//\n\t// All the values are removed from ctx after returning from the top\n\t// RequestHandler. Additionally, Close method is called on each value\n\t// implementing io.Closer before removing the value from ctx.\n\tLocals(key any, value ...any) any\n\t// Method returns the HTTP request method for the context, optionally overridden by the provided argument.\n\t// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context.\n\t// Otherwise, it updates the context's method and returns the overridden method as a string.\n\tMethod(override ...string) string\n\t// MultipartForm parse form entries from binary.\n\t// This returns a map[string][]string, so given a key, the value will be a string slice.\n\tMultipartForm() (*multipart.Form, error)\n\t// Params is used to get the route parameters.\n\t// Defaults to empty string \"\" if the param doesn't exist.\n\t// If a default value is given, it will return that value if the param doesn't exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tParams(key string, defaultValue ...string) string\n\t// Scheme contains the request protocol string: http or https for TLS requests.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tScheme() string\n\t// Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2.\n\tProtocol() string\n\t// Query returns the query string parameter in the url.\n\t// Defaults to empty string \"\" if the query doesn't exist.\n\t// If a default value is given, it will return that value if the query doesn't exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tQuery(key string, defaultValue ...string) string\n\t// Queries returns a map of query parameters and their values.\n\t//\n\t// GET /?name=alex&wanna_cake=2&id=\n\t// Queries()[\"name\"] == \"alex\"\n\t// Queries()[\"wanna_cake\"] == \"2\"\n\t// Queries()[\"id\"] == \"\"\n\t//\n\t// GET /?field1=value1&field1=value2&field2=value3\n\t// Queries()[\"field1\"] == \"value2\"\n\t// Queries()[\"field2\"] == \"value3\"\n\t//\n\t// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3\n\t// Queries()[\"list_a\"] == \"3\"\n\t// Queries()[\"list_b[]\"] == \"3\"\n\t// Queries()[\"list_c\"] == \"1,2,3\"\n\t//\n\t// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending\n\t// Queries()[\"filters.author.name\"] == \"John\"\n\t// Queries()[\"filters.category.name\"] == \"Technology\"\n\t// Queries()[\"filters[customer][name]\"] == \"Alice\"\n\t// Queries()[\"filters[status]\"] == \"pending\"\n\tQueries() map[string]string\n\t// Range returns a struct containing the type and a slice of ranges.\n\tRange(size int64) (Range, error)\n\t// Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.\n\t// If the offset is negative or exceeds the number of subdomains, an empty slice is returned.\n\t// If the offset is zero every label (no trimming) is returned.\n\tSubdomains(offset ...int) []string\n\t// Stale returns the inverse of Fresh, indicating if the client's cached response is considered stale.\n\tStale() bool\n\t// IsProxyTrusted checks trustworthiness of remote ip.\n\t// If Config.TrustProxy false, it returns false.\n\t// IsProxyTrusted can check remote ip by proxy ranges and ip map.\n\tIsProxyTrusted() bool\n\t// IsFromLocal will return true if request came from local.\n\tIsFromLocal() bool\n\tgetBody() []byte\n\t// Append the specified value to the HTTP response header field.\n\t// If the header is not already set, it creates the header with the specified value.\n\tAppend(field string, values ...string)\n\t// Attachment sets the HTTP response Content-Disposition header field to attachment.\n\tAttachment(filename ...string)\n\t// ClearCookie expires a specific cookie by key on the client side.\n\t// If no key is provided it expires all cookies that came with the request.\n\tClearCookie(key ...string)\n\t// Cookie sets a cookie by passing a cookie struct.\n\tCookie(cookie *Cookie)\n\t// Download transfers the file from path as an attachment.\n\t// Typically, browsers will prompt the user for download.\n\t// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).\n\t// Override this default with the filename parameter.\n\tDownload(file string, filename ...string) error\n\t// Format performs content-negotiation on the Accept HTTP header.\n\t// It uses Accepts to select a proper format and calls the matching\n\t// user-provided handler function.\n\t// If no accepted format is found, and a format with MediaType \"default\" is given,\n\t// that default handler is called. If no format is found and no default is given,\n\t// StatusNotAcceptable is sent.\n\tFormat(handlers ...ResFmt) error\n\t// AutoFormat performs content-negotiation on the Accept HTTP header.\n\t// It uses Accepts to select a proper format.\n\t// The supported content types are text/html, text/plain, application/json, application/xml, application/vnd.msgpack, and application/cbor.\n\t// For more flexible content negotiation, use Format.\n\t// If the header is not specified or there is no proper format, text/plain is used.\n\tAutoFormat(body any) error\n\t// JSON converts any interface or string to JSON.\n\t// Array and slice values encode as JSON arrays,\n\t// except that []byte encodes as a base64-encoded string,\n\t// and a nil slice encodes as the null JSON value.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/json; charset=utf-8.\n\tJSON(data any, ctype ...string) error\n\t// MsgPack converts any interface or string to MessagePack encoded bytes.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/vnd.msgpack.\n\tMsgPack(data any, ctype ...string) error\n\t// CBOR converts any interface or string to CBOR encoded bytes.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/cbor.\n\tCBOR(data any, ctype ...string) error\n\t// JSONP sends a JSON response with JSONP support.\n\t// This method is identical to JSON, except that it opts-in to JSONP callback support.\n\t// By default, the callback name is simply callback.\n\tJSONP(data any, callback ...string) error\n\t// XML converts any interface or string to XML.\n\t// This method also sets the content header to application/xml; charset=utf-8.\n\tXML(data any) error\n\t// Links joins the links followed by the property to populate the response's Link HTTP header field.\n\tLinks(link ...string)\n\t// Location sets the response Location HTTP header to the specified path parameter.\n\tLocation(path string)\n\t// getLocationFromRoute get URL location from route using parameters\n\tgetLocationFromRoute(route *Route, params Map) (string, error)\n\t// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: \"/user/1831\"\n\tGetRouteURL(routeName string, params Map) (string, error)\n\t// Render a template with data and sends a text/html response.\n\t// We support the following engines: https://github.com/gofiber/template\n\tRender(name string, bind any, layouts ...string) error\n\t// Send sets the HTTP response body without copying it.\n\t// From this point onward the body argument must not be changed.\n\tSend(body []byte) error\n\t// SendEarlyHints allows the server to hint to the browser what resources a page would need\n\t// so the browser can preload them while waiting for the server's full response. Only Link\n\t// headers already written to the response will be transmitted as Early Hints.\n\t//\n\t// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.\n\t//\n\t// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.\n\t//\n\t// See: https://developer.chrome.com/docs/web-platform/early-hints and\n\t// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax\n\tSendEarlyHints(hints []string) error\n\t// SendFile transfers the file from the specified path.\n\t// By default, the file is not compressed. To enable compression, set SendFile.Compress to true.\n\t// The Content-Type response HTTP header field is set based on the file's extension.\n\t// If the file extension is missing or invalid, the Content-Type is detected from the file's format.\n\tSendFile(file string, config ...SendFile) error\n\t// SendStatus sets the HTTP status code and if the response body is empty,\n\t// it sets the correct status message in the body.\n\tSendStatus(status int) error\n\t// SendString sets the HTTP response body for string types.\n\t// This means no type assertion, recommended for faster performance\n\tSendString(body string) error\n\t// SendStream sets response body stream and optional body size.\n\tSendStream(stream io.Reader, size ...int) error\n\t// SendStreamWriter sets response body stream writer\n\tSendStreamWriter(streamWriter func(*bufio.Writer)) error\n\t// Set sets the response's HTTP header field to the specified key, value.\n\tSet(key, val string)\n\tsetCanonical(key, val string)\n\t// Type sets the Content-Type HTTP header to the MIME type specified by the file extension.\n\tType(extension string, charset ...string) Ctx\n\t// Vary adds the given header field to the Vary response header.\n\t// This will append the header, if not already listed; otherwise, leaves it listed in the current location.\n\tVary(fields ...string)\n\t// Write appends p into response body.\n\tWrite(p []byte) (int, error)\n\t// Writef appends f & a into response body writer.\n\tWritef(f string, a ...any) (int, error)\n\t// WriteString appends s to response body.\n\tWriteString(s string) (int, error)\n\t// Drop closes the underlying connection without sending any response headers or body.\n\t// This can be useful for silently terminating client connections, such as in DDoS mitigation\n\t// or when blocking access to sensitive endpoints.\n\tDrop() error\n\t// End immediately flushes the current response and closes the underlying connection.\n\tEnd() error\n}\n"
  },
  {
    "path": "ctx_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"compress/zlib\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"embed\"\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/shamaton/msgpack/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n)\n\nconst epsilon = 0.001\n\ntype testContextKey struct{}\n\ntype testNetAddr struct {\n\tnetwork string\n\taddress string\n}\n\nfunc (t testNetAddr) Network() string {\n\treturn t.network\n}\n\nfunc (t testNetAddr) String() string {\n\treturn t.address\n}\n\n// go test -run Test_Ctx_Accepts\nfunc Test_Ctx_Accepts(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAccept, \"text/html,application/xhtml+xml,application/xml;q=0.9\")\n\trequire.Empty(t, c.Accepts(\"\"))\n\trequire.Empty(t, c.Req().Accepts())\n\trequire.Equal(t, \".xml\", c.Accepts(\".xml\"))\n\trequire.Empty(t, c.Accepts(\".john\"))\n\trequire.Equal(t, \"application/xhtml+xml\", c.Accepts(\"application/xml\", \"application/xml+rss\", \"application/yaml\", \"application/xhtml+xml\"), \"must use client-preferred mime type\")\n\n\tc.Request().Header.Set(HeaderAccept, \"application/json, text/plain, */*;q=0\")\n\trequire.Empty(t, c.Accepts(\"html\"), \"must treat */*;q=0 as not acceptable\")\n\n\tc.Request().Header.Set(HeaderAccept, \"text/*, application/json\")\n\trequire.Equal(t, \"html\", c.Accepts(\"html\"))\n\trequire.Equal(t, \"text/html\", c.Accepts(\"text/html\"))\n\trequire.Equal(t, \"json\", c.Req().Accepts(\"json\", \"text\"))\n\trequire.Equal(t, \"application/json\", c.Accepts(\"application/json\"))\n\trequire.Empty(t, c.Accepts(\"image/png\"))\n\trequire.Empty(t, c.Accepts(\"png\"))\n\n\tc.Request().Header.Set(HeaderAccept, \"text/html, application/json\")\n\trequire.Equal(t, \"text/*\", c.Req().Accepts(\"text/*\"))\n\n\tc.Request().Header.Set(HeaderAccept, \"*/*\")\n\trequire.Equal(t, \"html\", c.Accepts(\"html\"))\n\n\tc.Request().Header.Del(HeaderAccept)\n\trequire.Equal(t, \"json\", c.Accepts(\"json\", \"html\"))\n\trequire.Equal(t, \"application/json\", c.Accepts(\"application/json\", \"text/html\"))\n}\n\n// go test -run Test_Ctx_AcceptsHelpers\nfunc Test_Ctx_AcceptsHelpers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAccept, \"text/html,application/json;q=0.9\")\n\trequire.True(t, c.AcceptsHTML())\n\trequire.True(t, c.AcceptsJSON())\n\trequire.False(t, c.AcceptsXML())\n\trequire.False(t, c.AcceptsEventStream())\n\n\tc.Request().Header.Set(HeaderAccept, \"application/xml\")\n\trequire.True(t, c.AcceptsXML())\n\trequire.False(t, c.AcceptsJSON())\n\n\tc.Request().Header.Set(HeaderAccept, \"text/event-stream\")\n\trequire.True(t, c.AcceptsEventStream())\n}\n\n// go test -run Test_Ctx_ContentTypeHelpers\nfunc Test_Ctx_ContentTypeHelpers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Empty(t, c.MediaType())\n\trequire.Empty(t, c.Charset())\n\trequire.False(t, c.IsJSON())\n\n\tc.Request().Header.Set(HeaderContentType, \"application/json; charset=utf-8\")\n\t//nolint:testifylint // This is a MIME type string, not JSON payload.\n\trequire.Equal(t, MIMEApplicationJSON, c.MediaType())\n\trequire.Equal(t, \"utf-8\", c.Charset())\n\trequire.True(t, c.IsJSON())\n\trequire.False(t, c.IsForm())\n\trequire.False(t, c.IsMultipart())\n\n\tc.Request().Header.Set(HeaderContentType, \"text/html ; charset=\\\"UTF-8\\\"\")\n\trequire.Equal(t, MIMETextHTML, c.MediaType())\n\trequire.Equal(t, \"UTF-8\", c.Charset())\n\n\tc.Request().Header.Set(HeaderContentType, MIMEApplicationForm)\n\trequire.Equal(t, MIMEApplicationForm, c.MediaType())\n\trequire.Empty(t, c.Charset())\n\trequire.True(t, c.IsForm())\n\trequire.False(t, c.IsMultipart())\n\n\tc.Request().Header.Set(HeaderContentType, MIMEMultipartForm+\"; boundary=abc123\")\n\trequire.Equal(t, MIMEMultipartForm, c.MediaType())\n\trequire.Empty(t, c.Charset())\n\trequire.True(t, c.IsMultipart())\n\trequire.False(t, c.IsForm())\n}\n\n// go test -run Test_Ctx_Charset\nfunc Test_Ctx_Charset(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tcontentType string\n\t\texpected    string\n\t}{\n\t\t{\n\t\t\tname:        \"no_parameters\",\n\t\t\tcontentType: \"text/plain\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"trailing_semicolon\",\n\t\t\tcontentType: \"text/plain;\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty_param_before_charset\",\n\t\t\tcontentType: \"text/plain; ; charset=utf-8\",\n\t\t\texpected:    \"utf-8\",\n\t\t},\n\t\t{\n\t\t\tname:        \"charset_with_spaces\",\n\t\t\tcontentType: \"text/plain; charset = utf-8\",\n\t\t\texpected:    \"utf-8\",\n\t\t},\n\t\t{\n\t\t\tname:        \"charset_in_middle\",\n\t\t\tcontentType: \"text/plain; foo=bar; charset=iso-8859-1; baz=qux\",\n\t\t\texpected:    \"iso-8859-1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"charset_quoted\",\n\t\t\tcontentType: \"text/plain; charset=\\\"utf-8\\\"; foo=bar\",\n\t\t\texpected:    \"utf-8\",\n\t\t},\n\t\t{\n\t\t\tname:        \"non_charset_only\",\n\t\t\tcontentType: \"text/plain; foo=bar\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"missing_equals\",\n\t\t\tcontentType: \"text/plain; charset\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty_charset_value\",\n\t\t\tcontentType: \"text/plain; charset=\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"case_insensitive_charset\",\n\t\t\tcontentType: \"text/plain; chArSet=Shift_JIS\",\n\t\t\texpected:    \"Shift_JIS\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := New()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().Header.Set(HeaderContentType, testCase.contentType)\n\t\t\trequire.Equal(t, testCase.expected, c.Charset())\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_HeaderHelpers\nfunc Test_Ctx_HeaderHelpers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.SetHost(\"example.com\")\n\tc.Request().SetRequestURI(\"/search?q=fiber\")\n\trequire.Equal(t, \"http://example.com/search?q=fiber\", c.FullURL())\n\n\tc.Request().Header.Set(HeaderUserAgent, \"fiber-agent\")\n\tc.Request().Header.Set(HeaderReferer, \"https://example.com\")\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-US,en;q=0.9\")\n\tc.Request().Header.Set(HeaderAcceptEncoding, \"gzip, br\")\n\tc.Request().Header.Set(\"X-Trace-Id\", \"trace\")\n\trequire.True(t, c.HasHeader(\"X-Trace-Id\"))\n\trequire.True(t, c.HasHeader(\"x-trace-id\"))\n\trequire.Equal(t, \"fiber-agent\", c.UserAgent())\n\trequire.Equal(t, \"https://example.com\", c.Referer())\n\trequire.Equal(t, \"en-US,en;q=0.9\", c.AcceptLanguage())\n\trequire.Equal(t, \"gzip, br\", c.AcceptEncoding())\n\n\tc.Request().Header.Set(HeaderXRequestID, \"request-id\")\n\tc.Response().Header.Set(HeaderXRequestID, \"response-id\")\n\trequire.Equal(t, \"response-id\", c.RequestID())\n\tc.Response().Header.Del(HeaderXRequestID)\n\trequire.Equal(t, \"request-id\", c.RequestID())\n\n\tc.Request().Header.Del(\"X-Trace-Id\")\n\trequire.False(t, c.HasHeader(\"X-Trace-Id\"))\n}\n\n// go test -run Test_Ctx_TypedParsingDefaults\nfunc Test_Ctx_TypedParsingDefaults(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/:id\", func(c Ctx) error {\n\t\trequire.Equal(t, 5, Query[int](c, \"count\", 1))\n\t\trequire.Equal(t, 9, Query[int](c, \"missing\", 9))\n\t\trequire.Equal(t, 3, Query[int](c, \"bad\", 3))\n\n\t\trequire.Equal(t, 42, Params[int](c, \"id\", 7))\n\t\trequire.Equal(t, 7, Params[int](c, \"missing\", 7))\n\n\t\trequire.Equal(t, 11, GetReqHeader[int](c, \"X-Limit\", 4))\n\t\trequire.Equal(t, 4, GetReqHeader[int](c, \"X-Bad\", 4))\n\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/42?count=5&bad=oops\", http.NoBody)\n\treq.Header.Set(\"X-Limit\", \"11\")\n\treq.Header.Set(\"X-Bad\", \"oops\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Accepts -benchmem -count=4\nfunc Benchmark_Ctx_Accepts(b *testing.B) {\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tacceptHeader := \"text/html,application/xhtml+xml,application/xml;q=0.9\"\n\tc.Request().Header.Set(\"Accept\", acceptHeader)\n\tacceptValues := [][]string{\n\t\t{\".xml\"},\n\t\t{\"json\", \"xml\"},\n\t\t{\"application/json\", \"application/xml\"},\n\t}\n\texpectedResults := []string{\".xml\", \"xml\", \"application/xml\"}\n\n\tfor i := range acceptValues {\n\t\tb.Run(fmt.Sprintf(\"run-%#v\", acceptValues[i]), func(bb *testing.B) {\n\t\t\tvar res string\n\t\t\tbb.ReportAllocs()\n\n\t\t\tfor bb.Loop() {\n\t\t\t\tres = c.Accepts(acceptValues[i]...)\n\t\t\t}\n\t\t\trequire.Equal(bb, expectedResults[i], res)\n\t\t})\n\t}\n}\n\ntype customCtx struct {\n\tDefaultCtx\n}\n\nfunc (c *customCtx) Params(key string, defaultValue ...string) string { //revive:disable-line:unused-parameter // We need defaultValue for some cases\n\treturn \"prefix_\" + c.DefaultCtx.Params(key)\n}\n\n// go test -run Test_Ctx_CustomCtx\nfunc Test_Ctx_CustomCtx(t *testing.T) {\n\tt.Parallel()\n\n\tapp := NewWithCustomCtx(func(app *App) CustomCtx {\n\t\treturn &customCtx{\n\t\t\tDefaultCtx: *NewDefaultCtx(app),\n\t\t}\n\t})\n\n\tapp.Get(\"/:id\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"id\"))\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v3\", &bytes.Buffer{}))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Len(t, body, len(\"prefix_v3\"))\n\trequire.Equal(t, \"prefix_v3\", string(body))\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType))\n\trequire.Equal(t, int64(len(body)), resp.ContentLength)\n}\n\nfunc Test_Ctx_CustomCtx_WithMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\tapp := NewWithCustomCtx(func(app *App) CustomCtx {\n\t\treturn &customCtx{\n\t\t\tDefaultCtx: *NewDefaultCtx(app),\n\t\t}\n\t})\n\n\tapp.Use(func(c Ctx) error {\n\t\t_, ok := c.(*customCtx)\n\t\trequire.True(t, ok)\n\t\treturn c.Next()\n\t})\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tcustom, ok := c.(*customCtx)\n\t\trequire.True(t, ok)\n\t\treturn c.SendString(custom.Params(\"\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Equal(t, \"prefix_\", string(body))\n}\n\n// go test -run Test_Ctx_CustomCtx\nfunc Test_Ctx_CustomCtx_and_Method(t *testing.T) {\n\tt.Parallel()\n\n\t// Create app with custom request methods\n\tmethods := append(DefaultMethods, \"JOHN\") //nolint:gocritic // We want a new slice here\n\tapp := NewWithCustomCtx(func(app *App) CustomCtx {\n\t\treturn &customCtx{\n\t\t\tDefaultCtx: *NewDefaultCtx(app),\n\t\t}\n\t}, Config{\n\t\tRequestMethods: methods,\n\t})\n\n\t// Add route with custom method\n\tapp.Add([]string{\"JOHN\"}, \"/doe\", testEmptyHandler)\n\tresp, err := app.Test(httptest.NewRequest(\"JOHN\", \"/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Empty(t, body)\n\trequire.Empty(t, resp.Header.Get(HeaderContentType))\n\trequire.Equal(t, int64(0), resp.ContentLength)\n\n\t// Add a new method\n\trequire.Panics(t, func() {\n\t\tapp.Add([]string{\"JANE\"}, \"/jane\", testEmptyHandler)\n\t})\n}\n\n// go test -run Test_Ctx_Accepts_EmptyAccept\nfunc Test_Ctx_Accepts_EmptyAccept(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \".forwarded\", c.Accepts(\".forwarded\"))\n}\n\n// go test -run Test_Ctx_Accepts_Wildcard\nfunc Test_Ctx_Accepts_Wildcard(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAccept, \"*/*;q=0.9\")\n\trequire.Equal(t, \"html\", c.Accepts(\"html\"))\n\trequire.Equal(t, \"foo\", c.Accepts(\"foo\"))\n\trequire.Equal(t, \".bar\", c.Accepts(\".bar\"))\n\tc.Request().Header.Set(HeaderAccept, \"text/html,application/*;q=0.9\")\n\trequire.Equal(t, \"xml\", c.Accepts(\"xml\"))\n}\n\n// go test -run Test_Ctx_Accepts_MultiHeader\nfunc Test_Ctx_Accepts_MultiHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Add(HeaderAccept, \"text/plain;q=0.5\")\n\tc.Request().Header.Add(HeaderAccept, \"application/json\")\n\trequire.Equal(t, \"application/json\", c.Accepts(\"text/plain\", \"application/json\"))\n}\n\n// go test -run Test_Ctx_AcceptsCharsets\nfunc Test_Ctx_AcceptsCharsets(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptCharset, \"utf-8, iso-8859-1;q=0.5\")\n\trequire.Equal(t, \"utf-8\", c.AcceptsCharsets(\"utf-8\"))\n}\n\n// go test -run Test_Ctx_AcceptsCharsets_MultiHeader\nfunc Test_Ctx_AcceptsCharsets_MultiHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Add(HeaderAcceptCharset, \"utf-8;q=0.1\")\n\tc.Request().Header.Add(HeaderAcceptCharset, \"iso-8859-1\")\n\trequire.Equal(t, \"iso-8859-1\", c.AcceptsCharsets(\"utf-8\", \"iso-8859-1\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4\nfunc Benchmark_Ctx_AcceptsCharsets(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().Header.Set(\"Accept-Charset\", \"utf-8, iso-8859-1;q=0.5\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.AcceptsCharsets(\"utf-8\")\n\t}\n\trequire.Equal(b, \"utf-8\", res)\n}\n\n// go test -run Test_Ctx_AcceptsEncodings\nfunc Test_Ctx_AcceptsEncodings(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptEncoding, \"deflate, gzip;q=1.0, *;q=0.5\")\n\trequire.Equal(t, \"gzip\", c.AcceptsEncodings(\"gzip\"))\n\trequire.Equal(t, \"abc\", c.AcceptsEncodings(\"abc\"))\n}\n\n// go test -run Test_Ctx_AcceptsEncodings_MultiHeader\nfunc Test_Ctx_AcceptsEncodings_MultiHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Add(HeaderAcceptEncoding, \"deflate;q=0.3\")\n\tc.Request().Header.Add(HeaderAcceptEncoding, \"gzip\")\n\trequire.Equal(t, \"gzip\", c.AcceptsEncodings(\"deflate\", \"gzip\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4\nfunc Benchmark_Ctx_AcceptsEncodings(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().Header.Set(HeaderAcceptEncoding, \"deflate, gzip;q=1.0, *;q=0.5\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.AcceptsEncodings(\"gzip\")\n\t}\n\trequire.Equal(b, \"gzip\", res)\n}\n\n// go test -run Test_Ctx_AcceptsLanguages\nfunc Test_Ctx_AcceptsLanguages(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5\")\n\trequire.Equal(t, \"fr\", c.AcceptsLanguages(\"fr\"))\n}\n\n// go test -run Test_Ctx_AcceptsLanguages_MultiHeader\nfunc Test_Ctx_AcceptsLanguages_MultiHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Add(HeaderAcceptLanguage, \"de;q=0.4\")\n\tc.Request().Header.Add(HeaderAcceptLanguage, \"en\")\n\trequire.Equal(t, \"en\", c.AcceptsLanguages(\"de\", \"en\"))\n}\n\n// go test -run Test_Ctx_AcceptsLanguages_BasicFiltering\nfunc Test_Ctx_AcceptsLanguages_BasicFiltering(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-US\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguages(\"en\", \"en-US\"))\n\trequire.Empty(t, c.AcceptsLanguages(\"en\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-US, fr\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguages(\"de\", \"en-US\", \"fr\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguages(\"en-US\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"*\")\n\trequire.Equal(t, \"en\", c.AcceptsLanguages(\"en\", \"fr\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en_US\")\n\trequire.Empty(t, c.AcceptsLanguages(\"en-US\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-*\")\n\trequire.Empty(t, c.AcceptsLanguages(\"en-US\"))\n}\n\n// go test -run Test_Ctx_AcceptsLanguages_CaseInsensitive\nfunc Test_Ctx_AcceptsLanguages_CaseInsensitive(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"EN-us\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguages(\"en-US\"))\n}\n\n// go test -run Test_Ctx_AcceptsLanguagesExtended\nfunc Test_Ctx_AcceptsLanguagesExtended(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-*\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguagesExtended(\"en-US\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"*-US\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguagesExtended(\"en-US\", \"fr-CA\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-US-*\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguagesExtended(\"en-US\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguagesExtended(\"en-US\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"*\")\n\trequire.Equal(t, \"en-US\", c.AcceptsLanguagesExtended(\"en-US\", \"fr-CA\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-US\")\n\trequire.Equal(t, \"en-US-CA\", c.AcceptsLanguagesExtended(\"en-US-CA\"))\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"en-*\")\n\trequire.Equal(t, \"en-US-CA\", c.AcceptsLanguagesExtended(\"en-US-CA\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4\nfunc Benchmark_Ctx_AcceptsLanguages(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().Header.Set(HeaderAcceptLanguage, \"fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.AcceptsLanguages(\"fr\")\n\t}\n\trequire.Equal(b, \"fr\", res)\n}\n\n// go test -run Test_Ctx_App\nfunc Test_Ctx_App(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.config.BodyLimit = 1000\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, 1000, c.App().config.BodyLimit)\n}\n\n// go test -run Test_Ctx_Append\nfunc Test_Ctx_Append(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Append(\"X-Test\", \"Hello\")\n\tc.Append(\"X-Test\", \"World\")\n\tc.Append(\"X-Test\", \"Hello\", \"World\")\n\t// similar value in the middle\n\tc.Append(\"X2-Test\", \"World\")\n\tc.Append(\"X2-Test\", \"XHello\")\n\tc.Append(\"X2-Test\", \"Hello\", \"World\")\n\t// similar value at the start\n\tc.Append(\"X3-Test\", \"XHello\")\n\tc.Append(\"X3-Test\", \"World\")\n\tc.Append(\"X3-Test\", \"Hello\", \"World\")\n\t// try it with multiple similar values\n\tc.Append(\"X4-Test\", \"XHello\")\n\tc.Append(\"X4-Test\", \"Hello\")\n\tc.Append(\"X4-Test\", \"HelloZ\")\n\tc.Append(\"X4-Test\", \"YHello\")\n\tc.Append(\"X4-Test\", \"Hello\")\n\tc.Append(\"X4-Test\", \"YHello\")\n\tc.Append(\"X4-Test\", \"HelloZ\")\n\tc.Append(\"X4-Test\", \"XHello\")\n\t// without append value\n\tc.Append(\"X-Custom-Header\")\n\n\trequire.Equal(t, \"Hello, World\", string(c.Response().Header.Peek(\"X-Test\")))\n\trequire.Equal(t, \"World, XHello, Hello\", string(c.Response().Header.Peek(\"X2-Test\")))\n\trequire.Equal(t, \"XHello, World, Hello\", string(c.Response().Header.Peek(\"X3-Test\")))\n\trequire.Equal(t, \"XHello, Hello, HelloZ, YHello\", string(c.Response().Header.Peek(\"X4-Test\")))\n\trequire.Empty(t, string(c.Response().Header.Peek(\"x-custom-header\")))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4\nfunc Benchmark_Ctx_Append(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Append(\"X-Custom-Header\", \"Hello\")\n\t\tc.Append(\"X-Custom-Header\", \"World\")\n\t\tc.Append(\"X-Custom-Header\", \"Hello\")\n\t}\n\trequire.Equal(b, \"Hello, World\", app.toString(c.Response().Header.Peek(\"X-Custom-Header\")))\n}\n\n// go test -run Test_Ctx_Attachment\nfunc Test_Ctx_Attachment(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// empty\n\tc.Attachment()\n\trequire.Equal(t, `attachment`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\t// real filename\n\tc.Attachment(\"./static/img/logo.png\")\n\trequire.Equal(t, `attachment; filename=\"logo.png\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\trequire.Equal(t, \"image/png\", string(c.Response().Header.Peek(HeaderContentType)))\n\t// filename with spaces\n\tc.Attachment(\"report 2024.txt\")\n\trequire.Equal(t, `attachment; filename=\"report+2024.txt\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\t// filename with nested path\n\tc.Attachment(\"../docs/archive.tar.gz\")\n\trequire.Equal(t, `attachment; filename=\"archive.tar.gz\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\t// check quoting\n\tc.Attachment(\"another document.pdf\\\"\\r\\nBla: \\\"fasel\")\n\trequire.Equal(t, `attachment; filename=\"another+document.pdf%22Bla%3A+%22fasel\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\n\tc.Attachment(\"файл.txt\")\n\theader := string(c.Response().Header.Peek(HeaderContentDisposition))\n\trequire.Contains(t, header, `filename=\"файл.txt\"`)\n\trequire.Contains(t, header, `filename*=UTF-8''%D1%84%D0%B0%D0%B9%D0%BB.txt`)\n}\n\n// go test -run Test_Ctx_Attachment_SanitizesFilenameControls\nfunc Test_Ctx_Attachment_SanitizesFilenameControls(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\ttestCases := []struct {\n\t\tname     string\n\t\tfilename string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"base name only\",\n\t\t\tfilename: \"../docs/archive.tar.gz\",\n\t\t\texpected: `attachment; filename=\"archive.tar.gz\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"controls stripped\",\n\t\t\tfilename: \"down\\r\\nload\\t\\x00.txt\",\n\t\t\texpected: `attachment; filename=\"download.txt\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"controls stripped without extension\",\n\t\t\tfilename: \"report\\r\\n\\t\\x00\",\n\t\t\texpected: `attachment; filename=\"report\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty after sanitize\",\n\t\t\tfilename: \"\\r\\n\\t\\x00\",\n\t\t\texpected: `attachment; filename=\"download\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"controls stripped in middle\",\n\t\t\tfilename: \"file\\rname\\n\\t\\x00.bin\",\n\t\t\texpected: `attachment; filename=\"filename.bin\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"dot fallback\",\n\t\t\tfilename: \".\",\n\t\t\texpected: `attachment; filename=\"download\"`,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Attachment(tc.filename)\n\t\t\theader := string(c.Response().Header.Peek(HeaderContentDisposition))\n\t\t\trequire.Equal(t, tc.expected, header)\n\t\t\trequire.NotContains(t, header, \"\\r\")\n\t\t\trequire.NotContains(t, header, \"\\n\")\n\t\t\trequire.NotContains(t, header, \"\\t\")\n\t\t\trequire.NotContains(t, header, \"\\x00\")\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4\nfunc Benchmark_Ctx_Attachment(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t// example with quote params\n\t\tc.Attachment(\"another document.pdf\\\"\\r\\nBla: \\\"fasel\")\n\t}\n\trequire.Equal(b, `attachment; filename=\"another+document.pdf%22Bla%3A+%22fasel\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n}\n\n// go test -run Test_Ctx_BaseURL\nfunc Test_Ctx_BaseURL(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\trequire.Equal(t, \"http://google.com\", c.BaseURL())\n\t// Check cache\n\trequire.Equal(t, \"http://google.com\", c.BaseURL())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem\nfunc Benchmark_Ctx_BaseURL(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetHost(\"google.com:1337\")\n\tc.Request().URI().SetPath(\"/haha/oke/lol\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.BaseURL()\n\t}\n\trequire.Equal(b, \"http://google.com:1337\", res)\n}\n\n// go test -run Test_Ctx_Body\nfunc Test_Ctx_Body(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBody([]byte(\"john=doe\"))\n\trequire.Equal(t, []byte(\"john=doe\"), c.Body())\n}\n\n// go test -run Test_Ctx_BodyRaw\nfunc Test_Ctx_BodyRaw(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBodyRaw([]byte(\"john=doe\"))\n\trequire.Equal(t, []byte(\"john=doe\"), c.BodyRaw())\n}\n\n// go test -run Test_Ctx_BodyRaw_Immutable\nfunc Test_Ctx_BodyRaw_Immutable(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{Immutable: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBodyRaw([]byte(\"john=doe\"))\n\trequire.Equal(t, []byte(\"john=doe\"), c.BodyRaw())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Body -benchmem -count=4\nfunc Benchmark_Ctx_Body(b *testing.B) {\n\tconst input = \"john=doe\"\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBody([]byte(input))\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = c.Body()\n\t}\n\n\trequire.Equal(b, []byte(input), c.Body())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_BodyRaw -benchmem -count=4\nfunc Benchmark_Ctx_BodyRaw(b *testing.B) {\n\tconst input = \"john=doe\"\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBodyRaw([]byte(input))\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = c.BodyRaw()\n\t}\n\n\trequire.Equal(b, []byte(input), c.BodyRaw())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_BodyRaw_Immutable -benchmem -count=4\nfunc Benchmark_Ctx_BodyRaw_Immutable(b *testing.B) {\n\tconst input = \"john=doe\"\n\n\tapp := New(Config{Immutable: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBodyRaw([]byte(input))\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = c.BodyRaw()\n\t}\n\n\trequire.Equal(b, []byte(input), c.BodyRaw())\n}\n\n// go test -run Test_Ctx_Body_Immutable\nfunc Test_Ctx_Body_Immutable(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.config.Immutable = true\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBody([]byte(\"john=doe\"))\n\trequire.Equal(t, []byte(\"john=doe\"), c.Body())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Body_Immutable -benchmem -count=4\nfunc Benchmark_Ctx_Body_Immutable(b *testing.B) {\n\tconst input = \"john=doe\"\n\n\tapp := New()\n\tapp.config.Immutable = true\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Request().SetBody([]byte(input))\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\t_ = c.Body()\n\t}\n\n\trequire.Equal(b, []byte(input), c.Body())\n}\n\n// go test -run Test_Ctx_Body_With_Compression\nfunc Test_Ctx_Body_With_Compression(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname            string\n\t\tcontentEncoding string\n\t\tbody            []byte\n\t\texpectedBody    []byte\n\t}{\n\t\t{\n\t\t\tname:            \"gzip\",\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\tbody:            []byte(\"john=doe\"),\n\t\t\texpectedBody:    []byte(\"john=doe\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"gzip twice\",\n\t\t\tcontentEncoding: \"gzip, gzip\",\n\t\t\tbody:            []byte(\"double\"),\n\t\t\texpectedBody:    []byte(\"double\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"unsupported_encoding\",\n\t\t\tcontentEncoding: \"undefined\",\n\t\t\tbody:            []byte(\"keeps_ORIGINAL\"),\n\t\t\texpectedBody:    []byte(\"Unsupported Media Type\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"compress_not_implemented\",\n\t\t\tcontentEncoding: \"compress\",\n\t\t\tbody:            []byte(\"foo\"),\n\t\t\texpectedBody:    []byte(\"Not Implemented\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"gzip then unsupported\",\n\t\t\tcontentEncoding: \"gzip, undefined\",\n\t\t\tbody:            []byte(\"Go, be gzipped\"),\n\t\t\texpectedBody:    []byte(\"Unsupported Media Type\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"invalid_deflate\",\n\t\t\tcontentEncoding: \"gzip,deflate\",\n\t\t\tbody:            []byte(\"I'm not correctly compressed\"),\n\t\t\texpectedBody:    []byte(zlib.ErrHeader.Error()),\n\t\t},\n\t\t{\n\t\t\tname:            \"identity\",\n\t\t\tcontentEncoding: \"identity\",\n\t\t\tbody:            []byte(\"bar\"),\n\t\t\texpectedBody:    []byte(\"bar\"),\n\t\t},\n\t}\n\n\tfor _, testObject := range tests {\n\t\ttCase := testObject // Duplicate object to ensure it will be unique across all runs\n\t\tt.Run(tCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\t\t\tc.Request().Header.Set(\"Content-Encoding\", tCase.contentEncoding)\n\n\t\t\tencs := strings.SplitSeq(tCase.contentEncoding, \",\")\n\t\t\tfor enc := range encs {\n\t\t\t\tenc = utils.TrimSpace(enc)\n\t\t\t\tif strings.Contains(tCase.name, \"invalid_deflate\") && enc == StrDeflate {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tswitch enc {\n\t\t\t\tcase \"gzip\":\n\t\t\t\t\tvar b bytes.Buffer\n\t\t\t\t\tgz := gzip.NewWriter(&b)\n\t\t\t\t\t_, err := gz.Write(tCase.body)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.NoError(t, gz.Flush())\n\t\t\t\t\trequire.NoError(t, gz.Close())\n\t\t\t\t\ttCase.body = b.Bytes()\n\t\t\t\tcase StrDeflate:\n\t\t\t\t\tvar b bytes.Buffer\n\t\t\t\t\tfl := zlib.NewWriter(&b)\n\t\t\t\t\t_, err := fl.Write(tCase.body)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.NoError(t, fl.Flush())\n\t\t\t\t\trequire.NoError(t, fl.Close())\n\t\t\t\t\ttCase.body = b.Bytes()\n\t\t\t\tdefault:\n\t\t\t\t\t// we do nothing and expect the original body to be returned\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.Request().SetBody(tCase.body)\n\t\t\tbody := c.Body()\n\t\t\trequire.Equal(t, tCase.expectedBody, body)\n\n\t\t\tswitch {\n\t\t\tcase strings.Contains(tCase.name, \"unsupported\"):\n\t\t\t\trequire.Equal(t, StatusUnsupportedMediaType, c.Response().StatusCode())\n\t\t\tcase strings.Contains(tCase.name, \"compress_not_implemented\"):\n\t\t\t\trequire.Equal(t, StatusNotImplemented, c.Response().StatusCode())\n\t\t\tdefault:\n\t\t\t\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\t\t\t}\n\n\t\t\t// Check if body raw is the same as previous before decompression\n\t\t\trequire.Equal(\n\t\t\t\tt, tCase.body, c.Request().Body(),\n\t\t\t\t\"Body raw must be the same as set before\",\n\t\t\t)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression -benchmem -count=4\nfunc Benchmark_Ctx_Body_With_Compression(b *testing.B) {\n\tencodingErr := errors.New(\"failed to encoding data\")\n\n\tvar (\n\t\tcompressGzip = func(data []byte) ([]byte, error) {\n\t\t\tvar buf bytes.Buffer\n\t\t\twriter := gzip.NewWriter(&buf)\n\t\t\tif _, err := writer.Write(data); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Flush(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Close(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\treturn buf.Bytes(), nil\n\t\t}\n\t\tcompressDeflate = func(data []byte) ([]byte, error) {\n\t\t\tvar buf bytes.Buffer\n\t\t\twriter := zlib.NewWriter(&buf)\n\t\t\tif _, err := writer.Write(data); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Flush(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Close(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\treturn buf.Bytes(), nil\n\t\t}\n\t)\n\tconst input = \"john=doe\"\n\tcompressionTests := []struct {\n\t\tcompressWriter  func([]byte) ([]byte, error)\n\t\tcontentEncoding string\n\t\texpectedBody    []byte\n\t}{\n\t\t{\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\tcompressWriter:  compressGzip,\n\t\t\texpectedBody:    []byte(input),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: \"gzip,invalid\",\n\t\t\tcompressWriter:  compressGzip,\n\t\t\texpectedBody:    []byte(ErrUnsupportedMediaType.Error()),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: StrDeflate,\n\t\t\tcompressWriter:  compressDeflate,\n\t\t\texpectedBody:    []byte(input),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: \"gzip,deflate\",\n\t\t\tcompressWriter: func(data []byte) ([]byte, error) {\n\t\t\t\tvar (\n\t\t\t\t\tbuf    bytes.Buffer\n\t\t\t\t\twriter interface {\n\t\t\t\t\t\tio.WriteCloser\n\t\t\t\t\t\tFlush() error\n\t\t\t\t\t}\n\t\t\t\t\terr error\n\t\t\t\t)\n\n\t\t\t\t// deflate\n\t\t\t\twriter = zlib.NewWriter(&buf)\n\t\t\t\tif _, err = writer.Write(data); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Flush(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Close(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\n\t\t\t\tdata = make([]byte, buf.Len())\n\t\t\t\tcopy(data, buf.Bytes())\n\t\t\t\tbuf.Reset()\n\n\t\t\t\t// gzip\n\t\t\t\twriter = gzip.NewWriter(&buf)\n\t\t\t\tif _, err = writer.Write(data); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Flush(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Close(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\n\t\t\t\treturn buf.Bytes(), nil\n\t\t\t},\n\t\t\texpectedBody: []byte(zlib.ErrHeader.Error()),\n\t\t},\n\t}\n\n\tb.ReportAllocs()\n\tfor _, ct := range compressionTests {\n\t\tb.Run(ct.contentEncoding, func(b *testing.B) {\n\t\t\tapp := New()\n\t\t\tconst input = \"john=doe\"\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t\tc.Request().Header.Set(\"Content-Encoding\", ct.contentEncoding)\n\t\t\tcompressedBody, err := ct.compressWriter([]byte(input))\n\t\t\trequire.NoError(b, err)\n\n\t\t\tc.Request().SetBody(compressedBody)\n\t\t\tfor b.Loop() {\n\t\t\t\t_ = c.Body()\n\t\t\t}\n\n\t\t\trequire.Equal(b, ct.expectedBody, c.Body())\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_Body_With_Compression_Immutable\nfunc Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname            string\n\t\tcontentEncoding string\n\t\tbody            []byte\n\t\texpectedBody    []byte\n\t}{\n\t\t{\n\t\t\tname:            \"gzip\",\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\tbody:            []byte(\"john=doe\"),\n\t\t\texpectedBody:    []byte(\"john=doe\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"gzip twice\",\n\t\t\tcontentEncoding: \"gzip, gzip\",\n\t\t\tbody:            []byte(\"double\"),\n\t\t\texpectedBody:    []byte(\"double\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"unsupported_encoding\",\n\t\t\tcontentEncoding: \"undefined\",\n\t\t\tbody:            []byte(\"keeps_ORIGINAL\"),\n\t\t\texpectedBody:    []byte(\"Unsupported Media Type\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"compress_not_implemented\",\n\t\t\tcontentEncoding: \"compress\",\n\t\t\tbody:            []byte(\"foo\"),\n\t\t\texpectedBody:    []byte(\"Not Implemented\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"gzip then unsupported\",\n\t\t\tcontentEncoding: \"gzip, undefined\",\n\t\t\tbody:            []byte(\"Go, be gzipped\"),\n\t\t\texpectedBody:    []byte(\"Unsupported Media Type\"),\n\t\t},\n\t\t{\n\t\t\tname:            \"invalid_deflate\",\n\t\t\tcontentEncoding: \"gzip,deflate\",\n\t\t\tbody:            []byte(\"I'm not correctly compressed\"),\n\t\t\texpectedBody:    []byte(zlib.ErrHeader.Error()),\n\t\t},\n\t\t{\n\t\t\tname:            \"identity\",\n\t\t\tcontentEncoding: \"identity\",\n\t\t\tbody:            []byte(\"bar\"),\n\t\t\texpectedBody:    []byte(\"bar\"),\n\t\t},\n\t}\n\n\tfor _, testObject := range tests {\n\t\ttCase := testObject // Duplicate object to ensure it will be unique across all runs\n\t\tt.Run(tCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\t\t\tapp.config.Immutable = true\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\t\t\tc.Request().Header.Set(\"Content-Encoding\", tCase.contentEncoding)\n\n\t\t\tencs := strings.SplitSeq(tCase.contentEncoding, \",\")\n\t\t\tfor enc := range encs {\n\t\t\t\tenc = utils.TrimSpace(enc)\n\t\t\t\tif strings.Contains(tCase.name, \"invalid_deflate\") && enc == StrDeflate {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tswitch enc {\n\t\t\t\tcase \"gzip\":\n\t\t\t\t\tvar b bytes.Buffer\n\t\t\t\t\tgz := gzip.NewWriter(&b)\n\t\t\t\t\t_, err := gz.Write(tCase.body)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.NoError(t, gz.Flush())\n\t\t\t\t\trequire.NoError(t, gz.Close())\n\t\t\t\t\ttCase.body = b.Bytes()\n\t\t\t\tcase StrDeflate:\n\t\t\t\t\tvar b bytes.Buffer\n\t\t\t\t\tfl := zlib.NewWriter(&b)\n\t\t\t\t\t_, err := fl.Write(tCase.body)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.NoError(t, fl.Flush())\n\t\t\t\t\trequire.NoError(t, fl.Close())\n\t\t\t\t\ttCase.body = b.Bytes()\n\t\t\t\tdefault:\n\t\t\t\t\t// we do nothing and expect the original body to be returned\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.Request().SetBody(tCase.body)\n\t\t\tbody := c.Body()\n\t\t\trequire.Equal(t, tCase.expectedBody, body)\n\n\t\t\tswitch {\n\t\t\tcase strings.Contains(tCase.name, \"unsupported\"):\n\t\t\t\trequire.Equal(t, StatusUnsupportedMediaType, c.Response().StatusCode())\n\t\t\tcase strings.Contains(tCase.name, \"compress_not_implemented\"):\n\t\t\t\trequire.Equal(t, StatusNotImplemented, c.Response().StatusCode())\n\t\t\tdefault:\n\t\t\t\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\t\t\t}\n\n\t\t\t// Check if body raw is the same as previous before decompression\n\t\t\trequire.Equal(\n\t\t\t\tt, tCase.body, c.Request().Body(),\n\t\t\t\t\"Body raw must be the same as set before\",\n\t\t\t)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression_Immutable -benchmem -count=4\nfunc Benchmark_Ctx_Body_With_Compression_Immutable(b *testing.B) {\n\tencodingErr := errors.New(\"failed to encoding data\")\n\n\tvar (\n\t\tcompressGzip = func(data []byte) ([]byte, error) {\n\t\t\tvar buf bytes.Buffer\n\t\t\twriter := gzip.NewWriter(&buf)\n\t\t\tif _, err := writer.Write(data); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Flush(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Close(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\treturn buf.Bytes(), nil\n\t\t}\n\t\tcompressDeflate = func(data []byte) ([]byte, error) {\n\t\t\tvar buf bytes.Buffer\n\t\t\twriter := zlib.NewWriter(&buf)\n\t\t\tif _, err := writer.Write(data); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Flush(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\tif err := writer.Close(); err != nil {\n\t\t\t\treturn nil, encodingErr\n\t\t\t}\n\t\t\treturn buf.Bytes(), nil\n\t\t}\n\t)\n\tconst input = \"john=doe\"\n\tcompressionTests := []struct {\n\t\tcompressWriter  func([]byte) ([]byte, error)\n\t\tcontentEncoding string\n\t\texpectedBody    []byte\n\t}{\n\t\t{\n\t\t\tcontentEncoding: \"gzip\",\n\t\t\tcompressWriter:  compressGzip,\n\t\t\texpectedBody:    []byte(input),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: \"gzip,invalid\",\n\t\t\tcompressWriter:  compressGzip,\n\t\t\texpectedBody:    []byte(ErrUnsupportedMediaType.Error()),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: StrDeflate,\n\t\t\tcompressWriter:  compressDeflate,\n\t\t\texpectedBody:    []byte(input),\n\t\t},\n\t\t{\n\t\t\tcontentEncoding: \"gzip,deflate\",\n\t\t\tcompressWriter: func(data []byte) ([]byte, error) {\n\t\t\t\tvar (\n\t\t\t\t\tbuf    bytes.Buffer\n\t\t\t\t\twriter interface {\n\t\t\t\t\t\tio.WriteCloser\n\t\t\t\t\t\tFlush() error\n\t\t\t\t\t}\n\t\t\t\t\terr error\n\t\t\t\t)\n\n\t\t\t\t// deflate\n\t\t\t\twriter = zlib.NewWriter(&buf)\n\t\t\t\tif _, err = writer.Write(data); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Flush(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Close(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\n\t\t\t\tdata = make([]byte, buf.Len())\n\t\t\t\tcopy(data, buf.Bytes())\n\t\t\t\tbuf.Reset()\n\n\t\t\t\t// gzip\n\t\t\t\twriter = gzip.NewWriter(&buf)\n\t\t\t\tif _, err = writer.Write(data); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Flush(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\t\t\t\tif err = writer.Close(); err != nil {\n\t\t\t\t\treturn nil, encodingErr\n\t\t\t\t}\n\n\t\t\t\treturn buf.Bytes(), nil\n\t\t\t},\n\t\t\texpectedBody: []byte(zlib.ErrHeader.Error()),\n\t\t},\n\t}\n\n\tb.ReportAllocs()\n\tfor _, ct := range compressionTests {\n\t\tb.Run(ct.contentEncoding, func(b *testing.B) {\n\t\t\tapp := New()\n\t\t\tapp.config.Immutable = true\n\t\t\tconst input = \"john=doe\"\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t\tc.Request().Header.Set(\"Content-Encoding\", ct.contentEncoding)\n\t\t\tcompressedBody, err := ct.compressWriter([]byte(input))\n\t\t\trequire.NoError(b, err)\n\n\t\t\tc.Request().SetBody(compressedBody)\n\t\t\tfor b.Loop() {\n\t\t\t\t_ = c.Body()\n\t\t\t}\n\n\t\t\trequire.Equal(b, ct.expectedBody, c.Body())\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_RequestCtx\nfunc Test_Ctx_RequestCtx(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \"*fasthttp.RequestCtx\", fmt.Sprintf(\"%T\", c.RequestCtx()))\n}\n\n// go test -run Test_Ctx_Cookie\nfunc Test_Ctx_Cookie(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\texpire := time.Now().Add(24 * time.Hour)\n\tvar dst []byte\n\tdst = expire.In(time.UTC).AppendFormat(dst, time.RFC1123)\n\thttpdate := strings.ReplaceAll(string(dst), \"UTC\", \"GMT\")\n\tcookie := &Cookie{\n\t\tName:    \"username\",\n\t\tValue:   \"john\",\n\t\tExpires: expire,\n\t\t// SameSite: CookieSameSiteStrictMode, // default is \"lax\"\n\t}\n\tc.Res().Cookie(cookie)\n\texpect := \"username=john; expires=\" + httpdate + \"; path=/; SameSite=Lax\"\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; expires=\" + httpdate + \"; path=/\"\n\tcookie.SameSite = CookieSameSiteDisabled\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; expires=\" + httpdate + \"; path=/; SameSite=Strict\"\n\tcookie.SameSite = CookieSameSiteStrictMode\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; expires=\" + httpdate + \"; path=/; secure; SameSite=None\"\n\tcookie.Secure = true\n\tcookie.SameSite = CookieSameSiteNoneMode\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; path=/; secure; SameSite=None\"\n\t// should remove expires and max-age headers\n\tcookie.SessionOnly = true\n\tcookie.Expires = expire\n\tcookie.MaxAge = 10000\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; path=/; secure; SameSite=None\"\n\t// should remove expires and max-age headers when no expire and no MaxAge (default time)\n\tcookie.SessionOnly = false\n\tcookie.Expires = time.Time{}\n\tcookie.MaxAge = 0\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n\n\texpect = \"username=john; path=/; secure; SameSite=None; Partitioned\"\n\tcookie.Partitioned = true\n\tc.Res().Cookie(cookie)\n\trequire.Equal(t, expect, c.Res().Get(HeaderSetCookie))\n}\n\n// go test -run Test_Ctx_Cookie_PartitionedSecure\nfunc Test_Ctx_Cookie_PartitionedSecure(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tck := &Cookie{\n\t\tName:        \"ps\",\n\t\tValue:       \"v\",\n\t\tSecure:      true,\n\t\tSameSite:    CookieSameSiteNoneMode,\n\t\tPartitioned: true,\n\t}\n\tc.Res().Cookie(ck)\n\trequire.Equal(t, \"ps=v; path=/; secure; SameSite=None; Partitioned\", c.Res().Get(HeaderSetCookie))\n}\n\n// go test -run Test_Ctx_Cookie_Invalid\nfunc Test_Ctx_Cookie_Invalid(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tcases := []*Cookie{\n\t\t{Name: \"\", Value: \"a\"},                                                        // empty name\n\t\t{Name: \"foo bar\", Value: \"a\"},                                                 // invalid char in name\n\t\t{Name: \"n\", Value: \"bad\\nval\"},                                                // invalid value byte\n\t\t{Name: \"d\", Value: \"b\", Domain: \"in valid\"},                                   // invalid domain spaces\n\t\t{Name: \"d\", Value: \"b\", Domain: \"example..com\"},                               // invalid domain dots\n\t\t{Name: \"i\", Value: \"b\", Domain: \"2001:db8::1\"},                                // ipv6 not allowed\n\t\t{Name: \"p\", Value: \"b\", Path: \"\\x00\"},                                         // invalid path byte\n\t\t{Name: \"e\", Value: \"b\", Expires: time.Date(1500, 1, 1, 0, 0, 0, 0, time.UTC)}, // invalid expires\n\t\t// Note: Partitioned without Secure is auto-fixed (Secure=true set automatically per CHIPS spec)\n\t}\n\n\tfor _, invalid := range cases {\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Res().Cookie(invalid)\n\t\trequire.Empty(t, c.Res().Get(HeaderSetCookie))\n\t\tc.Response().Header.Reset()\n\t\tapp.ReleaseCtx(c)\n\t}\n}\n\n// go test -run Test_Ctx_Cookie_DefaultPath\nfunc Test_Ctx_Cookie_DefaultPath(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tck := &Cookie{\n\t\tName:  \"p\",\n\t\tValue: \"v\",\n\t\t// Path intentionally empty to verify defaulting\n\t}\n\n\tc.Res().Cookie(ck)\n\trequire.Equal(t,\n\t\t\"p=v; path=/; SameSite=Lax\",\n\t\tc.Res().Get(HeaderSetCookie),\n\t)\n}\n\n// go test -run Test_Ctx_Cookie_MaxAgeOnly\nfunc Test_Ctx_Cookie_MaxAgeOnly(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tck := &Cookie{\n\t\tName:   \"ttl\",\n\t\tValue:  \"v\",\n\t\tMaxAge: 3600,\n\t}\n\tc.Res().Cookie(ck)\n\n\trequire.Equal(t,\n\t\t\"ttl=v; max-age=3600; path=/; SameSite=Lax\",\n\t\tc.Res().Get(HeaderSetCookie),\n\t)\n}\n\n// go test -run Test_Ctx_Cookie_StrictPartitioned\nfunc Test_Ctx_Cookie_StrictPartitioned(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tck := &Cookie{\n\t\tName:        \"sp\",\n\t\tValue:       \"v\",\n\t\tSecure:      true,\n\t\tSameSite:    CookieSameSiteStrictMode,\n\t\tPartitioned: true,\n\t}\n\tc.Res().Cookie(ck)\n\n\trequire.Equal(t,\n\t\t\"sp=v; path=/; secure; SameSite=Strict; Partitioned\",\n\t\tc.Res().Get(HeaderSetCookie),\n\t)\n}\n\n// go test -run Test_Ctx_Cookie_SameSite_CaseInsensitive\nfunc Test_Ctx_Cookie_SameSite_CaseInsensitive(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t// Test case-insensitive Strict\n\t\t{\"Strict lowercase\", \"strict\", \"SameSite=Strict\"},\n\t\t{\"Strict uppercase\", \"STRICT\", \"SameSite=Strict\"},\n\t\t{\"Strict mixed case\", \"StRiCt\", \"SameSite=Strict\"},\n\t\t{\"Strict proper case\", \"Strict\", \"SameSite=Strict\"},\n\n\t\t// Test case-insensitive Lax\n\t\t{\"Lax lowercase\", \"lax\", \"SameSite=Lax\"},\n\t\t{\"Lax uppercase\", \"LAX\", \"SameSite=Lax\"},\n\t\t{\"Lax mixed case\", \"LaX\", \"SameSite=Lax\"},\n\t\t{\"Lax proper case\", \"Lax\", \"SameSite=Lax\"},\n\n\t\t// Test case-insensitive None\n\t\t{\"None lowercase\", \"none\", \"SameSite=None\"},\n\t\t{\"None uppercase\", \"NONE\", \"SameSite=None\"},\n\t\t{\"None mixed case\", \"NoNe\", \"SameSite=None\"},\n\t\t{\"None proper case\", \"None\", \"SameSite=None\"},\n\n\t\t// Test case-insensitive disabled\n\t\t{\"Disabled lowercase\", \"disabled\", \"\"},\n\t\t{\"Disabled uppercase\", \"DISABLED\", \"\"},\n\t\t{\"Disabled mixed case\", \"DiSaBlEd\", \"\"},\n\t\t{\"Disabled proper case\", \"disabled\", \"\"},\n\n\t\t// Test invalid values default to Lax\n\t\t{\"Invalid value\", \"invalid\", \"SameSite=Lax\"},\n\t\t{\"Empty value\", \"\", \"SameSite=Lax\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefer app.ReleaseCtx(c)\n\n\t\t\t// Reset response\n\t\t\tc.Response().Reset()\n\n\t\t\tcookie := &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: tc.input,\n\t\t\t}\n\t\t\tc.Res().Cookie(cookie)\n\n\t\t\tsetCookieHeader := c.Res().Get(HeaderSetCookie)\n\t\t\tif tc.expected == \"\" {\n\t\t\t\t// For disabled, SameSite should not appear in the header\n\t\t\t\trequire.NotContains(t, setCookieHeader, \"SameSite\")\n\t\t\t} else {\n\t\t\t\t// For all other cases, the expected SameSite should appear\n\t\t\t\trequire.Contains(t, setCookieHeader, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_Cookie_SameSite_None_Secure\nfunc Test_Ctx_Cookie_SameSite_None_Secure(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname             string\n\t\tcookie           *Cookie\n\t\texpectedInHeader string\n\t\tshouldBeSecure   bool\n\t}{\n\t\t{\n\t\t\tname: \"Empty value\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"\",\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=Lax\",\n\t\t\tshouldBeSecure:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"None uppercase\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"None\",\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=None\",\n\t\t\tshouldBeSecure:   true,\n\t\t},\n\t\t{\n\t\t\tname: \"None lowercase\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"none\",\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=None\",\n\t\t\tshouldBeSecure:   true,\n\t\t},\n\t\t{\n\t\t\tname: \"Lax proper case\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"Lax\",\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=Lax\",\n\t\t\tshouldBeSecure:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"Strict uppercase\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"STRICT\",\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=Strict\",\n\t\t\tshouldBeSecure:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"Disabled Secure\",\n\t\t\tcookie: &Cookie{\n\t\t\t\tName:     \"test\",\n\t\t\t\tValue:    \"value\",\n\t\t\t\tSameSite: \"none\",\n\t\t\t\tSecure:   false,\n\t\t\t},\n\t\t\texpectedInHeader: \"SameSite=None\",\n\t\t\tshouldBeSecure:   true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\t\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t\tctx.Cookie(tc.cookie)\n\n\t\t\tcookie := string(ctx.Response().Header.PeekCookie(tc.cookie.Name))\n\t\t\trequire.Contains(t, cookie, tc.expectedInHeader)\n\n\t\t\tif tc.shouldBeSecure {\n\t\t\t\trequire.Contains(t, cookie, \"secure\")\n\t\t\t} else {\n\t\t\t\trequire.NotContains(t, cookie, \"secure\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4\nfunc Benchmark_Ctx_Cookie(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Cookie(&Cookie{\n\t\t\tName:  \"John\",\n\t\t\tValue: \"Doe\",\n\t\t})\n\t}\n\trequire.Equal(b, \"John=Doe; path=/; SameSite=Lax\", app.toString(c.Response().Header.Peek(\"Set-Cookie\")))\n}\n\n// go test -run Test_Ctx_Cookies\nfunc Test_Ctx_Cookies(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Cookie\", \"john=doe\")\n\trequire.Equal(t, \"doe\", c.Req().Cookies(\"john\"))\n\trequire.Equal(t, \"default\", c.Req().Cookies(\"unknown\", \"default\"))\n}\n\n// go test -run Test_Ctx_Format\nfunc Test_Ctx_Format(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// set `accepted` to whatever media type was chosen by Format\n\tvar accepted string\n\tformatHandlers := func(types ...string) []ResFmt {\n\t\tfmts := []ResFmt{}\n\t\tfor _, t := range types {\n\t\t\ttyp := utils.CopyString(t)\n\t\t\tfmts = append(fmts, ResFmt{MediaType: typ, Handler: func(_ Ctx) error {\n\t\t\t\taccepted = typ\n\t\t\t\treturn nil\n\t\t\t}})\n\t\t}\n\t\treturn fmts\n\t}\n\n\tc.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`)\n\terr := c.Res().Format(formatHandlers(\"application/xhtml+xml\", \"application/xml\", \"foo/bar\")...)\n\trequire.Equal(t, \"application/xhtml+xml\", accepted)\n\trequire.Equal(t, \"application/xhtml+xml\", c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"application/xhtml+xml\", c.Res().Get(HeaderContentType))\n\trequire.NoError(t, err)\n\trequire.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode())\n\n\terr = c.Res().Format(formatHandlers(\"foo/bar;a=b\")...)\n\trequire.Equal(t, \"foo/bar;a=b\", accepted)\n\trequire.Equal(t, \"foo/bar;a=b\", c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"foo/bar;a=b\", c.Res().Get(HeaderContentType))\n\trequire.NoError(t, err)\n\trequire.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode())\n\n\tmyError := errors.New(\"this is an error\")\n\terr = c.Format(ResFmt{MediaType: \"text/html\", Handler: func(_ Ctx) error { return myError }})\n\trequire.ErrorIs(t, err, myError)\n\n\tc.Request().Header.Set(HeaderAccept, \"application/json\")\n\terr = c.Format(ResFmt{MediaType: \"text/html\", Handler: func(c Ctx) error { return c.SendStatus(StatusOK) }})\n\trequire.Equal(t, StatusNotAcceptable, c.Response().StatusCode())\n\trequire.NoError(t, err)\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationMsgPack)\n\terr = c.Format(ResFmt{MediaType: \"text/html\", Handler: func(c Ctx) error { return c.SendStatus(StatusOK) }})\n\trequire.Equal(t, StatusNotAcceptable, c.Response().StatusCode())\n\trequire.NoError(t, err)\n\n\terr = c.Format(formatHandlers(\"text/html\", \"default\")...)\n\trequire.Equal(t, \"default\", accepted)\n\trequire.Equal(t, \"text/html\", c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"text/html\", c.Res().Get(HeaderContentType))\n\trequire.NoError(t, err)\n\n\terr = c.Format()\n\trequire.ErrorIs(t, err, ErrNoHandlers)\n}\n\nfunc Test_Ctx_Format_NilHandler(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"nil handler in first entry\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\tvar err error\n\t\trequire.NotPanics(t, func() {\n\t\t\terr = c.Format(\n\t\t\t\tResFmt{MediaType: \"text/html\", Handler: nil},\n\t\t\t\tResFmt{MediaType: \"application/json\", Handler: func(_ Ctx) error { return nil }},\n\t\t\t)\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), `media type \"text/html\"`)\n\t\trequire.Contains(t, err.Error(), \"index 0\")\n\t})\n\n\tt.Run(\"nil handler in matched media type\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderAccept, \"application/json\")\n\n\t\tvar err error\n\t\trequire.NotPanics(t, func() {\n\t\t\terr = c.Format(\n\t\t\t\tResFmt{MediaType: \"text/html\", Handler: func(_ Ctx) error { return nil }},\n\t\t\t\tResFmt{MediaType: \"application/json\", Handler: nil},\n\t\t\t)\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), `media type \"application/json\"`)\n\t\trequire.Contains(t, err.Error(), \"index 1\")\n\t})\n\n\tt.Run(\"nil default handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderAccept, \"application/json\")\n\n\t\tvar err error\n\t\trequire.NotPanics(t, func() {\n\t\t\terr = c.Format(\n\t\t\t\tResFmt{MediaType: \"text/html\", Handler: func(_ Ctx) error { return nil }},\n\t\t\t\tResFmt{MediaType: \"default\", Handler: nil},\n\t\t\t)\n\t\t})\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), `media type \"default\"`)\n\t\trequire.Contains(t, err.Error(), \"index 1\")\n\t})\n}\n\nfunc Benchmark_Ctx_Format(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderAccept, \"application/json,text/plain; format=flowed; q=0.9\")\n\n\tfail := func(_ Ctx) error {\n\t\trequire.FailNow(b, \"Wrong type chosen\")\n\t\treturn errors.New(\"Wrong type chosen\")\n\t}\n\tok := func(_ Ctx) error {\n\t\treturn nil\n\t}\n\n\tvar err error\n\tb.Run(\"with arg allocation\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\terr = c.Format(\n\t\t\t\tResFmt{MediaType: \"application/xml\", Handler: fail},\n\t\t\t\tResFmt{MediaType: \"text/html\", Handler: fail},\n\t\t\t\tResFmt{MediaType: \"text/plain;format=fixed\", Handler: fail},\n\t\t\t\tResFmt{MediaType: \"text/plain;format=flowed\", Handler: ok},\n\t\t\t)\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n\n\tb.Run(\"pre-allocated args\", func(b *testing.B) {\n\t\toffers := []ResFmt{\n\t\t\t{MediaType: \"application/xml\", Handler: fail},\n\t\t\t{MediaType: \"text/html\", Handler: fail},\n\t\t\t{MediaType: \"text/plain;format=fixed\", Handler: fail},\n\t\t\t{MediaType: \"text/plain;format=flowed\", Handler: ok},\n\t\t}\n\t\tfor b.Loop() {\n\t\t\terr = c.Format(offers...)\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n\n\tc.Request().Header.Set(\"Accept\", \"text/plain\")\n\tb.Run(\"text/plain\", func(b *testing.B) {\n\t\toffers := []ResFmt{\n\t\t\t{MediaType: \"application/xml\", Handler: fail},\n\t\t\t{MediaType: \"text/plain\", Handler: ok},\n\t\t}\n\t\tfor b.Loop() {\n\t\t\terr = c.Format(offers...)\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n\n\tc.Request().Header.Set(\"Accept\", \"json\")\n\tb.Run(\"json\", func(b *testing.B) {\n\t\toffers := []ResFmt{\n\t\t\t{MediaType: \"xml\", Handler: fail},\n\t\t\t{MediaType: \"html\", Handler: fail},\n\t\t\t{MediaType: \"json\", Handler: ok},\n\t\t}\n\t\tfor b.Loop() {\n\t\t\terr = c.Format(offers...)\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n\n\tc.Request().Header.Set(\"Accept\", MIMEApplicationMsgPack)\n\tb.Run(\"msgpack\", func(b *testing.B) {\n\t\toffers := []ResFmt{\n\t\t\t{MediaType: \"xml\", Handler: fail},\n\t\t\t{MediaType: \"html\", Handler: fail},\n\t\t\t{MediaType: MIMEApplicationMsgPack, Handler: ok},\n\t\t}\n\t\tfor b.Loop() {\n\t\t\terr = c.Format(offers...)\n\t\t}\n\t\trequire.NoError(b, err)\n\t})\n}\n\n// go test -run Test_Ctx_AutoFormat\nfunc Test_Ctx_AutoFormat(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tMsgPackEncoder: msgpack.Marshal,\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\tCBOREncoder:    cbor.Marshal,\n\t\tCBORDecoder:    cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAccept, MIMETextPlain)\n\terr := c.AutoFormat([]byte(\"Hello, World!\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"Hello, World!\", string(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMETextHTML)\n\terr = c.Res().AutoFormat(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMETextHTMLCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"<p>Hello, World!</p>\", string(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationJSON)\n\terr = c.AutoFormat(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationJSONCharsetUTF8, c.GetRespHeader(HeaderContentType)) //nolint:testifylint // this is comparing content-type headers, not JSON content\n\trequire.Equal(t, `\"Hello, World!\"`, string(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationMsgPack)\n\terr = c.AutoFormat(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationMsgPack, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, []byte{\n\t\t0xad, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c,\n\t\t0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21,\n\t}, c.Response().Body())\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationCBOR)\n\terr = c.AutoFormat(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationCBOR, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, `6d48656c6c6f2c20576f726c6421`, hex.EncodeToString(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMETextPlain)\n\terr = c.Res().AutoFormat(complex(1, 1))\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"(1+1i)\", string(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationXML)\n\terr = c.AutoFormat(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationXMLCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, `<string>Hello, World!</string>`, string(c.Response().Body()))\n\n\terr = c.AutoFormat(complex(1, 1))\n\trequire.Error(t, err)\n\n\tc.Request().Header.Set(HeaderAccept, MIMETextPlain)\n\terr = c.AutoFormat(Map{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"map[]\", string(c.Response().Body()))\n\n\ttype broken string\n\tc.Request().Header.Set(HeaderAccept, \"broken/accept\")\n\trequire.NoError(t, err)\n\terr = c.AutoFormat(broken(\"Hello, World!\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMETextPlainCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, `Hello, World!`, string(c.Response().Body()))\n}\n\nfunc Test_Ctx_AutoFormat_Struct(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tMsgPackEncoder: msgpack.Marshal,\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\tCBOREncoder:    cbor.Marshal,\n\t\tCBORDecoder:    cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype Message struct {\n\t\tSender     string `xml:\"sender,attr\"`\n\t\tRecipients []string\n\t\tUrgency    int `xml:\"urgency,attr\"`\n\t}\n\tdata := Message{\n\t\tRecipients: []string{\"Alice\", \"Bob\"},\n\t\tSender:     \"Carol\",\n\t\tUrgency:    3,\n\t}\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationJSON)\n\terr := c.AutoFormat(data)\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationJSONCharsetUTF8, c.GetRespHeader(HeaderContentType)) //nolint:testifylint // this is comparing content-type headers, not JSON content\n\trequire.JSONEq(t,\n\t\t`{\"Sender\":\"Carol\",\"Recipients\":[\"Alice\",\"Bob\"],\"Urgency\":3}`,\n\t\tstring(c.Response().Body()),\n\t)\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationMsgPack)\n\terr = c.AutoFormat(data)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, MIMEApplicationMsgPack, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, []byte{\n\t\t// {\"Sender\":\"Carol\",\"Recipients\":[\"Alice\",\"Bob\"],\"Urgency\":3}\n\t\t0x83, 0xa6, 0x53, 0x65, 0x6e, 0x64, 0x65, 0x72, 0xa5, 0x43, 0x61,\n\t\t0x72, 0x6f, 0x6c, 0xaa, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65,\n\t\t0x6e, 0x74, 0x73, 0x92, 0xa5, 0x41, 0x6c, 0x69, 0x63, 0x65, 0xa3,\n\t\t0x42, 0x6f, 0x62, 0xa7, 0x55, 0x72, 0x67, 0x65, 0x6e, 0x63, 0x79,\n\t\t0x3,\n\t},\n\t\tc.Response().Body())\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationCBOR)\n\terr = c.AutoFormat(data)\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationCBOR, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t, \"a36653656e646572654361726f6c6a526563697069656e74738265416c69636563426f6267557267656e637903\",\n\t\thex.EncodeToString(c.Response().Body()))\n\n\tc.Request().Header.Set(HeaderAccept, MIMEApplicationXML)\n\terr = c.AutoFormat(data)\n\trequire.NoError(t, err)\n\trequire.Equal(t, MIMEApplicationXMLCharsetUTF8, c.GetRespHeader(HeaderContentType))\n\trequire.Equal(t,\n\t\t`<Message sender=\"Carol\" urgency=\"3\"><Recipients>Alice</Recipients><Recipients>Bob</Recipients></Message>`,\n\t\tstring(c.Response().Body()),\n\t)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat -benchmem -count=4\nfunc Benchmark_Ctx_AutoFormat(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Accept\", \"text/plain\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.AutoFormat(\"Hello, World!\")\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, `Hello, World!`, string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_HTML -benchmem -count=4\nfunc Benchmark_Ctx_AutoFormat_HTML(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Accept\", \"text/html\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.AutoFormat(\"Hello, World!\")\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"<p>Hello, World!</p>\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_JSON -benchmem -count=4\nfunc Benchmark_Ctx_AutoFormat_JSON(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Accept\", \"application/json\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.AutoFormat(\"Hello, World!\")\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, `\"Hello, World!\"`, string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_MsgPack -benchmem -count=4\nfunc Benchmark_Ctx_AutoFormat_MsgPack(b *testing.B) {\n\tapp := New(\n\t\tConfig{\n\t\t\tMsgPackEncoder: msgpack.Marshal,\n\t\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\t},\n\t)\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Accept\", MIMEApplicationMsgPack)\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.AutoFormat(\"Hello, World!\")\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"\\xadHello, World!\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_XML -benchmem -count=4\nfunc Benchmark_Ctx_AutoFormat_XML(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"Accept\", \"application/xml\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.AutoFormat(\"Hello, World!\")\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, `<string>Hello, World!</string>`, string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_FormFile\nfunc Test_Ctx_FormFile(t *testing.T) {\n\t// TODO: We should clean this up\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tfh, err := c.FormFile(\"file\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"test\", fh.Filename)\n\n\t\tf, err := fh.Open()\n\t\trequire.NoError(t, err)\n\t\tdefer func() {\n\t\t\trequire.NoError(t, f.Close())\n\t\t}()\n\n\t\tb := new(bytes.Buffer)\n\t\t_, err = io.Copy(b, f)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"hello world\", b.String())\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tioWriter, err := writer.CreateFormFile(\"file\", \"test\")\n\trequire.NoError(t, err)\n\n\t_, err = ioWriter.Write([]byte(\"hello world\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(HeaderContentType, writer.FormDataContentType())\n\treq.Header.Set(HeaderContentLength, strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Empty(t, respBody)\n\trequire.Empty(t, resp.Header.Get(HeaderContentType))\n\trequire.Equal(t, int64(0), resp.ContentLength)\n}\n\n// go test -run Test_Ctx_FormValue\nfunc Test_Ctx_FormValue(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.FormValue(\"name\"))\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\trequire.NoError(t, writer.WriteField(\"name\", \"john\"))\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(\"Content-Type\", \"multipart/form-data; boundary=\"+writer.Boundary())\n\treq.Header.Set(\"Content-Length\", strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Empty(t, respBody)\n\trequire.Empty(t, resp.Header.Get(HeaderContentType))\n\trequire.Equal(t, int64(0), resp.ContentLength)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_StaleEtag -benchmem -count=4\nfunc Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tfor b.Loop() {\n\t\tc.Request().Header.Set(HeaderIfNoneMatch, `\"a\", \"b\", \"c\", \"d\"`)\n\t\tc.Request().Header.Set(HeaderCacheControl, \"c\")\n\t\tc.Fresh()\n\n\t\tc.Request().Header.Set(HeaderIfNoneMatch, `\"a\", \"b\", \"c\", \"d\"`)\n\t\tc.Request().Header.Set(HeaderCacheControl, \"e\")\n\t\tc.Fresh()\n\t}\n}\n\n// go test -run Test_Ctx_Fresh\nfunc Test_Ctx_Fresh(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, \"*\")\n\tc.Request().Header.Set(HeaderCacheControl, \"no-cache\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, \"*\")\n\tc.Request().Header.Set(HeaderCacheControl, \",no-cache,\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, \"*\")\n\tc.Request().Header.Set(HeaderCacheControl, \"aa,no-cache,\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, \"*\")\n\tc.Request().Header.Set(HeaderCacheControl, \",no-cache,bb\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, `\"675af34563dc-tr34\"`)\n\tc.Request().Header.Set(HeaderCacheControl, \"public\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, `\"a\", \"b\"`)\n\tc.Response().Header.Set(HeaderETag, `\"c\"`)\n\trequire.False(t, c.Fresh())\n\n\tc.Response().Header.Set(HeaderETag, `\"a\"`)\n\trequire.True(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfModifiedSince, \"xxWed, 21 Oct 2015 07:28:00 GMT\")\n\tc.Response().Header.Set(HeaderLastModified, \"xxWed, 21 Oct 2015 07:28:00 GMT\")\n\trequire.False(t, c.Fresh())\n\n\tc.Response().Header.Set(HeaderLastModified, \"Wed, 21 Oct 2015 07:28:00 GMT\")\n\trequire.False(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfModifiedSince, \"Wed, 21 Oct 2015 07:28:00 GMT\")\n\trequire.True(t, c.Fresh())\n\n\tc.Request().Header.Set(HeaderIfModifiedSince, \"Wed, 21 Oct 2015 07:27:59 GMT\")\n\tc.Response().Header.Set(HeaderLastModified, \"Wed, 21 Oct 2015 07:28:00 GMT\")\n\trequire.False(t, c.Fresh())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_WithNoCache -benchmem -count=4\nfunc Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderIfNoneMatch, \"*\")\n\tc.Request().Header.Set(HeaderCacheControl, \"no-cache\")\n\tfor b.Loop() {\n\t\tc.Fresh()\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_LastModified -benchmem -count=4\nfunc Benchmark_Ctx_Fresh_LastModified(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Response().Header.Set(HeaderLastModified, \"Wed, 21 Oct 2015 07:28:00 GMT\")\n\tc.Request().Header.Set(HeaderIfModifiedSince, \"Wed, 21 Oct 2015 07:28:00 GMT\")\n\tfor b.Loop() {\n\t\tc.Fresh()\n\t}\n}\n\n// go test -run Test_Ctx_Binders -v\nfunc Test_Ctx_Binders(t *testing.T) {\n\tt.Parallel()\n\t// setup\n\tapp := New(Config{\n\t\tEnableSplittingOnParsers: true,\n\t})\n\n\ttype TestEmbeddedStruct struct {\n\t\tNames []string `query:\"names\"`\n\t}\n\n\ttype TestStruct struct {\n\t\tName            string\n\t\tNameWithDefault string `json:\"name2\" xml:\"Name2\" form:\"name2\" cookie:\"name2\" query:\"name2\" uri:\"name2\" header:\"Name2\"`\n\t\tTestEmbeddedStruct\n\t\tClass            int\n\t\tClassWithDefault int `json:\"class2\" xml:\"Class2\" form:\"class2\" cookie:\"class2\" query:\"class2\" uri:\"class2\" header:\"Class2\"`\n\t}\n\n\twithValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) {\n\t\tt.Helper()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\ttestStruct := new(TestStruct)\n\n\t\trequire.NoError(t, actionFn(c, testStruct))\n\t\trequire.Equal(t, \"foo\", testStruct.Name)\n\t\trequire.Equal(t, 111, testStruct.Class)\n\t\trequire.Equal(t, \"bar\", testStruct.NameWithDefault)\n\t\trequire.Equal(t, 222, testStruct.ClassWithDefault)\n\t\trequire.Equal(t, []string{\"foo\", \"bar\", \"test\"}, testStruct.Names)\n\t}\n\n\tt.Run(\"Body:xml\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().Header.SetContentType(MIMEApplicationXML)\n\t\t\tc.Request().SetBody([]byte(`<TestStruct><Name>foo</Name><Class>111</Class><Name2>bar</Name2><Class2>222</Class2><Names>foo</Names><Names>bar</Names><Names>test</Names></TestStruct>`))\n\t\t\treturn c.Bind().Body(testStruct)\n\t\t})\n\t})\n\tt.Run(\"Body:form\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().Header.SetContentType(MIMEApplicationForm)\n\t\t\tc.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222&names=foo,bar,test`))\n\t\t\treturn c.Bind().Body(testStruct)\n\t\t})\n\t})\n\tt.Run(\"BodyParser:json\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().Header.SetContentType(MIMEApplicationJSON)\n\t\t\tc.Request().SetBody([]byte(`{\"name\":\"foo\",\"class\":111,\"name2\":\"bar\",\"class2\":222,\"names\":[\"foo\",\"bar\",\"test\"]}`))\n\t\t\treturn c.Bind().Body(testStruct)\n\t\t})\n\t})\n\tt.Run(\"Body:multiform\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tbody := []byte(\"--b\\r\\nContent-Disposition: form-data; name=\\\"name\\\"\\r\\n\\r\\nfoo\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"class\\\"\\r\\n\\r\\n111\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"name2\\\"\\r\\n\\r\\nbar\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"class2\\\"\\r\\n\\r\\n222\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"names\\\"\\r\\n\\r\\nfoo\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"names\\\"\\r\\n\\r\\nbar\\r\\n--b\\r\\nContent-Disposition: form-data; name=\\\"names\\\"\\r\\n\\r\\ntest\\r\\n--b--\")\n\t\t\tc.Request().SetBody(body)\n\t\t\tc.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=\"b\"`)\n\t\t\tc.Request().Header.SetContentLength(len(body))\n\t\t\treturn c.Bind().Body(testStruct)\n\t\t})\n\t})\n\tt.Run(\"Cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().Header.Set(\"Cookie\", \"name=foo;name2=bar;class=111;class2=222;names=foo,bar,test\")\n\t\t\treturn c.Bind().Cookie(testStruct)\n\t\t})\n\t})\n\tt.Run(\"Query\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().URI().SetQueryString(\"name=foo&name2=bar&class=111&class2=222&names=foo,bar,test\")\n\t\t\treturn c.Bind().Query(testStruct)\n\t\t})\n\t})\n\n\tt.Run(\"URI\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.route = &Route{Params: []string{\"name\", \"name2\", \"class\", \"class2\"}}\n\t\tc.values = [maxParams]string{\"foo\", \"bar\", \"111\", \"222\"}\n\n\t\ttestStruct := new(TestStruct)\n\n\t\trequire.NoError(t, c.Bind().URI(testStruct))\n\t\trequire.Equal(t, \"foo\", testStruct.Name)\n\t\trequire.Equal(t, 111, testStruct.Class)\n\t\trequire.Equal(t, \"bar\", testStruct.NameWithDefault)\n\t\trequire.Equal(t, 222, testStruct.ClassWithDefault)\n\t\trequire.Nil(t, testStruct.Names)\n\t})\n\n\tt.Run(\"ReqHeader\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\twithValues(t, func(c Ctx, testStruct *TestStruct) error {\n\t\t\tc.Request().Header.Add(\"name\", \"foo\")\n\t\t\tc.Request().Header.Add(\"name2\", \"bar\")\n\t\t\tc.Request().Header.Add(\"class\", \"111\")\n\t\t\tc.Request().Header.Add(\"class2\", \"222\")\n\t\t\tc.Request().Header.Add(\"names\", \"foo,bar,test\")\n\t\t\treturn c.Bind().Header(testStruct)\n\t\t})\n\t})\n}\n\n// go test -run Test_Ctx_Get\nfunc Test_Ctx_Get(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderAcceptCharset, \"utf-8, iso-8859-1;q=0.5\")\n\tc.Request().Header.Set(HeaderReferer, \"Monster\")\n\trequire.Equal(t, \"utf-8, iso-8859-1;q=0.5\", c.Get(HeaderAcceptCharset))\n\trequire.Equal(t, \"Monster\", c.Get(HeaderReferer))\n\trequire.Equal(t, \"default\", c.Get(\"unknown\", \"default\"))\n}\n\n// go test -run Test_Ctx_GetReqHeader\nfunc Test_Ctx_GetReqHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"foo\", \"bar\")\n\tc.Request().Header.Set(\"id\", \"123\")\n\trequire.Equal(t, 123, GetReqHeader[int](c, \"id\"))\n\trequire.Equal(t, \"bar\", GetReqHeader[string](c, \"foo\"))\n}\n\n// go test -run Test_Ctx_Host\nfunc Test_Ctx_Host(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\trequire.Equal(t, \"google.com\", c.Host())\n}\n\n// go test -run Test_Ctx_Host_UntrustedProxy\nfunc Test_Ctx_Host_UntrustedProxy(t *testing.T) {\n\tt.Parallel()\n\t// Don't trust any proxy\n\t{\n\t\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{}}})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\trequire.Equal(t, \"google.com\", c.Host())\n\t\tapp.ReleaseCtx(c)\n\t}\n\t// Trust to specific proxy list\n\t{\n\t\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.8.0.0\", \"0.8.0.1\"}}})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\trequire.Equal(t, \"google.com\", c.Host())\n\t\tapp.ReleaseCtx(c)\n\t}\n}\n\n// go test -run Test_Ctx_Host_TrustedProxy\nfunc Test_Ctx_Host_TrustedProxy(t *testing.T) {\n\tt.Parallel()\n\t{\n\t\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\", \"0.8.0.1\"}}})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\trequire.Equal(t, \"google1.com\", c.Host())\n\t\tapp.ReleaseCtx(c)\n\t}\n\tt.Run(\"TrimWhitespaceFromForwardedHost\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestCases := []struct {\n\t\t\tname          string\n\t\t\tforwardedHost string\n\t\t\texpectedHost  string\n\t\t}{\n\t\t\t{\n\t\t\t\tname:          \"leading whitespace with comma\",\n\t\t\t\tforwardedHost: \" example.com, proxy1\",\n\t\t\t\texpectedHost:  \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:          \"trailing whitespace with comma\",\n\t\t\t\tforwardedHost: \"example.com , proxy1\",\n\t\t\t\texpectedHost:  \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:          \"leading and trailing whitespace with comma\",\n\t\t\t\tforwardedHost: \"  example.com  , proxy1\",\n\t\t\t\texpectedHost:  \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:          \"no whitespace with comma\",\n\t\t\t\tforwardedHost: \"example.com, proxy1\",\n\t\t\t\texpectedHost:  \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:          \"single value with whitespace\",\n\t\t\t\tforwardedHost: \"  example.com  \",\n\t\t\t\texpectedHost:  \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:          \"leading comma\",\n\t\t\t\tforwardedHost: \",example.com\",\n\t\t\t\texpectedHost:  \"\",\n\t\t\t},\n\t\t}\n\n\t\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\", \"0.8.0.1\"}}})\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\t\tdefer app.ReleaseCtx(c)\n\t\t\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\t\t\tc.Request().Header.Set(HeaderXForwardedHost, tc.forwardedHost)\n\t\t\t\trequire.Equal(t, tc.expectedHost, c.Host())\n\t\t\t})\n\t\t}\n\t})\n}\n\n// go test -run Test_Ctx_Host_TrustedProxyRange\nfunc Test_Ctx_Host_TrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0/30\"}}})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\trequire.Equal(t, \"google1.com\", c.Host())\n\tapp.ReleaseCtx(c)\n}\n\n// go test -run Test_Ctx_Host_UntrustedProxyRange\nfunc Test_Ctx_Host_UntrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"1.0.0.0/30\"}}})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\trequire.Equal(t, \"google.com\", c.Host())\n\tapp.ReleaseCtx(c)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Host -benchmem -count=4\nfunc Benchmark_Ctx_Host(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\tvar host string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\thost = c.Host()\n\t}\n\trequire.Equal(b, \"google.com\", host)\n}\n\n// go test -run Test_Ctx_IsProxyTrusted\nfunc Test_Ctx_IsProxyTrusted(t *testing.T) {\n\tt.Parallel()\n\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: false,\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"127.0.0.1\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"127.0.0.1/8\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.True(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.1/31\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.True(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.1/31junk\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tPrivate: true,\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tLoopback: true,\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tLinkLocal: true,\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.False(t, c.IsProxyTrusted())\n\t}\n}\n\n// go test -run Test_Ctx_Hostname\nfunc Test_Ctx_Hostname(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\trequire.Equal(t, \"google.com\", c.Hostname())\n\n\tc.Request().SetRequestURI(\"http://google.com:8080/test\")\n\trequire.Equal(t, \"google.com\", c.Hostname())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Hostname -benchmem -count=4\nfunc Benchmark_Ctx_Hostname(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com:8080/test\")\n\tvar hostname string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\thostname = c.Hostname()\n\t}\n\t// Trust to specific proxy list\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy:       true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.8.0.0\", \"0.8.0.1\"}},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\trequire.Equal(b, \"google.com\", hostname)\n\t\tapp.ReleaseCtx(c)\n\t}\n}\n\n// go test -run Test_Ctx_Hostname_Trusted\nfunc Test_Ctx_Hostname_TrustedProxy(t *testing.T) {\n\tt.Parallel()\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy:       true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\", \"0.8.0.1\"}},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\trequire.Equal(t, \"google1.com\", c.Hostname())\n\t\tapp.ReleaseCtx(c)\n\t}\n}\n\n// go test -run Test_Ctx_Hostname_Trusted_Multiple\nfunc Test_Ctx_Hostname_TrustedProxy_Multiple(t *testing.T) {\n\tt.Parallel()\n\t{\n\t\tapp := New(Config{\n\t\t\tTrustProxy:       true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\", \"0.8.0.1\"}},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com, google2.com\")\n\t\trequire.Equal(t, \"google1.com\", c.Hostname())\n\t\tapp.ReleaseCtx(c)\n\t}\n}\n\n// go test -run Test_Ctx_Hostname_UntrustedProxyRange\nfunc Test_Ctx_Hostname_TrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0/30\"}},\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\trequire.Equal(t, \"google1.com\", c.Hostname())\n\tapp.ReleaseCtx(c)\n}\n\n// go test -run Test_Ctx_Hostname_UntrustedProxyRange\nfunc Test_Ctx_Hostname_UntrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"1.0.0.0/30\"}},\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().SetRequestURI(\"http://google.com/test\")\n\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\trequire.Equal(t, \"google.com\", c.Hostname())\n\tapp.ReleaseCtx(c)\n}\n\n// go test -run Test_Ctx_Port\nfunc Test_Ctx_Port(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \"0\", c.Port())\n}\n\n// go test -run Test_Ctx_Port_RemoteAddrVariants\nfunc Test_Ctx_Port_RemoteAddrVariants(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname   string\n\t\tremote net.Addr\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname: \"tcp\",\n\t\t\tremote: &net.TCPAddr{\n\t\t\t\tIP:   net.IPv4(127, 0, 0, 1),\n\t\t\t\tPort: 8080,\n\t\t\t},\n\t\t\twant: \"8080\",\n\t\t},\n\t\t{\n\t\t\tname:   \"unix\",\n\t\t\tremote: &net.UnixAddr{Name: \"/tmp/fiber.sock\", Net: \"unix\"},\n\t\t\twant:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"default-remote\",\n\t\t\tremote: nil,\n\t\t\twant:   \"0\",\n\t\t},\n\t\t{\n\t\t\tname:   \"string-host-port\",\n\t\t\tremote: testNetAddr{network: \"tcp\", address: \"192.0.2.1:443\"},\n\t\t\twant:   \"443\",\n\t\t},\n\t\t{\n\t\t\tname:   \"string-missing-port\",\n\t\t\tremote: testNetAddr{network: \"tcp\", address: \"192.0.2.1\"},\n\t\t\twant:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"string-ipv6-port\",\n\t\t\tremote: testNetAddr{network: \"tcp\", address: \"[2001:db8::1]:8443\"},\n\t\t\twant:   \"8443\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\t\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefaultCtx, ok := ctx.(*DefaultCtx)\n\t\t\trequire.True(t, ok)\n\t\t\tdefaultCtx.fasthttp.SetRemoteAddr(test.remote)\n\t\t\trequire.Equal(t, test.want, ctx.Port())\n\t\t\tapp.ReleaseCtx(ctx)\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_PortInHandler\nfunc Test_Ctx_PortInHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/port\", func(c Ctx) error {\n\t\treturn c.SendString(c.Port())\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/port\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"0\", string(body))\n}\n\n// go test -run Test_Ctx_IP\nfunc Test_Ctx_IP(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// default behavior will return the remote IP from the stack\n\trequire.Equal(t, \"0.0.0.0\", c.IP())\n\n\t// X-Forwarded-For is set, but it is ignored because proxyHeader is not set\n\tc.Request().Header.Set(HeaderXForwardedFor, \"0.0.0.1\")\n\trequire.Equal(t, \"0.0.0.0\", c.IP())\n}\n\n// go test -run Test_Ctx_IP_ProxyHeader\nfunc Test_Ctx_IP_ProxyHeader(t *testing.T) {\n\tt.Parallel()\n\n\t// make sure that the same behavior exists for different proxy header names\n\tproxyHeaderNames := []string{\"Real-Ip\", HeaderXForwardedFor}\n\n\tfor _, proxyHeaderName := range proxyHeaderNames {\n\t\tapp := New(Config{\n\t\t\tProxyHeader: proxyHeaderName,\n\t\t\tTrustProxy:  true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t\t},\n\t\t})\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0.0.0.0\")}))\n\t\tc := app.AcquireCtx(fastCtx)\n\n\t\tc.Request().Header.Set(proxyHeaderName, \"0.0.0.1\")\n\t\trequire.Equal(t, \"0.0.0.1\", c.IP())\n\n\t\t// without IP validation we return the full string\n\t\tc.Request().Header.Set(proxyHeaderName, \"0.0.0.1, 0.0.0.2\")\n\t\trequire.Equal(t, \"0.0.0.1, 0.0.0.2\", c.IP())\n\n\t\t// without IP validation we return invalid IPs\n\t\tc.Request().Header.Set(proxyHeaderName, \"invalid, 0.0.0.2, 0.0.0.3\")\n\t\trequire.Equal(t, \"invalid, 0.0.0.2, 0.0.0.3\", c.IP())\n\n\t\t// when proxy header is enabled but the value is empty, without IP validation we return an empty string\n\t\tc.Request().Header.Set(proxyHeaderName, \"\")\n\t\trequire.Empty(t, c.IP())\n\n\t\t// without IP validation we return an invalid IP\n\t\tc.Request().Header.Set(proxyHeaderName, \"not-valid-ip\")\n\t\trequire.Equal(t, \"not-valid-ip\", c.IP())\n\t}\n}\n\n// go test -run Test_Ctx_IP_ProxyHeader\nfunc Test_Ctx_IP_ProxyHeader_With_IP_Validation(t *testing.T) {\n\tt.Parallel()\n\n\t// make sure that the same behavior exists for different proxy header names\n\tproxyHeaderNames := []string{\"Real-Ip\", HeaderXForwardedFor}\n\n\tfor _, proxyHeaderName := range proxyHeaderNames {\n\t\tapp := New(Config{\n\t\t\tEnableIPValidation: true,\n\t\t\tProxyHeader:        proxyHeaderName,\n\t\t\tTrustProxy:         true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t\t},\n\t\t})\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0.0.0.0\")}))\n\t\tc := app.AcquireCtx(fastCtx)\n\n\t\t// when proxy header & validation is enabled and the value is a valid IP, we return it\n\t\tc.Request().Header.Set(proxyHeaderName, \"0.0.0.1\")\n\t\trequire.Equal(t, \"0.0.0.1\", c.IP())\n\n\t\t// when proxy header & validation is enabled and the value is a list of IPs, we return the first valid IP\n\t\tc.Request().Header.Set(proxyHeaderName, \"0.0.0.1, 0.0.0.2\")\n\t\trequire.Equal(t, \"0.0.0.1\", c.IP())\n\n\t\tc.Request().Header.Set(proxyHeaderName, \"invalid, 0.0.0.2, 0.0.0.3\")\n\t\trequire.Equal(t, \"0.0.0.2\", c.IP())\n\n\t\t// when proxy header & validation is enabled but the value is empty, we will ignore the header\n\t\tc.Request().Header.Set(proxyHeaderName, \"\")\n\t\trequire.Equal(t, \"0.0.0.0\", c.IP())\n\n\t\t// when proxy header & validation is enabled but the value is not an IP, we will ignore the header\n\t\t// and return the IP of the caller\n\t\tc.Request().Header.Set(proxyHeaderName, \"not-valid-ip\")\n\t\trequire.Equal(t, \"0.0.0.0\", c.IP())\n\t}\n}\n\n// go test -run Test_Ctx_IP_UntrustedProxy\nfunc Test_Ctx_IP_UntrustedProxy(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.8.0.1\"}},\n\t\tProxyHeader:      HeaderXForwardedFor,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderXForwardedFor, \"0.0.0.1\")\n\trequire.Equal(t, \"0.0.0.0\", c.IP())\n}\n\n// go test -run Test_Ctx_IP_TrustedProxy\nfunc Test_Ctx_IP_TrustedProxy(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\"}},\n\t\tProxyHeader:      HeaderXForwardedFor,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderXForwardedFor, \"0.0.0.1\")\n\trequire.Equal(t, \"0.0.0.1\", c.IP())\n}\n\nfunc Test_Ctx_ProxyTrust_UnixRemoteAddr(t *testing.T) {\n\tt.Parallel()\n\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"unix sockets are not supported on windows in this test\")\n\t}\n\n\tt.Run(\"unix_socket_enabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tparts := strings.SplitN(runCtxProxyTrustUnixRemoteAddrCase(t, true), \"|\", 2)\n\t\trequire.Len(t, parts, 2)\n\t\trequire.Equal(t, \"true\", parts[0])\n\t\trequire.Equal(t, \"1.1.1.1\", parts[1])\n\t})\n\n\tt.Run(\"unix_socket_disabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tparts := strings.SplitN(runCtxProxyTrustUnixRemoteAddrCase(t, false), \"|\", 2)\n\t\trequire.Len(t, parts, 2)\n\t\trequire.Equal(t, \"false\", parts[0])\n\t\trequire.Equal(t, \"0.0.0.0\", parts[1])\n\t})\n}\n\nfunc runCtxProxyTrustUnixRemoteAddrCase(t *testing.T, unixSocket bool) string {\n\tt.Helper()\n\n\tapp := New(Config{\n\t\tTrustProxy: true,\n\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\tUnixSocket: unixSocket,\n\t\t},\n\t\tProxyHeader: HeaderXForwardedFor,\n\t})\n\tapp.Get(\"/ip\", func(c Ctx) error {\n\t\treturn c.SendString(fmt.Sprintf(\"%t|%s\", c.IsProxyTrusted(), c.IP()))\n\t})\n\n\ttmp, err := os.MkdirTemp(os.TempDir(), \"fiber-ctx-unix\")\n\trequire.NoError(t, err)\n\tt.Cleanup(func() { require.NoError(t, os.RemoveAll(tmp)) })\n\tsock := filepath.Join(tmp, \"fiber.sock\")\n\n\tresult := make(chan string, 1)\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\n\t\tclient := &fasthttp.HostClient{\n\t\t\tAddr: sock,\n\t\t\tDial: func(addr string) (net.Conn, error) {\n\t\t\t\treturn net.Dial(NetworkUnix, addr)\n\t\t\t},\n\t\t}\n\n\t\treq := &fasthttp.Request{}\n\t\tresp := &fasthttp.Response{}\n\t\treq.SetRequestURI(\"http://fiber/ip\")\n\t\treq.Header.Set(HeaderXForwardedFor, \"1.1.1.1\")\n\n\t\tif err = client.Do(req, resp); err != nil {\n\t\t\tresult <- \"\" // Ensure result channel always receives a value\n\t\t\terrCh <- errors.Join(err, app.Shutdown())\n\t\t\treturn\n\t\t}\n\n\t\tresult <- string(resp.Body())\n\t\terrCh <- app.Shutdown()\n\t}()\n\n\trequire.NoError(t, app.Listen(sock, ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tListenerNetwork:       NetworkUnix,\n\t\tUnixSocketFileMode:    0o660,\n\t}))\n\trequire.NoError(t, <-errCh)\n\n\treturn <-result\n}\n\n// go test -run Test_Ctx_IPs  -parallel\nfunc Test_Ctx_IPs(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// normal happy path test case\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, 127.0.0.2, 127.0.0.3\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\", \"127.0.0.3\"}, c.IPs())\n\n\t// inconsistent space formatting\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1,127.0.0.2  ,127.0.0.3\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\", \"127.0.0.3\"}, c.IPs())\n\n\t// invalid IPs are allowed to be returned\n\tc.Request().Header.Set(HeaderXForwardedFor, \"invalid, 127.0.0.1, 127.0.0.2\")\n\trequire.Equal(t, []string{\"invalid\", \"127.0.0.1\", \"127.0.0.2\"}, c.IPs())\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, invalid, 127.0.0.2\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"invalid\", \"127.0.0.2\"}, c.IPs())\n\n\t// ensure that the ordering of IPs in the header is maintained\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.3, 127.0.0.1, 127.0.0.2\")\n\trequire.Equal(t, []string{\"127.0.0.3\", \"127.0.0.1\", \"127.0.0.2\"}, c.IPs())\n\n\t// ensure for IPv6\n\tc.Request().Header.Set(HeaderXForwardedFor, \"9396:9549:b4f7:8ed0:4791:1330:8c06:e62d, invalid, 2345:0425:2CA1::0567:5673:23b5\")\n\trequire.Equal(t, []string{\"9396:9549:b4f7:8ed0:4791:1330:8c06:e62d\", \"invalid\", \"2345:0425:2CA1::0567:5673:23b5\"}, c.IPs())\n\n\t// empty header\n\tc.Request().Header.Set(HeaderXForwardedFor, \"\")\n\trequire.Empty(t, c.IPs())\n\n\t// missing header\n\tc.Request()\n\trequire.Empty(t, c.IPs())\n}\n\nfunc Test_Ctx_IPs_With_IP_Validation(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{EnableIPValidation: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// normal happy path test case\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, 127.0.0.2, 127.0.0.3\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\", \"127.0.0.3\"}, c.IPs())\n\n\t// inconsistent space formatting\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1,127.0.0.2  ,127.0.0.3\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\", \"127.0.0.3\"}, c.IPs())\n\n\t// invalid IPs are in the header\n\tc.Request().Header.Set(HeaderXForwardedFor, \"invalid, 127.0.0.1, 127.0.0.2\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\"}, c.IPs())\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, invalid, 127.0.0.2\")\n\trequire.Equal(t, []string{\"127.0.0.1\", \"127.0.0.2\"}, c.IPs())\n\n\t// ensure that the ordering of IPs in the header is maintained\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.3, 127.0.0.1, 127.0.0.2\")\n\trequire.Equal(t, []string{\"127.0.0.3\", \"127.0.0.1\", \"127.0.0.2\"}, c.IPs())\n\n\t// ensure for IPv6\n\tc.Request().Header.Set(HeaderXForwardedFor, \"f037:825e:eadb:1b7b:1667:6f0a:5356:f604, invalid, 9396:9549:b4f7:8ed0:4791:1330:8c06:e62d\")\n\trequire.Equal(t, []string{\"f037:825e:eadb:1b7b:1667:6f0a:5356:f604\", \"9396:9549:b4f7:8ed0:4791:1330:8c06:e62d\"}, c.IPs())\n\n\t// empty header\n\tc.Request().Header.Set(HeaderXForwardedFor, \"\")\n\trequire.Empty(t, c.IPs())\n\n\t// missing header\n\tc.Request()\n\trequire.Empty(t, c.IPs())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_IPs -benchmem -count=4\nfunc Benchmark_Ctx_IPs(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, invalid, 127.0.0.1\")\n\tvar res []string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IPs()\n\t}\n\trequire.Equal(b, []string{\"127.0.0.1\", \"invalid\", \"127.0.0.1\"}, res)\n}\n\nfunc Benchmark_Ctx_IPs_v6(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\tc.Request().Header.Set(HeaderXForwardedFor, \"f037:825e:eadb:1b7b:1667:6f0a:5356:f604, invalid, 2345:0425:2CA1::0567:5673:23b5\")\n\tvar res []string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IPs()\n\t}\n\trequire.Equal(b, []string{\"f037:825e:eadb:1b7b:1667:6f0a:5356:f604\", \"invalid\", \"2345:0425:2CA1::0567:5673:23b5\"}, res)\n}\n\nfunc Benchmark_Ctx_IPs_With_IP_Validation(b *testing.B) {\n\tapp := New(Config{EnableIPValidation: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1, invalid, 127.0.0.1\")\n\tvar res []string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IPs()\n\t}\n\trequire.Equal(b, []string{\"127.0.0.1\", \"127.0.0.1\"}, res)\n}\n\nfunc Benchmark_Ctx_IPs_v6_With_IP_Validation(b *testing.B) {\n\tapp := New(Config{EnableIPValidation: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\tc.Request().Header.Set(HeaderXForwardedFor, \"2345:0425:2CA1:0000:0000:0567:5673:23b5, invalid, 2345:0425:2CA1::0567:5673:23b5\")\n\tvar res []string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IPs()\n\t}\n\trequire.Equal(b, []string{\"2345:0425:2CA1:0000:0000:0567:5673:23b5\", \"2345:0425:2CA1::0567:5673:23b5\"}, res)\n}\n\nfunc Benchmark_Ctx_IP_With_ProxyHeader(b *testing.B) {\n\tapp := New(Config{\n\t\tProxyHeader: HeaderXForwardedFor,\n\t\tTrustProxy:  true,\n\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\tLoopback: true,\n\t\t},\n\t})\n\tfastCtx := &fasthttp.RequestCtx{}\n\tfastCtx.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"127.0.0.1\")}))\n\tc := app.AcquireCtx(fastCtx)\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IP()\n\t}\n\trequire.Equal(b, \"127.0.0.1\", res)\n}\n\nfunc Benchmark_Ctx_IP_With_ProxyHeader_and_IP_Validation(b *testing.B) {\n\tapp := New(Config{\n\t\tProxyHeader: HeaderXForwardedFor,\n\t\tTrustProxy:  true,\n\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\tLoopback: true,\n\t\t},\n\t\tEnableIPValidation: true,\n\t})\n\tfastCtx := &fasthttp.RequestCtx{}\n\tfastCtx.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"127.0.0.1\")}))\n\tc := app.AcquireCtx(fastCtx)\n\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IP()\n\t}\n\trequire.Equal(b, \"127.0.0.1\", res)\n}\n\nfunc Benchmark_Ctx_IP(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request()\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.IP()\n\t}\n\trequire.Equal(b, \"0.0.0.0\", res)\n}\n\n// go test -run Test_Ctx_Is\nfunc Test_Ctx_Is(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderContentType, MIMETextHTML+\"; boundary=something\")\n\trequire.True(t, c.Is(\".html\"))\n\trequire.True(t, c.Is(\"html\"))\n\trequire.False(t, c.Is(\"json\"))\n\trequire.False(t, c.Is(\".json\"))\n\trequire.False(t, c.Is(\"\"))\n\trequire.False(t, c.Is(\".foooo\"))\n\n\tc.Request().Header.Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8)\n\trequire.False(t, c.Is(\"html\"))\n\trequire.True(t, c.Is(\"json\"))\n\trequire.True(t, c.Is(\".json\"))\n\n\tc.Request().Header.Set(HeaderContentType, \" application/json;charset=UTF-8\")\n\trequire.False(t, c.Is(\"html\"))\n\trequire.True(t, c.Is(\"json\"))\n\trequire.True(t, c.Is(\".json\"))\n\n\tc.Request().Header.Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8)\n\trequire.False(t, c.Is(\"html\"))\n\trequire.True(t, c.Is(\"xml\"))\n\trequire.True(t, c.Is(\".xml\"))\n\n\tc.Request().Header.Set(HeaderContentType, MIMETextPlain)\n\trequire.False(t, c.Is(\"html\"))\n\trequire.True(t, c.Is(\"txt\"))\n\trequire.True(t, c.Is(\".txt\"))\n\n\t// case-insensitive and trimmed\n\tc.Request().Header.Set(HeaderContentType, \"APPLICATION/JSON; charset=utf-8\")\n\trequire.True(t, c.Is(\"json\"))\n\trequire.True(t, c.Is(\".json\"))\n\n\t// mismatched subtype should not match\n\tc.Request().Header.Set(HeaderContentType, \"application/json+xml\")\n\trequire.False(t, c.Is(\"json\"))\n\trequire.False(t, c.Is(\".json\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Is -benchmem -count=4\nfunc Benchmark_Ctx_Is(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderContentType, MIMEApplicationJSON)\n\tvar res bool\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = c.Is(\".json\")\n\t\tres = c.Is(\"json\")\n\t}\n\trequire.True(b, res)\n}\n\n// go test -run Test_Ctx_Locals\nfunc Test_Ctx_Locals(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\tc.Locals(\"john\", \"doe\")\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"doe\", c.Locals(\"john\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Deadline\nfunc Test_Ctx_Deadline(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tdeadline, ok := c.Deadline()\n\t\trequire.Equal(t, time.Time{}, deadline)\n\t\trequire.False(t, ok)\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Done\nfunc Test_Ctx_Done(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tvar nilChan <-chan struct{}\n\t\trequire.Equal(t, nilChan, c.Done())\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Err\nfunc Test_Ctx_Err(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.NoError(t, c.Err())\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Value\nfunc Test_Ctx_Value(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\tc.Locals(\"john\", \"doe\")\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"doe\", c.Value(\"john\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Value_AfterRelease\nfunc Test_Ctx_Value_AfterRelease(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar ctx Ctx\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tctx = c\n\t\tc.Locals(\"test\", \"value\")\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// After the handler completes, the context is released and fasthttp is nil\n\t// Value should return nil instead of panicking\n\trequire.NotPanics(t, func() {\n\t\tval := ctx.Value(\"test\")\n\t\trequire.Nil(t, val)\n\t})\n}\n\n// go test -run Test_Ctx_Value_InGoroutine\nfunc Test_Ctx_Value_InGoroutine(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tdone := make(chan bool, 1)   // Buffered to prevent goroutine leak\n\terrCh := make(chan error, 1) // Channel to communicate errors from goroutine\n\n\t// Use a synchronization point to avoid race detector complaints\n\t// while still testing the defensive nil behavior\n\tstart := make(chan struct{})\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tc.Locals(\"test\", \"value\")\n\n\t\t// Simulate a goroutine that uses the context (like minio.GetObject)\n\t\tgo func() {\n\t\t\t// Wait for handler to complete and context to be released\n\t\t\t<-start\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in goroutine: %v\", r)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tdone <- true\n\t\t\t}()\n\n\t\t\t// This simulates what happens when minio or other libraries\n\t\t\t// use the fiber.Ctx as a context.Context in a goroutine\n\t\t\t// The Value method should not panic even if fasthttp is nil\n\t\t\tval := c.Value(\"test\")\n\t\t\t// The value might be nil if the context was released\n\t\t\t_ = val\n\t\t}()\n\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// Signal goroutine to proceed - context has been released after app.Test returns\n\t// since the handler (and its deferred ReleaseCtx) has completed\n\tclose(start)\n\n\t// Wait for goroutine to complete with timeout\n\tselect {\n\tcase <-done:\n\t\t// Success - goroutine completed without panic\n\tcase err := <-errCh:\n\t\tt.Fatalf(\"error from goroutine: %v\", err)\n\tcase <-time.After(1 * time.Second):\n\t\tt.Fatal(\"test timed out waiting for goroutine\")\n\t}\n}\n\n// go test -run Test_Ctx_Context\nfunc Test_Ctx_Context(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tt.Run(\"Nil_Context\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := c.Context()\n\t\trequire.Equal(t, ctx, context.Background())\n\t})\n\n\tt.Run(\"ValueContext\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar testKey testContextKey\n\t\ttestValue := \"Test Value\"\n\t\tctx := context.WithValue(context.Background(), testKey, testValue)\n\t\trequire.Equal(t, testValue, ctx.Value(testKey))\n\t})\n}\n\nfunc Test_Ctx_AccessAfterHandlerPanics(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar ctx Ctx\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tctx = c\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Panics(t, func() {\n\t\tctx.Locals(\"foo\")\n\t})\n}\n\nfunc Test_Ctx_Context_AfterHandlerPanics(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar ctx Ctx\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tctx = c\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\t// After the fix, Context() returns context.Background() instead of panicking\n\trequire.NotPanics(t, func() {\n\t\tc := ctx.Context()\n\t\trequire.NotNil(t, c)\n\t\trequire.Equal(t, context.Background(), c)\n\t})\n}\n\n// go test -run Test_Ctx_Request_Response_AfterRelease\nfunc Test_Ctx_Request_Response_AfterRelease(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar ctx Ctx\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tctx = c\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// After the handler completes and context is released,\n\t// Request() and Response() should return nil instead of panicking\n\trequire.NotPanics(t, func() {\n\t\treq := ctx.Request()\n\t\trequire.Nil(t, req)\n\n\t\tres := ctx.Response()\n\t\trequire.Nil(t, res)\n\t})\n}\n\n// go test -run Test_Ctx_SetContext\nfunc Test_Ctx_SetContext(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tvar testKey testContextKey\n\ttestValue := \"Test Value\"\n\tctx := context.WithValue(context.Background(), testKey, testValue)\n\tc.SetContext(ctx)\n\trequire.Equal(t, testValue, c.Context().Value(testKey))\n}\n\ntype contextHelperTestKey struct{}\n\nfunc Test_Ctx_StoreInContext_Config(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"disabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tStoreInContext(c, contextHelperTestKey{}, \"locals-only\")\n\n\t\tvalue, ok := c.Locals(contextHelperTestKey{}).(string)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"locals-only\", value)\n\t\trequire.Nil(t, c.Context().Value(contextHelperTestKey{}))\n\t})\n\n\tt.Run(\"enabled\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{PassLocalsToContext: true})\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tStoreInContext(c, contextHelperTestKey{}, \"both\")\n\n\t\tvalue, ok := c.Locals(contextHelperTestKey{}).(string)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"both\", value)\n\n\t\tcontextValue, ok := c.Context().Value(contextHelperTestKey{}).(string)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"both\", contextValue)\n\t})\n}\n\nfunc Test_Ctx_ValueFromContext_Config(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fiber ctx disabled reads locals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.Locals(contextHelperTestKey{}, \"locals\")\n\t\tc.SetContext(context.WithValue(context.Background(), contextHelperTestKey{}, \"context\"))\n\n\t\tvalue, ok := ValueFromContext[string](c, contextHelperTestKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"locals\", value)\n\t})\n\n\tt.Run(\"fiber ctx enabled still reads locals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{PassLocalsToContext: true})\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.Locals(contextHelperTestKey{}, \"locals\")\n\t\tc.SetContext(context.WithValue(context.Background(), contextHelperTestKey{}, \"context\"))\n\n\t\tvalue, ok := ValueFromContext[string](c, contextHelperTestKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"locals\", value)\n\t})\n\n\tt.Run(\"fiber custom ctx enabled still reads locals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := NewWithCustomCtx(func(app *App) CustomCtx {\n\t\t\treturn &customCtx{DefaultCtx: *NewDefaultCtx(app)}\n\t\t}, Config{PassLocalsToContext: true})\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.Locals(contextHelperTestKey{}, \"locals\")\n\t\tc.SetContext(context.WithValue(context.Background(), contextHelperTestKey{}, \"context\"))\n\n\t\tvalue, ok := ValueFromContext[string](c, contextHelperTestKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"locals\", value)\n\t})\n\n\tt.Run(\"fasthttp request ctx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\traw := &fasthttp.RequestCtx{}\n\t\traw.SetUserValue(contextHelperTestKey{}, \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](raw, contextHelperTestKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n\n\tt.Run(\"context.Context\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx := context.WithValue(context.Background(), contextHelperTestKey{}, \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](ctx, contextHelperTestKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n}\n\n// go test -run Test_Ctx_Context_Multiple_Requests\nfunc Test_Ctx_Context_Multiple_Requests(t *testing.T) {\n\tt.Parallel()\n\tvar testKey testContextKey\n\ttestValue := \"foobar-value\"\n\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tctx := c.Context()\n\n\t\tif ctx.Value(testKey) != nil {\n\t\t\treturn c.SendStatus(StatusInternalServerError)\n\t\t}\n\n\t\tinput := utils.CopyString(Query(c, \"input\", \"NO_VALUE\"))\n\t\tctx = context.WithValue(ctx, testKey, fmt.Sprintf(\"%s_%s\", testValue, input))\n\t\tc.SetContext(ctx)\n\n\t\treturn c.Status(StatusOK).SendString(fmt.Sprintf(\"resp_%s_returned\", input))\n\t})\n\n\t// Consecutive Requests\n\tfor i := 1; i <= 10; i++ {\n\t\tt.Run(fmt.Sprintf(\"request_%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf(\"/?input=%d\", i), http.NoBody))\n\n\t\t\trequire.NoError(t, err, \"Unexpected error from response\")\n\t\t\trequire.Equal(t, StatusOK, resp.StatusCode, \"context.Context returned from c.Context() is reused\")\n\n\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err, \"Unexpected error from reading response body\")\n\t\t\trequire.Equal(t, fmt.Sprintf(\"resp_%d_returned\", i), string(b), \"response text incorrect\")\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_Locals_Generic\nfunc Test_Ctx_Locals_Generic(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\tLocals(c, \"john\", \"doe\")\n\t\tLocals(c, \"age\", 18)\n\t\tLocals(c, \"isHuman\", true)\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"doe\", Locals[string](c, \"john\"))\n\t\trequire.Equal(t, 18, Locals[int](c, \"age\"))\n\t\trequire.True(t, Locals[bool](c, \"isHuman\"))\n\t\trequire.Equal(t, 0, Locals[int](c, \"isHuman\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Locals_GenericCustomStruct\nfunc Test_Ctx_Locals_GenericCustomStruct(t *testing.T) {\n\tt.Parallel()\n\n\ttype User struct {\n\t\tname string\n\t\tage  int\n\t}\n\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\tLocals(c, \"user\", User{name: \"john\", age: 18})\n\t\treturn c.Next()\n\t})\n\tapp.Use(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, User{name: \"john\", age: 18}, Locals[User](c, \"user\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Method\nfunc Test_Ctx_Method(t *testing.T) {\n\tt.Parallel()\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tapp := New()\n\tc := app.AcquireCtx(fctx)\n\n\trequire.Equal(t, MethodGet, c.Method())\n\tc.Method(MethodPost)\n\trequire.Equal(t, MethodPost, c.Method())\n\n\tc.Method(\"MethodInvalid\")\n\trequire.Equal(t, MethodPost, c.Method())\n}\n\n// go test -run Test_Ctx_ClientHelloInfo\nfunc Test_Ctx_ClientHelloInfo(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/ServerName\", func(c Ctx) error {\n\t\tresult := c.ClientHelloInfo()\n\t\tif result != nil {\n\t\t\treturn c.SendString(result.ServerName)\n\t\t}\n\n\t\treturn c.SendString(\"ClientHelloInfo is nil\")\n\t})\n\tapp.Get(\"/SignatureSchemes\", func(c Ctx) error {\n\t\tresult := c.ClientHelloInfo()\n\t\tif result != nil {\n\t\t\treturn c.JSON(result.SignatureSchemes)\n\t\t}\n\n\t\treturn c.SendString(\"ClientHelloInfo is nil\")\n\t})\n\tapp.Get(\"/SupportedVersions\", func(c Ctx) error {\n\t\tresult := c.ClientHelloInfo()\n\t\tif result != nil {\n\t\t\treturn c.JSON(result.SupportedVersions)\n\t\t}\n\n\t\treturn c.SendString(\"ClientHelloInfo is nil\")\n\t})\n\n\t// Test without TLS handler\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/ServerName\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"ClientHelloInfo is nil\"), body)\n\n\t// Test with TLS Handler\n\tconst (\n\t\tpssWithSHA256 = 0x0804\n\t\tversionTLS13  = 0x0304\n\t)\n\tapp.tlsHandler = &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{\n\t\tServerName:        \"example.golang\",\n\t\tSignatureSchemes:  []tls.SignatureScheme{pssWithSHA256},\n\t\tSupportedVersions: []uint16{versionTLS13},\n\t}}\n\n\t// Test ServerName\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/ServerName\", http.NoBody))\n\trequire.NoError(t, err)\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"example.golang\"), body)\n\n\t// Test SignatureSchemes\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/SignatureSchemes\", http.NoBody))\n\trequire.NoError(t, err)\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"[\"+strconv.Itoa(pssWithSHA256)+\"]\", string(body))\n\n\t// Test SupportedVersions\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/SupportedVersions\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"[\"+strconv.Itoa(versionTLS13)+\"]\", string(body))\n}\n\n// go test -run Test_Ctx_InvalidMethod\nfunc Test_Ctx_InvalidMethod(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(\"InvalidMethod\")\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tapp.Handler()(fctx)\n\n\trequire.Equal(t, 501, fctx.Response.StatusCode())\n\trequire.Equal(t, []byte(\"Not Implemented\"), fctx.Response.Body())\n}\n\n// go test -run Test_Ctx_MultipartForm\nfunc Test_Ctx_MultipartForm(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tresult, err := c.MultipartForm()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"john\", result.Value[\"name\"][0])\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\trequire.NoError(t, writer.WriteField(\"name\", \"john\"))\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(HeaderContentType, \"multipart/form-data; boundary=\"+writer.Boundary())\n\treq.Header.Set(HeaderContentLength, strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_MultipartForm -benchmem -count=4\nfunc Benchmark_Ctx_MultipartForm(b *testing.B) {\n\tapp := New()\n\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\t_, err := c.MultipartForm()\n\t\treturn err\n\t})\n\n\tc := &fasthttp.RequestCtx{}\n\n\tbody := []byte(\"--b\\r\\nContent-Disposition: form-data; name=\\\"name\\\"\\r\\n\\r\\njohn\\r\\n--b--\")\n\tc.Request.SetBody(body)\n\tc.Request.Header.SetContentType(MIMEMultipartForm + `;boundary=\"b\"`)\n\tc.Request.Header.SetContentLength(len(body))\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(c)\n\t}\n}\n\n// go test -run Test_Ctx_OriginalURL\nfunc Test_Ctx_OriginalURL(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.SetRequestURI(\"http://google.com/test?search=demo\")\n\trequire.Equal(t, \"http://google.com/test?search=demo\", c.OriginalURL())\n}\n\n// go test -race -run Test_Ctx_Params\nfunc Test_Ctx_Params(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/test/:user\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.Params(\"user\"))\n\t\treturn nil\n\t})\n\tapp.Get(\"/test2/*\", func(c Ctx) error {\n\t\trequire.Equal(t, \"im/a/cookie\", c.Params(\"*\"))\n\t\treturn nil\n\t})\n\tapp.Get(\"/test3/*/blafasel/*\", func(c Ctx) error {\n\t\trequire.Equal(t, \"1111\", c.Params(\"*1\"))\n\t\trequire.Equal(t, 1111, Params(c, \"*1\", 0))\n\t\trequire.Equal(t, \"2222\", c.Params(\"*2\"))\n\t\trequire.Equal(t, 2222, Params(c, \"*2\", 0))\n\t\trequire.Equal(t, \"1111\", c.Params(\"*\"))\n\t\trequire.Equal(t, 1111, Params(c, \"*\", 0))\n\t\treturn nil\n\t})\n\tapp.Get(\"/test4/:optional?\", func(c Ctx) error {\n\t\trequire.Empty(t, c.Params(\"optional\"))\n\t\trequire.Equal(t, \"default\", Params(c, \"optional\", \"default\"))\n\t\treturn nil\n\t})\n\tapp.Get(\"/test5/:id/:Id\", func(c Ctx) error {\n\t\trequire.Equal(t, \"first\", c.Params(\"id\"))\n\t\trequire.Equal(t, \"first\", c.Params(\"Id\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/im/a/cookie\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test3/1111/blafasel/2222\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test4\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test5/first/second\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Ctx_Params_ErrorHandler_Panic_Issue_2832(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tErrorHandler: func(c Ctx, _ error) error {\n\t\t\treturn c.SendString(c.Params(\"user\"))\n\t\t},\n\t\tBodyLimit: 1 * 1024,\n\t})\n\n\tapp.Get(\"/test/:user\", func(_ Ctx) error {\n\t\treturn NewError(StatusInternalServerError, \"error\")\n\t})\n\n\tlargeBody := make([]byte, 2*1024)\n\t_, err := app.Test(httptest.NewRequest(MethodGet, \"/test/john\", bytes.NewReader(largeBody)))\n\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge, \"app.Test(req)\")\n}\n\nfunc Test_Ctx_Params_Case_Sensitive(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{CaseSensitive: true})\n\tapp.Get(\"/test/:User\", func(c Ctx) error {\n\t\trequire.Equal(t, \"john\", c.Params(\"User\"))\n\t\trequire.Empty(t, c.Params(\"user\"))\n\t\treturn nil\n\t})\n\tapp.Get(\"/test2/:id/:Id\", func(c Ctx) error {\n\t\trequire.Equal(t, \"first\", c.Params(\"id\"))\n\t\trequire.Equal(t, \"second\", c.Params(\"Id\"))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test/john\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test2/first/second\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Ctx_Params_Immutable(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{Immutable: true})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.route = &Route{Params: []string{\"user\"}}\n\tc.path = []byte(\"/test/john\")\n\tc.values[0] = c.app.toString(c.path[6:])\n\n\tparam := c.Params(\"user\")\n\tc.path[6] = 'p'\n\tc.path[7] = 'a'\n\tc.path[8] = 'u'\n\tc.path[9] = 'l'\n\n\trequire.Equal(t, \"john\", param)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4\nfunc Benchmark_Ctx_Params(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.route = &Route{\n\t\tParams: []string{\n\t\t\t\"param1\", \"param2\", \"param3\", \"param4\",\n\t\t},\n\t}\n\tc.values = [maxParams]string{\n\t\t\"john\", \"doe\", \"is\", \"awesome\",\n\t}\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = c.Params(\"param1\")\n\t\t_ = c.Params(\"param2\")\n\t\t_ = c.Params(\"param3\")\n\t\tres = c.Params(\"param4\")\n\t}\n\trequire.Equal(b, \"awesome\", res)\n}\n\n// go test -run Test_Ctx_Path\nfunc Test_Ctx_Path(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{UnescapePath: true})\n\tapp.Get(\"/test/:user\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/Test/John\", c.Path())\n\t\trequire.Equal(t, \"/Test/John\", string(c.Request().URI().Path()))\n\t\t// not strict && case-insensitive\n\t\trequire.Equal(t, \"/ABC/\", c.Path(\"/ABC/\"))\n\t\trequire.Equal(t, \"/ABC/\", string(c.Request().URI().Path()))\n\t\trequire.Equal(t, \"/test/john/\", c.Path(\"/test/john/\"))\n\t\trequire.Equal(t, \"/test/john/\", string(c.Request().URI().Path()))\n\t\treturn nil\n\t})\n\n\t// test with special chars\n\tapp.Get(\"/specialChars/:name\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/specialChars/créer\", c.Path())\n\t\t// unescape is also working if you set the path afterwards\n\t\trequire.Equal(t, \"/اختبار/\", c.Path(\"/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1/\"))\n\t\trequire.Equal(t, \"/اختبار/\", string(c.Request().URI().Path()))\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/specialChars/cr%C3%A9er\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Protocol\nfunc Test_Ctx_Protocol(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \"HTTP/1.1\", c.Protocol())\n\n\tc.Request().Header.SetProtocol(\"HTTP/2\")\n\trequire.Equal(t, \"HTTP/2\", c.Protocol())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Protocol -benchmem -count=4\nfunc Benchmark_Ctx_Protocol(b *testing.B) {\n\tapp := New()\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.Protocol()\n\t}\n\n\trequire.Equal(b, \"HTTP/1.1\", res)\n}\n\n// go test -run Test_Ctx_Scheme\nfunc Test_Ctx_Scheme(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tTrustProxy: true,\n\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t},\n\t})\n\n\tfreq := &fasthttp.RequestCtx{}\n\tfreq.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0.0.0.0\")}))\n\tfreq.Request.Header.Set(\"X-Forwarded\", \"invalid\")\n\n\tc := app.AcquireCtx(freq)\n\n\tc.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProto, \"https, http\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, \"https, http\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedSsl, \"on\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n}\n\n// go test -run Test_Ctx_Scheme_HeaderNormalization\nfunc Test_Ctx_Scheme_HeaderNormalization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tTrustProxy: true,\n\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t},\n\t})\n\n\tfreq := &fasthttp.RequestCtx{}\n\tfreq.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0.0.0.0\")}))\n\n\tc := app.AcquireCtx(freq)\n\n\tc.Request().Header.Set(\"x-forwarded-proto\", \" HTTPS , http\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(\"X-FORWARDED-PROTOCOL\", \" HTTPS\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(\"x-url-scheme\", \" HTTPS \")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(\"x-Forwarded-ProToCol\", \" HTTPS \")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Scheme -benchmem -count=4\nfunc Benchmark_Ctx_Scheme(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.Scheme()\n\t}\n\trequire.Equal(b, \"http\", res)\n}\n\n// go test -run Test_Ctx_Scheme_TrustedProxy\nfunc Test_Ctx_Scheme_TrustedProxy(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{TrustProxy: true, TrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0\"}}})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedSsl, \"on\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n}\n\n// go test -run Test_Ctx_Scheme_TrustedProxyRange\nfunc Test_Ctx_Scheme_TrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.0.0.0/30\"}},\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedSsl, \"on\")\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS)\n\trequire.Equal(t, schemeHTTPS, c.Scheme())\n\tc.Request().Header.Reset()\n\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n}\n\n// go test -run Test_Ctx_Scheme_UntrustedProxyRange\nfunc Test_Ctx_Scheme_UntrustedProxyRange(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"1.1.1.1/30\"}},\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedSsl, \"on\")\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n}\n\n// go test -run Test_Ctx_Scheme_UnTrustedProxy\nfunc Test_Ctx_Scheme_UnTrustedProxy(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tTrustProxy:       true,\n\t\tTrustProxyConfig: TrustProxyConfig{Proxies: []string{\"0.8.0.1\"}},\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXForwardedSsl, \"on\")\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\tc.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS)\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n\tc.Request().Header.Reset()\n\n\trequire.Equal(t, schemeHTTP, c.Scheme())\n}\n\n// go test -run Test_Ctx_Query\nfunc Test_Ctx_Query(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().URI().SetQueryString(\"search=john&age=20\")\n\trequire.Equal(t, \"john\", c.Query(\"search\"))\n\trequire.Equal(t, \"20\", c.Query(\"age\"))\n\trequire.Equal(t, \"default\", c.Query(\"unknown\", \"default\"))\n\n\t// test with generic\n\trequire.Equal(t, \"john\", Query[string](c, \"search\"))\n\trequire.Equal(t, \"20\", Query[string](c, \"age\"))\n\trequire.Equal(t, \"default\", Query(c, \"unknown\", \"default\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Query -benchmem -count=4\nfunc Benchmark_Ctx_Query(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().URI().SetQueryString(\"search=john&age=8\")\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = Query[string](c, \"search\")\n\t}\n\trequire.Equal(b, \"john\", res)\n}\n\n// go test -run Test_Ctx_Range\nfunc Test_Ctx_Range(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttestRange := func(header string, ranges ...RangeSet) {\n\t\tc.Request().Header.Set(HeaderRange, header)\n\t\tresult, err := c.Range(1000)\n\t\tif len(ranges) == 0 {\n\t\t\trequire.Error(t, err)\n\t\t} else {\n\t\t\trequire.Equal(t, \"bytes\", result.Type)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t\trequire.Len(t, ranges, len(result.Ranges))\n\t\tfor i := range ranges {\n\t\t\trequire.Equal(t, ranges[i], result.Ranges[i])\n\t\t}\n\t}\n\n\ttestRange(\"bytes=500\")\n\ttestRange(\"bytes=\")\n\ttestRange(\"bytes=500=\")\n\ttestRange(\"bytes=500-300\")\n\ttestRange(\"bytes=a-700\", RangeSet{Start: 300, End: 999})\n\ttestRange(\"bytes=500-b\", RangeSet{Start: 500, End: 999})\n\ttestRange(\"bytes=500-1000\", RangeSet{Start: 500, End: 999})\n\ttestRange(\"bytes=500-700\", RangeSet{Start: 500, End: 700})\n\ttestRange(\"bytes=0-0,2-1000\", RangeSet{Start: 0, End: 0}, RangeSet{Start: 2, End: 999})\n\ttestRange(\"bytes=0-99,450-549,-100\", RangeSet{Start: 0, End: 99}, RangeSet{Start: 450, End: 549}, RangeSet{Start: 900, End: 999})\n\ttestRange(\"bytes=500-700,601-999\", RangeSet{Start: 500, End: 700}, RangeSet{Start: 601, End: 999})\n\ttestRange(\"bytes= 0-1\", RangeSet{Start: 0, End: 1})\n\ttestRange(\"seconds=0-1\")\n}\n\nfunc Test_Ctx_Range_LargeFile(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\tsize := int64(math.MaxInt32) + 1024\n\tstart := int64(math.MaxInt32) + 10\n\tend := start + 50\n\n\tc.Request().Header.Set(HeaderRange, fmt.Sprintf(\"bytes=%d-%d\", start, end))\n\tresult, err := c.Range(size)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"bytes\", result.Type)\n\trequire.Len(t, result.Ranges, 1)\n\trequire.Equal(t, start, result.Ranges[0].Start)\n\trequire.Equal(t, end, result.Ranges[0].End)\n\n\tc.Request().Header.Set(HeaderRange, \"bytes=-200\")\n\tresult, err = c.Range(size)\n\trequire.NoError(t, err)\n\trequire.Equal(t, size-200, result.Ranges[0].Start)\n\trequire.Equal(t, size-1, result.Ranges[0].End)\n}\n\nfunc Test_Ctx_Range_Overflow(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\ttooBig := uint64((math.MaxUint64 >> 1) + 1)\n\n\tc.Request().Header.Set(HeaderRange, fmt.Sprintf(\"bytes=%d-100\", tooBig))\n\t_, err := c.Range(math.MaxInt64)\n\trequire.ErrorIs(t, err, ErrRangeMalformed)\n\n\tc.Request().Header.Set(HeaderRange, fmt.Sprintf(\"bytes=0-%d\", tooBig))\n\t_, err = c.Range(math.MaxInt64)\n\trequire.ErrorIs(t, err, ErrRangeMalformed)\n}\n\nfunc Test_Ctx_Range_Unsatisfiable(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\t_, err := c.Range(10)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"http://example.com/\", http.NoBody)\n\treq.Header.Set(HeaderRange, \"bytes=20-30\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusRequestedRangeNotSatisfiable, resp.StatusCode)\n\trequire.Equal(t, \"bytes */10\", resp.Header.Get(HeaderContentRange))\n}\n\nfunc Test_Ctx_Range_TooManyRanges(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{MaxRanges: 2})\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\t_, err := c.Range(10)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"http://example.com/\", http.NoBody)\n\treq.Header.Set(HeaderRange, \"bytes=0-1,2-3,4-5\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusRequestedRangeNotSatisfiable, resp.StatusCode)\n\trequire.Equal(t, \"bytes */10\", resp.Header.Get(HeaderContentRange))\n}\n\nfunc Test_Ctx_Range_SuffixNormalization(t *testing.T) {\n\tt.Parallel()\n\n\tbody := bytes.Repeat([]byte(\"x\"), 123)\n\n\tnewApp := func() *App {\n\t\tapp := New()\n\n\t\tapp.Get(\"/\", func(c Ctx) error {\n\t\t\trangesHeader := c.Get(HeaderRange)\n\t\t\tif rangesHeader == \"\" {\n\t\t\t\treturn c.Send(body)\n\t\t\t}\n\n\t\t\trangeData, err := c.Range(int64(len(body)))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif len(rangeData.Ranges) != 1 {\n\t\t\t\tc.Status(StatusRequestedRangeNotSatisfiable)\n\t\t\t\tc.Set(HeaderContentRange, fmt.Sprintf(\"bytes */%d\", len(body)))\n\t\t\t\treturn ErrRequestedRangeNotSatisfiable\n\t\t\t}\n\n\t\t\tcurrentRange := rangeData.Ranges[0]\n\t\t\tcontentRange := fmt.Sprintf(\"bytes %d-%d/%d\", currentRange.Start, currentRange.End, len(body))\n\t\t\tc.Set(HeaderContentRange, contentRange)\n\n\t\t\tstatusCode := StatusPartialContent\n\t\t\tif currentRange.Start == 0 && currentRange.End == int64(len(body))-1 {\n\t\t\t\tstatusCode = StatusOK\n\t\t\t}\n\n\t\t\tc.Status(statusCode)\n\t\t\treturn c.Send(body[currentRange.Start : currentRange.End+1])\n\t\t})\n\n\t\treturn app\n\t}\n\n\ttestCases := []struct {\n\t\tname             string\n\t\trangeHeader      string\n\t\tcontentRange     string\n\t\tstatusCode       int\n\t\texpectedBodySize int\n\t}{\n\t\t{\n\t\t\tname:             \"suffix less than size\",\n\t\t\trangeHeader:      \"bytes=-20\",\n\t\t\tcontentRange:     \"bytes 103-122/123\",\n\t\t\tstatusCode:       StatusPartialContent,\n\t\t\texpectedBodySize: 20,\n\t\t},\n\t\t{\n\t\t\tname:             \"suffix equal to size\",\n\t\t\trangeHeader:      \"bytes=-123\",\n\t\t\tcontentRange:     \"bytes 0-122/123\",\n\t\t\tstatusCode:       StatusOK,\n\t\t\texpectedBodySize: 123,\n\t\t},\n\t\t{\n\t\t\tname:             \"suffix larger than size\",\n\t\t\trangeHeader:      \"bytes=-9999\",\n\t\t\tcontentRange:     \"bytes 0-122/123\",\n\t\t\tstatusCode:       StatusOK,\n\t\t\texpectedBodySize: 123,\n\t\t},\n\t\t{\n\t\t\tname:         \"unsatisfiable mixed ranges\",\n\t\t\trangeHeader:  \"bytes=200-400,700-1200\",\n\t\t\tcontentRange: \"bytes */123\",\n\t\t\tstatusCode:   StatusRequestedRangeNotSatisfiable,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := newApp()\n\t\t\treq := httptest.NewRequest(MethodGet, \"http://example.com/\", http.NoBody)\n\t\t\tif tc.rangeHeader != \"\" {\n\t\t\t\treq.Header.Set(HeaderRange, tc.rangeHeader)\n\t\t\t}\n\n\t\t\tresp, err := app.Test(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t\t})\n\n\t\t\trequire.Equal(t, tc.statusCode, resp.StatusCode)\n\t\t\trequire.Equal(t, tc.contentRange, resp.Header.Get(HeaderContentRange))\n\n\t\t\tif tc.expectedBodySize > 0 {\n\t\t\t\tbodyBytes, bodyErr := io.ReadAll(resp.Body)\n\t\t\t\trequire.NoError(t, bodyErr)\n\t\t\t\trequire.Len(t, bodyBytes, tc.expectedBodySize)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Range -benchmem -count=4\nfunc Benchmark_Ctx_Range(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\ttestCases := []struct {\n\t\tstr   string\n\t\tstart int64\n\t\tend   int64\n\t}{\n\t\t{str: \"bytes=-700\", start: 300, end: 999},\n\t\t{str: \"bytes=500-\", start: 500, end: 999},\n\t\t{str: \"bytes=500-1000\", start: 500, end: 999},\n\t\t{str: \"bytes=0-700,800-1000\", start: 0, end: 700},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tb.Run(tc.str, func(b *testing.B) {\n\t\t\tc.Request().Header.Set(HeaderRange, tc.str)\n\t\t\tvar (\n\t\t\t\tresult Range\n\t\t\t\terr    error\n\t\t\t)\n\t\t\tfor b.Loop() {\n\t\t\t\tresult, err = c.Range(1000)\n\t\t\t}\n\t\t\trequire.NoError(b, err)\n\t\t\trequire.Equal(b, \"bytes\", result.Type)\n\t\t\trequire.Equal(b, tc.start, result.Ranges[0].Start)\n\t\t\trequire.Equal(b, tc.end, result.Ranges[0].End)\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_Route\nfunc Test_Ctx_Route(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/test\", c.Route().Path)\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \"/\", c.Route().Path)\n\trequire.Equal(t, MethodGet, c.Route().Method)\n\trequire.Empty(t, c.Route().Handlers)\n}\n\n// go test -run Test_Ctx_FullPath\nfunc Test_Ctx_FullPath(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/test\", c.FullPath())\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_Ctx_FullPath_Group\nfunc Test_Ctx_FullPath_Group(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tgroup := app.Group(\"/v1\")\n\tgroup.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/v1/test\", c.FullPath())\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_Ctx_FullPath_Middleware\nfunc Test_Ctx_FullPath_Middleware(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tvar recorded []string\n\n\tapp.Use(func(c Ctx) error {\n\t\trecorded = append(recorded, c.FullPath())\n\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trecorded = append(recorded, c.FullPath())\n\t\treturn nil\n\t})\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/test\", c.FullPath())\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\trequire.Equal(t, []string{\"/\", \"/test\"}, recorded)\n}\n\n// go test -run Test_Ctx_RouteNormalized\nfunc Test_Ctx_RouteNormalized(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\trequire.Equal(t, \"/test\", c.Route().Path)\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"//test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_SaveFile\nfunc Test_Ctx_SaveFile(t *testing.T) {\n\t// TODO We should clean this up\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tfh, err := c.Req().FormFile(\"file\")\n\t\trequire.NoError(t, err)\n\n\t\ttempFile, err := os.CreateTemp(os.TempDir(), \"test-\")\n\t\trequire.NoError(t, err)\n\n\t\tdefer func(file *os.File) {\n\t\t\tcloseErr := file.Close()\n\t\t\trequire.NoError(t, closeErr)\n\t\t\tcloseErr = os.Remove(file.Name())\n\t\t\trequire.NoError(t, closeErr)\n\t\t}(tempFile)\n\t\terr = c.SaveFile(fh, tempFile.Name())\n\t\trequire.NoError(t, err)\n\n\t\tbs, err := os.ReadFile(tempFile.Name())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"hello world\", string(bs))\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tioWriter, err := writer.CreateFormFile(\"file\", \"test\")\n\trequire.NoError(t, err)\n\n\t_, err = ioWriter.Write([]byte(\"hello world\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\treq.Header.Set(\"Content-Length\", strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc createMultipartFileHeader(t *testing.T, filename string, data []byte) *multipart.FileHeader {\n\tt.Helper()\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tioWriter, err := writer.CreateFormFile(\"file\", filename)\n\trequire.NoError(t, err)\n\n\t_, err = ioWriter.Write(data)\n\trequire.NoError(t, err)\n\trequire.NoError(t, writer.Close())\n\n\tmultipartReader := multipart.NewReader(bytes.NewReader(body.Bytes()), writer.Boundary())\n\tform, err := multipartReader.ReadForm(int64(len(body.Bytes())))\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, form.RemoveAll())\n\t})\n\n\tfiles := form.File[\"file\"]\n\trequire.Len(t, files, 1)\n\n\treturn files[0]\n}\n\n// go test -run Test_Ctx_SaveFileToStorage\nfunc Test_Ctx_SaveFileToStorage(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tstorage := memory.New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tfh, err := c.FormFile(\"file\")\n\t\trequire.NoError(t, err)\n\n\t\terr = c.SaveFileToStorage(fh, \"test\", storage)\n\t\trequire.NoError(t, err)\n\n\t\tfile, err := storage.Get(\"test\")\n\t\trequire.Equal(t, []byte(\"hello world\"), file)\n\t\trequire.NoError(t, err)\n\n\t\terr = storage.Delete(\"test\")\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tioWriter, err := writer.CreateFormFile(\"file\", \"test\")\n\trequire.NoError(t, err)\n\n\t_, err = ioWriter.Write([]byte(\"hello world\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\treq.Header.Set(\"Content-Length\", strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Ctx_SaveFileToStorage_LargeUpload(t *testing.T) {\n\tt.Parallel()\n\tconst (\n\t\tbodyLimit = 8 * 1024 * 1024\n\t\tfileSize  = 5 * 1024 * 1024\n\t)\n\n\tapp := New(Config{BodyLimit: bodyLimit})\n\tstorage := memory.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\tfileHeader := createMultipartFileHeader(t, \"large.bin\", bytes.Repeat([]byte{'a'}, fileSize))\n\n\terr := ctx.SaveFileToStorage(fileHeader, \"test\", storage)\n\trequire.NoError(t, err)\n\n\tstored, err := storage.Get(\"test\")\n\trequire.NoError(t, err)\n\trequire.Len(t, stored, fileSize)\n}\n\nfunc Test_Ctx_SaveFileToStorage_LimitExceeded(t *testing.T) {\n\tt.Parallel()\n\tconst (\n\t\tallowedSize = 1024\n\t\tfileSize    = allowedSize + 512\n\t)\n\n\tapp := New(Config{BodyLimit: allowedSize})\n\n\tstorage := memory.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\tfileHeader := createMultipartFileHeader(t, \"too-large.bin\", bytes.Repeat([]byte{'a'}, fileSize))\n\n\terr := ctx.SaveFileToStorage(fileHeader, \"test\", storage)\n\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)\n}\n\nfunc Test_Ctx_SaveFileToStorage_LimitExceededUnknownSize(t *testing.T) {\n\tt.Parallel()\n\tconst (\n\t\tallowedSize = 1024\n\t\tfileSize    = allowedSize + 256\n\t)\n\n\tapp := New(Config{BodyLimit: allowedSize})\n\n\tstorage := memory.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\tfileHeader := createMultipartFileHeader(t, \"unknown-size.bin\", bytes.Repeat([]byte{'a'}, fileSize))\n\tfileHeader.Size = -1\n\n\terr := ctx.SaveFileToStorage(fileHeader, \"test\", storage)\n\trequire.ErrorIs(t, err, fasthttp.ErrBodyTooLarge)\n}\n\ntype captureStorage struct {\n\tt    *testing.T\n\tdata map[string][]byte\n}\n\nfunc (s *captureStorage) helperFailure(msg string, args ...any) {\n\ts.t.Helper()\n\ts.t.Fatalf(msg, args...)\n}\n\nfunc (s *captureStorage) ensureStore(key string, val []byte) {\n\ts.t.Helper()\n\tif key == \"\" || len(val) == 0 {\n\t\treturn\n\t}\n\n\tif s.data == nil {\n\t\ts.data = make(map[string][]byte)\n\t}\n\n\ts.data[key] = val\n}\n\nfunc (s *captureStorage) GetWithContext(context.Context, string) ([]byte, error) {\n\ts.helperFailure(\"unexpected call to GetWithContext\")\n\treturn nil, nil\n}\n\nfunc (s *captureStorage) Get(string) ([]byte, error) {\n\ts.helperFailure(\"unexpected call to Get\")\n\treturn nil, nil\n}\n\nfunc (s *captureStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\ts.ensureStore(key, val)\n\treturn nil\n}\n\nfunc (s *captureStorage) Set(key string, _ []byte, _ time.Duration) error {\n\ts.helperFailure(\"unexpected call to Set for key %q\", key)\n\treturn nil\n}\n\nfunc (s *captureStorage) DeleteWithContext(context.Context, string) error {\n\ts.helperFailure(\"unexpected call to DeleteWithContext\")\n\treturn nil\n}\n\nfunc (s *captureStorage) Delete(string) error {\n\ts.helperFailure(\"unexpected call to Delete\")\n\treturn nil\n}\n\nfunc (s *captureStorage) ResetWithContext(context.Context) error {\n\ts.data = nil\n\treturn nil\n}\n\nfunc (s *captureStorage) Reset() error {\n\ts.data = nil\n\treturn nil\n}\n\nfunc (s *captureStorage) Close() error {\n\tif s == nil {\n\t\treturn nil\n\t}\n\n\ts.data = nil\n\treturn nil\n}\n\nfunc Test_Ctx_SaveFileToStorage_BufferNotReused(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tstorage := &captureStorage{t: t}\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tt.Cleanup(func() {\n\t\tapp.ReleaseCtx(ctx)\n\t})\n\n\tconst payloadSize = 1024\n\tfirstPayload := bytes.Repeat([]byte{'a'}, payloadSize)\n\tsecondPayload := bytes.Repeat([]byte{'b'}, payloadSize)\n\n\tfirstHeader := createMultipartFileHeader(t, \"first.bin\", firstPayload)\n\trequire.NoError(t, ctx.SaveFileToStorage(firstHeader, \"first\", storage))\n\n\tfirstStored := storage.data[\"first\"]\n\trequire.Equal(t, firstPayload, firstStored)\n\n\tsecondHeader := createMultipartFileHeader(t, \"second.bin\", secondPayload)\n\trequire.NoError(t, ctx.SaveFileToStorage(secondHeader, \"second\", storage))\n\trequire.Equal(t, secondPayload, storage.data[\"second\"])\n\n\trequire.Equal(t, firstPayload, firstStored, \"stored data must not rely on pooled buffers\")\n}\n\ntype mockContextAwareStorage struct {\n\tt              *testing.T\n\tkey            any\n\texpectedValue  any\n\tvalidateCtx    func(context.Context)\n\tcancel         context.CancelFunc\n\tctxMatched     atomic.Bool\n\tcancelObserved atomic.Bool\n}\n\nfunc (s *mockContextAwareStorage) helperFailure(msg string, args ...any) {\n\ts.t.Helper()\n\ts.t.Fatalf(msg, args...)\n}\n\nfunc (s *mockContextAwareStorage) GetWithContext(context.Context, string) ([]byte, error) {\n\ts.helperFailure(\"unexpected call to GetWithContext\")\n\treturn nil, nil\n}\n\nfunc (s *mockContextAwareStorage) Get(string) ([]byte, error) {\n\ts.helperFailure(\"unexpected call to Get\")\n\treturn nil, nil\n}\n\nfunc (s *mockContextAwareStorage) SetWithContext(ctx context.Context, _ string, _ []byte, _ time.Duration) error {\n\ts.t.Helper()\n\tif s.validateCtx == nil {\n\t\ts.helperFailure(\"validateCtx must be configured before SetWithContext\")\n\t}\n\ts.validateCtx(ctx)\n\tif val := ctx.Value(s.key); val != s.expectedValue {\n\t\ts.helperFailure(\"storage observed unexpected context value: %v\", val)\n\t}\n\ts.ctxMatched.Store(true)\n\tif s.cancel != nil {\n\t\ts.cancel()\n\t}\n\tselect {\n\tcase <-ctx.Done():\n\t\ts.cancelObserved.Store(true)\n\tcase <-time.After(100 * time.Millisecond):\n\t\ts.helperFailure(\"storage did not observe context cancellation\")\n\t}\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) Set(string, []byte, time.Duration) error {\n\ts.helperFailure(\"unexpected call to Set\")\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) DeleteWithContext(context.Context, string) error {\n\ts.helperFailure(\"unexpected call to DeleteWithContext\")\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) Delete(string) error {\n\ts.helperFailure(\"unexpected call to Delete\")\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) ResetWithContext(context.Context) error {\n\ts.helperFailure(\"unexpected call to ResetWithContext\")\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) Reset() error {\n\ts.helperFailure(\"unexpected call to Reset\")\n\treturn nil\n}\n\nfunc (s *mockContextAwareStorage) Close() error {\n\tif s == nil {\n\t\treturn nil\n\t}\n\treturn nil\n}\n\n// go test -run Test_Ctx_SaveFileToStorage_ContextPropagation\nfunc Test_Ctx_SaveFileToStorage_ContextPropagation(t *testing.T) {\n\tt.Parallel()\n\n\ttype ctxKeyType string\n\n\tconst ctxKey ctxKeyType = \"storage-context-key\"\n\n\tstorage := &mockContextAwareStorage{t: t, key: ctxKey, expectedValue: \"expected-context-value\"}\n\tapp := New()\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tfh, err := c.FormFile(\"file\")\n\t\trequire.NoError(t, err)\n\n\t\tctxWithValue := context.WithValue(context.Background(), ctxKey, storage.expectedValue)\n\t\tctx, cancel := context.WithCancel(ctxWithValue)\n\t\tstorage.validateCtx = func(received context.Context) {\n\t\t\tif received != ctx {\n\t\t\t\tstorage.helperFailure(\"storage received unexpected context instance\")\n\t\t\t}\n\t\t}\n\t\tstorage.cancel = cancel\n\n\t\tc.SetContext(ctx)\n\n\t\terr = c.SaveFileToStorage(fh, \"test\", storage)\n\t\trequire.NoError(t, err)\n\n\t\trequire.True(t, storage.ctxMatched.Load(), \"storage should receive the context installed on Ctx\")\n\t\trequire.True(t, storage.cancelObserved.Load(), \"storage should observe context cancellation\")\n\n\t\treturn nil\n\t})\n\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\tioWriter, err := writer.CreateFormFile(\"file\", \"test\")\n\trequire.NoError(t, err)\n\n\t_, err = ioWriter.Write([]byte(\"hello world\"))\n\trequire.NoError(t, err)\n\trequire.NoError(t, writer.Close())\n\n\treq := httptest.NewRequest(MethodPost, \"/test\", body)\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\treq.Header.Set(\"Content-Length\", strconv.Itoa(len(body.Bytes())))\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Secure\nfunc Test_Ctx_Secure(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// TODO Add TLS conn\n\trequire.False(t, c.Secure())\n}\n\n// go test -run Test_Ctx_Stale\nfunc Test_Ctx_Stale(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.True(t, c.Stale())\n}\n\n// go test -run Test_Ctx_Subdomains\nfunc Test_Ctx_Subdomains(t *testing.T) {\n\tapp := New()\n\n\ttype tc struct {\n\t\tname   string\n\t\thost   string\n\t\toffset []int // nil ⇒ call without argument\n\t\twant   []string\n\t}\n\n\tcases := []tc{\n\t\t{\n\t\t\tname:   \"default offset (2) drops registrable domain + TLD\",\n\t\t\thost:   \"john.doe.is.awesome.google.com\",\n\t\t\toffset: nil, // Subdomains()\n\t\t\twant:   []string{\"john\", \"doe\", \"is\", \"awesome\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"custom offset trims N right-hand labels\",\n\t\t\thost:   \"john.doe.is.awesome.google.com\",\n\t\t\toffset: []int{4},\n\t\t\twant:   []string{\"john\", \"doe\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"offset too high returns empty\",\n\t\t\thost:   \"john.doe.is.awesome.google.com\",\n\t\t\toffset: []int{10},\n\t\t\twant:   []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"zero offset returns all labels\",\n\t\t\thost:   \"john.doe.google.com\",\n\t\t\toffset: []int{0},\n\t\t\twant:   []string{\"john\", \"doe\", \"google\", \"com\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"offset 1 keeps registrable domain\",\n\t\t\thost:   \"john.doe.google.com\",\n\t\t\toffset: []int{1},\n\t\t\twant:   []string{\"john\", \"doe\", \"google\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"negative offset returns empty\",\n\t\t\thost:   \"john.doe.google.com\",\n\t\t\toffset: []int{-1},\n\t\t\twant:   []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"offset equal len returns empty\",\n\t\t\thost:   \"john.doe.com\",\n\t\t\toffset: []int{3},\n\t\t\twant:   []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"offset equal len returns empty\",\n\t\t\thost:   \"john.doe.com\",\n\t\t\toffset: []int{3},\n\t\t\twant:   []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"zero offset returns all labels with port present\",\n\t\t\thost:   \"localhost:3000\",\n\t\t\toffset: []int{0},\n\t\t\twant:   []string{\"localhost\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"host with port — custom offset trims 2 labels\",\n\t\t\thost:   \"foo.bar.example.com:8080\",\n\t\t\toffset: []int{2},\n\t\t\twant:   []string{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"fully qualified domain trims trailing dot\",\n\t\t\thost:   \"john.doe.example.com.\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{\"john\", \"doe\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"punycode domain is decoded\",\n\t\t\thost:   \"xn--bcher-kva.example.com\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{\"bücher\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"punycode fqdn is decoded\",\n\t\t\thost:   \"xn--bcher-kva.example.com.\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{\"bücher\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"punycode decode failure uses fallback\",\n\t\t\thost:   \"xn--bcher--.example.com\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{\"xn--bcher--\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid host keeps original lowercased\",\n\t\t\thost:   \"Foo Bar\",\n\t\t\toffset: []int{0},\n\t\t\twant:   []string{\"foo bar\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"IPv4 host returns empty\",\n\t\t\thost:   \"192.168.0.1\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"IPv6 host returns empty\",\n\t\t\thost:   \"[2001:db8::1]\",\n\t\t\toffset: nil,\n\t\t\twant:   []string{},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefer app.ReleaseCtx(c)\n\n\t\t\tc.Request().URI().SetHost(tc.host)\n\t\t\tgot := c.Subdomains(tc.offset...)\n\t\t\trequire.Equal(t, tc.want, got)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Subdomains -benchmem -count=4\nfunc Benchmark_Ctx_Subdomains(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetRequestURI(\"http://john.doe.google.com\")\n\tvar res []string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tres = c.Subdomains()\n\t}\n\trequire.Equal(b, []string{\"john\", \"doe\"}, res)\n}\n\n// go test -run Test_Ctx_ClearCookie\nfunc Test_Ctx_ClearCookie(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderCookie, \"john=doe\")\n\tc.Res().ClearCookie(\"john\")\n\trequire.True(t, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), \"john=; expires=\"))\n\n\tc.Request().Header.Set(HeaderCookie, \"test1=dummy\")\n\tc.Request().Header.Set(HeaderCookie, \"test2=dummy\")\n\tc.ClearCookie()\n\trequire.Contains(t, string(c.Response().Header.Peek(HeaderSetCookie)), \"test1=; expires=\")\n\trequire.Contains(t, string(c.Response().Header.Peek(HeaderSetCookie)), \"test2=; expires=\")\n}\n\n// go test -race -run Test_Ctx_Download\nfunc Test_Ctx_Download(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.NoError(t, c.Download(\"ctx.go\", \"Awesome File!\"))\n\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\texpect, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\trequire.Equal(t, expect, c.Response().Body())\n\trequire.Equal(t, `attachment; filename=\"Awesome+File%21\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\n\trequire.NoError(t, c.Res().Download(\"ctx.go\"))\n\trequire.Equal(t, `attachment; filename=\"ctx.go\"`, string(c.Response().Header.Peek(HeaderContentDisposition)))\n\n\trequire.NoError(t, c.Download(\"ctx.go\", \"файл.txt\"))\n\theader := string(c.Response().Header.Peek(HeaderContentDisposition))\n\trequire.Contains(t, header, `filename=\"файл.txt\"`)\n\trequire.Contains(t, header, `filename*=UTF-8''%D1%84%D0%B0%D0%B9%D0%BB.txt`)\n}\n\n// go test -race -run Test_Ctx_Download_SanitizesFilenameControls\nfunc Test_Ctx_Download_SanitizesFilenameControls(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\ttestCases := []struct {\n\t\tname     string\n\t\tfilename string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"base name only\",\n\t\t\tfilename: \"../docs/archive.tar.gz\",\n\t\t\texpected: `attachment; filename=\"archive.tar.gz\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"controls stripped\",\n\t\t\tfilename: \"down\\r\\nload\\t\\x00.txt\",\n\t\t\texpected: `attachment; filename=\"download.txt\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty after sanitize\",\n\t\t\tfilename: \"\\r\\n\\t\\x00\",\n\t\t\texpected: `attachment; filename=\"download\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"dot fallback\",\n\t\t\tfilename: \".\",\n\t\t\texpected: `attachment; filename=\"download\"`,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\trequire.NoError(t, c.Download(\"ctx.go\", tc.filename))\n\t\t\theader := string(c.Response().Header.Peek(HeaderContentDisposition))\n\t\t\trequire.Equal(t, tc.expected, header)\n\t\t\trequire.NotContains(t, header, \"\\r\")\n\t\t\trequire.NotContains(t, header, \"\\n\")\n\t\t\trequire.NotContains(t, header, \"\\t\")\n\t\t\trequire.NotContains(t, header, \"\\x00\")\n\t\t})\n\t}\n}\n\n// go test -race -run Test_Ctx_SendEarlyHints\nfunc Test_Ctx_SendEarlyHints(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\thints := []string{\"<https://cdn.com>; rel=preload; as=script\"}\n\tapp.Get(\"/earlyhints\", func(c Ctx) error {\n\t\terr := c.SendEarlyHints(hints)\n\t\trequire.NoError(t, err, \"SendEarlyHints\")\n\t\tc.Status(StatusBadRequest)\n\t\treturn c.SendString(\"fail\")\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/earlyhints\", http.NoBody)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusBadRequest, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, hints, resp.Header[\"Link\"], \"Link header\")\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"fail\", string(body))\n}\n\n// go test -race -run Test_Ctx_SendFile\nfunc Test_Ctx_SendFile(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// fetch file content\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\texpectFileContent, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\t// fetch file info for the not modified test case\n\tfI, err := os.Stat(\"./ctx.go\")\n\trequire.NoError(t, err)\n\n\t// simple test case\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"ctx.go\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectFileContent, c.Response().Body())\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\tapp.ReleaseCtx(c)\n\n\t// test with custom error code\n\tc = app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.Res().Status(StatusInternalServerError).SendFile(\"ctx.go\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectFileContent, c.Response().Body())\n\trequire.Equal(t, StatusInternalServerError, c.Response().StatusCode())\n\tapp.ReleaseCtx(c)\n\n\t// test not modified\n\tc = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(HeaderIfModifiedSince, fI.ModTime().Format(time.RFC1123))\n\terr = c.SendFile(\"ctx.go\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNotModified, c.Response().StatusCode())\n\trequire.Equal(t, []byte(nil), c.Response().Body())\n\tapp.ReleaseCtx(c)\n}\n\n// go test -race -run Test_Ctx_SendFile_ContentType\nfunc Test_Ctx_SendFile_ContentType(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// 1) simple case\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr := c.Res().SendFile(\"./.github/testdata/fs/img/fiber.png\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\trequire.Equal(t, \"image/png\", string(c.Response().Header.Peek(HeaderContentType)))\n\tapp.ReleaseCtx(c)\n\n\t// 2) set by valid file extension, not file header\n\t// see: https://github.com/valyala/fasthttp/blob/d795f13985f16622a949ea9fc3459cf54dc78b3e/fs.go#L1638\n\tc = app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"./.github/testdata/fs/img/fiberpng.jpeg\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\trequire.Equal(t, \"image/jpeg\", string(c.Response().Header.Peek(HeaderContentType)))\n\tapp.ReleaseCtx(c)\n\n\t// 3) set by file header if extension is invalid\n\tc = app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"./.github/testdata/fs/img/fiberpng.notvalidext\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\trequire.Equal(t, \"image/png\", string(c.Response().Header.Peek(HeaderContentType)))\n\tapp.ReleaseCtx(c)\n\n\t// 4) set by file header if extension is missing\n\tc = app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"./.github/testdata/fs/img/fiberpng\")\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\trequire.Equal(t, \"image/png\", string(c.Response().Header.Peek(HeaderContentType)))\n\tapp.ReleaseCtx(c)\n}\n\nfunc Test_Ctx_SendFile_Download(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// fetch file content\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\texpectFileContent, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\t// fetch file info for the not modified test case\n\t_, err = os.Stat(\"./ctx.go\")\n\trequire.NoError(t, err)\n\n\t// simple test case\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"ctx.go\", SendFile{\n\t\tDownload: true,\n\t})\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectFileContent, c.Response().Body())\n\trequire.Equal(t, \"attachment\", string(c.Response().Header.Peek(HeaderContentDisposition)))\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\tapp.ReleaseCtx(c)\n}\n\nfunc Test_Ctx_SendFile_MaxAge(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// fetch file content\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\texpectFileContent, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\n\t// fetch file info for the not modified test case\n\t_, err = os.Stat(\"./ctx.go\")\n\trequire.NoError(t, err)\n\n\t// simple test case\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\terr = c.SendFile(\"ctx.go\", SendFile{\n\t\tMaxAge: 100,\n\t})\n\n\t// check expectation\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectFileContent, c.Response().Body())\n\trequire.Equal(t, \"public, max-age=100\", string(c.RequestCtx().Response.Header.Peek(HeaderCacheControl)), \"CacheControl Control\")\n\trequire.Equal(t, StatusOK, c.Response().StatusCode())\n\tapp.ReleaseCtx(c)\n}\n\nfunc Test_Static_Compress(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/file\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\tCompress: true,\n\t\t})\n\t})\n\n\t// Note: deflate is not supported by fasthttp.FS\n\talgorithms := []string{\"zstd\", \"gzip\", \"br\"}\n\tfor _, algo := range algorithms {\n\t\tt.Run(algo+\"_compression\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\treq := httptest.NewRequest(MethodGet, \"/file\", http.NoBody)\n\t\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\t\t\tresp, err := app.Test(req, TestConfig{\n\t\t\t\tTimeout:       10 * time.Second,\n\t\t\t\tFailOnTimeout: true,\n\t\t\t})\n\n\t\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\t\trequire.NotEqual(t, \"58726\", resp.Header.Get(HeaderContentLength))\n\t\t})\n\t}\n}\n\nfunc Test_Ctx_SendFile_Compress_CheckCompressed(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// fetch file content\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, f.Close())\n\t})\n\n\texpectedFileContent, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\n\tsendFileBodyReader := func(compression string) ([]byte, error) {\n\t\tt.Helper()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.Request().Header.Add(HeaderAcceptEncoding, compression)\n\n\t\terr := c.SendFile(\"./ctx.go\", SendFile{\n\t\t\tCompress: true,\n\t\t})\n\n\t\treturn c.Response().Body(), err\n\t}\n\n\tt.Run(\"gzip\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tb, err := sendFileBodyReader(\"gzip\")\n\t\trequire.NoError(t, err)\n\t\tbody, err := fasthttp.AppendGunzipBytes(nil, b)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, expectedFileContent, body)\n\t})\n\n\tt.Run(\"zstd\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tb, err := sendFileBodyReader(\"zstd\")\n\t\trequire.NoError(t, err)\n\t\tbody, err := fasthttp.AppendUnzstdBytes(nil, b)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, expectedFileContent, body)\n\t})\n\n\tt.Run(\"br\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tb, err := sendFileBodyReader(\"br\")\n\t\trequire.NoError(t, err)\n\t\tbody, err := fasthttp.AppendUnbrotliBytes(nil, b)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, expectedFileContent, body)\n\t})\n}\n\n//go:embed ctx.go\nvar embedFile embed.FS\n\nfunc Test_Ctx_SendFile_EmbedFS(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tf, err := os.Open(\"./ctx.go\")\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\trequire.NoError(t, f.Close())\n\t}()\n\n\texpectFileContent, err := io.ReadAll(f)\n\trequire.NoError(t, err)\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\tFS: embedFile,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectFileContent, body)\n}\n\n// go test -race -run Test_Ctx_SendFile_404\nfunc Test_Ctx_SendFile_404(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx12.go\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNotFound, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"sendfile: file ctx12.go not found\", string(body))\n}\n\nfunc Test_Ctx_SendFile_Multiple(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tswitch c.Query(\"file\") {\n\t\tcase \"1\":\n\t\t\treturn c.SendFile(\"ctx.go\")\n\t\tcase \"2\":\n\t\t\treturn c.SendFile(\"app.go\")\n\t\tcase \"3\":\n\t\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\t\tDownload: true,\n\t\t\t})\n\t\tcase \"4\":\n\t\t\treturn c.SendFile(\"app_test.go\", SendFile{\n\t\t\t\tFS: os.DirFS(\".\"),\n\t\t\t})\n\t\tdefault:\n\t\t\treturn c.SendStatus(StatusNotFound)\n\t\t}\n\t})\n\n\tapp.Get(\"/test2\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\tDownload: true,\n\t\t})\n\t})\n\n\ttestCases := []struct {\n\t\turl                string\n\t\tbody               string\n\t\tcontentDisposition string\n\t}{\n\t\t{url: \"/test?file=1\", body: \"type DefaultCtx struct\", contentDisposition: \"\"},\n\t\t{url: \"/test?file=2\", body: \"type App struct\", contentDisposition: \"\"},\n\t\t{url: \"/test?file=3\", body: \"type DefaultCtx struct\", contentDisposition: \"attachment\"},\n\t\t{url: \"/test?file=4\", body: \"Test_App_MethodNotAllowed\", contentDisposition: \"\"},\n\t\t{url: \"/test2\", body: \"type DefaultCtx struct\", contentDisposition: \"attachment\"},\n\t\t{url: \"/test2\", body: \"type DefaultCtx struct\", contentDisposition: \"attachment\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, tc.url, http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t\trequire.Equal(t, tc.contentDisposition, resp.Header.Get(HeaderContentDisposition))\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(body), tc.body)\n\t}\n\n\tapp.sendfilesMutex.RLock()\n\tdefer app.sendfilesMutex.RUnlock()\n\trequire.Len(t, app.sendfiles, 3)\n}\n\n// go test -race -run Test_Ctx_SendFile_Immutable\nfunc Test_Ctx_SendFile_Immutable(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar endpointsForTest []string\n\taddEndpoint := func(file, endpoint string) {\n\t\tendpointsForTest = append(endpointsForTest, endpoint)\n\t\tapp.Get(endpoint, func(c Ctx) error {\n\t\t\tif err := c.SendFile(file); err != nil {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn c.SendStatus(200)\n\t\t})\n\t}\n\n\t// relative paths\n\taddEndpoint(\"./.github/index.html\", \"/relativeWithDot\")\n\taddEndpoint(filepath.FromSlash(\"./.github/index.html\"), \"/relativeOSWithDot\")\n\taddEndpoint(\".github/index.html\", \"/relative\")\n\taddEndpoint(filepath.FromSlash(\".github/index.html\"), \"/relativeOS\")\n\n\t// absolute paths\n\tif path, err := filepath.Abs(\".github/index.html\"); err != nil {\n\t\trequire.NoError(t, err)\n\t} else {\n\t\taddEndpoint(path, \"/absolute\")\n\t\taddEndpoint(filepath.FromSlash(path), \"/absoluteOS\") // os related\n\t}\n\n\tfor _, endpoint := range endpointsForTest {\n\t\tt.Run(endpoint, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// 1st try\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, endpoint, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t\t\t// 2nd try\n\t\t\tresp, err = app.Test(httptest.NewRequest(MethodGet, endpoint, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t\t})\n\t}\n}\n\n// go test -race -run Test_Ctx_SendFile_RestoreOriginalURL\nfunc Test_Ctx_SendFile_RestoreOriginalURL(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\toriginalURL := utils.CopyString(c.OriginalURL())\n\t\terr := c.SendFile(\"ctx.go\")\n\t\trequire.Equal(t, originalURL, c.OriginalURL())\n\t\treturn err\n\t})\n\n\t_, err1 := app.Test(httptest.NewRequest(MethodGet, \"/?test=true\", http.NoBody))\n\t// second request required to confirm with zero allocation\n\t_, err2 := app.Test(httptest.NewRequest(MethodGet, \"/?test=true\", http.NoBody))\n\n\trequire.NoError(t, err1)\n\trequire.NoError(t, err2)\n}\n\nfunc Test_SendFile_withRoutes(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/file\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\")\n\t})\n\n\tapp.Get(\"/file/download\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\tDownload: true,\n\t\t})\n\t})\n\n\tapp.Get(\"/file/fs\", func(c Ctx) error {\n\t\treturn c.SendFile(\"ctx.go\", SendFile{\n\t\t\tFS: os.DirFS(\".\"),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/file\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/file/download\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"attachment\", resp.Header.Get(HeaderContentDisposition))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/file/fs\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\nfunc Test_SendFile_ByteRange(t *testing.T) {\n\tif runtime.GOOS == windowsOS {\n\t\tt.Skip(\"SendFile byte-range tests are flaky on Windows\")\n\t}\n\n\tcontent := []byte(\"0123456789\")\n\ttmpDir := t.TempDir()\n\tfixture := filepath.Join(tmpDir, \"fixture.txt\")\n\trequire.NoError(t, os.WriteFile(fixture, content, 0o600))\n\n\tapp := New()\n\n\tapp.Get(\"/range\", func(c Ctx) error {\n\t\treturn c.SendFile(fixture, SendFile{ByteRange: true})\n\t})\n\n\tapp.Get(\"/norange\", func(c Ctx) error {\n\t\treturn c.SendFile(fixture)\n\t})\n\n\tt.Run(\"satisfiable single range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=0-4\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 0-4/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content[:5]), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content[:5], body)\n\t})\n\n\tt.Run(\"single byte range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=4-4\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 4-4/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, 1, resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content[4:5], body)\n\t})\n\n\tt.Run(\"open ended range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=4-\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 4-9/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content[4:]), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content[4:], body)\n\t})\n\n\tt.Run(\"range exceeding end\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=5-20\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 5-9/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content[5:]), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content[5:], body)\n\t})\n\n\tt.Run(\"suffix range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=-3\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 7-9/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content[len(content)-3:]), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content[len(content)-3:], body)\n\t})\n\n\tt.Run(\"suffix range exceeding size\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=-20\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusPartialContent, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes\", resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Equal(t, \"bytes 0-9/10\", resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content, body)\n\t})\n\n\tt.Run(\"unsatisfiable range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=1000-2000\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusRequestedRangeNotSatisfiable, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes */10\", resp.Header.Get(HeaderContentRange))\n\t})\n\n\tt.Run(\"unsatisfiable reversed range\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=6-5\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusRequestedRangeNotSatisfiable, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes */10\", resp.Header.Get(HeaderContentRange))\n\t})\n\n\tt.Run(\"unsatisfiable start past end\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/range\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=10-\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusRequestedRangeNotSatisfiable, resp.StatusCode)\n\t\trequire.Equal(t, \"bytes */10\", resp.Header.Get(HeaderContentRange))\n\t})\n\n\tt.Run(\"range ignored when byte range disabled\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/norange\", http.NoBody)\n\t\treq.Header.Set(HeaderRange, \"bytes=0-4\")\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tdefer func() {\n\t\t\trequire.NoError(t, resp.Body.Close())\n\t\t}()\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t\trequire.Empty(t, resp.Header.Get(HeaderAcceptRanges))\n\t\trequire.Empty(t, resp.Header.Get(HeaderContentRange))\n\t\trequire.EqualValues(t, len(content), resp.ContentLength)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, content, body)\n\t})\n}\n\nfunc Benchmark_Ctx_SendFile(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.SendFile(\"ctx.go\")\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Contains(b, string(c.Response().Body()), \"type DefaultCtx struct\")\n}\n\n// go test -run Test_Ctx_JSON\nfunc Test_Ctx_JSON(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Error(t, c.JSON(complex(1, 1)))\n\n\t// Test without ctype\n\terr := c.JSON(Map{ // map has no order\n\t\t\"Name\": \"Grame\",\n\t\t\"Age\":  20,\n\t})\n\trequire.NoError(t, err)\n\trequire.JSONEq(t, `{\"Age\":20,\"Name\":\"Grame\"}`, string(c.Response().Body()))\n\trequire.Equal(t, \"application/json; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\n\t// Test with ctype\n\terr = c.JSON(Map{ // map has no order\n\t\t\"Name\": \"Grame\",\n\t\t\"Age\":  20,\n\t}, \"application/problem+json\")\n\trequire.NoError(t, err)\n\trequire.JSONEq(t, `{\"Age\":20,\"Name\":\"Grame\"}`, string(c.Response().Body()))\n\trequire.Equal(t, \"application/problem+json\", string(c.Response().Header.Peek(\"content-type\")))\n\n\ttestEmpty := func(v any, r string) {\n\t\terr := c.JSON(v)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, r, string(c.Response().Body()))\n\t}\n\n\ttestEmpty(nil, \"null\")\n\ttestEmpty(\"\", `\"\"`)\n\ttestEmpty(0, \"0\")\n\ttestEmpty([]int{}, \"[]\")\n\n\tt.Run(\"custom json encoder\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tJSONEncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(`[\"custom\",\"json\"]`), nil\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\terr := c.JSON(Map{ // map has no order\n\t\t\t\"Name\": \"Grame\",\n\t\t\t\"Age\":  20,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, `[\"custom\",\"json\"]`, string(c.Response().Body()))\n\t\trequire.Equal(t, \"application/json; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\t})\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_JSON -benchmem -count=4\nfunc Benchmark_Ctx_JSON(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype SomeStruct struct {\n\t\tName string\n\t\tAge  uint8\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.JSON(data)\n\t}\n\trequire.NoError(b, err)\n\trequire.JSONEq(b, `{\"Name\":\"Grame\",\"Age\":20}`, string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_MsgPack\nfunc Test_Ctx_MsgPack(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{\n\t\tMsgPackEncoder: msgpack.Marshal,\n\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t})\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.MsgPack(complex(1, 1))\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"\\u0600?\\xf0\\x00\\x00\\x00\\x00\\x00\\x00?\\xf0\\x00\\x00\\x00\\x00\\x00\\x00\", string(c.Response().Body()))\n\n\t// Test without ctype\n\terr = c.MsgPack(Map{ // map has no order\n\t\t\"Name\": \"Grame\",\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"\\x81\\xa4Name\\xa5Grame\", string(c.Response().Body()))\n\trequire.Equal(t, MIMEApplicationMsgPack, string(c.Response().Header.Peek(\"content-type\")))\n\n\t// Test with ctype\n\terr = c.MsgPack(Map{ // map has no order\n\t\t\"Name\": \"Grame\",\n\t}, \"application/problem+msgpack\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"\\x81\\xa4Name\\xa5Grame\", string(c.Response().Body()))\n\trequire.Equal(t, \"application/problem+msgpack\", string(c.Response().Header.Peek(\"content-type\")))\n\n\ttestEmpty := func(v any, r string) {\n\t\terr := c.MsgPack(v)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, r, string(c.Response().Body()))\n\t}\n\n\ttestEmpty(nil, \"\\xc0\")\n\ttestEmpty(\"\", \"\\xa0\")\n\ttestEmpty(0, \"\\x00\")\n\ttestEmpty([]int{}, \"\\x90\")\n\n\tt.Run(\"custom msgpack encoder\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tMsgPackEncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(`[\"custom\",\"msgpack\"]`), nil\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\terr := c.MsgPack(Map{ // map has no order\n\t\t\t\"Name\": \"Grame\",\n\t\t\t\"Age\":  20,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, `[\"custom\",\"msgpack\"]`, string(c.Response().Body()))\n\t\trequire.Equal(t, MIMEApplicationMsgPack, string(c.Response().Header.Peek(\"content-type\")))\n\t})\n\n\tt.Run(\"error msgpack\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tMsgPackEncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(\"error\"), errors.New(\"msgpack error\")\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\terr := c.MsgPack(Map{ // map has no order\n\t\t\t\"Name\": \"Grame\",\n\t\t\t\"Age\":  20,\n\t\t})\n\t\trequire.Error(t, err)\n\t})\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_MsgPack -benchmem -count=4\nfunc Benchmark_Ctx_MsgPack(b *testing.B) {\n\tapp := New(\n\t\tConfig{\n\t\t\tMsgPackEncoder: msgpack.Marshal,\n\t\t\tMsgPackDecoder: msgpack.Unmarshal,\n\t\t},\n\t)\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype SomeStruct struct {\n\t\tName string\n\t\tAge  uint8\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.MsgPack(data)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"\\x82\\xa4Name\\xa5Grame\\xa3Age\\x14\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_CBOR\nfunc Test_Ctx_CBOR(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Error(t, c.CBOR(complex(1, 1)))\n\n\ttype dummyStruct struct {\n\t\tName string\n\t\tAge  int\n\t}\n\n\t// Test without ctype\n\terr := c.CBOR(dummyStruct{ // map has no order\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body()))\n\trequire.Equal(t, \"application/cbor\", string(c.Response().Header.Peek(\"content-type\")))\n\n\t// Test with ctype\n\terr = c.CBOR(dummyStruct{ // map has no order\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}, \"application/problem+cbor\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body()))\n\trequire.Equal(t, \"application/problem+cbor\", string(c.Response().Header.Peek(\"content-type\")))\n\n\ttestEmpty := func(v any, r string) {\n\t\tcbErr := c.CBOR(v)\n\t\trequire.NoError(t, cbErr)\n\t\trequire.Equal(t, r, hex.EncodeToString(c.Response().Body()))\n\t}\n\n\ttestEmpty(nil, \"f6\")\n\ttestEmpty(\"\", `60`)\n\ttestEmpty(0, \"00\")\n\ttestEmpty([]int{}, \"80\")\n\n\t// Test invalid types\n\terr = c.CBOR(make(chan int))\n\trequire.Error(t, err)\n\n\terr = c.CBOR(func() {})\n\trequire.Error(t, err)\n\n\tt.Run(\"custom cbor encoder\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tCBOREncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(hex.EncodeToString([]byte(\"random\"))), nil\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\terr := c.CBOR(Map{ // map has no order\n\t\t\t\"Name\": \"Grame\",\n\t\t\t\"Age\":  20,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, `72616e646f6d`, string(c.Response().Body()))\n\t\trequire.Equal(t, \"application/cbor\", string(c.Response().Header.Peek(\"content-type\")))\n\t})\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_CBOR -benchmem -count=4\nfunc Benchmark_Ctx_CBOR(b *testing.B) {\n\tapp := New(Config{\n\t\tCBOREncoder: cbor.Marshal,\n\t\tCBORDecoder: cbor.Unmarshal,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\ttype SomeStruct struct {\n\t\tName string\n\t\tAge  uint8\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.CBOR(data)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, `a2644e616d65654772616d656341676514`, hex.EncodeToString(c.Response().Body()))\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4\nfunc Benchmark_Ctx_JSON_Ctype(b *testing.B) {\n\tapp := New()\n\t// TODO: Check extra allocs because of the interface stuff\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\ttype SomeStruct struct {\n\t\tName string\n\t\tAge  uint8\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.JSON(data, \"application/problem+json\")\n\t}\n\trequire.NoError(b, err)\n\trequire.JSONEq(b, `{\"Name\":\"Grame\",\"Age\":20}`, string(c.Response().Body()))\n\trequire.Equal(b, \"application/problem+json\", string(c.Response().Header.Peek(\"content-type\")))\n}\n\n// go test -run Test_Ctx_JSONP\nfunc Test_Ctx_JSONP(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Error(t, c.JSONP(complex(1, 1)))\n\n\terr := c.JSONP(Map{\n\t\t\"Name\": \"Grame\",\n\t\t\"Age\":  20,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, `callback({\"Age\":20,\"Name\":\"Grame\"});`, string(c.Response().Body()))\n\trequire.Equal(t, \"text/javascript; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\n\terr = c.Res().JSONP(Map{\n\t\t\"Name\": \"Grame\",\n\t\t\"Age\":  20,\n\t}, \"john\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, `john({\"Age\":20,\"Name\":\"Grame\"});`, string(c.Response().Body()))\n\trequire.Equal(t, \"text/javascript; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\n\tt.Run(\"custom json encoder\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tJSONEncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(`[\"custom\",\"json\"]`), nil\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\terr := c.JSONP(Map{ // map has no order\n\t\t\t\"Name\": \"Grame\",\n\t\t\t\"Age\":  20,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, `callback([\"custom\",\"json\"]);`, string(c.Response().Body()))\n\t\trequire.Equal(t, \"text/javascript; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\t})\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4\nfunc Benchmark_Ctx_JSONP(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\ttype SomeStruct struct {\n\t\tName string\n\t\tAge  uint8\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tcallback := \"emit\"\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.JSONP(data, callback)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, `emit({\"Name\":\"Grame\",\"Age\":20});`, string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_XML\nfunc Test_Ctx_XML(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\trequire.Error(t, c.JSON(complex(1, 1)))\n\n\ttype xmlResult struct {\n\t\tXMLName xml.Name `xml:\"Users\"`\n\t\tNames   []string `xml:\"Names\"`\n\t\tAges    []int    `xml:\"Ages\"`\n\t}\n\n\terr := c.XML(xmlResult{\n\t\tNames: []string{\"Grame\", \"John\"},\n\t\tAges:  []int{1, 12, 20},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, `<Users><Names>Grame</Names><Names>John</Names><Ages>1</Ages><Ages>12</Ages><Ages>20</Ages></Users>`, string(c.Response().Body()))\n\trequire.Equal(t, \"application/xml; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\n\ttestEmpty := func(v any, r string) {\n\t\terr := c.XML(v)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, r, string(c.Response().Body()))\n\t}\n\n\ttestEmpty(nil, \"\")\n\ttestEmpty(\"\", `<string></string>`)\n\ttestEmpty(0, \"<int>0</int>\")\n\ttestEmpty([]int{}, \"\")\n\n\tt.Run(\"custom xml encoder\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New(Config{\n\t\t\tXMLEncoder: func(_ any) ([]byte, error) {\n\t\t\t\treturn []byte(`<custom>xml</custom>`), nil\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\ttype xmlResult struct {\n\t\t\tXMLName xml.Name `xml:\"Users\"`\n\t\t\tNames   []string `xml:\"Names\"`\n\t\t\tAges    []int    `xml:\"Ages\"`\n\t\t}\n\n\t\terr := c.XML(xmlResult{\n\t\t\tNames: []string{\"Grame\", \"John\"},\n\t\t\tAges:  []int{1, 12, 20},\n\t\t})\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, `<custom>xml</custom>`, string(c.Response().Body()))\n\t\trequire.Equal(t, \"application/xml; charset=utf-8\", string(c.Response().Header.Peek(\"content-type\")))\n\t})\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4\nfunc Benchmark_Ctx_XML(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\ttype SomeStruct struct {\n\t\tName string `xml:\"Name\"`\n\t\tAge  uint8  `xml:\"Age\"`\n\t}\n\tdata := SomeStruct{\n\t\tName: \"Grame\",\n\t\tAge:  20,\n\t}\n\tvar err error\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.XML(data)\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, `<SomeStruct><Name>Grame</Name><Age>20</Age></SomeStruct>`, string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_Links\nfunc Test_Ctx_Links(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Links()\n\trequire.Empty(t, string(c.Response().Header.Peek(HeaderLink)))\n\n\tc.Links(\n\t\t\"http://api.example.com/users?page=2\", \"next\",\n\t\t\"http://api.example.com/users?page=5\", \"last\",\n\t)\n\trequire.Equal(t, `<http://api.example.com/users?page=2>; rel=\"next\",<http://api.example.com/users?page=5>; rel=\"last\"`, string(c.Response().Header.Peek(HeaderLink)))\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4\nfunc Benchmark_Ctx_Links(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Links(\n\t\t\t\"http://api.example.com/users?page=2\", \"next\",\n\t\t\t\"http://api.example.com/users?page=5\", \"last\",\n\t\t)\n\t}\n}\n\n// go test -run Test_Ctx_Location\nfunc Test_Ctx_Location(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Location(\"http://example.com\")\n\trequire.Equal(t, \"http://example.com\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Ctx_Next\nfunc Test_Ctx_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(\"/\", func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tc.Set(\"X-Next-Result\", \"Works\")\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"http://example.com/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"Works\", resp.Header.Get(\"X-Next-Result\"))\n}\n\n// go test -run Test_Ctx_Next_Error\nfunc Test_Ctx_Next_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(\"/\", func(c Ctx) error {\n\t\tc.Set(\"X-Next-Result\", \"Works\")\n\t\treturn ErrNotFound\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"http://example.com/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"Works\", resp.Header.Get(\"X-Next-Result\"))\n}\n\n// go test -run Test_Ctx_Render\nfunc Test_Ctx_Render(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Render(\"./.github/testdata/index.tmpl\", Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n\n\terr = c.Render(\"./.github/testdata/template-non-exists.html\", nil)\n\trequire.Error(t, err)\n\n\terr = c.Res().Render(\"./.github/testdata/template-invalid.html\", nil)\n\trequire.Error(t, err)\n}\n\nfunc Test_Ctx_RenderWithoutLocals(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tPassLocalsToViews: false,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Locals(\"Title\", \"Hello, World!\")\n\n\terr := c.Render(\"./.github/testdata/index.tmpl\", Map{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1></h1>\", string(c.Response().Body()))\n}\n\nfunc Test_Ctx_RenderWithLocals(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tPassLocalsToViews: true,\n\t})\n\n\tt.Run(\"EmptyBind\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\tc.Locals(\"Title\", \"Hello, World!\")\n\t\terr := c.Render(\"./.github/testdata/index.tmpl\", Map{})\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n\t})\n\n\tt.Run(\"NilBind\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\tc.Locals(\"Title\", \"Hello, World!\")\n\t\terr := c.Render(\"./.github/testdata/index.tmpl\", nil)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n\t})\n}\n\nfunc Test_Ctx_Matched_AfterNext(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\trequire.False(t, c.Matched())\n\t\terr := c.Next()\n\t\tif c.Path() == \"/one\" {\n\t\t\trequire.True(t, c.Matched())\n\t\t} else {\n\t\t\trequire.False(t, c.Matched())\n\t\t}\n\t\treturn err\n\t})\n\n\tapp.Get(\"/one\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/one\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/missing\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Ctx_Matched_RouteError(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tErrorHandler: func(c Ctx, err error) error {\n\t\t\trequire.True(t, c.Matched())\n\t\t\treturn c.Status(StatusNotFound).SendString(err.Error())\n\t\t},\n\t})\n\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn ErrNotFound\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Ctx_IsMiddleware(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Use(func(c Ctx) error {\n\t\trequire.True(t, c.IsMiddleware())\n\t\treturn c.Next()\n\t})\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\trequire.False(t, c.IsMiddleware())\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp.Get(\"/route\", func(c Ctx) error {\n\t\trequire.True(t, c.IsMiddleware())\n\t\treturn c.Next()\n\t}, func(c Ctx) error {\n\t\trequire.False(t, c.IsMiddleware())\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/route\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\nfunc Test_Ctx_HasBody(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tacquire := func(t *testing.T) CustomCtx {\n\t\tt.Helper()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\trequire.NotNil(t, ctx)\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\treturn ctx\n\t}\n\n\tsetTransferEncoding := func(t *testing.T, ctx Ctx, value string) {\n\t\tt.Helper()\n\t\thdr := &ctx.Request().Header\n\t\thdr.DisableSpecialHeader()\n\t\thdr.Set(HeaderTransferEncoding, value)\n\t\thdr.Set(HeaderContentLength, \"0\")\n\t\thdr.EnableSpecialHeader()\n\t\trequire.Zero(t, hdr.ContentLength())\n\t\trequire.Empty(t, ctx.Request().Body())\n\t}\n\n\tt.Run(\"body bytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tctx.Request().SetBody([]byte(\"test\"))\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"content length header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tctx.Request().Header.SetContentLength(4)\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"chunked sentinel\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tctx.Request().Header.SetContentLength(-1)\n\t\trequire.Equal(t, -1, ctx.Request().Header.ContentLength())\n\t\trequire.Empty(t, ctx.Request().Body())\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding chunked\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"chunked\")\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding whitespace\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"  ChUnKeD  \")\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding parameters\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"chunked; q=1\")\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding multiple values\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"gzip, chunked\")\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding identity\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"identity\")\n\t\trequire.False(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"transfer encoding identity then chunked\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\tsetTransferEncoding(t, ctx, \"identity, chunked\")\n\t\trequire.True(t, ctx.HasBody())\n\t})\n\n\tt.Run(\"no body\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := acquire(t)\n\t\trequire.False(t, ctx.HasBody())\n\t})\n}\n\nfunc Test_Ctx_IsWebSocket(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tws := app.AcquireCtx(&fasthttp.RequestCtx{})\n\trequire.NotNil(t, ws)\n\tt.Cleanup(func() { app.ReleaseCtx(ws) })\n\tws.Request().Header.Set(HeaderConnection, \"keep-alive, Upgrade\")\n\tws.Request().Header.Set(HeaderUpgrade, \"websocket\")\n\trequire.True(t, ws.IsWebSocket())\n\n\tnon := app.AcquireCtx(&fasthttp.RequestCtx{})\n\trequire.NotNil(t, non)\n\tt.Cleanup(func() { app.ReleaseCtx(non) })\n\tnon.Request().Header.Set(HeaderConnection, \"not-an-upgrade\")\n\tnon.Request().Header.Set(HeaderUpgrade, \"websocket\")\n\trequire.False(t, non.IsWebSocket())\n}\n\nfunc Test_Ctx_IsPreflight(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tpreCtx := &fasthttp.RequestCtx{}\n\tpreCtx.Request.Header.SetMethod(MethodOptions)\n\tpreCtx.Request.Header.Set(HeaderAccessControlRequestMethod, MethodGet)\n\tpreCtx.Request.Header.Set(HeaderOrigin, \"https://example.com\")\n\tpre := app.AcquireCtx(preCtx)\n\trequire.NotNil(t, pre)\n\tt.Cleanup(func() { app.ReleaseCtx(pre) })\n\trequire.True(t, pre.IsPreflight())\n\n\tnoOriginCtx := &fasthttp.RequestCtx{}\n\tnoOriginCtx.Request.Header.SetMethod(MethodOptions)\n\tnoOriginCtx.Request.Header.Set(HeaderAccessControlRequestMethod, MethodGet)\n\tnoOrigin := app.AcquireCtx(noOriginCtx)\n\trequire.NotNil(t, noOrigin)\n\tt.Cleanup(func() { app.ReleaseCtx(noOrigin) })\n\trequire.False(t, noOrigin.IsPreflight())\n\n\toptCtx := &fasthttp.RequestCtx{}\n\toptCtx.Request.Header.SetMethod(MethodOptions)\n\toptCtx.Request.Header.Set(HeaderOrigin, \"https://example.com\")\n\topt := app.AcquireCtx(optCtx)\n\trequire.NotNil(t, opt)\n\tt.Cleanup(func() { app.ReleaseCtx(opt) })\n\trequire.False(t, opt.IsPreflight())\n}\n\nfunc Test_Ctx_RenderWithViewBind(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.ViewBind(Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\n\terr = c.Render(\"./.github/testdata/index.tmpl\", Map{})\n\trequire.NoError(t, err)\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(\"overwrite\")\n\tdefer bytebufferpool.Put(buf)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\nfunc Test_Ctx_RenderWithOverwrittenViewBind(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.ViewBind(Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\n\terr = c.Render(\"./.github/testdata/index.tmpl\", Map{\n\t\t\"Title\": \"Hello from Fiber!\",\n\t})\n\trequire.NoError(t, err)\n\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(\"overwrite\")\n\tdefer bytebufferpool.Put(buf)\n\n\trequire.Equal(t, \"<h1>Hello from Fiber!</h1>\", string(c.Response().Body()))\n}\n\nfunc Test_Ctx_RenderWithViewBindLocals(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tPassLocalsToViews: true,\n\t})\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.ViewBind(Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\n\tc.Locals(\"Summary\", \"Test\")\n\n\terr = c.Render(\"./.github/testdata/template.tmpl\", Map{})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World! Test</h1>\", string(c.Response().Body()))\n\n\trequire.Equal(t, \"<h1>Hello, World! Test</h1>\", string(c.Response().Body()))\n}\n\nfunc Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) {\n\tt.Parallel()\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(t, err)\n\n\tapp := New(Config{\n\t\tPassLocalsToViews: true,\n\t\tViews:             engine,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Locals(\"Title\", \"This is a test.\")\n\n\terr = c.Render(\"index.tmpl\", Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\nfunc Benchmark_Ctx_RenderWithLocalsAndViewBind(b *testing.B) {\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(b, err)\n\tapp := New(Config{\n\t\tPassLocalsToViews: true,\n\t\tViews:             engine,\n\t})\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr = c.ViewBind(Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(b, err)\n\tc.Locals(\"Summary\", \"Test\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Render(\"template.tmpl\", Map{})\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"<h1>Hello, World! Test</h1>\", string(c.Response().Body()))\n}\n\nfunc Benchmark_Ctx_RenderLocals(b *testing.B) {\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(b, err)\n\tapp := New(Config{\n\t\tPassLocalsToViews: true,\n\t})\n\tapp.config.Views = engine\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Locals(\"Title\", \"Hello, World!\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Render(\"index.tmpl\", Map{})\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\nfunc Benchmark_Ctx_RenderViewBind(b *testing.B) {\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(b, err)\n\tapp := New()\n\tapp.config.Views = engine\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr = c.ViewBind(Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(b, err)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr = c.Render(\"index.tmpl\", Map{})\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_RestartRouting\nfunc Test_Ctx_RestartRouting(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tcalls := 0\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tcalls++\n\t\tif calls < 3 {\n\t\t\treturn c.RestartRouting()\n\t\t}\n\t\treturn nil\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"http://example.com/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, 3, calls, \"Number of calls\")\n}\n\n// go test -run Test_Ctx_RestartRoutingWithChangedPath\nfunc Test_Ctx_RestartRoutingWithChangedPath(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar executedOldHandler, executedNewHandler bool\n\n\tapp.Get(\"/old\", func(c Ctx) error {\n\t\tc.Path(\"/new\")\n\t\treturn c.RestartRouting()\n\t})\n\tapp.Get(\"/old\", func(_ Ctx) error {\n\t\texecutedOldHandler = true\n\t\treturn nil\n\t})\n\tapp.Get(\"/new\", func(_ Ctx) error {\n\t\texecutedNewHandler = true\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"http://example.com/old\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.False(t, executedOldHandler, \"Executed old handler\")\n\trequire.True(t, executedNewHandler, \"Executed new handler\")\n}\n\n// go test -run Test_Ctx_RestartRoutingWithChangedPathAnd404\nfunc Test_Ctx_RestartRoutingWithChangedPathAndCatchAll(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/new\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tapp.Use(func(c Ctx) error {\n\t\tc.Path(\"/new\")\n\t\t// c.Next() would fail this test as a 404 is returned from the next handler\n\t\treturn c.RestartRouting()\n\t})\n\tapp.Use(func(_ Ctx) error {\n\t\treturn ErrNotFound\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"http://example.com/old\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\ntype testTemplateEngine struct {\n\ttemplates *template.Template\n\tpath      string\n}\n\nfunc (t *testTemplateEngine) Render(w io.Writer, name string, bind any, layout ...string) error {\n\tif len(layout) == 0 {\n\t\tif err := t.templates.ExecuteTemplate(w, name, bind); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute template without layout: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\tif err := t.templates.ExecuteTemplate(w, name, bind); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute template: %w\", err)\n\t}\n\tif err := t.templates.ExecuteTemplate(w, layout[0], bind); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute template with layout: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (t *testTemplateEngine) Load() error {\n\tif t.path == \"\" {\n\t\tt.path = \"testdata\"\n\t}\n\tt.templates = template.Must(template.ParseGlob(\"./.github/\" + t.path + \"/*.tmpl\"))\n\treturn nil\n}\n\n// go test -run Test_Ctx_Render_Engine\nfunc Test_Ctx_Render_Engine(t *testing.T) {\n\tt.Parallel()\n\tengine := &testTemplateEngine{}\n\trequire.NoError(t, engine.Load())\n\tapp := New()\n\tapp.config.Views = engine\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Render(\"index.tmpl\", Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_Render_Engine_With_View_Layout\nfunc Test_Ctx_Render_Engine_With_View_Layout(t *testing.T) {\n\tt.Parallel()\n\tengine := &testTemplateEngine{}\n\trequire.NoError(t, engine.Load())\n\tapp := New(Config{ViewsLayout: \"main.tmpl\"})\n\tapp.config.Views = engine\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Render(\"index.tmpl\", Map{\n\t\t\"Title\": \"Hello, World!\",\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World!</h1><h1>I'm main</h1>\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Render_Engine -benchmem -count=4\nfunc Benchmark_Ctx_Render_Engine(b *testing.B) {\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(b, err)\n\tapp := New()\n\tapp.config.Views = engine\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr = c.Render(\"index.tmpl\", Map{\n\t\t\t\"Title\": \"Hello, World!\",\n\t\t})\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"<h1>Hello, World!</h1>\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4\nfunc Benchmark_Ctx_Get_Location_From_Route(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"name\"))\n\t}).Name(\"User\")\n\n\tvar err error\n\tvar location string\n\tfor b.Loop() {\n\t\troute := app.GetRoute(\"User\")\n\t\tlocation, err = c.getLocationFromRoute(&route, Map{\"name\": \"fiber\"})\n\t}\n\n\trequire.Equal(b, \"/user/fiber\", location)\n\trequire.NoError(b, err)\n}\n\n// go test -run Test_Ctx_Get_Location_From_Route_name\nfunc Test_Ctx_Get_Location_From_Route_name(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"case-insensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\t\treturn c.SendString(c.Params(\"name\"))\n\t\t}).Name(\"User\")\n\n\t\tlocation, err := c.GetRouteURL(\"User\", Map{\"name\": \"fiber\"})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"/user/fiber\", location)\n\n\t\tlocation, err = c.GetRouteURL(\"User\", Map{\"Name\": \"fiber\"})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"/user/fiber\", location)\n\t})\n\n\tt.Run(\"case-sensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New(Config{CaseSensitive: true})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\t\treturn c.SendString(c.Params(\"name\"))\n\t\t}).Name(\"User\")\n\n\t\tlocation, err := c.GetRouteURL(\"User\", Map{\"name\": \"fiber\"})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"/user/fiber\", location)\n\n\t\tlocation, err = c.GetRouteURL(\"User\", Map{\"Name\": \"fiber\"})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"/user/\", location)\n\t})\n}\n\n// go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy\nfunc Test_Ctx_Get_Location_From_Route_name_Optional_greedy(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tapp.Get(\"/:phone/*/send/*\", func(c Ctx) error {\n\t\treturn c.SendString(\"Phone: \" + c.Params(\"phone\") + \"\\nFirst Param: \" + c.Params(\"*1\") + \"\\nSecond Param: \" + c.Params(\"*2\"))\n\t}).Name(\"SendSms\")\n\n\tlocation, err := c.GetRouteURL(\"SendSms\", Map{\n\t\t\"phone\": \"23456789\",\n\t\t\"*1\":    \"sms\",\n\t\t\"*2\":    \"test-msg\",\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/23456789/sms/send/test-msg\", location)\n}\n\n// go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param\nfunc Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tapp.Get(\"/:phone/*/send\", func(c Ctx) error {\n\t\treturn c.SendString(\"Phone: \" + c.Params(\"phone\") + \"\\nFirst Param: \" + c.Params(\"*1\"))\n\t}).Name(\"SendSms\")\n\n\tlocation, err := c.GetRouteURL(\"SendSms\", Map{\n\t\t\"phone\": \"23456789\",\n\t\t\"*\":     \"sms\",\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/23456789/sms/send\", location)\n}\n\ntype errorTemplateEngine struct{}\n\nfunc (errorTemplateEngine) Render(_ io.Writer, _ string, _ any, _ ...string) error {\n\treturn errors.New(\"errorTemplateEngine\")\n}\n\nfunc (errorTemplateEngine) Load() error { return nil }\n\n// go test -run Test_Ctx_Render_Engine_Error\nfunc Test_Ctx_Render_Engine_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.config.Views = errorTemplateEngine{}\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Render(\"index.tmpl\", nil)\n\trequire.Error(t, err)\n}\n\n// go test -run Test_Ctx_Render_Go_Template\nfunc Test_Ctx_Render_Go_Template(t *testing.T) {\n\tt.Parallel()\n\tfile, err := os.CreateTemp(os.TempDir(), \"fiber\")\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\tremoveErr := os.Remove(file.Name())\n\t\trequire.NoError(t, removeErr)\n\t}()\n\n\t_, err = file.WriteString(\"template\")\n\trequire.NoError(t, err)\n\n\terr = file.Close()\n\trequire.NoError(t, err)\n\n\tapp := New()\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr = c.Render(file.Name(), nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"template\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_Send\nfunc Test_Ctx_Send(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.NoError(t, c.Send([]byte(\"Hello, World\")))\n\trequire.NoError(t, c.Send([]byte(\"Don't crash please\")))\n\trequire.NoError(t, c.Send([]byte(\"1337\")))\n\trequire.Equal(t, \"1337\", string(c.Response().Body()))\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Send -benchmem -count=4\nfunc Benchmark_Ctx_Send(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tbyt := []byte(\"Hello, World!\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.Send(byt)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, \"Hello, World!\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_SendStatus\nfunc Test_Ctx_SendStatus(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.SendStatus(415)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 415, c.Response().StatusCode())\n\trequire.Equal(t, \"Unsupported Media Type\", string(c.Response().Body()))\n}\n\nfunc Test_Ctx_SendStatusNoBodyResponses(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname   string\n\t\tstatus int\n\t}{\n\t\t{\n\t\t\tname:   \"Informational\",\n\t\t\tstatus: StatusContinue,\n\t\t},\n\t\t{\n\t\t\tname:   \"Processing\",\n\t\t\tstatus: StatusProcessing,\n\t\t},\n\t\t{\n\t\t\tname:   \"SwitchingProtocols\",\n\t\t\tstatus: StatusSwitchingProtocols,\n\t\t},\n\t\t{\n\t\t\tname:   \"EarlyHints\",\n\t\t\tstatus: StatusEarlyHints,\n\t\t},\n\t\t{\n\t\t\tname:   \"NoContent\",\n\t\t\tstatus: StatusNoContent,\n\t\t},\n\t\t{\n\t\t\tname:   \"ResetContent\",\n\t\t\tstatus: StatusResetContent,\n\t\t},\n\t\t{\n\t\t\tname:   \"NotModified\",\n\t\t\tstatus: StatusNotModified,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := New()\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t\tc.Response().SetBodyString(\"preset body\")\n\n\t\t\terr := c.SendStatus(testCase.status)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, c.Response().Body())\n\t\t\trequire.Equal(t, 0, c.Response().Header.ContentLength())\n\t\t})\n\t}\n}\n\n// go test -run Test_Ctx_SendString\nfunc Test_Ctx_SendString(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.SendString(\"Don't crash please\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Don't crash please\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_SendStream\nfunc Test_Ctx_SendStream(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.SendStream(bytes.NewReader([]byte(\"Don't crash please\")))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Don't crash please\", string(c.Response().Body()))\n\n\terr = c.SendStream(bytes.NewReader([]byte(\"Don't crash please\")), len([]byte(\"Don't crash please\")))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Don't crash please\", string(c.Response().Body()))\n\n\terr = c.SendStream(bufio.NewReader(bytes.NewReader([]byte(\"Hello bufio\"))))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello bufio\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_SendStreamWriter\nfunc Test_Ctx_SendStreamWriter(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.SendStreamWriter(func(w *bufio.Writer) {\n\t\tw.WriteString(\"Don't crash please\") //nolint:errcheck // It is fine to ignore the error\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Don't crash please\", string(c.Response().Body()))\n\n\terr = c.SendStreamWriter(func(w *bufio.Writer) {\n\t\tfor lineNum := 1; lineNum <= 5; lineNum++ {\n\t\t\tfmt.Fprintf(w, \"Line %d\\n\", lineNum)\n\t\t\tif flushErr := w.Flush(); flushErr != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", flushErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Line 1\\nLine 2\\nLine 3\\nLine 4\\nLine 5\\n\", string(c.Response().Body()))\n\n\terr = c.SendStreamWriter(func(_ *bufio.Writer) {})\n\trequire.NoError(t, err)\n\trequire.Empty(t, c.Response().Body())\n}\n\n// go test -run Test_Ctx_SendStreamWriter_Interrupted\nfunc Test_Ctx_SendStreamWriter_Interrupted(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tvar flushed atomic.Int32\n\tvar flushErrLine atomic.Int32\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendStreamWriter(func(w *bufio.Writer) {\n\t\t\tfor lineNum := 1; lineNum <= 5; lineNum++ {\n\t\t\t\tfmt.Fprintf(w, \"Line %d\\n\", lineNum)\n\n\t\t\t\tif err := w.Flush(); err != nil {\n\t\t\t\t\tflushErrLine.Store(int32(lineNum)) //nolint:gosec // G115 - lineNum is 1-5, fits int32\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif lineNum <= 3 {\n\t\t\t\t\tflushed.Add(1)\n\t\t\t\t}\n\n\t\t\t\tif lineNum == 3 {\n\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n\n\treq := httptest.NewRequest(MethodGet, \"/\", http.NoBody)\n\ttestConfig := TestConfig{\n\t\t// allow enough time for three lines to flush before\n\t\t// the test connection is closed but stop before the\n\t\t// fourth line is sent\n\t\tTimeout:       200 * time.Millisecond,\n\t\tFailOnTimeout: true, // Changed to true to test interrupted behavior\n\t}\n\tresp, err := app.Test(req, testConfig)\n\t// With FailOnTimeout: true, we should get a timeout error\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n\trequire.Nil(t, resp)\n}\n\n// go test -run Test_Ctx_Set\nfunc Test_Ctx_Set(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Set(\"X-1\", \"1\")\n\tc.Set(\"X-2\", \"2\")\n\tc.Set(\"X-3\", \"3\")\n\tc.Set(\"X-3\", \"1337\")\n\trequire.Equal(t, \"1\", string(c.Response().Header.Peek(\"x-1\")))\n\trequire.Equal(t, \"2\", string(c.Response().Header.Peek(\"x-2\")))\n\trequire.Equal(t, \"1337\", string(c.Response().Header.Peek(\"x-3\")))\n}\n\n// go test -run Test_Ctx_Set_Splitter\nfunc Test_Ctx_Set_Splitter(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Set(\"Location\", \"foo\\r\\nSet-Cookie:%20SESSIONID=MaliciousValue\\r\\n\")\n\th := string(c.Response().Header.Peek(\"Location\"))\n\trequire.NotContains(t, h, \"\\r\\n\")\n\n\tc.Set(\"Location\", \"foo\\nSet-Cookie:%20SESSIONID=MaliciousValue\\n\")\n\th = string(c.Response().Header.Peek(\"Location\"))\n\trequire.NotContains(t, h, \"\\n\")\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Set -benchmem -count=4\nfunc Benchmark_Ctx_Set(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tval := \"1431-15132-3423\"\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Set(HeaderXRequestID, val)\n\t}\n}\n\n// go test -run Test_Ctx_Status\nfunc Test_Ctx_Status(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Status(400)\n\trequire.Equal(t, 400, c.Response().StatusCode())\n\terr := c.Status(415).Send([]byte(\"Hello, World\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 415, c.Response().StatusCode())\n\trequire.Equal(t, \"Hello, World\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_Type\nfunc Test_Ctx_Type(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Type(\".json\")\n\trequire.Equal(t, \"application/json; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"json\", \"utf-8\")\n\trequire.Equal(t, \"application/json; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\".html\")\n\trequire.Equal(t, \"text/html; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"html\", \"utf-8\")\n\trequire.Equal(t, \"text/html; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\t// Test other text types get UTF-8 by default\n\tc.Type(\"txt\")\n\trequire.Equal(t, \"text/plain; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"css\")\n\trequire.Equal(t, \"text/css; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"js\")\n\trequire.Equal(t, \"text/javascript; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"xml\")\n\trequire.Equal(t, \"application/xml; charset=utf-8\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\t// Test binary types don't get charset\n\tc.Type(\"png\")\n\trequire.Equal(t, \"image/png\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\tc.Type(\"pdf\")\n\trequire.Equal(t, \"application/pdf\", string(c.Response().Header.Peek(\"Content-Type\")))\n\n\t// Test custom charset override\n\tc.Type(\"html\", \"iso-8859-1\")\n\trequire.Equal(t, \"text/html; charset=iso-8859-1\", string(c.Response().Header.Peek(\"Content-Type\")))\n}\n\n// go test -run Test_shouldIncludeCharset\nfunc Test_shouldIncludeCharset(t *testing.T) {\n\tt.Parallel()\n\n\t// Test text/* types - should include charset\n\trequire.True(t, shouldIncludeCharset(\"text/html\"))\n\trequire.True(t, shouldIncludeCharset(\"text/plain\"))\n\trequire.True(t, shouldIncludeCharset(\"text/css\"))\n\trequire.True(t, shouldIncludeCharset(\"text/javascript\"))\n\trequire.True(t, shouldIncludeCharset(\"text/xml\"))\n\n\t// Test explicit application types - should include charset\n\trequire.True(t, shouldIncludeCharset(\"application/json\"))\n\trequire.True(t, shouldIncludeCharset(\"application/javascript\"))\n\trequire.True(t, shouldIncludeCharset(\"application/xml\"))\n\n\t// Test +json suffixes - should include charset\n\trequire.True(t, shouldIncludeCharset(\"application/problem+json\"))\n\trequire.True(t, shouldIncludeCharset(\"application/vnd.api+json\"))\n\trequire.True(t, shouldIncludeCharset(\"application/hal+json\"))\n\trequire.True(t, shouldIncludeCharset(\"application/merge-patch+json\"))\n\n\t// Test +xml suffixes - should include charset\n\trequire.True(t, shouldIncludeCharset(\"application/soap+xml\"))\n\trequire.True(t, shouldIncludeCharset(\"application/xhtml+xml\"))\n\trequire.True(t, shouldIncludeCharset(\"application/atom+xml\"))\n\trequire.True(t, shouldIncludeCharset(\"application/rss+xml\"))\n\n\t// Test binary types - should NOT include charset\n\trequire.False(t, shouldIncludeCharset(\"image/png\"))\n\trequire.False(t, shouldIncludeCharset(\"image/jpeg\"))\n\trequire.False(t, shouldIncludeCharset(\"application/pdf\"))\n\trequire.False(t, shouldIncludeCharset(\"application/octet-stream\"))\n\trequire.False(t, shouldIncludeCharset(\"video/mp4\"))\n\trequire.False(t, shouldIncludeCharset(\"audio/mpeg\"))\n\n\t// Test other application types - should NOT include charset\n\trequire.False(t, shouldIncludeCharset(\"application/cbor\"))\n\trequire.False(t, shouldIncludeCharset(\"application/x-www-form-urlencoded\"))\n\trequire.False(t, shouldIncludeCharset(\"application/vnd.msgpack\"))\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Type -benchmem -count=4\nfunc Benchmark_Ctx_Type(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Type(\".json\")\n\t\tc.Type(\"json\")\n\t}\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4\nfunc Benchmark_Ctx_Type_Charset(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Type(\".json\", \"utf-8\")\n\t\tc.Type(\"json\", \"utf-8\")\n\t}\n}\n\n// go test -run Test_Ctx_Vary\nfunc Test_Ctx_Vary(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Vary(\"Origin\")\n\tc.Vary(\"User-Agent\")\n\tc.Vary(\"Accept-Encoding\", \"Accept\")\n\trequire.Equal(t, \"Origin, User-Agent, Accept-Encoding, Accept\", string(c.Response().Header.Peek(\"Vary\")))\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4\nfunc Benchmark_Ctx_Vary(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tc.Vary(\"Origin\", \"User-Agent\")\n\t}\n}\n\n// go test -run Test_Ctx_Write\nfunc Test_Ctx_Write(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t_, err := c.WriteString(\"Hello, \")\n\trequire.NoError(t, err)\n\t_, err = c.WriteString(\"World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello, World!\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Write -benchmem -count=4\nfunc Benchmark_Ctx_Write(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tbyt := []byte(\"Hello, World!\")\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\t_, err = c.Write(byt)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -run Test_Ctx_Writef\nfunc Test_Ctx_Writef(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tworld := \"World!\"\n\t_, err := c.Writef(\"Hello, %s\", world)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello, World!\", string(c.Response().Body()))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4\nfunc Benchmark_Ctx_Writef(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tworld := \"World!\"\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\t_, err = c.Writef(\"Hello, %s\", world)\n\t}\n\trequire.NoError(b, err)\n}\n\n// go test -run Test_Ctx_WriteString\nfunc Test_Ctx_WriteString(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t_, err := c.WriteString(\"Hello, \")\n\trequire.NoError(t, err)\n\t_, err = c.WriteString(\"World!\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello, World!\", string(c.Response().Body()))\n}\n\n// go test -run Test_Ctx_XHR\nfunc Test_Ctx_XHR(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXRequestedWith, \"XMLHttpRequest\")\n\trequire.True(t, c.XHR())\n}\n\n// go test -run=^$ -bench=Benchmark_Ctx_XHR -benchmem -count=4\nfunc Benchmark_Ctx_XHR(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderXRequestedWith, \"XMLHttpRequest\")\n\tvar equal bool\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tequal = c.XHR()\n\t}\n\trequire.True(b, equal)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_SendString_B -benchmem -count=4\nfunc Benchmark_Ctx_SendString_B(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tbody := \"Hello, world!\"\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\terr = c.SendString(body)\n\t}\n\trequire.NoError(b, err)\n\trequire.Equal(b, []byte(\"Hello, world!\"), c.Response().Body())\n}\n\n// go test -run Test_Ctx_Queries -v\nfunc Test_Ctx_Queries(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().SetBody([]byte(``))\n\tc.Request().Header.SetContentType(\"\")\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1&field1=value1&field1=value2&field2=value3&list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3\")\n\n\tqueries := c.Queries()\n\trequire.Equal(t, \"1\", queries[\"id\"])\n\trequire.Equal(t, \"tom\", queries[\"name\"])\n\trequire.Equal(t, \"basketball,football\", queries[\"hobby\"])\n\trequire.Equal(t, \"milo,coke,pepsi\", queries[\"favouriteDrinks\"])\n\trequire.Empty(t, queries[\"alloc\"])\n\trequire.Equal(t, \"1\", queries[\"no\"])\n\trequire.Equal(t, \"value2\", queries[\"field1\"])\n\trequire.Equal(t, \"value3\", queries[\"field2\"])\n\trequire.Equal(t, \"3\", queries[\"list_a\"])\n\trequire.Equal(t, \"3\", queries[\"list_b[]\"])\n\trequire.Equal(t, \"1,2,3\", queries[\"list_c\"])\n\n\tc.Request().URI().SetQueryString(\"filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending\")\n\n\tqueries = c.Queries()\n\trequire.Equal(t, \"John\", queries[\"filters.author.name\"])\n\trequire.Equal(t, \"Technology\", queries[\"filters.category.name\"])\n\trequire.Equal(t, \"Alice\", queries[\"filters[customer][name]\"])\n\trequire.Equal(t, \"pending\", queries[\"filters[status]\"])\n\n\tc.Request().URI().SetQueryString(\"tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits\")\n\n\tqueries = c.Req().Queries()\n\trequire.Equal(t, \"apple,orange,banana\", queries[\"tags\"])\n\trequire.Equal(t, \"apple,orange,banana\", queries[\"filters[tags]\"])\n\trequire.Equal(t, \"fruits\", queries[\"filters[category][name]\"])\n\trequire.Equal(t, \"apple,orange,banana\", queries[\"filters.tags\"])\n\trequire.Equal(t, \"fruits\", queries[\"filters.category.name\"])\n\n\tc.Request().URI().SetQueryString(\"filters[tags][0]=apple&filters[tags][1]=orange&filters[tags][2]=banana&filters[category][name]=fruits\")\n\n\tqueries = c.Queries()\n\trequire.Equal(t, \"apple\", queries[\"filters[tags][0]\"])\n\trequire.Equal(t, \"orange\", queries[\"filters[tags][1]\"])\n\trequire.Equal(t, \"banana\", queries[\"filters[tags][2]\"])\n\trequire.Equal(t, \"fruits\", queries[\"filters[category][name]\"])\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_Queries -benchmem -count=4\nfunc Benchmark_Ctx_Queries(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tb.ReportAllocs()\n\tc.Request().URI().SetQueryString(\"id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1\")\n\n\tvar queries map[string]string\n\tfor b.Loop() {\n\t\tqueries = c.Queries()\n\t}\n\n\trequire.Equal(b, \"1\", queries[\"id\"])\n\trequire.Equal(b, \"tom\", queries[\"name\"])\n\trequire.Equal(b, \"basketball,football\", queries[\"hobby\"])\n\trequire.Equal(b, \"milo,coke,pepsi\", queries[\"favouriteDrinks\"])\n\trequire.Empty(b, queries[\"alloc\"])\n\trequire.Equal(b, \"1\", queries[\"no\"])\n}\n\n// go test -run Test_Ctx_BodyStreamWriter\nfunc Test_Ctx_BodyStreamWriter(t *testing.T) {\n\tt.Parallel()\n\tctx := &fasthttp.RequestCtx{}\n\n\tctx.SetBodyStreamWriter(func(w *bufio.Writer) {\n\t\tfmt.Fprintf(w, \"body writer line 1\\n\")\n\t\tif err := w.Flush(); err != nil {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t}\n\t\tfmt.Fprintf(w, \"body writer line 2\\n\")\n\t})\n\n\trequire.True(t, ctx.IsBodyStream())\n\n\ts := ctx.Response.String()\n\tbr := bufio.NewReader(bytes.NewBufferString(s))\n\tvar resp fasthttp.Response\n\trequire.NoError(t, resp.Read(br))\n\n\tbody := string(resp.Body())\n\texpectedBody := \"body writer line 1\\nbody writer line 2\\n\"\n\trequire.Equal(t, expectedBody, body)\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_BodyStreamWriter -benchmem -count=4\nfunc Benchmark_Ctx_BodyStreamWriter(b *testing.B) {\n\tctx := &fasthttp.RequestCtx{}\n\tuser := []byte(`{\"name\":\"john\"}`)\n\tb.ReportAllocs()\n\n\tvar err error\n\tfor b.Loop() {\n\t\tctx.ResetBody()\n\t\tctx.SetBodyStreamWriter(func(w *bufio.Writer) {\n\t\t\tfor range 10 {\n\t\t\t\t_, err = w.Write(user)\n\t\t\t\tif flushErr := w.Flush(); flushErr != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\trequire.NoError(b, err)\n}\n\nfunc Test_Ctx_String(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\trequire.Equal(t, \"#0000000000000000 - 0.0.0.0:0 <-> 0.0.0.0:0 - GET http:///\", c.String())\n}\n\n// go test -v  -run=^$ -bench=Benchmark_Ctx_String -benchmem -count=4\nfunc Benchmark_Ctx_String(b *testing.B) {\n\tvar str string\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tstr = ctx.String()\n\t}\n\trequire.Equal(b, \"#0000000000000000 - 0.0.0.0:0 <-> 0.0.0.0:0 - GET http:///\", str)\n}\n\n// go test -run Test_Ctx_IsFromLocal_X_Forwarded\nfunc Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) {\n\tt.Parallel()\n\t// Test unset X-Forwarded-For header.\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t// fasthttp returns \"0.0.0.0\" as IP as there is no remote address.\n\t\trequire.Equal(t, \"0.0.0.0\", c.IP())\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test when setting X-Forwarded-For header to localhost \"127.0.0.1\"\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderXForwardedFor, \"127.0.0.1\")\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test when setting X-Forwarded-For header to localhost \"::1\"\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderXForwardedFor, \"::1\")\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test when setting X-Forwarded-For to full localhost IPv6 address \"0:0:0:0:0:0:0:1\"\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderXForwardedFor, \"0:0:0:0:0:0:0:1\")\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test for a random IP address.\n\t{\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().Header.Set(HeaderXForwardedFor, \"93.46.8.90\")\n\n\t\trequire.False(t, c.Req().IsFromLocal())\n\t}\n}\n\n// go test -run Test_Ctx_IsFromLocal_RemoteAddr\nfunc Test_Ctx_IsFromLocal_RemoteAddr(t *testing.T) {\n\tt.Parallel()\n\n\tlocalIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP(\"127.0.0.1\")})\n\tlocalIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP(\"::1\")})\n\tlocalIPv6long := net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0:0:0:0:0:0:0:1\")})\n\n\tzeroIPv4 := net.Addr(&net.TCPAddr{IP: net.IPv4zero})\n\n\tsomeIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP(\"93.46.8.90\")})\n\tsomeIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\")})\n\n\t// Test for the case fasthttp remoteAddr is set to \"127.0.0.1\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(localIPv4)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\trequire.Equal(t, \"127.0.0.1\", c.IP())\n\t\trequire.True(t, c.IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to \"::1\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(localIPv6)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.Equal(t, \"::1\", c.Req().IP())\n\t\trequire.True(t, c.Req().IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to \"0:0:0:0:0:0:0:1\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(localIPv6long)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\t// fasthttp should return \"::1\" for \"0:0:0:0:0:0:0:1\".\n\t\t// otherwise IsFromLocal() will break.\n\t\trequire.Equal(t, \"::1\", c.IP())\n\t\trequire.True(t, c.IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to \"0.0.0.0\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(zeroIPv4)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.Equal(t, \"0.0.0.0\", c.IP())\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to \"93.46.8.90\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(someIPv4)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.Equal(t, \"93.46.8.90\", c.IP())\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\".\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tfastCtx.SetRemoteAddr(someIPv6)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.Equal(t, \"2001:db8:85a3::8a2e:370:7334\", c.IP())\n\t\trequire.False(t, c.IsFromLocal())\n\t}\n\t// Test for the case fasthttp remoteAddr is set to a Unix socket.\n\t// Unix sockets are inherently local - only processes on the same host can connect.\n\t{\n\t\tapp := New()\n\t\tfastCtx := &fasthttp.RequestCtx{}\n\t\tunixAddr := &net.UnixAddr{Name: \"/tmp/fiber.sock\", Net: \"unix\"}\n\t\tfastCtx.SetRemoteAddr(unixAddr)\n\t\tc := app.AcquireCtx(fastCtx)\n\t\tdefer app.ReleaseCtx(c)\n\t\trequire.True(t, c.IsFromLocal())\n\t}\n}\n\n// go test -run Test_Ctx_extractIPsFromHeader -v\nfunc Test_Ctx_extractIPsFromHeader(t *testing.T) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(\"x-forwarded-for\", \"1.1.1.1,8.8.8.8 , /n, \\n,1.1, a.c, 6.,6., , a,,42.118.81.169,10.0.137.108\")\n\tips := c.IPs()\n\tres := ips[len(ips)-2]\n\trequire.Equal(t, \"42.118.81.169\", res)\n}\n\n// go test -run Test_Ctx_extractIPsFromHeader -v\nfunc Test_Ctx_extractIPsFromHeader_EnableValidateIp(t *testing.T) {\n\tapp := New()\n\tapp.config.EnableIPValidation = true\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(\"x-forwarded-for\", \"1.1.1.1,8.8.8.8 , /n, \\n,1.1, a.c, 6.,6., , a,,42.118.81.169,10.0.137.108\")\n\tips := c.IPs()\n\tres := ips[len(ips)-2]\n\trequire.Equal(t, \"42.118.81.169\", res)\n}\n\n// go test -run Test_Ctx_GetRespHeaders\nfunc Test_Ctx_GetRespHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Set(\"test\", \"Hello, World 👋!\")\n\tc.Set(\"foo\", \"bar\")\n\tc.Response().Header.Set(\"multi\", \"one\")\n\tc.Response().Header.Add(\"multi\", \"two\")\n\tc.Response().Header.Set(HeaderContentType, \"application/json\")\n\n\trequire.Equal(t, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Multi\":        {\"one\", \"two\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t}, c.GetRespHeaders())\n\trequire.Equal(t, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Multi\":        {\"one\", \"two\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t}, c.Res().GetHeaders())\n}\n\nfunc Benchmark_Ctx_GetRespHeaders(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Response().Header.Set(\"test\", \"Hello, World 👋!\")\n\tc.Response().Header.Set(\"foo\", \"bar\")\n\tc.Response().Header.Set(HeaderContentType, \"application/json\")\n\n\tb.ReportAllocs()\n\n\tvar headers map[string][]string\n\tfor b.Loop() {\n\t\theaders = c.GetRespHeaders()\n\t}\n\n\trequire.Equal(b, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t}, headers)\n}\n\n// go test -run Test_Ctx_GetReqHeaders\nfunc Test_Ctx_GetReqHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"test\", \"Hello, World 👋!\")\n\tc.Request().Header.Set(\"foo\", \"bar\")\n\tc.Request().Header.Set(\"multi\", \"one\")\n\tc.Request().Header.Add(\"multi\", \"two\")\n\tc.Request().Header.Set(HeaderContentType, \"application/json\")\n\n\trequire.Equal(t, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t\t\"Multi\":        {\"one\", \"two\"},\n\t}, c.GetReqHeaders())\n\trequire.Equal(t, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t\t\"Multi\":        {\"one\", \"two\"},\n\t}, c.GetHeaders())\n}\n\nfunc Test_Ctx_Set_SanitizeHeaderValue(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Set(\"X-Test\", \"foo\\r\\nbar: bad\")\n\n\theaderVal := string(c.Response().Header.Peek(\"X-Test\"))\n\trequire.Equal(t, \"foo  bar: bad\", headerVal)\n}\n\nfunc Benchmark_Ctx_GetReqHeaders(b *testing.B) {\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(\"test\", \"Hello, World 👋!\")\n\tc.Request().Header.Set(\"foo\", \"bar\")\n\tc.Request().Header.Set(HeaderContentType, \"application/json\")\n\n\tb.ReportAllocs()\n\n\tvar headers map[string][]string\n\tfor b.Loop() {\n\t\theaders = c.GetReqHeaders()\n\t}\n\n\trequire.Equal(b, map[string][]string{\n\t\t\"Content-Type\": {\"application/json\"},\n\t\t\"Foo\":          {\"bar\"},\n\t\t\"Test\":         {\"Hello, World 👋!\"},\n\t}, headers)\n}\n\n// go test -run Test_Ctx_Drop -v\nfunc Test_Ctx_Drop(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\t// Handler that calls Drop\n\tapp.Get(\"/block-me\", func(c Ctx) error {\n\t\treturn c.Drop()\n\t})\n\n\t// Additional handler that just calls return\n\tapp.Get(\"/no-response\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\t// Test the Drop method\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/block-me\", http.NoBody))\n\trequire.ErrorIs(t, err, ErrTestGotEmptyResponse)\n\trequire.Nil(t, resp)\n\n\t// Test the no-response handler\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/no-response\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"0\", resp.Header.Get(\"Content-Length\"))\n}\n\n// go test -run Test_Ctx_DropWithMiddleware -v\nfunc Test_Ctx_DropWithMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\t// Middleware that calls Drop\n\tapp.Use(func(c Ctx) error {\n\t\terr := c.Next()\n\t\tc.Set(\"X-Test\", \"test\")\n\t\treturn err\n\t})\n\n\t// Handler that calls Drop\n\tapp.Get(\"/block-me\", func(c Ctx) error {\n\t\treturn c.Drop()\n\t})\n\n\t// Test the Drop method\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/block-me\", http.NoBody))\n\trequire.ErrorIs(t, err, ErrTestGotEmptyResponse)\n\trequire.Nil(t, resp)\n}\n\n// go test -run Test_Ctx_End\nfunc Test_Ctx_End(t *testing.T) {\n\tapp := New()\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tc.SendString(\"Hello, World!\") //nolint:errcheck // unnecessary to check error\n\t\treturn c.End()\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"io.ReadAll(resp.Body)\")\n\trequire.Equal(t, \"Hello, World!\", string(body))\n}\n\n// go test -run Test_Ctx_End_after_timeout\nfunc Test_Ctx_End_after_timeout(t *testing.T) {\n\tapp := New()\n\n\t// Early flushing handler\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\ttime.Sleep(2 * time.Second)\n\t\treturn c.End()\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n\trequire.Nil(t, resp)\n}\n\n// go test -run Test_Ctx_End_with_drop_middleware\nfunc Test_Ctx_End_with_drop_middleware(t *testing.T) {\n\tapp := New()\n\n\t// Middleware that will drop connections\n\t// that persist after c.Next()\n\tapp.Use(func(c Ctx) error {\n\t\tc.Next() //nolint:errcheck // unnecessary to check error\n\t\treturn c.Drop()\n\t})\n\n\t// Early flushing handler\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\tc.SendStatus(StatusOK) //nolint:errcheck // unnecessary to check error\n\t\treturn c.End()\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_Ctx_End_after_drop\nfunc Test_Ctx_End_after_drop(t *testing.T) {\n\tapp := New()\n\n\t// Middleware that ends the request\n\t// after c.Next()\n\tapp.Use(func(c Ctx) error {\n\t\tc.Next() //nolint:errcheck // unnecessary to check error\n\t\treturn c.End()\n\t})\n\n\t// Early flushing handler\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.Drop()\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.ErrorIs(t, err, ErrTestGotEmptyResponse)\n\trequire.Nil(t, resp)\n}\n\n// go test -run Test_Ctx_OverrideParam\nfunc Test_Ctx_OverrideParam(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"route_params\", func(t *testing.T) {\n\t\t// a basic request to check if OverrideParam functions correctly on different scenarios\n\t\t// - Does it change an existing param (it should)\n\t\t// - Does it ignore a non-existing param (it should)\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tapp.Get(\"/user/:name/:id\", func(c Ctx) error {\n\t\t\tc.OverrideParam(\"name\", \"overridden\")\n\t\t\tc.OverrideParam(\"nonexistent\", \"ignored\")\n\t\t\trequire.Equal(t, \"overridden\", c.Params(\"name\"))\n\t\t\trequire.Equal(t, \"123\", c.Params(\"id\"))\n\t\t\trequire.Empty(t, c.Params(\"nonexistent\"))\n\t\t\trequire.Equal(t, []string{\"name\", \"id\"}, c.Route().Params)\n\n\t\t\treturn c.JSON(map[string]any{\n\t\t\t\t\"name\": c.Params(\"name\"),\n\t\t\t\t\"id\":   c.Params(\"id\"),\n\t\t\t\t\"all\":  c.Route().Params,\n\t\t\t})\n\t\t})\n\n\t\treq, err := http.NewRequest(http.MethodGet, \"/user/original/123\", http.NoBody)\n\t\trequire.NoError(t, err)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"plus_wildcard_params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tapp.Get(\"/files+/+\",\n\t\t\tfunc(c Ctx) error {\n\t\t\t\tc.OverrideParam(\"+\", \"changed\")\n\t\t\t\tc.OverrideParam(\"+2\", \"changed2\")\n\n\t\t\t\trequire.Equal(t, \"changed\", c.Params(\"+\"))\n\t\t\t\trequire.Equal(t, \"changed2\", c.Params(\"+2\"))\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\t\treq, err := http.NewRequest(http.MethodGet, \"/filesoriginal/original2\", http.NoBody)\n\t\trequire.NoError(t, err)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"wildcard_params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tapp.Get(\"/files/*\", func(c Ctx) error {\n\t\t\tc.OverrideParam(\"*\", \"changed\")\n\t\t\trequire.Equal(t, \"changed\", c.Params(\"*\"))\n\t\t\treturn nil\n\t\t})\n\t\treq, err := http.NewRequest(http.MethodGet, \"/files/testing\", http.NoBody)\n\t\trequire.NoError(t, err)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"multi_wildcard_params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\tapp.Get(\"/files/*/*\", func(c Ctx) error {\n\t\t\tc.OverrideParam(\"*\", \"changed\")\n\t\t\tc.OverrideParam(\"*2\", \"changed2\")\n\t\t\trequire.Equal(t, \"changed\", c.Params(\"*\"))\n\t\t\trequire.Equal(t, \"changed2\", c.Params(\"*2\"))\n\t\t\treturn nil\n\t\t})\n\t\treq, err := http.NewRequest(http.MethodGet, \"/files/testing/testing\", http.NoBody)\n\t\trequire.NoError(t, err)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"case_sensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Ensure OverrideParam respects the CaseSensitive configuration\n\t\tapp := New(Config{\n\t\t\tCaseSensitive: true,\n\t\t})\n\n\t\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\t\tc.OverrideParam(\"name\", \"overridden\")\n\n\t\t\trequire.Equal(t, \"overridden\", c.Params(\"name\"))\n\t\t\trequire.Empty(t, c.Params(\"NAME\"))\n\n\t\t\treturn c.SendStatus(StatusOK)\n\t\t})\n\n\t\treq, err := http.NewRequest(http.MethodGet, \"/user/original\", http.NoBody)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"case_insensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// CaseInsensitive mode (default)\n\t\tapp := New(Config{\n\t\t\tCaseSensitive: false,\n\t\t})\n\n\t\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\t\tc.OverrideParam(\"NAME\", \"overridden\")\n\n\t\t\trequire.Equal(t, \"overridden\", c.Params(\"name\"))\n\t\t\trequire.Equal(t, \"overridden\", c.Params(\"NAME\"))\n\n\t\t\treturn c.SendStatus(StatusOK)\n\t\t})\n\n\t\treq, err := http.NewRequest(http.MethodGet, \"/user/original\", http.NoBody)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tdefer func() { require.NoError(t, resp.Body.Close()) }()\n\n\t\trequire.Equal(t, StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"nil_router\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Ensure OverrideParam handles nil route context gracefully\n\t\tapp := New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc, ok := ctx.(*DefaultCtx)\n\t\trequire.True(t, ok)\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.route = nil\n\n\t\tc.OverrideParam(\"test\", \"value\") // Should not change\n\t\trequire.Empty(t, c.Params(\"test\"))\n\t})\n}\n\nfunc Test_Ctx_AbandonSkipsReleaseCtx(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // controlled test setup\n\tctx.route = &Route{}\n\n\tt.Cleanup(func() {\n\t\tctx.ForceRelease()\n\t})\n\n\trequire.False(t, ctx.IsAbandoned())\n\n\tctx.Abandon()\n\trequire.True(t, ctx.IsAbandoned())\n\n\tapp.ReleaseCtx(ctx)\n\n\trequire.True(t, ctx.IsAbandoned(), \"ReleaseCtx must not pool abandoned contexts\")\n\trequire.NotNil(t, ctx.fasthttp, \"ReleaseCtx should not reset fasthttp on abandoned ctx\")\n\trequire.NotNil(t, ctx.route, \"ReleaseCtx should not reset route on abandoned ctx\")\n}\n\nfunc Test_Ctx_ForceReleaseClearsAbandon(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // controlled test setup\n\tctx.route = &Route{}\n\n\tctx.Abandon()\n\tctx.ForceRelease()\n\n\trequire.False(t, ctx.IsAbandoned(), \"ForceRelease should clear abandon flag\")\n\trequire.Nil(t, ctx.fasthttp, \"ForceRelease should release fasthttp reference\")\n\trequire.Nil(t, ctx.route, \"ForceRelease should reset route before pooling\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_IsProxyTrusted -benchmem -count=4\nfunc Benchmark_Ctx_IsProxyTrusted(b *testing.B) {\n\t// Scenario without trusted proxy check\n\tb.Run(\"NoProxyCheck\", func(b *testing.B) {\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com:8080/test\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario without trusted proxy check in parallel\n\tb.Run(\"NoProxyCheckParallel\", func(b *testing.B) {\n\t\tapp := New()\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com:8080/test\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check simple\n\tb.Run(\"WithProxyCheckSimple\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check simple in parallel\n\tb.Run(\"WithProxyCheckSimpleParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check\n\tb.Run(\"WithProxyCheck\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check in parallel\n\tb.Run(\"WithProxyCheckParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0\"},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check allow private\n\tb.Run(\"WithProxyCheckAllowPrivate\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tPrivate: true,\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check allow private in parallel\n\tb.Run(\"WithProxyCheckAllowPrivateParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tPrivate: true,\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check allow private as subnets\n\tb.Run(\"WithProxyCheckAllowPrivateAsSubnets\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"fc00::/7\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check allow private as subnets in parallel\n\tb.Run(\"WithProxyCheckAllowPrivateAsSubnetsParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"fc00::/7\"},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check allow private, loopback, and link-local\n\tb.Run(\"WithProxyCheckAllowAll\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tPrivate:   true,\n\t\t\t\tLoopback:  true,\n\t\t\t\tLinkLocal: true,\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check allow private, loopback, and link-local in parallel\n\tb.Run(\"WithProxyCheckAllowAllParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tPrivate:   true,\n\t\t\t\tLoopback:  true,\n\t\t\t\tLinkLocal: true,\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check allow private, loopback, and link-local as subnets\n\tb.Run(\"WithProxyCheckAllowAllowAllAsSubnets\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\n\t\t\t\t\t// Link-local\n\t\t\t\t\t\"169.254.0.0/16\",\n\t\t\t\t\t\"fe80::/10\",\n\t\t\t\t\t// Loopback\n\t\t\t\t\t\"127.0.0.0/8\",\n\t\t\t\t\t\"::1/128\",\n\t\t\t\t\t// Private\n\t\t\t\t\t\"10.0.0.0/8\",\n\t\t\t\t\t\"172.16.0.0/12\",\n\t\t\t\t\t\"192.168.0.0/16\",\n\t\t\t\t\t\"fc00::/7\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check allow private, loopback, and link-local as subnets in parallel\n\tb.Run(\"WithProxyCheckAllowAllowAllAsSubnetsParallel\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\n\t\t\t\t\t// Link-local\n\t\t\t\t\t\"169.254.0.0/16\",\n\t\t\t\t\t\"fe80::/10\",\n\t\t\t\t\t// Loopback\n\t\t\t\t\t\"127.0.0.0/8\",\n\t\t\t\t\t\"::1/128\",\n\t\t\t\t\t// Private\n\t\t\t\t\t\"10.0.0.0/8\",\n\t\t\t\t\t\"172.16.0.0/12\",\n\t\t\t\t\t\"192.168.0.0/16\",\n\t\t\t\t\t\"fc00::/7\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check with subnet\n\tb.Run(\"WithProxyCheckSubnet\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0/8\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check with subnet in parallel\n\tb.Run(\"WithProxyCheckParallelSubnet\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"0.0.0.0/8\"},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check with multiple subnet\n\tb.Run(\"WithProxyCheckMultipleSubnet\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"192.168.0.0/24\", \"10.0.0.0/16\", \"0.0.0.0/8\"},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check with multiple subnet in parallel\n\tb.Run(\"WithProxyCheckParallelMultipleSubnet\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\"192.168.0.0/24\", \"10.0.0.0/16\", \"0.0.0.0/8\"},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n\n\t// Scenario with trusted proxy check with all subnets\n\tb.Run(\"WithProxyCheckAllSubnets\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\n\t\t\t\t\t\"127.0.0.0/8\",     // Loopback addresses\n\t\t\t\t\t\"169.254.0.0/16\",  // Link-Local addresses\n\t\t\t\t\t\"fe80::/10\",       // Link-Local addresses\n\t\t\t\t\t\"192.168.0.0/16\",  // Private Network addresses\n\t\t\t\t\t\"172.16.0.0/12\",   // Private Network addresses\n\t\t\t\t\t\"10.0.0.0/8\",      // Private Network addresses\n\t\t\t\t\t\"fc00::/7\",        // Unique Local addresses\n\t\t\t\t\t\"173.245.48.0/20\", // My custom range\n\t\t\t\t\t\"0.0.0.0/8\",       // All IPv4 addresses\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com/test\")\n\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsProxyTrusted()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with trusted proxy check with all subnets in parallel\n\tb.Run(\"WithProxyCheckParallelAllSubnets\", func(b *testing.B) {\n\t\tapp := New(Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: TrustProxyConfig{\n\t\t\t\tProxies: []string{\n\t\t\t\t\t\"127.0.0.0/8\",     // Loopback addresses\n\t\t\t\t\t\"169.254.0.0/16\",  // Link-Local addresses\n\t\t\t\t\t\"fe80::/10\",       // Link-Local addresses\n\t\t\t\t\t\"192.168.0.0/16\",  // Private Network addresses\n\t\t\t\t\t\"172.16.0.0/12\",   // Private Network addresses\n\t\t\t\t\t\"10.0.0.0/8\",      // Private Network addresses\n\t\t\t\t\t\"fc00::/7\",        // Unique Local addresses\n\t\t\t\t\t\"173.245.48.0/20\", // My custom range\n\t\t\t\t\t\"0.0.0.0/8\",       // All IPv4 addresses\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tc.Request().SetRequestURI(\"http://google.com/\")\n\t\t\tc.Request().Header.Set(HeaderXForwardedHost, \"google1.com\")\n\t\t\tfor pb.Next() {\n\t\t\t\tc.IsProxyTrusted()\n\t\t\t}\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t})\n}\n\nfunc Benchmark_Ctx_IsFromLocalhost(b *testing.B) {\n\t// Scenario without localhost check\n\tb.Run(\"Non_Localhost\", func(b *testing.B) {\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://google.com:8080/test\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsFromLocal()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n\n\t// Scenario with localhost check\n\tb.Run(\"Localhost\", func(b *testing.B) {\n\t\tapp := New()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tc.Request().SetRequestURI(\"http://localhost:8080/test\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tc.IsFromLocal()\n\t\t}\n\t\tapp.ReleaseCtx(c)\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Ctx_OverrideParam -benchmem -count=4\nfunc Benchmark_Ctx_OverrideParam(b *testing.B) {\n\tapp := New()\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc, ok := ctx.(*DefaultCtx)\n\tif !ok {\n\t\tb.Fatal(\"AcquireCtx did not return *DefaultCtx\")\n\t}\n\n\tdefer app.ReleaseCtx(c)\n\n\tc.values = [maxParams]string{\"original\", \"12345\"}\n\tc.route = &Route{Params: []string{\"name\", \"id\"}}\n\tc.setMatched(true)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor b.Loop() {\n\t\tc.OverrideParam(\"name\", \"changed\")\n\t}\n}\n"
  },
  {
    "path": "docs/addon/_category_.json",
    "content": "{\n  \"label\": \"\\uD83D\\uDD0C Addon\",\n  \"position\": 5,\n  \"collapsed\": true,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"Addon is an additional useful package that can be used in Fiber.\"\n  }\n}\n"
  },
  {
    "path": "docs/addon/retry.md",
    "content": "---\nid: retry\n---\n\n# Retry Addon\n\nThe Retry addon for [Fiber](https://github.com/gofiber/fiber) retries failed network operations using exponential\nbackoff with jitter. It repeatedly invokes a function until it succeeds or the maximum number of attempts is\nexhausted. Jitter at each step breaks client synchronization and helps avoid collisions. If all attempts fail, the\naddon returns an error.\n\n## Table of Contents\n\n- [Signatures](#signatures)\n- [Examples](#examples)\n- [Default Config](#default-config)\n- [Custom Config](#custom-config)\n- [Config](#config)\n- [Default Config Example](#default-config-example)\n\n## Signatures\n\n```go\nfunc NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff\n```\n\n## Examples\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3/addon/retry\"\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    expBackoff := retry.NewExponentialBackoff(retry.Config{})\n\n    // Local variables used inside Retry\n    var resp *client.Response\n    var err error\n\n    // Retry a network request and return an error to signal another attempt\n    err = expBackoff.Retry(func() error {\n        client := client.New()\n        resp, err = client.Get(\"https://gofiber.io\")\n        if err != nil {\n            return fmt.Errorf(\"GET gofiber.io failed: %w\", err)\n        }\n        if resp.StatusCode() != 200 {\n            return fmt.Errorf(\"GET gofiber.io did not return 200 OK\")\n        }\n        return nil\n    })\n\n    // If all retries failed, panic\n    if err != nil {\n        panic(err)\n    }\n    fmt.Printf(\"GET gofiber.io succeeded with status code %d\\n\", resp.StatusCode())\n}\n```\n\n## Default Config\n\n```go\nretry.NewExponentialBackoff()\n```\n\n## Custom Config\n\n```go\nretry.NewExponentialBackoff(retry.Config{\n    InitialInterval: 2 * time.Second,\n    MaxBackoffTime:  64 * time.Second,\n    Multiplier:      2.0,\n    MaxRetryCount:   15,\n})\n```\n\n## Config\n\n```go\n// Config defines the config for addon.\ntype Config struct {\n    // InitialInterval defines the initial time interval for backoff algorithm.\n    //\n    // Optional. Default: 1 * time.Second\n    InitialInterval time.Duration\n\n    // MaxBackoffTime defines maximum time duration for backoff algorithm. When\n    // the algorithm is reached this time, rest of the retries will be maximum\n    // 32 seconds.\n    //\n    // Optional. Default: 32 * time.Second\n    MaxBackoffTime time.Duration\n\n    // Multiplier defines multiplier number of the backoff algorithm.\n    //\n    // Optional. Default: 2.0\n    Multiplier float64\n\n    // MaxRetryCount defines maximum retry count for the backoff algorithm.\n    //\n    // Optional. Default: 10\n    MaxRetryCount int\n}\n```\n\n## Default Config Example\n\n```go\n// DefaultConfig is the default config for retry.\nvar DefaultConfig = Config{\n    InitialInterval: 1 * time.Second,\n    MaxBackoffTime:  32 * time.Second,\n    Multiplier:      2.0,\n    MaxRetryCount:   10,\n    currentInterval: 1 * time.Second,\n}\n```\n"
  },
  {
    "path": "docs/api/_category_.json",
    "content": "{\n  \"label\": \"\\uD83D\\uDEE0\\uFE0F API\",\n  \"position\": 3,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"API documentation for Fiber.\"\n  }\n}\n"
  },
  {
    "path": "docs/api/app.md",
    "content": "---\nid: app\ntitle: 🚀 App\ndescription: The `App` type represents your Fiber application.\nsidebar_position: 2\n---\n\nimport Reference from '@site/src/components/reference';\n\n## Helpers\n\n### GetString\n\nReturns `s` unchanged when [`Immutable`](./fiber.md#immutable) is disabled or `s` resides in read-only memory. Otherwise, it returns a detached copy using `strings.Clone`.\n\n```go title=\"Signature\"\nfunc (app *App) GetString(s string) string\n```\n\n### GetBytes\n\nReturns `b` unchanged when [`Immutable`](./fiber.md#immutable) is disabled or `b` resides in read-only memory. Otherwise, it returns a detached copy.\n\n```go title=\"Signature\"\nfunc (app *App) GetBytes(b []byte) []byte\n```\n\n### ReloadViews\n\nReloads the configured view engine on demand by calling its `Load` method. Use this helper in development workflows (e.g., file watchers or debug-only routes) to pick up template changes without restarting the server. Returns an error if no view engine is configured or reloading fails.\n\n```go title=\"Signature\"\nfunc (app *App) ReloadViews() error\n```\n\n```go title=\"Example\"\napp := fiber.New(fiber.Config{Views: engine})\n\napp.Get(\"/dev/reload\", func(c fiber.Ctx) error {\n    if err := app.ReloadViews(); err != nil {\n        return err\n    }\n    return c.SendString(\"Templates reloaded\")\n})\n```\n\n## Routing\n\nimport RoutingHandler from './../partials/routing/handler.md';\n\n### Route Handlers\n\n<RoutingHandler />\n\n### Mounting\n\nMount another Fiber instance with [`app.Use`](./app.md#use), similar to Express's [`router.use`](https://expressjs.com/en/api.html#router.use).\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n    micro := fiber.New()\n\n    // Mount the micro app on the \"/john\" route\n    app.Use(\"/john\", micro) // GET /john/doe -> 200 OK\n\n    micro.Get(\"/doe\", func(c fiber.Ctx) error {\n        return c.SendStatus(fiber.StatusOK)\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### MountPath\n\nThe `MountPath` property contains one or more path patterns on which a sub-app was mounted.\n\n```go title=\"Signature\"\nfunc (app *App) MountPath() string\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n    one := fiber.New()\n    two := fiber.New()\n    three := fiber.New()\n\n    two.Use(\"/three\", three)\n    one.Use(\"/two\", two)\n    app.Use(\"/one\", one)\n\n    fmt.Println(\"Mount paths:\")\n    fmt.Println(\"one.MountPath():\", one.MountPath())       // \"/one\"\n    fmt.Println(\"two.MountPath():\", two.MountPath())       // \"/one/two\"\n    fmt.Println(\"three.MountPath():\", three.MountPath())   // \"/one/two/three\"\n    fmt.Println(\"app.MountPath():\", app.MountPath())       // \"\"\n}\n```\n\n:::caution\nMounting order is important for `MountPath`. To get mount paths properly, you should start mounting from the deepest app.\n:::\n\n### Group\n\nYou can group routes by creating a `*Group` struct.\n\n```go title=\"Signature\"\nfunc (app *App) Group(prefix string, handlers ...any) Router\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    api := app.Group(\"/api\", handler)  // /api\n\n    v1 := api.Group(\"/v1\", handler)    // /api/v1\n    v1.Get(\"/list\", handler)           // /api/v1/list\n    v1.Get(\"/user\", handler)           // /api/v1/user\n\n    v2 := api.Group(\"/v2\", handler)    // /api/v2\n    v2.Get(\"/list\", handler)           // /api/v2/list\n    v2.Get(\"/user\", handler)           // /api/v2/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n\nfunc handler(c fiber.Ctx) error {\n    return c.SendString(\"Handler response\")\n}\n```\n\n### RouteChain\n\nReturns an instance of a single route, which you can then use to handle HTTP verbs with optional middleware.\n\nSimilar to [`Express`](https://expressjs.com/en/api.html#app.route).\n\n```go title=\"Signature\"\nfunc (app *App) RouteChain(path string) Register\n```\n\n<details>\n<summary>Click here to see the `Register` interface</summary>\n\n```go\ntype Register interface {\n    All(handler any, handlers ...any) Register\n    Get(handler any, handlers ...any) Register\n    Head(handler any, handlers ...any) Register\n    Post(handler any, handlers ...any) Register\n    Put(handler any, handlers ...any) Register\n    Delete(handler any, handlers ...any) Register\n    Connect(handler any, handlers ...any) Register\n    Options(handler any, handlers ...any) Register\n    Trace(handler any, handlers ...any) Register\n    Patch(handler any, handlers ...any) Register\n\n    Add(methods []string, handler any, handlers ...any) Register\n\n    RouteChain(path string) Register\n}\n```\n\n</details>\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Use `RouteChain` as a chainable route declaration method\n    app.RouteChain(\"/test\").Get(func(c fiber.Ctx) error {\n        return c.SendString(\"GET /test\")\n    })\n\n    app.RouteChain(\"/events\").All(func(c fiber.Ctx) error {\n        // Runs for all HTTP verbs first\n        // Think of it as route-specific middleware!\n    }).\n    Get(func(c fiber.Ctx) error {\n        return c.SendString(\"GET /events\")\n    }).\n    Post(func(c fiber.Ctx) error {\n        // Maybe add a new event...\n        return c.SendString(\"POST /events\")\n    })\n\n    // Combine multiple routes\n    app.RouteChain(\"/reports\").RouteChain(\"/daily\").Get(func(c fiber.Ctx) error {\n        return c.SendString(\"GET /reports/daily\")\n    })\n\n    // Use multiple methods\n    app.RouteChain(\"/api\").Get(func(c fiber.Ctx) error {\n        return c.SendString(\"GET /api\")\n    }).Post(func(c fiber.Ctx) error {\n        return c.SendString(\"POST /api\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Route\n\nDefines routes with a common prefix inside the supplied function. Internally it uses [`Group`](#group) to create a sub-router and accepts an optional name prefix.\n\n```go title=\"Signature\"\nfunc (app *App) Route(prefix string, fn func(router Router), name ...string) Router\n```\n\n```go title=\"Example\"\napp.Route(\"/test\", func(api fiber.Router) {\n    api.Get(\"/foo\", handler).Name(\"foo\") // /test/foo (name: test.foo)\n    api.Get(\"/bar\", handler).Name(\"bar\") // /test/bar (name: test.bar)\n}, \"test.\")\n```\n\n### HandlersCount\n\nReturns the number of registered handlers.\n\n```go title=\"Signature\"\nfunc (app *App) HandlersCount() uint32\n```\n\n### Stack\n\nReturns the underlying router stack.\n\n```go title=\"Signature\"\nfunc (app *App) Stack() [][]*Route\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"encoding/json\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nvar handler = func(c fiber.Ctx) error { return nil }\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/john/:age\", handler)\n    app.Post(\"/register\", handler)\n\n    data, _ := json.MarshalIndent(app.Stack(), \"\", \"  \")\n    fmt.Println(string(data))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n[\n  [\n    {\n      \"method\": \"GET\",\n      \"path\": \"/john/:age\",\n      \"params\": [\n        \"age\"\n      ]\n    }\n  ],\n  [\n    {\n      \"method\": \"HEAD\",\n      \"path\": \"/john/:age\",\n      \"params\": [\n        \"age\"\n      ]\n    }\n  ],\n  [\n    {\n      \"method\": \"POST\",\n      \"path\": \"/register\",\n      \"params\": null\n    }\n  ]\n]\n```\n\n</details>\n\n### Name\n\nThis method assigns the name to the latest created route.\n\n```go title=\"Signature\"\nfunc (app *App) Name(name string) Router\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    var handler = func(c fiber.Ctx) error { return nil }\n\n    app := fiber.New()\n\n    app.Get(\"/\", handler)\n    app.Name(\"index\")\n    app.Get(\"/doe\", handler).Name(\"home\")\n    app.Trace(\"/tracer\", handler).Name(\"tracert\")\n    app.Delete(\"/delete\", handler).Name(\"delete\")\n\n    a := app.Group(\"/a\")\n    a.Name(\"fd.\")\n\n    a.Get(\"/test\", handler).Name(\"test\")\n\n    data, _ := json.MarshalIndent(app.Stack(), \"\", \"  \")\n    fmt.Println(string(data))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n[\n  [\n    {\n      \"method\": \"GET\",\n      \"name\": \"index\",\n      \"path\": \"/\",\n      \"params\": null\n    },\n    {\n      \"method\": \"GET\",\n      \"name\": \"home\",\n      \"path\": \"/doe\",\n      \"params\": null\n    },\n    {\n      \"method\": \"GET\",\n      \"name\": \"fd.test\",\n      \"path\": \"/a/test\",\n      \"params\": null\n    }\n  ],\n  [\n    {\n      \"method\": \"HEAD\",\n      \"name\": \"\",\n      \"path\": \"/\",\n      \"params\": null\n    },\n    {\n      \"method\": \"HEAD\",\n      \"name\": \"\",\n      \"path\": \"/doe\",\n      \"params\": null\n    },\n    {\n      \"method\": \"HEAD\",\n      \"name\": \"\",\n      \"path\": \"/a/test\",\n      \"params\": null\n    }\n  ],\n  null,\n  null,\n  [\n    {\n      \"method\": \"DELETE\",\n      \"name\": \"delete\",\n      \"path\": \"/delete\",\n      \"params\": null\n    }\n  ],\n  null,\n  null,\n  [\n    {\n      \"method\": \"TRACE\",\n      \"name\": \"tracert\",\n      \"path\": \"/tracer\",\n      \"params\": null\n    }\n  ],\n  null\n]\n```\n\n</details>\n\n### GetRoute\n\nThis method retrieves a route by its name.\n\n```go title=\"Signature\"\nfunc (app *App) GetRoute(name string) Route\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/\", handler).Name(\"index\")\n\n    route := app.GetRoute(\"index\")\n\n    data, _ := json.MarshalIndent(route, \"\", \"  \")\n    fmt.Println(string(data))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n{\n  \"method\": \"GET\",\n  \"name\": \"index\",\n  \"path\": \"/\",\n  \"params\": null\n}\n```\n\n</details>\n\n### GetRoutes\n\nThis method retrieves all routes.\n\n```go title=\"Signature\"\nfunc (app *App) GetRoutes(filterUseOption ...bool) []Route\n```\n\nWhen `filterUseOption` is set to `true`, it filters out routes registered by middleware.\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Post(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    }).Name(\"index\")\n\n    routes := app.GetRoutes(true)\n\n    data, _ := json.MarshalIndent(routes, \"\", \"  \")\n    fmt.Println(string(data))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n[\n    {\n        \"method\": \"POST\",\n        \"name\": \"index\",\n        \"path\": \"/\",\n        \"params\": null\n    }\n]\n```\n\n</details>\n\n## Config\n\n`Config` returns the [app config](./fiber.md#config) as a value (read-only).\n\n```go title=\"Signature\"\nfunc (app *App) Config() Config\n```\n\n## Handler\n\n`Handler` returns the server handler that can be used to serve custom [`\\*fasthttp.RequestCtx`](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) requests.\n\n```go title=\"Signature\"\nfunc (app *App) Handler() fasthttp.RequestHandler\n```\n\n## ErrorHandler\n\n`ErrorHandler` executes the process defined for the application in case of errors. This is used in some cases in middlewares.\n\n```go title=\"Signature\"\nfunc (app *App) ErrorHandler(ctx Ctx, err error) error\n```\n\n## NewWithCustomCtx\n\n`NewWithCustomCtx` creates a new `*App` and sets the custom context factory\nfunction at construction time.\n\n```go title=\"Signature\"\nfunc NewWithCustomCtx(fn func(app *App) CustomCtx, config ...Config) *App\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\ntype CustomCtx struct {\n    fiber.DefaultCtx\n}\n\nfunc (c *CustomCtx) Params(key string, defaultValue ...string) string {\n    return \"prefix_\" + c.DefaultCtx.Params(key)\n}\n\nfunc main() {\n    app := fiber.NewWithCustomCtx(func(app *fiber.App) fiber.CustomCtx {\n        return &CustomCtx{\n            DefaultCtx: *fiber.NewDefaultCtx(app),\n        }\n    })\n\n    app.Get(\"/:id\", func(c fiber.Ctx) error {\n        return c.SendString(c.Params(\"id\"))\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## RegisterCustomBinder\n\nYou can register custom binders to use with [`Bind().Custom(\"name\")`](bind.md#custom). They should be compatible with the `CustomBinder` interface.\n\n```go title=\"Signature\"\nfunc (app *App) RegisterCustomBinder(binder CustomBinder)\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"gopkg.in/yaml.v2\"\n)\n\ntype User struct {\n    Name string `yaml:\"name\"`\n}\n\ntype customBinder struct{}\n\nfunc (*customBinder) Name() string {\n    return \"custom\"\n}\n\nfunc (*customBinder) MIMETypes() []string {\n    return []string{\"application/yaml\"}\n}\n\nfunc (*customBinder) Parse(c fiber.Ctx, out any) error {\n    // Parse YAML body\n    return yaml.Unmarshal(c.Body(), out)\n}\n\nfunc main() {\n    app := fiber.New()\n\n    // Register custom binder\n    app.RegisterCustomBinder(&customBinder{})\n\n    app.Post(\"/custom\", func(c fiber.Ctx) error {\n        var user User\n        // Use Custom binder by name\n        if err := c.Bind().Custom(\"custom\", &user); err != nil {\n            return err\n        }\n        return c.JSON(user)\n    })\n\n    app.Post(\"/normal\", func(c fiber.Ctx) error {\n        var user User\n        // Custom binder is used by the MIME type\n        if err := c.Bind().Body(&user); err != nil {\n            return err\n        }\n        return c.JSON(user)\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## RegisterCustomConstraint\n\n`RegisterCustomConstraint` allows you to register custom constraints.\n\n```go title=\"Signature\"\nfunc (app *App) RegisterCustomConstraint(constraint CustomConstraint)\n```\n\nSee the [Custom Constraint](../guide/routing.md#custom-constraint) section for more information.\n\n## SetTLSHandler\n\nUse `SetTLSHandler` to set [`ClientHelloInfo`](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) when using TLS with a `Listener`.\n\n```go title=\"Signature\"\nfunc (app *App) SetTLSHandler(tlsHandler *TLSHandler)\n```\n\n## Test\n\nTesting your application is done with the `Test` method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s`; to disable a timeout altogether, pass a `TestConfig` struct with `Timeout: 0`.\n\n```go title=\"Signature\"\nfunc (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, error)\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"log\"\n    \"net/http\"\n    \"net/http/httptest\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Create route with GET method for test:\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        fmt.Println(c.BaseURL())              // => http://google.com\n        fmt.Println(c.Get(\"X-Custom-Header\")) // => hi\n        return c.SendString(\"hello, World!\")\n    })\n\n    // Create http.Request\n    req := httptest.NewRequest(\"GET\", \"http://google.com\", nil)\n    req.Header.Set(\"X-Custom-Header\", \"hi\")\n\n    // Perform the test\n    resp, _ := app.Test(req)\n\n    // Do something with the results:\n    if resp.StatusCode == fiber.StatusOK {\n        body, _ := io.ReadAll(resp.Body)\n        fmt.Println(string(body)) // => hello, World!\n    }\n}\n```\n\nIf not provided, TestConfig is set to the following defaults:\n\n```go title=\"Default TestConfig\"\nconfig := fiber.TestConfig{\n  Timeout:      time.Second,\n  FailOnTimeout: true,\n}\n```\n\n:::caution\n\nThis is **not** the same as supplying an empty `TestConfig{}` to\n`app.Test(), but rather be the equivalent of supplying:\n\n```go title=\"Empty TestConfig\"\ncfg := fiber.TestConfig{\n  Timeout:      0,\n  FailOnTimeout: false,\n}\n```\n\nThis would make a Test that has no timeout.\n\n:::\n\n## Hooks\n\n`Hooks` is a method to return the [hooks](./hooks.md) property.\n\n```go title=\"Signature\"\nfunc (app *App) Hooks() *Hooks\n```\n\n## RebuildTree\n\nThe `RebuildTree` method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the `App` instance.\n\n```go title=\"Signature\"\nfunc (app *App) RebuildTree() *App\n```\n\n**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.\n\n### Example Usage\n\nHere’s an example of how to define and register routes dynamically:\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/define\", func(c fiber.Ctx) error {\n        // Define a new route dynamically\n        app.Get(\"/dynamically-defined\", func(c fiber.Ctx) error {\n            return c.SendStatus(fiber.StatusOK)\n        })\n\n        // Rebuild the route tree to register the new route\n        app.RebuildTree()\n\n        return c.SendStatus(fiber.StatusOK)\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nIn this example, a new route is defined and then `RebuildTree()` is called to ensure the new route is registered and available.\n\n## RemoveRoute\n\nThis method removes a route by path. You must call the `RebuildTree()` method after the removal to finalize the update and rebuild the routing tree.\nIf no methods are specified, the route will be removed for all HTTP methods defined in the app. To limit removal to specific methods, provide them as additional arguments.\n\n```go title=\"Signature\"\nfunc (app *App) RemoveRoute(path string, methods ...string)\n```\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/api/feature-a\", func(c fiber.Ctx) error {\n           app.RemoveRoute(\"/api/feature\", fiber.MethodGet)\n           app.RebuildTree()\n           // Redefine route\n           app.Get(\"/api/feature\", func(c fiber.Ctx) error {\n                   return c.SendString(\"Testing feature-a\")\n           })\n\n           app.RebuildTree()\n           return c.SendStatus(fiber.StatusOK)\n    })\n    app.Get(\"/api/feature-b\", func(c fiber.Ctx) error {\n           app.RemoveRoute(\"/api/feature\", fiber.MethodGet)\n           app.RebuildTree()\n           // Redefine route\n           app.Get(\"/api/feature\", func(c fiber.Ctx) error {\n                   return c.SendString(\"Testing feature-b\")\n           })\n\n           app.RebuildTree()\n           return c.SendStatus(fiber.StatusOK)\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## RemoveRouteByName\n\nThis method removes a route by name.\nIf no methods are specified, the route will be removed for all HTTP methods defined in the app. To limit removal to specific methods, provide them as additional arguments.\n\n```go title=\"Signature\"\nfunc (app *App) RemoveRouteByName(name string, methods ...string)\n```\n\n## RemoveRouteFunc\n\nThis method removes a route by function having `*Route` parameter.\nIf no methods are specified, the route will be removed for all HTTP methods defined in the app. To limit removal to specific methods, provide them as additional arguments.\n\n```go title=\"Signature\"\nfunc (app *App) RemoveRouteFunc(matchFunc func(r *Route) bool, methods ...string)\n```\n"
  },
  {
    "path": "docs/api/bind.md",
    "content": "---\nid: bind\ntitle: 📎 Bind\ndescription: Binds the request and response items to a struct.\nsidebar_position: 4\ntoc_max_heading_level: 4\n---\n\nBindings parse request and response bodies, query parameters, cookies, and more into structs.\n\n:::info\nBinder-returned values are valid only within the handler. To keep them, copy the data\nor enable the [**`Immutable`**](./ctx.md) setting. [Read more...](../#zero-allocation)\n:::\n\n## Binders\n\n- [All](#all)\n- [Body](#body)\n  - [CBOR](#cbor)\n  - [Form](#form)\n  - [JSON](#json)\n  - [MsgPack](#msgpack)\n  - [XML](#xml)\n- [Cookie](#cookie)\n- [Header](#header)\n- [Query](#query)\n- [RespHeader](#respheader)\n- [URI](#uri)\n\n### All\n\nThe `All` function binds data from URL parameters, the request body, query parameters, headers, and cookies into `out`. Sources are applied in the following order using struct field tags.\n\n#### Precedence Order\n\nThe binding sources have the following precedence:\n\n1. **URL Parameters (URI)**\n2. **Request Body (e.g., JSON or form data)**\n3. **Query Parameters**\n4. **Request Headers**\n5. **Cookies**\n\n:::info\nThe request body is only included as a binding source when the request has both a non-empty body **and** a non-empty `Content-Type` header.\n:::\n\n```go title=\"Signature\"\nfunc (b *Bind) All(out any) error\n```\n\n```go title=\"Example\"\ntype User struct {\n    Name      string                `query:\"name\" json:\"name\" form:\"name\"`\n    Email     string                `json:\"email\" form:\"email\"`\n    Role      string                `header:\"X-User-Role\"`\n    SessionID string                `json:\"session_id\" cookie:\"session_id\"`\n    ID        int                   `uri:\"id\" query:\"id\" json:\"id\" form:\"id\"`\n}\n\napp.Post(\"/users\", func(c fiber.Ctx) error {\n    user := new(User)\n\n    if err := c.Bind().All(user); err != nil {\n        return err\n    }\n\n    // All available data is now bound to the user struct\n    return c.JSON(user)\n})\n```\n\n### Body\n\nBinds the request body to a struct.\n\nUse tags that match the content type. For example, to parse a JSON body with a `Pass` field, declare `json:\"pass\"`.\n\n| Content-Type                        | Struct Tag |\n| ----------------------------------- | ---------- |\n| `application/x-www-form-urlencoded` | `form`     |\n| `multipart/form-data`               | `form`     |\n| `application/json`                  | `json`     |\n| `application/xml`                   | `xml`      |\n| `text/xml`                          | `xml`      |\n| `application/vnd.msgpack`           | `msgpack`  |\n\n```go title=\"Signature\"\nfunc (b *Bind) Body(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `json:\"name\" xml:\"name\" form:\"name\" msgpack:\"name\"`\n    Pass string `json:\"pass\" xml:\"pass\" form:\"pass\" msgpack:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Body(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nTest the handler with these `curl` commands:\n\n```bash\n# JSON\ncurl -X POST -H \"Content-Type: application/json\" --data \"{\\\"name\\\":\\\"john\\\",\\\"pass\\\":\\\"doe\\\"}\" localhost:3000\n\n# MsgPack\ncurl -X POST -H \"Content-Type: application/vnd.msgpack\" --data-binary $'\\x82\\xa4name\\xa4john\\xa4pass\\xa3doe'  localhost:3000\n\n# XML\ncurl -X POST -H \"Content-Type: application/xml\" --data \"<login><name>john</name><pass>doe</pass></login>\" localhost:3000\n\n# Form URL-Encoded\ncurl -X POST -H \"Content-Type: application/x-www-form-urlencoded\" --data \"name=john&pass=doe\" localhost:3000\n\n# Multipart Form\ncurl -X POST -F name=john -F pass=doe http://localhost:3000\n```\n\n### CBOR\n\n> **Note:** Before using any CBOR-related features, make sure to follow the [CBOR setup instructions](../guide/advance-format.md#cbor).\n\nBinds the request CBOR body to a struct.\n\nIt is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a CBOR body with a field called `Pass`, you would use a struct field with `cbor:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) CBOR(out any) error\n```\n\n```go title=\"Example\"\n// Field names should start with an uppercase letter\ntype Person struct {\n    Name string `cbor:\"name\"`\n    Pass string `cbor:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().CBOR(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nTest the defaults with this `curl` command:\n\n```bash\ncurl -X POST -H \"Content-Type: application/cbor\" --data \"\\xa2dnamedjohndpasscdoe\" localhost:3000\n```\n\n### Form\n\nBinds the request or multipart form body data to a struct.\n\nIt 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 with `form:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) Form(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `form:\"name\"`\n    Pass string `form:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Form(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nRun tests with the following `curl` commands for both `application/x-www-form-urlencoded` and `multipart/form-data`:\n\n```bash\ncurl -X POST -H \"Content-Type: application/x-www-form-urlencoded\" --data \"name=john&pass=doe\" localhost:3000\n```\n\n```bash\ncurl -X POST -H \"Content-Type: multipart/form-data\" -F \"name=john\" -F \"pass=doe\" localhost:3000\n```\n\n:::info\nIf you need to bind multipart file, you can use `*multipart.FileHeader`, `*[]*multipart.FileHeader` or `[]*multipart.FileHeader` as a field type.\n:::\n\n```go title=\"Example\"\ntype Person struct {\n    Name string                `form:\"name\"`\n    Pass string                `form:\"pass\"`\n    Avatar *multipart.FileHeader `form:\"avatar\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Form(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n    log.Println(p.Avatar.Filename) // file.txt\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl -X POST -H \"Content-Type: multipart/form-data\" -F \"name=john\" -F \"pass=doe\" -F 'avatar=@filename' localhost:3000\n```\n\n### JSON\n\nBinds the request JSON body to a struct.\n\nIt 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 with `json:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) JSON(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `json:\"name\"`\n    Pass string `json:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().JSON(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl -X POST -H \"Content-Type: application/json\" --data \"{\\\"name\\\":\\\"john\\\",\\\"pass\\\":\\\"doe\\\"}\" localhost:3000\n```\n\n### MsgPack\n\n> **Note:** Before using any MsgPack-related features, make sure to follow the [MsgPack setup instructions](../guide/advance-format.md#msgpack).\n\nBinds the request MsgPack body to a struct.\n\nIt is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a Msgpack body with a field called `Pass`, you would use a struct field with `msgpack:\"pass\"`.\n\n> Our library uses [shamaton-msgpack](https://github.com/shamaton/msgpack) which uses `msgpack` struct tags by default. If you want to use other libraries, you may need to update the struct tags accordingly.\n\n```go title=\"Signature\"\nfunc (b *Bind) MsgPack(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `msgpack:\"name\"`\n    Pass string `msgpack:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().MsgPack(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl -X POST -H \"Content-Type: application/vnd.msgpack\" --data-binary $'\\x82\\xa4name\\xa4john\\xa4pass\\xa3doe'  localhost:3000\n```\n\n### XML\n\nBinds the request XML body to a struct.\n\nIt 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 with `xml:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) XML(out any) error\n```\n\n```go title=\"Example\"\n// Field names should start with an uppercase letter\ntype Person struct {\n    Name string `xml:\"name\"`\n    Pass string `xml:\"pass\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().XML(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name) // john\n    log.Println(p.Pass) // doe\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl -X POST -H \"Content-Type: application/xml\" --data \"<login><name>john</name><pass>doe</pass></login>\" localhost:3000\n```\n\n### Cookie\n\nThis method is similar to [Body Binding](#body), but for cookie parameters.\nIt is important to use the struct tag `cookie`. For example, if you want to parse a cookie with a field called `Age`, you would use a struct field with `cookie:\"age\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) Cookie(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `cookie:\"name\"`\n    Age  int    `cookie:\"age\"`\n    Job  bool   `cookie:\"job\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Cookie(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name)  // Joseph\n    log.Println(p.Age)   // 23\n    log.Println(p.Job)   // true\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl --cookie \"name=Joseph; age=23; job=true\" http://localhost:8000/\n```\n\n### Header\n\nThis method is similar to [Body Binding](#body), but for request headers.\nIt is important to use the struct tag `header`. For example, if you want to parse a request header with a field called `Pass`, you would use a struct field with `header:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) Header(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name     string   `header:\"name\"`\n    Pass     string   `header:\"pass\"`\n    Products []string `header:\"products\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Header(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name)     // john\n    log.Println(p.Pass)     // doe\n    log.Println(p.Products) // [shoe hat]\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl \"http://localhost:3000/\" -H \"name: john\" -H \"pass: doe\" -H \"products: shoe,hat\"\n```\n\n### Query\n\nThis method is similar to [Body Binding](#body), but for query parameters.\nIt is important to use the struct tag `query`. For example, if you want to parse a query parameter with a field called `Pass`, you would use a struct field with `query:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) Query(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name     string   `query:\"name\"`\n    Pass     string   `query:\"pass\"`\n    Products []string `query:\"products\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Query(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name)     // john\n    log.Println(p.Pass)     // doe\n    // Depending on fiber.Config{EnableSplittingOnParsers: false} - default\n    log.Println(p.Products) // [\"shoe,hat\"]\n    // With fiber.Config{EnableSplittingOnParsers: true}\n    // log.Println(p.Products) // [\"shoe\", \"hat\"]\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl \"http://localhost:3000/?name=john&pass=doe&products=shoe,hat\"\n```\n\n:::info\nFor more parser settings, please refer to [Config](fiber.md#enablesplittingonparsers)\n:::\n\n#### Array Query Parameters\n\nFiber supports several formats for passing array values via query parameters. The following table gives an overview:\n\n| Format                   | Example                                        | Requires `EnableSplittingOnParsers` |\n| ------------------------ | ---------------------------------------------- | ----------------------------------- |\n| Repeated key             | `?colors=red&colors=blue`                      | No                                  |\n| Bracket notation         | `?colors[]=red&colors[]=blue`                  | No                                  |\n| Comma-separated          | `?colors=red,blue`                             | **Yes**                             |\n| Indexed bracket notation | `?posts[0][title]=Hello&posts[1][title]=World` | No                                  |\n| Nested bracket notation  | `?preferences[tags]=golang,api`                | No (comma splitting: **Yes**)       |\n\n##### Repeated Key\n\nThe most common approach. Repeat the same query key for each value:\n\n```text\nGET /api?colors=red&colors=blue&colors=green\n```\n\n```go title=\"Struct\"\ntype Filter struct {\n    Colors []string `query:\"colors\"`\n}\n// Result: Colors = [\"red\", \"blue\", \"green\"]\n```\n\n```bash title=\"curl\"\ncurl \"http://localhost:3000/api?colors=red&colors=blue&colors=green\"\n```\n\n##### Bracket Notation\n\nAppend `[]` to the key name. This is common in PHP-style and JavaScript frameworks:\n\n```text\nGET /api?colors[]=red&colors[]=blue&colors[]=green\n```\n\n```go title=\"Struct\"\ntype Filter struct {\n    Colors []string `query:\"colors\"`\n}\n// Result: Colors = [\"red\", \"blue\", \"green\"]\n```\n\n```bash title=\"curl\"\ncurl \"http://localhost:3000/api?colors[]=red&colors[]=blue&colors[]=green\"\n```\n\n:::note\nThe struct field tag stays `query:\"colors\"` (without brackets). Fiber strips the `[]` automatically.\n:::\n\n##### Comma-Separated Values\n\nPass multiple values in a single parameter, separated by commas. This format requires [`EnableSplittingOnParsers`](fiber.md#enablesplittingonparsers) to be set to `true`.\n\n```text\nGET /api?colors=red,blue,green\n```\n\n```go title=\"Struct\"\ntype Filter struct {\n    Colors []string `query:\"colors\"`\n}\n```\n\n```go title=\"App Setup\"\n// EnableSplittingOnParsers is required for comma splitting\napp := fiber.New(fiber.Config{\n    EnableSplittingOnParsers: true,\n})\n// Result: Colors = [\"red\", \"blue\", \"green\"]\n```\n\nWithout `EnableSplittingOnParsers`, the entire string `\"red,blue,green\"` is treated as a **single** element.\n\n```go title=\"Default behavior (EnableSplittingOnParsers: false)\"\n// GET /api?colors=red,blue,green\n// Result: Colors = [\"red,blue,green\"]  ← single element\n```\n\n```bash title=\"curl\"\ncurl \"http://localhost:3000/api?colors=red,blue,green\"\n```\n\nYou can also mix comma-separated values with repeated keys when splitting is enabled:\n\n```text\nGET /api?hobby=soccer&hobby=basketball,football\n```\n\n```go\ntype Query struct {\n    Hobby []string `query:\"hobby\"`\n}\n// With EnableSplittingOnParsers: true\n// Result: Hobby = [\"soccer\", \"basketball\", \"football\"]  ← 3 elements\n```\n\n##### Indexed Bracket Notation (Nested Structs)\n\nUse indexed brackets to bind arrays of nested structs:\n\n```text\nGET /api?posts[0][title]=Hello&posts[0][author]=Alice&posts[1][title]=World&posts[1][author]=Bob\n```\n\n```go title=\"Struct\"\ntype Post struct {\n    Title  string `query:\"title\"`\n    Author string `query:\"author\"`\n}\n\ntype Request struct {\n    Posts []Post `query:\"posts\"`\n}\n// Result: Posts = [{Title: \"Hello\", Author: \"Alice\"}, {Title: \"World\", Author: \"Bob\"}]\n```\n\n```bash title=\"curl\"\ncurl \"http://localhost:3000/api?posts[0][title]=Hello&posts[0][author]=Alice&posts[1][title]=World&posts[1][author]=Bob\"\n```\n\n##### Nested Bracket Notation (Without Index)\n\nUse bracket notation to access fields of a nested struct:\n\n```text\nGET /api?preferences[tags]=golang,api\n```\n\n```go title=\"Struct\"\ntype Preferences struct {\n    Tags *[]string `query:\"tags\"`\n}\n\ntype Profile struct {\n    Prefs *Preferences `query:\"preferences\"`\n}\n// With EnableSplittingOnParsers: true\n// Result: *Prefs.Tags = [\"golang\", \"api\"]\n```\n\n```bash title=\"curl\"\ncurl \"http://localhost:3000/api?preferences[tags]=golang,api\"\n```\n\n:::note\nPointer fields (`*[]string`, `*Preferences`) let you distinguish between a missing parameter (`nil`) and an empty one. When the parameter is present, Fiber allocates the pointer automatically.\n:::\n\n### RespHeader\n\nThis method is similar to [Body Binding](#body), but for response headers.\nIt is important to use the struct tag `respHeader`. For example, if you want to parse a response header with a field called `Pass`, you would use a struct field with `respHeader:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) RespHeader(out any) error\n```\n\n```go title=\"Example\"\ntype Person struct {\n    Name     string   `respHeader:\"name\"`\n    Pass     string   `respHeader:\"pass\"`\n    Products []string `respHeader:\"products\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().RespHeader(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name)     // john\n    log.Println(p.Pass)     // doe\n    log.Println(p.Products) // [shoe hat]\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl \"http://localhost:3000/\" -H \"name: john\" -H \"pass: doe\" -H \"products: shoe,hat\"\n```\n\n### URI\n\nThis method is similar to [Body Binding](#body), but for path parameters.\nIt is important to use the struct tag `uri`. For example, if you want to parse a path parameter with a field called `Pass`, you would use a struct field with `uri:\"pass\"`.\n\n```go title=\"Signature\"\nfunc (b *Bind) URI(out any) error\n```\n\n```go title=\"Example\"\n// GET http://example.com/user/111\napp.Get(\"/user/:id\", func(c fiber.Ctx) error {\n    param := struct {\n        ID uint `uri:\"id\"`\n    }{}\n\n    if err := c.Bind().URI(&param); err != nil {\n        return err\n    }\n\n    // ...\n    return c.SendString(fmt.Sprintf(\"User ID: %d\", param.ID))\n})\n```\n\n## BindError\n\nWhen a bind method fails to parse (e.g. invalid JSON, bad type conversion), the behavior depends on the error-handling mode. In **manual handling** (the default), the binder returns a `*BindError` wrapping the underlying error — use `errors.As` to extract it and branch on the binding source or field. In **automatic handling** (enabled via `WithAutoHandling`), parse failures are instead converted to a `*fiber.Error` with HTTP status 400; `*BindError` is never surfaced to the caller in that mode. If you are using `WithAutoHandling`, check for `*fiber.Error` or an HTTP 400 response rather than using `errors.As` for `*BindError`.\n\n```go\ntype BindError struct {\n    Source string // \"uri\", \"query\", \"body\", \"header\", \"cookie\", or \"respHeader\"\n    Field  string // struct field or tag key that failed (best-effort, may be empty)\n    Err    error  // underlying error; use errors.As to inspect\n}\n```\n\nSource constants: `BindSourceURI`, `BindSourceQuery`, `BindSourceHeader`, `BindSourceCookie`, `BindSourceBody`, `BindSourceRespHeader`.\n\n### Branching on source\n\nUse `errors.As` to extract `*BindError` and branch on `Source` for RFC-correct status codes (e.g. 404 for URI failures vs 400 for body/query):\n\n```go title=\"Example\"\n// With manual handling mode (default behavior)\n// Will not work with WithAutoHandling()\nvar req struct {\n    ID   int    `uri:\"id\"`\n    Name string `json:\"name\"`\n}\nif err := c.Bind().All(&req); err != nil {\n    var be *fiber.BindError\n    if errors.As(err, &be) && be.Source == fiber.BindSourceURI {\n        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{\"error\": \"not found\"})\n    }\n    return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": \"invalid request\"})\n}\n```\n\n### Validation vs binding errors\n\nValidation errors (from `StructValidator`) are **not** wrapped in `BindError`. Use `errors.As(err, &be)` to distinguish: it succeeds only for parsing/binding failures, not for validation failures.\n\n## Custom\n\nTo use custom binders, you have to use this method.\n\nYou can register them using the [RegisterCustomBinder](./app.md#registercustombinder) method of the Fiber instance.\n\n```go title=\"Signature\"\nfunc (b *Bind) Custom(name string, dest any) error\n```\n\n```go title=\"Example\"\napp := fiber.New()\n\n// My custom binder\ntype customBinder struct{}\n\nfunc (cb *customBinder) Name() string {\n    return \"custom\"\n}\n\nfunc (cb *customBinder) MIMETypes() []string {\n    return []string{\"application/yaml\"}\n}\n\nfunc (cb *customBinder) Parse(c fiber.Ctx, out any) error {\n    // parse YAML body\n    return yaml.Unmarshal(c.Body(), out)\n}\n\n// Register custom binder\napp.RegisterCustomBinder(&customBinder{})\n\ntype User struct {\n    Name string `yaml:\"name\"`\n}\n\n// curl -X POST http://localhost:3000/custom -H \"Content-Type: application/yaml\" -d \"name: John\"\napp.Post(\"/custom\", func(c fiber.Ctx) error {\n    var user User\n    // Use Custom binder by name\n    if err := c.Bind().Custom(\"custom\", &user); err != nil {\n        return err\n    }\n    return c.JSON(user)\n})\n```\n\nInternally, custom binders are also used in the [Body](#body) method.\nThe `MIMETypes` method is used to check if the custom binder should be used for the given content type.\n\n## Options\n\nFor more control over error handling, you can use the following methods.\n\n### WithAutoHandling\n\nIf you want to handle binder errors automatically, you can use `WithAutoHandling`.\nIf there's an error, it will return the error and set HTTP status to `400 Bad Request`.\nThis function does NOT panic therefore you must still return on error explicitly\n\n```go title=\"Signature\"\nfunc (b *Bind) WithAutoHandling() *Bind\n```\n\n### WithoutAutoHandling\n\nTo handle binder errors manually, you can use the `WithoutAutoHandling` method.\nIt's the default behavior of the binder.\n\n```go title=\"Signature\"\nfunc (b *Bind) WithoutAutoHandling() *Bind\n```\n\n### SkipValidation\n\nTo enable or disable validation for the current bind chain, use `SkipValidation`.\nBy default, validation is enabled (`skip = false`).\n\n```go title=\"Signature\"\nfunc (b *Bind) SkipValidation(skip bool) *Bind\n```\n\n## SetParserDecoder\n\nAllows you to configure the BodyParser/QueryParser decoder based on schema options, providing the possibility to add custom types for parsing.\n\n```go title=\"Signature\"\nfunc SetParserDecoder(parserConfig binder.ParserConfig)\n```\n\n`binder.ParserConfig` has the following fields:\n\n```go\ntype ParserConfig struct {\n    IgnoreUnknownKeys bool\n    ParserType        []ParserType\n    ZeroEmpty         bool\n    SetAliasTag       string\n}\n\ntype ParserType struct {\n    CustomType any\n    Converter  func(string) reflect.Value\n}\n```\n\n```go title=\"Example\"\n\ntype CustomTime time.Time\n\n// String returns the time in string format\nfunc (ct *CustomTime) String() string {\n    t := time.Time(*ct).String()\n    return t\n}\n\n// Converter for CustomTime type with format \"2006-01-02\"\nvar timeConverter = func(value string) reflect.Value {\n    fmt.Println(\"timeConverter:\", value)\n    if v, err := time.Parse(\"2006-01-02\", value); err == nil {\n        return reflect.ValueOf(CustomTime(v))\n    }\n    return reflect.Value{}\n}\n\ncustomTime := binder.ParserType{\n    CustomType: CustomTime{},\n    Converter:  timeConverter,\n}\n\n// Add custom type to the Decoder settings\nbinder.SetParserDecoder(binder.ParserConfig{\n    IgnoreUnknownKeys: true,\n    ParserType:        []binder.ParserType{customTime},\n    ZeroEmpty:         true,\n})\n\n// Example using CustomTime with non-RFC3339 format\ntype Demo struct {\n    Date  CustomTime `form:\"date\" query:\"date\"`\n    Title string     `form:\"title\" query:\"title\"`\n    Body  string     `form:\"body\" query:\"body\"`\n}\n\napp.Post(\"/body\", func(c fiber.Ctx) error {\n    var d Demo\n    if err := c.Bind().Body(&d); err != nil {\n        return err\n    }\n    fmt.Println(\"d.Date:\", d.Date.String())\n    return c.JSON(d)\n})\n\napp.Get(\"/query\", func(c fiber.Ctx) error {\n    var d Demo\n    if err := c.Bind().Query(&d); err != nil {\n        return err\n    }\n    fmt.Println(\"d.Date:\", d.Date.String())\n    return c.JSON(d)\n})\n\n// Run tests with the following curl commands:\n\n# Body Binding\ncurl -X POST -F title=title -F body=body -F date=2021-10-20 http://localhost:3000/body\n\n# Query Binding\ncurl -X GET \"http://localhost:3000/query?title=title&body=body&date=2021-10-20\"\n```\n\n## Validation\n\nValidation is also possible with the binding methods. You can specify your validation rules using the `validate` struct tag.\n\nSpecify your struct validator in the [config](./fiber.md#structvalidator).\n\nThe validator must implement the `StructValidator` interface, which requires a `Validate` method that takes an `any` type and returns an error.\n\n```go title=\"Interface\"\ntype StructValidator interface {\n    Validate(out any) error\n}\n```\n\n### Setup Your Validator in the Config\n\n```go title=\"Example\"\nimport \"github.com/go-playground/validator/v10\"\n\ntype structValidator struct {\n    validate *validator.Validate\n}\n\n// Validate method implementation\nfunc (v *structValidator) Validate(out any) error {\n    return v.validate.Struct(out)\n}\n\n// Setup your validator in the Fiber config\napp := fiber.New(fiber.Config{\n    StructValidator: &structValidator{validate: validator.New()},\n})\n```\n\nFiber only runs `StructValidator` for struct destinations (or pointers to structs).\nBinding into maps and other non-struct types skips the validator step.\n\n### Usage of Validation in Binding Methods\n\n```go title=\"Example\"\ntype Person struct {\n    Name string `json:\"name\" validate:\"required\"`\n    Age  int    `json:\"age\" validate:\"gte=18,lte=60\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().JSON(p); err != nil { // Receives validation errors\n        return err\n    }\n})\n```\n\n## Default Fields\n\nYou can set default values for fields in the struct by using the `default` struct tag. Supported types:\n\n- `bool`\n- Float variants (`float32`, `float64`)\n- Int variants (`int`, `int8`, `int16`, `int32`, `int64`)\n- Uint variants (`uint`, `uint8`, `uint16`, `uint32`, `uint64`)\n- `string`\n- A slice of the above types. Use `|` to separate slice items.\n- A pointer to one of the above types (**pointers to slices and slices of pointers are not supported**).\n\n```go title=\"Example\"\ntype Person struct {\n    Name     string     `query:\"name,default:john\"`\n    Pass     string     `query:\"pass\"`\n    Products []string   `query:\"products,default:shoe|hat\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    p := new(Person)\n\n    if err := c.Bind().Query(p); err != nil {\n        return err\n    }\n\n    log.Println(p.Name)     // john\n    log.Println(p.Pass)     // doe\n    log.Println(p.Products) // [\"shoe\", \"hat\"]\n\n    // ...\n})\n```\n\nRun tests with the following `curl` command:\n\n```bash\ncurl \"http://localhost:3000/?pass=doe\"\n```\n"
  },
  {
    "path": "docs/api/constants.md",
    "content": "---\nid: constants\ntitle: 📋 Constants\ndescription: Core HTTP constants used throughout Fiber.\nsidebar_position: 10\n---\n\n### HTTP methods (mirrors `net/http`)\n\n```go\nconst (\n    MethodGet     = \"GET\"     // RFC 7231, 4.3.1\n    MethodHead    = \"HEAD\"    // RFC 7231, 4.3.2\n    MethodPost    = \"POST\"    // RFC 7231, 4.3.3\n    MethodPut     = \"PUT\"     // RFC 7231, 4.3.4\n    MethodPatch   = \"PATCH\"   // RFC 5789\n    MethodDelete  = \"DELETE\"  // RFC 7231, 4.3.5\n    MethodConnect = \"CONNECT\" // RFC 7231, 4.3.6\n    MethodOptions = \"OPTIONS\" // RFC 7231, 4.3.7\n    MethodTrace   = \"TRACE\"   // RFC 7231, 4.3.8\n    methodUse     = \"USE\"\n)\n```\n\n### Common MIME types\n\n```go\nconst (\n    MIMETextXML                          = \"text/xml\"\n    MIMETextHTML                         = \"text/html\"\n    MIMETextPlain                        = \"text/plain\"\n    MIMETextJavaScript                   = \"text/javascript\"\n    MIMETextCSS                          = \"text/css\"\n    MIMEApplicationXML                   = \"application/xml\"\n    MIMEApplicationJSON                  = \"application/json\"\n    MIMEApplicationCBOR                  = \"application/cbor\"\n    MIMEApplicationForm                  = \"application/x-www-form-urlencoded\"\n    MIMEOctetStream                      = \"application/octet-stream\"\n    MIMEMultipartForm                    = \"multipart/form-data\"\n\n    MIMETextXMLCharsetUTF8               = \"text/xml; charset=utf-8\"\n    MIMETextHTMLCharsetUTF8              = \"text/html; charset=utf-8\"\n    MIMETextPlainCharsetUTF8             = \"text/plain; charset=utf-8\"\n    MIMETextJavaScriptCharsetUTF8        = \"text/javascript; charset=utf-8\"\n    MIMETextCSSCharsetUTF8               = \"text/css; charset=utf-8\"\n    MIMEApplicationXMLCharsetUTF8        = \"application/xml; charset=utf-8\"\n    MIMEApplicationJSONCharsetUTF8       = \"application/json; charset=utf-8\"\n)\n```\n\n### HTTP status codes (mirrors `net/http`)\n\n```go\nconst (\n    StatusContinue                      = 100 // RFC 7231, 6.2.1\n    StatusSwitchingProtocols            = 101 // RFC 7231, 6.2.2\n    StatusProcessing                    = 102 // RFC 2518, 10.1\n    StatusEarlyHints                    = 103 // RFC 8297\n    StatusOK                            = 200 // RFC 7231, 6.3.1\n    StatusCreated                       = 201 // RFC 7231, 6.3.2\n    StatusAccepted                      = 202 // RFC 7231, 6.3.3\n    StatusNonAuthoritativeInformation   = 203 // RFC 7231, 6.3.4\n    StatusNoContent                     = 204 // RFC 7231, 6.3.5\n    StatusResetContent                  = 205 // RFC 7231, 6.3.6\n    StatusPartialContent                = 206 // RFC 7233, 4.1\n    StatusMultiStatus                   = 207 // RFC 4918, 11.1\n    StatusAlreadyReported               = 208 // RFC 5842, 7.1\n    StatusIMUsed                        = 226 // RFC 3229, 10.4.1\n    StatusMultipleChoices               = 300 // RFC 7231, 6.4.1\n    StatusMovedPermanently              = 301 // RFC 7231, 6.4.2\n    StatusFound                         = 302 // RFC 7231, 6.4.3\n    StatusSeeOther                      = 303 // RFC 7231, 6.4.4\n    StatusNotModified                   = 304 // RFC 7232, 4.1\n    StatusUseProxy                      = 305 // RFC 7231, 6.4.5\n    StatusSwitchProxy                   = 306 // RFC 9110, 15.4.7 (Unused)\n    StatusTemporaryRedirect             = 307 // RFC 7231, 6.4.7\n    StatusPermanentRedirect             = 308 // RFC 7538, 3\n    StatusBadRequest                    = 400 // RFC 7231, 6.5.1\n    StatusUnauthorized                  = 401 // RFC 7235, 3.1\n    StatusPaymentRequired               = 402 // RFC 7231, 6.5.2\n    StatusForbidden                     = 403 // RFC 7231, 6.5.3\n    StatusNotFound                      = 404 // RFC 7231, 6.5.4\n    StatusMethodNotAllowed              = 405 // RFC 7231, 6.5.5\n    StatusNotAcceptable                 = 406 // RFC 7231, 6.5.6\n    StatusProxyAuthRequired             = 407 // RFC 7235, 3.2\n    StatusRequestTimeout                = 408 // RFC 7231, 6.5.7\n    StatusConflict                      = 409 // RFC 7231, 6.5.8\n    StatusGone                          = 410 // RFC 7231, 6.5.9\n    StatusLengthRequired                = 411 // RFC 7231, 6.5.10\n    StatusPreconditionFailed            = 412 // RFC 7232, 4.2\n    StatusRequestEntityTooLarge         = 413 // RFC 7231, 6.5.11\n    StatusRequestURITooLong             = 414 // RFC 7231, 6.5.12\n    StatusUnsupportedMediaType          = 415 // RFC 7231, 6.5.13\n    StatusRequestedRangeNotSatisfiable  = 416 // RFC 7233, 4.4\n    StatusExpectationFailed             = 417 // RFC 7231, 6.5.14\n    StatusTeapot                        = 418 // RFC 7168, 2.3.3\n    StatusMisdirectedRequest            = 421 // RFC 7540, 9.1.2\n    StatusUnprocessableEntity           = 422 // RFC 4918, 11.2\n    StatusLocked                        = 423 // RFC 4918, 11.3\n    StatusFailedDependency              = 424 // RFC 4918, 11.4\n    StatusTooEarly                      = 425 // RFC 8470, 5.2.\n    StatusUpgradeRequired               = 426 // RFC 7231, 6.5.15\n    StatusPreconditionRequired          = 428 // RFC 6585, 3\n    StatusTooManyRequests               = 429 // RFC 6585, 4\n    StatusRequestHeaderFieldsTooLarge   = 431 // RFC 6585, 5\n    StatusUnavailableForLegalReasons    = 451 // RFC 7725, 3\n    StatusInternalServerError           = 500 // RFC 7231, 6.6.1\n    StatusNotImplemented                = 501 // RFC 7231, 6.6.2\n    StatusBadGateway                    = 502 // RFC 7231, 6.6.3\n    StatusServiceUnavailable            = 503 // RFC 7231, 6.6.4\n    StatusGatewayTimeout                = 504 // RFC 7231, 6.6.5\n    StatusHTTPVersionNotSupported       = 505 // RFC 7231, 6.6.6\n    StatusVariantAlsoNegotiates         = 506 // RFC 2295, 8.1\n    StatusInsufficientStorage           = 507 // RFC 4918, 11.5\n    StatusLoopDetected                  = 508 // RFC 5842, 7.2\n    StatusNotExtended                   = 510 // RFC 2774, 7\n    StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6\n)\n```\n\n### Errors\n\n```go\nvar (\n    ErrBadRequest                    = NewError(StatusBadRequest)                    // RFC 7231, 6.5.1\n    ErrUnauthorized                  = NewError(StatusUnauthorized)                  // RFC 7235, 3.1\n    ErrPaymentRequired               = NewError(StatusPaymentRequired)               // RFC 7231, 6.5.2\n    ErrForbidden                     = NewError(StatusForbidden)                     // RFC 7231, 6.5.3\n    ErrNotFound                      = NewError(StatusNotFound)                      // RFC 7231, 6.5.4\n    ErrMethodNotAllowed              = NewError(StatusMethodNotAllowed)              // RFC 7231, 6.5.5\n    ErrNotAcceptable                 = NewError(StatusNotAcceptable)                 // RFC 7231, 6.5.6\n    ErrProxyAuthRequired             = NewError(StatusProxyAuthRequired)             // RFC 7235, 3.2\n    ErrRequestTimeout                = NewError(StatusRequestTimeout)                // RFC 7231, 6.5.7\n    ErrConflict                      = NewError(StatusConflict)                      // RFC 7231, 6.5.8\n    ErrGone                          = NewError(StatusGone)                          // RFC 7231, 6.5.9\n    ErrLengthRequired                = NewError(StatusLengthRequired)                // RFC 7231, 6.5.10\n    ErrPreconditionFailed            = NewError(StatusPreconditionFailed)            // RFC 7232, 4.2\n    ErrRequestEntityTooLarge         = NewError(StatusRequestEntityTooLarge)         // RFC 7231, 6.5.11\n    ErrRequestURITooLong             = NewError(StatusRequestURITooLong)             // RFC 7231, 6.5.12\n    ErrUnsupportedMediaType          = NewError(StatusUnsupportedMediaType)          // RFC 7231, 6.5.13\n    ErrRequestedRangeNotSatisfiable  = NewError(StatusRequestedRangeNotSatisfiable)  // RFC 7233, 4.4\n    ErrExpectationFailed             = NewError(StatusExpectationFailed)             // RFC 7231, 6.5.14\n    ErrTeapot                        = NewError(StatusTeapot)                        // RFC 7168, 2.3.3\n    ErrMisdirectedRequest            = NewError(StatusMisdirectedRequest)            // RFC 7540, 9.1.2\n    ErrUnprocessableEntity           = NewError(StatusUnprocessableEntity)           // RFC 4918, 11.2\n    ErrLocked                        = NewError(StatusLocked)                        // RFC 4918, 11.3\n    ErrFailedDependency              = NewError(StatusFailedDependency)              // RFC 4918, 11.4\n    ErrTooEarly                      = NewError(StatusTooEarly)                      // RFC 8470, 5.2.\n    ErrUpgradeRequired               = NewError(StatusUpgradeRequired)               // RFC 7231, 6.5.15\n    ErrPreconditionRequired          = NewError(StatusPreconditionRequired)          // RFC 6585, 3\n    ErrTooManyRequests               = NewError(StatusTooManyRequests)               // RFC 6585, 4\n    ErrRequestHeaderFieldsTooLarge   = NewError(StatusRequestHeaderFieldsTooLarge)   // RFC 6585, 5\n    ErrUnavailableForLegalReasons    = NewError(StatusUnavailableForLegalReasons)    // RFC 7725, 3\n    ErrInternalServerError           = NewError(StatusInternalServerError)           // RFC 7231, 6.6.1\n    ErrNotImplemented                = NewError(StatusNotImplemented)                // RFC 7231, 6.6.2\n    ErrBadGateway                    = NewError(StatusBadGateway)                    // RFC 7231, 6.6.3\n    ErrServiceUnavailable            = NewError(StatusServiceUnavailable)            // RFC 7231, 6.6.4\n    ErrGatewayTimeout                = NewError(StatusGatewayTimeout)                // RFC 7231, 6.6.5\n    ErrHTTPVersionNotSupported       = NewError(StatusHTTPVersionNotSupported)       // RFC 7231, 6.6.6\n    ErrVariantAlsoNegotiates         = NewError(StatusVariantAlsoNegotiates)         // RFC 2295, 8.1\n    ErrInsufficientStorage           = NewError(StatusInsufficientStorage)           // RFC 4918, 11.5\n    ErrLoopDetected                  = NewError(StatusLoopDetected)                  // RFC 5842, 7.2\n    ErrNotExtended                   = NewError(StatusNotExtended)                   // RFC 2774, 7\n    ErrNetworkAuthenticationRequired = NewError(StatusNetworkAuthenticationRequired) // RFC 6585, 6\n)\n```\n\nHTTP Headers were copied from net/http.\n\n```go\nconst (\n    HeaderAuthorization                      = \"Authorization\"\n    HeaderProxyAuthenticate                  = \"Proxy-Authenticate\"\n    HeaderProxyAuthorization                 = \"Proxy-Authorization\"\n    HeaderWWWAuthenticate                    = \"WWW-Authenticate\"\n    HeaderAge                                = \"Age\"\n    HeaderCacheControl                       = \"Cache-Control\"\n    HeaderClearSiteData                      = \"Clear-Site-Data\"\n    HeaderExpires                            = \"Expires\"\n    HeaderPragma                             = \"Pragma\"\n    HeaderWarning                            = \"Warning\"\n    HeaderAcceptCH                           = \"Accept-CH\"\n    HeaderAcceptCHLifetime                   = \"Accept-CH-Lifetime\"\n    HeaderContentDPR                         = \"Content-DPR\"\n    HeaderDPR                                = \"DPR\"\n    HeaderEarlyData                          = \"Early-Data\"\n    HeaderSaveData                           = \"Save-Data\"\n    HeaderViewportWidth                      = \"Viewport-Width\"\n    HeaderWidth                              = \"Width\"\n    HeaderETag                               = \"ETag\"\n    HeaderIfMatch                            = \"If-Match\"\n    HeaderIfModifiedSince                    = \"If-Modified-Since\"\n    HeaderIfNoneMatch                        = \"If-None-Match\"\n    HeaderIfUnmodifiedSince                  = \"If-Unmodified-Since\"\n    HeaderLastModified                       = \"Last-Modified\"\n    HeaderVary                               = \"Vary\"\n    HeaderConnection                         = \"Connection\"\n    HeaderKeepAlive                          = \"Keep-Alive\"\n    HeaderAccept                             = \"Accept\"\n    HeaderAcceptCharset                      = \"Accept-Charset\"\n    HeaderAcceptEncoding                     = \"Accept-Encoding\"\n    HeaderAcceptLanguage                     = \"Accept-Language\"\n    HeaderCookie                             = \"Cookie\"\n    HeaderExpect                             = \"Expect\"\n    HeaderMaxForwards                        = \"Max-Forwards\"\n    HeaderSetCookie                          = \"Set-Cookie\"\n    HeaderAccessControlAllowCredentials      = \"Access-Control-Allow-Credentials\"\n    HeaderAccessControlAllowHeaders          = \"Access-Control-Allow-Headers\"\n    HeaderAccessControlAllowMethods          = \"Access-Control-Allow-Methods\"\n    HeaderAccessControlAllowOrigin           = \"Access-Control-Allow-Origin\"\n    HeaderAccessControlExposeHeaders         = \"Access-Control-Expose-Headers\"\n    HeaderAccessControlMaxAge                = \"Access-Control-Max-Age\"\n    HeaderAccessControlRequestHeaders        = \"Access-Control-Request-Headers\"\n    HeaderAccessControlRequestMethod         = \"Access-Control-Request-Method\"\n    HeaderOrigin                             = \"Origin\"\n    HeaderTimingAllowOrigin                  = \"Timing-Allow-Origin\"\n    HeaderXPermittedCrossDomainPolicies      = \"X-Permitted-Cross-Domain-Policies\"\n    HeaderDNT                                = \"DNT\"\n    HeaderTk                                 = \"Tk\"\n    HeaderContentDisposition                 = \"Content-Disposition\"\n    HeaderContentEncoding                    = \"Content-Encoding\"\n    HeaderContentLanguage                    = \"Content-Language\"\n    HeaderContentLength                      = \"Content-Length\"\n    HeaderContentLocation                    = \"Content-Location\"\n    HeaderContentType                        = \"Content-Type\"\n    HeaderForwarded                          = \"Forwarded\"\n    HeaderVia                                = \"Via\"\n    HeaderXForwardedFor                      = \"X-Forwarded-For\"\n    HeaderXForwardedHost                     = \"X-Forwarded-Host\"\n    HeaderXForwardedProto                    = \"X-Forwarded-Proto\"\n    HeaderXForwardedProtocol                 = \"X-Forwarded-Protocol\"\n    HeaderXForwardedSsl                      = \"X-Forwarded-Ssl\"\n    HeaderXUrlScheme                         = \"X-Url-Scheme\"\n    HeaderLocation                           = \"Location\"\n    HeaderFrom                               = \"From\"\n    HeaderHost                               = \"Host\"\n    HeaderReferer                            = \"Referer\"\n    HeaderReferrerPolicy                     = \"Referrer-Policy\"\n    HeaderUserAgent                          = \"User-Agent\"\n    HeaderAllow                              = \"Allow\"\n    HeaderServer                             = \"Server\"\n    HeaderAcceptRanges                       = \"Accept-Ranges\"\n    HeaderContentRange                       = \"Content-Range\"\n    HeaderIfRange                            = \"If-Range\"\n    HeaderRange                              = \"Range\"\n    HeaderContentSecurityPolicy              = \"Content-Security-Policy\"\n    HeaderContentSecurityPolicyReportOnly    = \"Content-Security-Policy-Report-Only\"\n    HeaderCrossOriginResourcePolicy          = \"Cross-Origin-Resource-Policy\"\n    HeaderExpectCT                           = \"Expect-CT\"\n    HeaderFeaturePolicy                      = \"Feature-Policy\"\n    HeaderPublicKeyPins                      = \"Public-Key-Pins\"\n    HeaderPublicKeyPinsReportOnly            = \"Public-Key-Pins-Report-Only\"\n    HeaderStrictTransportSecurity            = \"Strict-Transport-Security\"\n    HeaderUpgradeInsecureRequests            = \"Upgrade-Insecure-Requests\"\n    HeaderXContentTypeOptions                = \"X-Content-Type-Options\"\n    HeaderXDownloadOptions                   = \"X-Download-Options\"\n    HeaderXFrameOptions                      = \"X-Frame-Options\"\n    HeaderXPoweredBy                         = \"X-Powered-By\"\n    HeaderXXSSProtection                     = \"X-XSS-Protection\"\n    HeaderLastEventID                        = \"Last-Event-ID\"\n    HeaderNEL                                = \"NEL\"\n    HeaderPingFrom                           = \"Ping-From\"\n    HeaderPingTo                             = \"Ping-To\"\n    HeaderReportTo                           = \"Report-To\"\n    HeaderTE                                 = \"TE\"\n    HeaderTrailer                            = \"Trailer\"\n    HeaderTransferEncoding                   = \"Transfer-Encoding\"\n    HeaderSecWebSocketAccept                 = \"Sec-WebSocket-Accept\"\n    HeaderSecWebSocketExtensions             = \"Sec-WebSocket-Extensions\"\n    HeaderSecWebSocketKey                    = \"Sec-WebSocket-Key\"\n    HeaderSecWebSocketProtocol               = \"Sec-WebSocket-Protocol\"\n    HeaderSecWebSocketVersion                = \"Sec-WebSocket-Version\"\n    HeaderAcceptPatch                        = \"Accept-Patch\"\n    HeaderAcceptPushPolicy                   = \"Accept-Push-Policy\"\n    HeaderAcceptSignature                    = \"Accept-Signature\"\n    HeaderAltSvc                             = \"Alt-Svc\"\n    HeaderDate                               = \"Date\"\n    HeaderIndex                              = \"Index\"\n    HeaderLargeAllocation                    = \"Large-Allocation\"\n    HeaderLink                               = \"Link\"\n    HeaderPushPolicy                         = \"Push-Policy\"\n    HeaderRetryAfter                         = \"Retry-After\"\n    HeaderServerTiming                       = \"Server-Timing\"\n    HeaderSignature                          = \"Signature\"\n    HeaderSignedHeaders                      = \"Signed-Headers\"\n    HeaderSourceMap                          = \"SourceMap\"\n    HeaderUpgrade                            = \"Upgrade\"\n    HeaderXDNSPrefetchControl                = \"X-DNS-Prefetch-Control\"\n    HeaderXPingback                          = \"X-Pingback\"\n    HeaderXRequestID                         = \"X-Request-ID\"\n    HeaderXRequestedWith                     = \"X-Requested-With\"\n    HeaderXRobotsTag                         = \"X-Robots-Tag\"\n    HeaderXUACompatible                      = \"X-UA-Compatible\"\n    HeaderAccessControlAllowPrivateNetwork   = \"Access-Control-Allow-Private-Network\"\n    HeaderAccessControlRequestPrivateNetwork = \"Access-Control-Request-Private-Network\"\n)\n```\n"
  },
  {
    "path": "docs/api/ctx.md",
    "content": "---\nid: ctx\ntitle: 🧠 Ctx\ndescription: >-\n  The Ctx interface represents the Context which holds the HTTP request and\n  response. It has methods for the request query string, parameters, body, HTTP\n  headers, and so on.\nsidebar_position: 3\n---\n\n### Abandon\n\nMarks the context as abandoned. An abandoned context will not be returned to the pool when `ReleaseCtx` is called. This is used internally by the [timeout middleware](../middleware/timeout.md) to return immediately while the handler goroutine continues safely.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Abandon()\nfunc (c fiber.Ctx) IsAbandoned() bool\nfunc (c fiber.Ctx) ForceRelease()\n```\n\n| Method         | Description                                                                 |\n|:---------------|:----------------------------------------------------------------------------|\n| `Abandon()`    | Marks the context as abandoned. ReleaseCtx becomes a no-op for this context. |\n| `IsAbandoned()`| Returns `true` if `Abandon()` was called on this context.                   |\n| `ForceRelease()`| Releases an abandoned context back to the pool. Must only be called after the handler has completely finished. |\n\n:::caution\nThese methods are primarily for internal use and advanced middleware development. Most applications should not need to call them directly.\n:::\n\n### App\n\nReturns the [\\*App](app.md) reference so you can easily access all application settings.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) App() *App\n```\n\n```go title=\"Example\"\napp.Get(\"/stack\", func(c fiber.Ctx) error {\n  return c.JSON(c.App().Stack())\n})\n```\n\n### Bind\n\nBind returns a helper for decoding the request body, query string, headers, cookies, and more.\n\nFor full details, see the [Bind](./bind.md) documentation.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Bind() *Bind\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  user := new(User)\n  // Bind the request body to a struct:\n  return c.Bind().Body(user)\n})\n```\n\n### Context\n\nReturns a `context.Context` that was previously set with [`SetContext`](#setcontext).\nIf no context was set, it returns `context.Background()`. Unlike `fiber.Ctx` itself,\nthe returned context is safe to use after the handler completes.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Context() context.Context\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  ctx := c.Context()\n  go doWork(ctx)\n  return nil\n})\n```\n\n### context.Context\n\n`Ctx` implements `context.Context`. However due to [current limitations in how fasthttp](https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945) works, `Deadline()`, `Done()` and `Err()` are no-ops. The `fiber.Ctx` instance is reused after the handler returns and must not be used for asynchronous operations once the handler has completed. Call [`Context`](#context) within the handler to obtain a `context.Context` that can be used outside the handler.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Deadline() (deadline time.Time, ok bool)\nfunc (c fiber.Ctx) Done() <-chan struct{}\nfunc (c fiber.Ctx) Err() error\nfunc (c fiber.Ctx) Value(key any) any\n```\n\n```go title=\"Example\"\n\nfunc doSomething(ctx context.Context) {\n  // ...\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  doSomething(c)\n})\n```\n\n#### Value\n\nValue can be used to retrieve [**`Locals`**](#locals).\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Locals(userKey, \"admin\")\n  user := c.Value(userKey) // returns \"admin\"\n})\n```\n\n### Drop\n\nTerminates the client connection silently without sending any HTTP headers or response body.\n\nThis can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating\nDDoS attacks or protecting sensitive endpoints from unauthorized access.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Drop() error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  if c.IP() == \"192.168.1.1\" {\n    return c.Drop()\n  }\n\n  return c.SendString(\"Hello World!\")\n})\n```\n\n### FullPath\n\nReturns the full path of the matched route. This includes any prefixes that were added by [groups](../guide/routing.md#grouping) or mounts.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) FullPath() string\n```\n\n```go title=\"Example\"\napi := app.Group(\"/api\")\napi.Get(\"/users/:id\", func(c fiber.Ctx) error {\n  return c.JSON(fiber.Map{\n    \"route\": c.FullPath(), // \"/api/users/:id\"\n  })\n})\n\napp.Use(func(c fiber.Ctx) error {\n  beforeNext := c.FullPath() // \"/\"\n\n  if err := c.Next(); err != nil {\n    return err\n  }\n\n  afterNext := c.FullPath() // \"/api/users/:id\"\n  // ... react to the downstream handler's route path\n  return nil\n})\n```\n\n### GetReqHeaders\n\nReturns the HTTP request headers as a map. Because a header can appear multiple times in a request, each key maps to a slice with all values for that header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) GetReqHeaders() map[string][]string\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### GetRespHeader\n\nReturns the HTTP response header specified by the field.\n\n:::tip\nThe match is **case-insensitive**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) GetRespHeader(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.GetRespHeader(\"X-Request-Id\")       // \"8d7ad5e3-aaf3-450b-a241-2beb887efd54\"\n  c.GetRespHeader(\"Content-Type\")       // \"text/plain\"\n  c.GetRespHeader(\"something\", \"john\")  // \"john\"\n  // ..\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### GetRespHeaders\n\nReturns the HTTP response headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) GetRespHeaders() map[string][]string\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### GetRouteURL\n\nGenerates URLs to named routes, with parameters. URLs are relative, for example: \"/user/1831\"\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) GetRouteURL(routeName string, params Map) (string, error)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Home page\")\n}).Name(\"home\")\n\napp.Get(\"/user/:id\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"id\"))\n}).Name(\"user.show\")\n\napp.Get(\"/test\", func(c fiber.Ctx) error {\n    location, _ := c.GetRouteURL(\"user.show\", fiber.Map{\"id\": 1})\n    return c.SendString(location)\n})\n\n// /test returns \"/user/1\"\n```\n\n### HasBody\n\nReturns `true` if the incoming request contains a body or a `Content-Length` header greater than zero.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) HasBody() bool\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  if !c.HasBody() {\n    return c.SendStatus(fiber.StatusBadRequest)\n  }\n  return c.SendString(\"OK\")\n})\n```\n\n### IsMiddleware\n\nReturns `true` if the current request handler was registered as middleware.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsMiddleware() bool\n```\n\n```go title=\"Example\"\napp.Get(\"/route\", func(c fiber.Ctx) error {\n  fmt.Println(c.IsMiddleware()) // true\n  return c.Next()\n}, func(c fiber.Ctx) error {\n  fmt.Println(c.IsMiddleware()) // false\n  return c.SendStatus(fiber.StatusOK)\n})\n```\n\n### IsPreflight\n\nReturns `true` if the request is a CORS preflight (`OPTIONS` + `Access-Control-Request-Method` + `Origin`).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsPreflight() bool\n```\n\n```go title=\"Example\"\napp.Use(func(c fiber.Ctx) error {\n  if c.IsPreflight() {\n    return c.SendStatus(fiber.StatusNoContent)\n  }\n  return c.Next()\n})\n```\n\n### IsWebSocket\n\nReturns `true` if the request includes a WebSocket upgrade handshake.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsWebSocket() bool\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  if c.IsWebSocket() {\n    // handle websocket\n  }\n  return c.Next()\n})\n```\n\n### Locals\n\nStores variables scoped to the request, making them available only to matching routes. The variables are removed after the request completes. If a stored value implements `io.Closer`, Fiber calls its `Close` method before removal.\n\n:::tip\nThis is useful if you want to pass some **specific** data to the next middleware. Remember to perform type assertions when retrieving the data to ensure it is of the expected type. You can also use a non-exported type as a key to avoid collisions.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Locals(key any, value ...any) any\n```\n\n```go title=\"Example\"\n\n// keyType is an unexported type for keys defined in this package.\n// This prevents collisions with keys defined in other packages.\ntype keyType int\n\n// userKey is the key for user.User values in Contexts. It is\n// unexported; clients use user.NewContext and user.FromContext\n// instead of using this key directly.\nvar userKey keyType\n\napp.Use(func(c fiber.Ctx) error {\n  c.Locals(userKey, \"admin\") // Stores the string \"admin\" under a non-exported type key\n  return c.Next()\n})\n\napp.Get(\"/admin\", func(c fiber.Ctx) error {\n  user, ok := c.Locals(userKey).(string) // Retrieves the data stored under the key and performs a type assertion\n  if ok && user == \"admin\" {\n    return c.Status(fiber.StatusOK).SendString(\"Welcome, admin!\")\n  }\n  return c.SendStatus(fiber.StatusForbidden)\n})\n```\n\nAn 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.\n\n```go title=\"Signature\"\nfunc Locals[V any](c fiber.Ctx, key any, value ...V) V\n```\n\n```go title=\"Example\"\napp.Use(func(c fiber.Ctx) error {\n  fiber.Locals[string](c, \"john\", \"doe\")\n  fiber.Locals[int](c, \"age\", 18)\n  fiber.Locals[bool](c, \"isHuman\", true)\n  return c.Next()\n})\n\napp.Get(\"/test\", func(c fiber.Ctx) error {\n  fiber.Locals[string](c, \"john\")    // \"doe\"\n  fiber.Locals[int](c, \"age\")        // 18\n  fiber.Locals[bool](c, \"isHuman\")   // true\n  return nil\n})\n```\n\nMake 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.\n\n### Matched\n\nReturns `true` if the current request path was matched by the router.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Matched() bool\n```\n\n```go title=\"Example\"\napp.Use(func(c fiber.Ctx) error {\n  if c.Matched() {\n    return c.Next()\n  }\n  return c.Status(fiber.StatusNotFound).SendString(\"Not Found\")\n})\n```\n\n### Next\n\nWhen **Next** is called, it executes the next method in the stack that matches the current route. You can pass an error struct within the method that will end the chaining and call the [error handler](../guide/error-handling).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Next() error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  fmt.Println(\"1st route!\")\n  return c.Next()\n})\n\napp.Get(\"*\", func(c fiber.Ctx) error {\n  fmt.Println(\"2nd route!\")\n  return c.Next()\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  fmt.Println(\"3rd route!\")\n  return c.SendString(\"Hello, World!\")\n})\n```\n\n### OverrideParam\n\nOverwrites the value of an existing route parameter.\n\n:::note\nIf the parameter does not exist, this method does nothing.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) OverrideParam(name, value string)\n```\n\n```go title=\"Example\"\n// GET http://example.com/user\napp.Get(\"/user/:name\", func(c fiber.Ctx) error {\n  // mutate parameter\n  c.OverrideParam(\"name\", \"new value\")\n  return c.SendString(c.Params(\"name\")) // sends \"new value\"\n})\n// GET http://example.com/shop/tech/1\napp.Get(\"/shop/*\", func(c fiber.Ctx) error {\n  // mutate parameter\n  c.OverrideParam(\"*\", \"new tech\") // replaces \"tech/1\" with \"new tech\"\n  return c.SendString(c.Params(\"*\")) // sends \"new tech\"\n})\n\n```\n\nUnnamed route parameters can be accessed by their character (`*` or `+`) followed by their position index (e.g., `*1` for the first wildcard, `*2` for the second).\n\n```go title=\"Example\"\n// GET /v1/brand/4/shop/blue/xs\napp.Get(\"/v1/*/shop/*\", func(c fiber.Ctx) error {\n  // mutate parameter\n  c.OverrideParam(\"*1\", \"updated brand\")\n  c.OverrideParam(\"*2\", \"updated data\")\n\n  param1 := c.Params(\"*1\") // \"updated brand\"\n  param2 := c.Params(\"*2\") // \"updated data\"\n\n  // ...\n})\n```\n\n### Redirect\n\nReturns the Redirect reference.\n\nFor detailed information, check the [Redirect](./redirect.md) documentation.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Redirect() *Redirect\n```\n\n```go title=\"Example\"\napp.Get(\"/coffee\", func(c fiber.Ctx) error {\n    return c.Redirect().To(\"/teapot\")\n})\n\napp.Get(\"/teapot\", func(c fiber.Ctx) error {\n    return c.Status(fiber.StatusTeapot).Send(\"🍵 short and stout 🍵\")\n})\n```\n\n### Request\n\nReturns the [*fasthttp.Request](https://pkg.go.dev/github.com/valyala/fasthttp#Request) pointer.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Request() *fasthttp.Request\n```\n\n:::info\nReturns `nil` if the context has been released (e.g., after the handler completes and the context is returned to the pool).\n:::\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Request().Header.Method()\n  // => []byte(\"GET\")\n})\n```\n\n### RequestCtx\n\nReturns [\\*fasthttp.RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) that is compatible with the `context.Context` interface that requires a deadline, a cancellation signal, and other values across API boundaries.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) RequestCtx() *fasthttp.RequestCtx\n```\n\n:::info\nPlease read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information.\n:::\n\n### Reset\n\nResets the context fields by the given request when using server handlers.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Reset(fctx *fasthttp.RequestCtx)\n```\n\nIt is used outside of the Fiber Handlers to reset the context for the next request.\n\n### Response\n\nReturns the [\\*fasthttp.Response](https://pkg.go.dev/github.com/valyala/fasthttp#Response) pointer.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Response() *fasthttp.Response\n```\n\n:::info\nReturns `nil` if the context has been released (e.g., after the handler completes and the context is returned to the pool).\n:::\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Response().BodyWriter().Write([]byte(\"Hello, World!\"))\n  // => \"Hello, World!\"\n  return nil\n})\n```\n\n### RestartRouting\n\nInstead of executing the next method when calling [Next](ctx.md#next), **RestartRouting** restarts execution from the first method that matches the current route. This may be helpful after overriding the path, i.e., an internal redirect. Note that handlers might be executed again, which could result in an infinite loop.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) RestartRouting() error\n```\n\n```go title=\"Example\"\napp.Get(\"/new\", func(c fiber.Ctx) error {\n  return c.SendString(\"From /new\")\n})\n\napp.Get(\"/old\", func(c fiber.Ctx) error {\n  c.Path(\"/new\")\n  return c.RestartRouting()\n})\n```\n\n### Route\n\nReturns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Route() *Route\n```\n\n```go title=\"Example\"\n// http://localhost:8080/hello\n\napp.Get(\"/hello/:name\", func(c fiber.Ctx) error {\n  r := c.Route()\n  fmt.Println(r.Method, r.Path, r.Params, r.Handlers)\n  // GET /hello/:name handler [name]\n\n  // ...\n})\n```\n\n:::caution\nDo not rely on `c.Route()` in middlewares **before** calling `c.Next()` - `c.Route()` returns the **last executed route**.\n:::\n\n```go title=\"Example\"\nfunc MyMiddleware() fiber.Handler {\n  return func(c fiber.Ctx) error {\n    beforeNext := c.Route().Path // Will be '/'\n    err := c.Next()\n    afterNext := c.Route().Path // Will be '/hello/:name'\n    return err\n  }\n}\n```\n\n### SetContext\n\nSets the base `context.Context` used by [`Context`](#context). Use this to\npropagate deadlines, cancellation signals, or values to asynchronous operations.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SetContext(ctx context.Context)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.SetContext(context.WithValue(context.Background(), \"user\", \"alice\"))\n  ctx := c.Context()\n  go doWork(ctx)\n  return nil\n})\n```\n\n### String\n\nReturns a unique string representation of the context.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) String() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.String() // => \"#0000000100000001 - 127.0.0.1:3000 <-> 127.0.0.1:61516 - GET http://localhost:3000/\"\n\n  // ...\n})\n```\n\n### ViewBind\n\nAdds variables to the default view variable map binding to the template engine.\nVariables are read by the `Render` method and may be overwritten.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) ViewBind(vars Map) error\n```\n\n```go title=\"Example\"\napp.Use(func(c fiber.Ctx) error {\n  c.ViewBind(fiber.Map{\n    \"Title\": \"Hello, World!\",\n  })\n  return c.Next()\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.Render(\"xxx.tmpl\", fiber.Map{}) // Render will use the Title variable\n})\n```\n\n## Request\n\nMethods which operate on the incoming request.\n\n:::tip\nUse `c.Req()` to limit gopls suggestions to only these methods!\n:::\n\n### AcceptEncoding\n\nReturns the `Accept-Encoding` request header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptEncoding() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptEncoding() // \"gzip, br\"\n  return nil\n})\n```\n\n### AcceptLanguage\n\nReturns the `Accept-Language` request header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptLanguage() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptLanguage() // \"en-US,en;q=0.9\"\n  return nil\n})\n```\n\n### Accepts\n\nChecks if the specified **extensions** or **content** **types** are acceptable.\n\n:::info\nBased on the request’s [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Accepts(offers ...string) string\nfunc (c fiber.Ctx) AcceptsCharsets(offers ...string) string\nfunc (c fiber.Ctx) AcceptsEncodings(offers ...string) string\nfunc (c fiber.Ctx) AcceptsLanguages(offers ...string) string\nfunc (c fiber.Ctx) AcceptsLanguagesExtended(offers ...string) string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Accepts(\"html\")             // \"html\"\n  c.Accepts(\"text/html\")        // \"text/html\"\n  c.Accepts(\"json\", \"text\")     // \"json\"\n  c.Accepts(\"application/json\") // \"application/json\"\n  c.Accepts(\"text/plain\", \"application/json\") // \"application/json\", due to quality\n  c.Accepts(\"image/png\")        // \"\"\n  c.Accepts(\"png\")              // \"\"\n  // ...\n})\n```\n\n```go title=\"Example 2\"\n// Accept: text/html, text/*, application/json, */*; q=0\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Accepts(\"text/plain\", \"application/json\") // \"application/json\", due to specificity\n  c.Accepts(\"application/json\", \"text/html\") // \"text/html\", due to first match\n  c.Accepts(\"image/png\")                      // \"\", due to */* with q=0 is Not Acceptable\n  // ...\n})\n```\n\nMedia-Type parameters are supported.\n\n```go title=\"Example 3\"\n// Accept: text/plain, application/json; version=1; foo=bar\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Extra parameters in the accept are ignored\n  c.Accepts(\"text/plain;format=flowed\") // \"text/plain;format=flowed\"\n\n  // An offer must contain all parameters present in the Accept type\n  c.Accepts(\"application/json\") // \"\"\n\n  // Parameter order and capitalization do not matter. Quotes on values are stripped.\n  c.Accepts(`application/json;foo=\"bar\";VERSION=1`) // \"application/json;foo=\"bar\";VERSION=1\"\n})\n```\n\n```go title=\"Example 4\"\n// Accept: text/plain;format=flowed;q=0.9, text/plain\n// i.e., \"I prefer text/plain;format=flowed less than other forms of text/plain\"\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Beware: the order in which offers are listed matters.\n  // Although the client specified they prefer not to receive format=flowed,\n  // the text/plain Accept matches with \"text/plain;format=flowed\" first, so it is returned.\n  c.Accepts(\"text/plain;format=flowed\", \"text/plain\") // \"text/plain;format=flowed\"\n\n  // Here, things behave as expected:\n  c.Accepts(\"text/plain\", \"text/plain;format=flowed\") // \"text/plain\"\n})\n```\n\nFiber provides similar functions for the other accept headers.\n\nFor `Accept-Language`, Fiber uses the [Basic Filtering](https://www.rfc-editor.org/rfc/rfc4647#section-3.3.1) algorithm. A language range matches an offer only if it exactly equals the tag or is a prefix followed by a hyphen. For example, the range `en` matches `en-US`, but `en-US` does not match `en`.\n\n`AcceptsLanguagesExtended` applies [Extended Filtering](https://www.rfc-editor.org/rfc/rfc4647#section-3.3.2) where `*` may match zero or more subtags and wildcard matches can slide across subtags unless blocked by a singleton like `x`.\n\n```go\n// Accept-Charset: utf-8, iso-8859-1;q=0.2\n// Accept-Encoding: gzip, compress;q=0.2\n// Accept-Language: en;q=0.8, nl, ru\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptsCharsets(\"utf-16\", \"iso-8859-1\")\n  // \"iso-8859-1\"\n\n  c.AcceptsEncodings(\"compress\", \"br\")\n  // \"compress\"\n\n  c.AcceptsLanguages(\"pt\", \"nl\", \"ru\")\n  // \"nl\"\n\n  c.AcceptsLanguagesExtended(\"en-US\", \"fr-CA\")\n  // depends on extended ranges in the request header\n  // ...\n})\n```\n\n### AcceptsEventStream\n\nReturns `true` when the `Accept` header allows `text/event-stream`.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptsEventStream() bool\n```\n\n```go title=\"Example\"\n// Accept: text/html, application/json;q=0.9\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptsEventStream() // false\n  return nil\n})\n```\n\n### AcceptsHTML\n\nReturns `true` when the `Accept` header allows HTML.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptsHTML() bool\n```\n\n```go title=\"Example\"\n// Accept: text/html, application/json;q=0.9\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptsHTML() // true\n  return nil\n})\n```\n\n### AcceptsJSON\n\nReturns `true` when the `Accept` header allows JSON.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptsJSON() bool\n```\n\n```go title=\"Example\"\n// Accept: text/html, application/json;q=0.9\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptsJSON() // true\n  return nil\n})\n```\n\n### AcceptsXML\n\nReturns `true` when the `Accept` header allows XML.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AcceptsXML() bool\n```\n\n```go title=\"Example\"\n// Accept: text/html, application/json;q=0.9\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.AcceptsXML() // false\n  return nil\n})\n```\n\n### BaseURL\n\nReturns the base URL (**protocol** + **host**) as a `string`.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) BaseURL() string\n```\n\n```go title=\"Example\"\n// GET https://example.com/page#chapter-1\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.BaseURL() // \"https://example.com\"\n  // ...\n})\n```\n\n### Body\n\nAs per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent (or when it is set to `identity`), it will perform as [BodyRaw](#bodyraw). If an unknown or unsupported encoding is encountered, the response status will be `415 Unsupported Media Type` or `501 Not Implemented`.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Body() []byte\n```\n\n```go title=\"Example\"\n// echo 'user=john' | gzip | curl -v -i --data-binary @- -H \"Content-Encoding: gzip\" http://localhost:8080\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Decompress body from POST request based on the Content-Encoding and return the raw content:\n  return c.Send(c.Body()) // []byte(\"user=john\")\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### BodyRaw\n\nReturns the raw request **body**.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) BodyRaw() []byte\n```\n\n```go title=\"Example\"\n// curl -X POST http://localhost:8080 -d user=john\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Get raw body from POST request:\n  return c.Send(c.BodyRaw()) // []byte(\"user=john\")\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### Charset\n\nReturns the `charset` parameter from the `Content-Type` header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Charset() string\n```\n\n```go title=\"Example\"\n// Content-Type: application/json; charset=utf-8\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.Charset() // \"utf-8\"\n  return nil\n})\n```\n\n### ClientHelloInfo\n\n`ClientHelloInfo` contains information from a ClientHello message to guide application logic in the `GetCertificate` and `GetConfigForClient` callbacks.\nRefer to the [ClientHelloInfo](https://golang.org/pkg/crypto/tls/#ClientHelloInfo) struct documentation for details on the returned struct.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) ClientHelloInfo() *tls.ClientHelloInfo\n```\n\n```go title=\"Example\"\n// GET http://example.com/hello\napp.Get(\"/hello\", func(c fiber.Ctx) error {\n  chi := c.ClientHelloInfo()\n  // ...\n})\n```\n\n### Cookies\n\nGets a cookie value by key. You can pass an optional default value that will be returned if the cookie key does not exist.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Cookies(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Get cookie by key:\n  c.Cookies(\"name\")         // \"john\"\n  c.Cookies(\"empty\", \"doe\") // \"doe\"\n  // ...\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nUse [`App.GetString`](./app.md#getstring) or [`App.GetBytes`](./app.md#getbytes) when immutability is enabled, or manually copy values (for example with [`utils.CopyString`](https://github.com/gofiber/utils) / `utils.CopyBytes`) when it's disabled. [Read more...](../#zero-allocation)\n:::\n\n### FormFile\n\nMultipartForm files can be retrieved by name, the **first** file from the given key is returned.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) FormFile(key string) (*multipart.FileHeader, error)\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Get first file from form field \"document\":\n  file, err := c.FormFile(\"document\")\n\n  // Save file to root directory:\n  return c.SaveFile(file, fmt.Sprintf(\"./%s\", file.Filename))\n})\n```\n\n### FormValue\n\nForm values can be retrieved by name, the **first** value for the given key is returned.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) FormValue(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Get first value from form field \"name\":\n  c.FormValue(\"name\")\n  // => \"john\" or \"\" if not exist\n\n  // ..\n})\n```\n\n:::info\n\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n\n:::\n\n### Fresh\n\nWhen the response is still **fresh** in the client's cache **true** is returned; otherwise, **false** is returned to indicate that the client cache is now stale and the full response should be sent.\n\nWhen a client sends the Cache-Control: no-cache request header to indicate an end-to-end reload request, `Fresh` will return false to make handling these requests transparent.\n\nRead more on [https://expressjs.com/en/4x/api.html\\#req.fresh](https://expressjs.com/en/4x/api.html#req.fresh)\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Fresh() bool\n```\n\n### FullURL\n\nReturns the full request URL (protocol + host + original URL).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) FullURL() string\n```\n\n```go title=\"Example\"\n// GET http://example.com/search?q=fiber\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.FullURL() // \"http://example.com/search?q=fiber\"\n  return nil\n})\n```\n\n### Get\n\nReturns the HTTP request header specified by the field.\n\n:::tip\nThe match is **case-insensitive**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Get(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Get(\"Content-Type\")       // \"text/plain\"\n  c.Get(\"CoNtEnT-TypE\")       // \"text/plain\"\n  c.Get(\"something\", \"john\")  // \"john\"\n  // ..\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### HasHeader\n\nReports whether the request includes a header with the given key.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) HasHeader(key string) bool\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.HasHeader(\"X-Trace-Id\")\n  return nil\n})\n```\n\n### Host\n\nReturns the host derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header.\n\nIn a network context, [`Host`](#host) refers to the combination of a hostname and potentially a port number used for connecting, while [`Hostname`](#hostname) refers specifically to the name assigned to a device on a network, excluding any port information.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Host() string\n```\n\n```go title=\"Example\"\n// GET http://google.com:8080/search\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Host()      // \"google.com:8080\"\n  c.Hostname()  // \"google.com\"\n\n  // ...\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### Hostname\n\nReturns the hostname derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Hostname() string\n```\n\n```go title=\"Example\"\n// GET http://google.com/search\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Hostname() // \"google.com\"\n\n  // ...\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### IP\n\nReturns the remote IP address of the request.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IP() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.IP() // \"127.0.0.1\"\n\n  // ...\n})\n```\n\n:::info\nBy default, `c.IP()` returns the remote IP address from the TCP connection. When your Fiber app is behind a reverse proxy (like Nginx, Traefik, or a load balancer), you need to configure **both** [`TrustProxy`](fiber.md#trustproxy) and [`ProxyHeader`](fiber.md#proxyheader) to read the client IP from proxy headers like `X-Forwarded-For`.\n\n**Important:** You must enable `TrustProxy` and configure trusted proxy IPs to prevent header spoofing. Simply setting `ProxyHeader` alone will not work.\n\n**Note:** When using a proxy header such as `X-Forwarded-For`, `c.IP()` returns the raw header value unless [`EnableIPValidation`](fiber.md#enableipvalidation) is enabled. For `X-Forwarded-For`, this raw value may be a comma-separated list of IPs; enable `EnableIPValidation` if you need `c.IP()` to return a single, validated client IP.\n:::\n\n#### Configuration for apps behind a reverse proxy\n\n```go title=\"Example - Basic Configuration\"\napp := fiber.New(fiber.Config{\n  // Enable proxy support\n  TrustProxy: true,\n  // Specify which header contains the real client IP\n  ProxyHeader: fiber.HeaderXForwardedFor,\n  // Configure which proxy IPs to trust\n  TrustProxyConfig: fiber.TrustProxyConfig{\n    // Trust private IP ranges (for internal load balancers)\n    Private: true,\n    // Or specify exact proxy IPs/ranges\n    // Proxies: []string{\"10.10.0.58\", \"192.168.0.0/24\"},\n  },\n})\n```\n\n```go title=\"Example - Specific Proxy IPs\"\napp := fiber.New(fiber.Config{\n  TrustProxy: true,\n  ProxyHeader: fiber.HeaderXForwardedFor,\n  TrustProxyConfig: fiber.TrustProxyConfig{\n    // Trust only specific proxy IP addresses\n    Proxies: []string{\"10.10.0.58\", \"192.168.1.0/24\"},\n  },\n})\n```\n\nSee [`TrustProxy`](fiber.md#trustproxy) and [`TrustProxyConfig`](fiber.md#trustproxyconfig) for more details on security considerations and configuration options.\n\n### IPs\n\nReturns an array of IP addresses specified in the [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) request header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IPs() []string\n```\n\n```go title=\"Example\"\n// X-Forwarded-For: proxy1, 127.0.0.1, proxy3\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.IPs() // [\"proxy1\", \"127.0.0.1\", \"proxy3\"]\n\n  // ...\n})\n```\n\n:::caution\nImproper use of the X-Forwarded-For header can be a security risk. For details, see the [Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns) section.\n:::\n\n### Is\n\nReturns the matching **content type**, if the incoming request’s [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header field matches the [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) specified by the type parameter.\n\n:::info\nIf the request has **no** body, it returns **false**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Is(extension string) bool\n```\n\n```go title=\"Example\"\n// Content-Type: text/html; charset=utf-8\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Is(\"html\")  // true\n  c.Is(\".html\") // true\n  c.Is(\"json\")  // false\n\n  // ...\n})\n```\n\n### IsForm\n\nReports whether the `Content-Type` header is form-encoded.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsForm() bool\n```\n\n```go title=\"Example\"\n// Content-Type: application/x-www-form-urlencoded\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.IsForm() // true\n  return nil\n})\n```\n\n### IsFromLocal\n\nReturns `true` if the request came from localhost.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsFromLocal() bool\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // If request came from localhost, return true; else return false\n  c.IsFromLocal()\n\n  // ...\n})\n```\n\n### IsJSON\n\nReports whether the `Content-Type` header is JSON.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsJSON() bool\n```\n\n```go title=\"Example\"\n// Content-Type: application/json; charset=utf-8\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.IsJSON() // true\n  return nil\n})\n```\n\n### IsMultipart\n\nReports whether the `Content-Type` header is multipart form data.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsMultipart() bool\n```\n\n```go title=\"Example\"\n// Content-Type: multipart/form-data; boundary=abc123\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.IsMultipart() // true\n  return nil\n})\n```\n\n### IsProxyTrusted\n\nChecks the trustworthiness of the remote IP.\nIf [`TrustProxy`](fiber.md#trustproxy) is `false`, it returns `true`.\n`IsProxyTrusted` can check the remote IP by proxy ranges and IP map.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) IsProxyTrusted() bool\n```\n\n```go title=\"Example\"\napp := fiber.New(fiber.Config{\n  // TrustProxy enables the trusted proxy check\n  TrustProxy: true,\n  // TrustProxyConfig allows for configuring trusted proxies.\n  // Proxies is a list of trusted proxy IP ranges/addresses\n  TrustProxyConfig: fiber.TrustProxyConfig{\n    Proxies: []string{\"0.8.0.0\", \"1.1.1.1/30\"}, // IP address or IP address range\n    Loopback: true,   // Trust loopback addresses (127.0.0.0/8, ::1/128)\n    UnixSocket: true, // Trust Unix domain socket connections\n  },\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // If request came from trusted proxy, return true; else return false\n  c.IsProxyTrusted()\n\n  // ...\n})\n```\n\n### MediaType\n\nReturns the MIME type from the `Content-Type` header without parameters.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) MediaType() string\n```\n\n```go title=\"Example\"\n// Content-Type: application/json; charset=utf-8\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.MediaType() // \"application/json\"\n  return nil\n})\n```\n\n### Method\n\nReturns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on.\nOptionally, you can override the method by passing a string.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Method(override ...string) string\n```\n\n```go title=\"Example\"\napp.Post(\"/override\", func(c fiber.Ctx) error {\n  c.Method()          // \"POST\"\n\n  c.Method(\"GET\")\n  c.Method()          // \"GET\"\n\n  // ...\n})\n```\n\n### MultipartForm\n\nTo access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `*multipart.Form`, allowing you to access form values and files.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) MultipartForm() (*multipart.Form, error)\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Parse the multipart form:\n  if form, err := c.MultipartForm(); err == nil {\n    // => *multipart.Form\n\n    if token := form.Value[\"token\"]; len(token) > 0 {\n      // Get key value:\n      fmt.Println(token[0])\n    }\n\n    // Get all files from \"documents\" key:\n    files := form.File[\"documents\"]\n    // => []*multipart.FileHeader\n\n    // Loop through files:\n    for _, file := range files {\n      fmt.Println(file.Filename, file.Size, file.Header[\"Content-Type\"][0])\n      // => \"tutorial.pdf\" 360641 \"application/pdf\"\n\n      // Save the files to disk:\n      if err := c.SaveFile(file, fmt.Sprintf(\"./%s\", file.Filename)); err != nil {\n        return err\n      }\n    }\n  }\n\n  return nil\n})\n```\n\n### OriginalURL\n\nReturns the original request URL.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) OriginalURL() string\n```\n\n```go title=\"Example\"\n// GET http://example.com/search?q=something\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.OriginalURL() // \"/search?q=something\"\n\n  // ...\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\n### Params\n\nThis method can be used to get the route parameters. You can pass an optional default value that will be returned if the param key does not exist.\n\n:::info\nDefaults to an empty string \\(`\"\"`\\) if the param **doesn't** exist.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Params(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\n// GET http://example.com/user/fenny\napp.Get(\"/user/:name\", func(c fiber.Ctx) error {\n  c.Params(\"name\") // \"fenny\"\n\n  // ...\n})\n\n// GET http://example.com/user/fenny/123\napp.Get(\"/user/*\", func(c fiber.Ctx) error {\n  c.Params(\"*\")  // \"fenny/123\"\n  c.Params(\"*1\") // \"fenny/123\"\n\n  // ...\n})\n```\n\nUnnamed route parameters \\(\\*, +\\) can be fetched by the **character** and the **counter** in the route.\n\n```go title=\"Example\"\n// ROUTE: /v1/*/shop/*\n// GET:   /v1/brand/4/shop/blue/xs\nc.Params(\"*1\")  // \"brand/4\"\nc.Params(\"*2\")  // \"blue/xs\"\n```\n\nFor reasons of **downward compatibility**, the first parameter segment for the parameter character can also be accessed without the counter.\n\n```go title=\"Example\"\napp.Get(\"/v1/*/shop/*\", func(c fiber.Ctx) error {\n  c.Params(\"*\") // outputs the value of the first wildcard segment\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\nIn certain scenarios, it can be useful to have an alternative approach to handle different types of parameters, not\njust strings. This can be achieved using a generic `Params` function known as `Params[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V`.\nThis function is capable of parsing a route parameter and returning a value of a type that is assumed and specified by `V GenericType`.\n\n```go title=\"Signature\"\nfunc Params[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V\n```\n\n```go title=\"Example\"\n// GET http://example.com/user/114\napp.Get(\"/user/:id\", func(c fiber.Ctx) error{\n  fiber.Params[string](c, \"id\") // returns \"114\" as string.\n  fiber.Params[int](c, \"id\")    // returns 114 as integer\n  fiber.Params[string](c, \"number\") // returns \"\" (default string type)\n  fiber.Params[int](c, \"number\")    // returns 0 (default integer value type)\n})\n```\n\nThe generic `Params` function supports returning the following data types based on `V GenericType`:\n\n- Integer: `int`, `int8`, `int16`, `int32`, `int64`\n- Unsigned integer: `uint`, `uint8`, `uint16`, `uint32`, `uint64`\n- Floating-point numbers: `float32`, `float64`\n- Boolean: `bool`\n- String: `string`\n- Byte array: `[]byte`\n\n### Path\n\nContains the path part of the request URL. Optionally, you can 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).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Path(override ...string) string\n```\n\n```go title=\"Example\"\n// GET http://example.com/users?sort=desc\n\napp.Get(\"/users\", func(c fiber.Ctx) error {\n  c.Path()       // \"/users\"\n\n  c.Path(\"/john\")\n  c.Path()       // \"/john\"\n\n  // ...\n})\n```\n\n### Port\n\nReturns the remote port of the request.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Port() string\n```\n\n```go title=\"Example\"\n// GET http://example.com:8080\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Port() // \"8080\"\n\n  // ...\n})\n```\n\n### Protocol\n\nContains the request protocol string: `http` or `https` for **TLS** requests.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Protocol() string\n```\n\n```go title=\"Example\"\n// GET http://example.com\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Protocol() // \"http\"\n\n  // ...\n})\n```\n\n### Queries\n\n`Queries` is a function that returns an object containing a property for each query string parameter in the route.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Queries() map[string]string\n```\n\n```go title=\"Example\"\n// GET http://example.com/?name=alex&want_pizza=false&id=\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    m := c.Queries()\n    m[\"name\"]        // \"alex\"\n    m[\"want_pizza\"]  // \"false\"\n    m[\"id\"]          // \"\"\n    // ...\n})\n```\n\n```go title=\"Example\"\n// GET http://example.com/?field1=value1&field1=value2&field2=value3\n\napp.Get(\"/\", func (c fiber.Ctx) error {\n    m := c.Queries()\n    m[\"field1\"] // \"value2\"\n    m[\"field2\"] // \"value3\"\n})\n```\n\n```go title=\"Example\"\n// 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\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    m := c.Queries()\n    m[\"list_a\"] // \"3\"\n    m[\"list_b[]\"] // \"3\"\n    m[\"list_c\"] // \"1,2,3\"\n})\n```\n\n```go title=\"Example\"\n// GET /api/posts?filters.author.name=John&filters.category.name=Technology\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    m := c.Queries()\n    m[\"filters.author.name\"] // John\n    m[\"filters.category.name\"] // Technology\n})\n```\n\n```go title=\"Example\"\n// 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\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    m := c.Queries()\n    m[\"tags\"] // apple,orange,banana\n    m[\"filters[tags]\"] // apple,orange,banana\n    m[\"filters[category][name]\"] // fruits\n    m[\"filters.tags\"] // apple,orange,banana\n    m[\"filters.category.name\"] // fruits\n})\n```\n\n### Query\n\nThis method returns a string corresponding to a query string parameter by name. You can pass an optional default value that will be returned if the query key does not exist.\n\n:::info\nIf there is **no** query string, it returns an **empty string**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Query(key string, defaultValue ...string) string\n```\n\n```go title=\"Example\"\n// GET http://example.com/?order=desc&brand=nike\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Query(\"order\")         // \"desc\"\n  c.Query(\"brand\")         // \"nike\"\n  c.Query(\"empty\", \"nike\") // \"nike\"\n\n  // ...\n})\n```\n\n:::info\nThe returned value is valid only within the handler. Do not store references.\nMake copies or use the [**`Immutable`**](./fiber.md#immutable) setting instead. [Read more...](../#zero-allocation)\n:::\n\nIn certain scenarios, it can be useful to have an alternative approach to handle different types of query parameters, not\njust strings. This can be achieved using a generic `Query` function known as `Query[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V`.\nThis function is capable of parsing a query string and returning a value of a type that is assumed and specified by `V GenericType`.\n\nHere is the signature for the generic `Query` function:\n\n```go title=\"Signature\"\nfunc Query[V GenericType](c fiber.Ctx, key string, defaultValue ...V) V\n```\n\n```go title=\"Example\"\n// GET http://example.com/?page=1&brand=nike&new=true\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  fiber.Query[int](c, \"page\")     // 1\n  fiber.Query[string](c, \"brand\") // \"nike\"\n  fiber.Query[bool](c, \"new\")     // true\n\n  // ...\n})\n```\n\nIn 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.\nThe generic `Query` function supports returning the following data types based on `V GenericType`:\n\n- Integer: `int`, `int8`, `int16`, `int32`, `int64`\n- Unsigned integer: `uint`, `uint8`, `uint16`, `uint32`, `uint64`\n- Floating-point numbers: `float32`, `float64`\n- Boolean: `bool`\n- String: `string`\n- Byte array: `[]byte`\n\n### Range\n\nReturns a struct containing the type and a slice of ranges.\nOnly the canonical `bytes` unit is recognized and any optional\nwhitespace around range specifiers will be ignored, as specified\nin RFC 9110.\nIf none of the requested ranges are satisfiable, the method automatically\nsets the HTTP status code to **416 Range Not Satisfiable** and populates the\n`Content-Range` header with the current representation size.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Range(size int64) (Range, error)\n```\n\n```go title=\"Example\"\n// Range: bytes=500-700, 700-900\napp.Get(\"/\", func(c fiber.Ctx) error {\n  r := c.Range(1000)\n  if r.Type == \"bytes\" {\n      for _, rng := range r.Ranges {\n      fmt.Println(rng)\n      // [500, 700]\n    }\n  }\n})\n```\n\n### Referer\n\nReturns the `Referer` request header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Referer() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Referer() // \"https://example.com\"\n  return nil\n})\n```\n\n### RequestID\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) RequestID() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.RequestID() // \"8d7ad5e3-aaf3-450b-a241-2beb887efd54\"\n  return nil\n})\n```\n\n### SaveFile\n\nMethod is used to save **any** multipart file to disk.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SaveFile(fh *multipart.FileHeader, path string) error\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Parse the multipart form:\n  if form, err := c.MultipartForm(); err == nil {\n    // => *multipart.Form\n\n    // Get all files from \"documents\" key:\n    files := form.File[\"documents\"]\n    // => []*multipart.FileHeader\n\n    // Loop through files:\n    for _, file := range files {\n      fmt.Println(file.Filename, file.Size, file.Header[\"Content-Type\"][0])\n      // => \"tutorial.pdf\" 360641 \"application/pdf\"\n\n      // Save the files to disk:\n      if err := c.SaveFile(file, fmt.Sprintf(\"./%s\", file.Filename)); err != nil {\n        return err\n      }\n    }\n    return err\n  }\n})\n```\n\n### SaveFileToStorage\n\nMethod is used to save **any** multipart file to an external storage system.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error\n```\n\n```go title=\"Example\"\nstorage := memory.New()\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n  // Parse the multipart form:\n  if form, err := c.MultipartForm(); err == nil {\n    // => *multipart.Form\n\n    // Get all files from \"documents\" key:\n    files := form.File[\"documents\"]\n    // => []*multipart.FileHeader\n\n    // Loop through files:\n    for _, file := range files {\n      fmt.Println(file.Filename, file.Size, file.Header[\"Content-Type\"][0])\n      // => \"tutorial.pdf\" 360641 \"application/pdf\"\n\n      // Save the files to storage:\n      if err := c.SaveFileToStorage(file, fmt.Sprintf(\"./%s\", file.Filename), storage); err != nil {\n        return err\n      }\n    }\n    return err\n  }\n})\n```\n\n### Schema\n\nContains the request protocol string: `http` or `https` for TLS requests.\n\n:::info\nPlease use [`Config.TrustProxy`](fiber.md#trustproxy) to prevent header spoofing if your app is behind a proxy.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Schema() string\n```\n\n```go title=\"Example\"\n// GET http://example.com\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Schema() // \"http\"\n\n  // ...\n})\n```\n\n### Secure\n\nA boolean property that is `true` if a **TLS** connection is established.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Secure() bool\n```\n\n```go title=\"Example\"\n// Secure() method is equivalent to:\nc.Protocol() == \"https\"\n```\n\n### Stale\n\nWhen the client's cached response is **stale**, this method returns **true**. It\nis the logical complement of [`Fresh`](#fresh), which checks whether the cached\nrepresentation is still valid.\n\n[https://expressjs.com/en/4x/api.html#req.stale](https://expressjs.com/en/4x/api.html#req.stale)\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Stale() bool\n```\n\n### Subdomains\n\nReturns a slice with the host’s sub-domain labels. The dot-separated parts that precede the registrable domain (`example`) and the top-level domain (ex: `com`).\n\nThe `subdomain offset` (default `2`) tells Fiber how many labels, counting from the right-hand side, are always discarded.\nPassing an `offset` argument lets you override that value for a single call.\n\n```go\nfunc (c fiber.Ctx) Subdomains(offset ...int) []string\n```\n\n| `offset`               | Result                                  | Meaning                                       |\n| ---------------------- | --------------------------------------- | --------------------------------------------- |\n| *omitted* → **2**      | trim 2 right-most labels                | drop the registrable domain **and** the TLD   |\n| `1` to `len(labels)-1` | trim exactly `offset` right-most labels | custom trimming of available labels           |\n| `>= len(labels)`       | **return `[]`**                         | offset exceeds available labels → empty slice |\n| `0`                    | **return every label**                  | keep the entire host unchanged                |\n| `< 0`                  | **return `[]`**                         | negative offsets are invalid → empty slice    |\n\n#### Example\n\n```go\n// Host: \"tobi.ferrets.example.com\"\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Subdomains()    // [\"tobi\", \"ferrets\"]\n  c.Subdomains(1)   // [\"tobi\", \"ferrets\", \"example\"]\n  c.Subdomains(0)   // [\"tobi\", \"ferrets\", \"example\", \"com\"]\n  c.Subdomains(-1)  // []\n  // ...\n})\n```\n\n### UserAgent\n\nReturns the `User-Agent` request header.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) UserAgent() string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.UserAgent() // \"Mozilla/5.0 ...\"\n  return nil\n})\n```\n\n### XHR\n\nA boolean property that is `true` if the request’s [X-Requested-With](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) header field is [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), indicating that the request was issued by a client library (such as [jQuery](https://api.jquery.com/jQuery.ajax/)).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) XHR() bool\n```\n\n```go title=\"Example\"\n// X-Requested-With: XMLHttpRequest\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.XHR() // true\n\n  // ...\n})\n```\n\n## Response\n\nMethods which modify the response object.\n\n:::tip\nUse `c.Res()` to limit gopls suggestions to only these methods!\n:::\n\n### Append\n\nAppends the specified **value** to the HTTP response header field.\n\n:::caution\nIf the header is **not** already set, it creates the header with the specified value.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Append(field string, values ...string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Append(\"Link\", \"http://google.com\", \"http://localhost\")\n  // => Link: http://google.com, http://localhost\n\n  c.Append(\"Link\", \"Test\")\n  // => Link: http://google.com, http://localhost, Test\n\n  // ...\n})\n```\n\n### Attachment\n\nSets the HTTP response [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header field to `attachment`.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Attachment(filename ...string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Attachment()\n  // => Content-Disposition: attachment\n\n  c.Attachment(\"./upload/images/logo.png\")\n  // => Content-Disposition: attachment; filename=\"logo.png\"\n  // => Content-Type: image/png\n\n  // ...\n})\n```\n\nNon-ASCII filenames are encoded using the `filename*` parameter as defined in\n[RFC 6266](https://www.rfc-editor.org/rfc/rfc6266) and\n[RFC 8187](https://www.rfc-editor.org/rfc/rfc8187):\n\n```go title=\"Example\"\napp.Get(\"/non-ascii\", func(c fiber.Ctx) error {\n  c.Attachment(\"./files/文件.txt\")\n  // => Content-Disposition: attachment; filename=\"文件.txt\"; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt\n  return nil\n})\n```\n\n### AutoFormat\n\nPerforms content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format.\nThe supported content types are `text/html`, `text/plain`, `application/json`, `application/vnd.msgpack`, `application/xml`, and `application/cbor`.\nFor more flexible content negotiation, use [Format](ctx.md#format).\n\n:::info\nIf the header is **not** specified or there is **no** proper format, **text/plain** is used.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) AutoFormat(body any) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Accept: text/plain\n  c.AutoFormat(\"Hello, World!\")\n  // => Hello, World!\n\n  // Accept: text/html\n  c.AutoFormat(\"Hello, World!\")\n  // => <p>Hello, World!</p>\n\n  type User struct {\n    Name string\n  }\n  user := User{\"John Doe\"}\n\n  // Accept: application/json\n  c.AutoFormat(user)\n  // => {\"Name\":\"John Doe\"}\n\n  // Accept: application/vnd.msgpack\n  c.AutoFormat(user)\n  // => 82 a4 6e 61 6d 65 a4 6a 6f 68 6e a4 70 61 73 73 a3 64 6f 65\n\n  // Accept: application/cbor\n  c.AutoFormat(user)\n  // => a1 64 4e 61 6d 65 68 4a 6f 68 6e 20 44 6f 65\n\n  // Accept: application/xml\n  c.AutoFormat(user)\n  // => <User><Name>John Doe</Name></User>\n  // ..\n})\n```\n\n### CBOR\n\nCBOR converts any interface or string to CBOR encoded bytes.\n\n> **Note:** Before using any CBOR-related features, make sure to follow the [CBOR setup instructions](../guide/advance-format.md#cbor).\n\n:::info\nCBOR also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/cbor`.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) CBOR(data any, ctype ...string) error\n```\n\n```go title=\"Example\"\ntype SomeStruct struct {\n  Name string `cbor:\"name\"`\n  Age  uint8 `cbor:\"age\"`\n}\n\napp.Get(\"/cbor\", func(c fiber.Ctx) error {\n  // Create data struct:\n  data := SomeStruct{\n    Name: \"Grame\",\n    Age:  20,\n  }\n\n  return c.CBOR(data)\n  // => Content-Type: application/cbor\n  // => \\xa2dnameeGramecage\\x14\n\n  return c.CBOR(fiber.Map{\n    \"name\": \"Grame\",\n    \"age\":  20,\n  })\n  // => Content-Type: application/cbor\n  // => \\xa2dnameeGramecage\\x14\n\n  return c.CBOR(fiber.Map{\n    \"type\":     \"https://example.com/probs/out-of-credit\",\n    \"title\":    \"You do not have enough credit.\",\n    \"status\":   403,\n    \"detail\":   \"Your current balance is 30, but that costs 50.\",\n    \"instance\": \"/account/12345/msgs/abc\",\n  })\n  // => Content-Type: application/cbor\n  // => \\xa5dtypex'https://example.com/probs/out-of-creditetitlex\\x1eYou do not have enough credit.fstatus\\x19\\x01\\x93fdetailx.Your current balance is 30, but that costs 50.hinstancew/account/12345/msgs/abc\n})\n```\n\n### ClearCookie\n\nExpires a client cookie (or all cookies if left empty).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) ClearCookie(key ...string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Clears all cookies:\n  c.ClearCookie()\n\n  // Expire specific cookie by name:\n  c.ClearCookie(\"user\")\n\n  // Expire multiple cookies by names:\n  c.ClearCookie(\"token\", \"session\", \"track_id\", \"version\")\n  // ...\n})\n```\n\n:::caution\nWeb browsers and other compliant clients will only clear the cookie if the given options are identical to those when creating the cookie, excluding `Expires` and `MaxAge`. `ClearCookie` will not set these values for you - a technique similar to the one shown below should be used to ensure your cookie is deleted.\n:::\n\n```go title=\"Example\"\napp.Get(\"/set\", func(c fiber.Ctx) error {\n    c.Cookie(&fiber.Cookie{\n        Name:     \"token\",\n        Value:    \"randomvalue\",\n        Expires:  time.Now().Add(24 * time.Hour),\n        HTTPOnly: true,\n        SameSite: \"Lax\",\n    })\n\n    // ...\n})\n\napp.Get(\"/delete\", func(c fiber.Ctx) error {\n    c.Cookie(&fiber.Cookie{\n        Name:     \"token\",\n        Expires:  fasthttp.CookieExpireDelete, // Use fasthttp's built-in constant\n        HTTPOnly: true,\n        SameSite: \"Lax\",\n    })\n\n    // ...\n})\n```\n\nYou can also use `c.Cookie()` to expire cookies with specific `Path` or `Domain` attributes:\n\n```go title=\"Example\"\napp.Get(\"/logout\", func(c fiber.Ctx) error {\n    // Expire a cookie with path and domain\n    c.Cookie(&fiber.Cookie{\n        Name:    \"token\",\n        Path:    \"/api\",\n        Domain:  \"example.com\",\n        Expires: fasthttp.CookieExpireDelete,\n    })\n\n    return c.SendStatus(fiber.StatusOK)\n})\n```\n\n### Cookie\n\nSets a cookie.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Cookie(cookie *Cookie)\n```\n\n```go\ntype Cookie struct {\n    Name        string    `json:\"name\"`         // The name of the cookie\n    Value       string    `json:\"value\"`        // The value of the cookie\n    Path        string    `json:\"path\"`         // Specifies a URL path which is allowed to receive the cookie\n    Domain      string    `json:\"domain\"`       // Specifies the domain which is allowed to receive the cookie\n    MaxAge      int       `json:\"max_age\"`      // The maximum age (in seconds) of the cookie\n    Expires     time.Time `json:\"expires\"`      // The expiration date of the cookie\n    Secure      bool      `json:\"secure\"`       // Indicates that the cookie should only be transmitted over a secure HTTPS connection\n    HTTPOnly    bool      `json:\"http_only\"`    // Indicates that the cookie is accessible only through the HTTP protocol\n    SameSite    string    `json:\"same_site\"`    // Controls whether or not a cookie is sent with cross-site requests\n    Partitioned bool      `json:\"partitioned\"`  // Indicates if the cookie is stored in a partitioned cookie jar\n    SessionOnly bool      `json:\"session_only\"` // Indicates if the cookie is a session-only cookie\n}\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Create cookie\n  cookie := new(fiber.Cookie)\n  cookie.Name = \"john\"\n  cookie.Value = \"doe\"\n  cookie.Expires = time.Now().Add(24 * time.Hour)\n\n  // Set cookie\n  c.Cookie(cookie)\n  // ...\n})\n```\n\n:::info\nWhen setting a cookie with `SameSite=None`, Fiber automatically sets `Secure=true` as required by RFC 6265bis and modern browsers. This ensures compliance with the \"None\" SameSite policy which mandates that cookies must be sent over secure connections.\n\nFor more information, see:\n\n- [Mozilla Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#none)\n- [Chrome Documentation](https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure)\n\n:::\n\n:::info\nPartitioned cookies allow partitioning the cookie jar by top-level site, enhancing user privacy by preventing cookies from being shared across different sites. This feature is particularly useful in scenarios where a user interacts with embedded third-party services that should not have access to the main site's cookies. You can check out [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) for more information.\n:::\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Create a new partitioned cookie\n  cookie := new(fiber.Cookie)\n  cookie.Name = \"user_session\"\n  cookie.Value = \"abc123\"\n  cookie.Partitioned = true  // This cookie will be stored in a separate jar when it's embedded into another website\n\n  // Set the cookie in the response\n  c.Cookie(cookie)\n  return c.SendString(\"Partitioned cookie set\")\n})\n```\n\n### Download\n\nTransfers the file from the given path as an `attachment`.\n\nTypically, browsers will prompt the user to download. By default, the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header `filename=` parameter is the file path (this typically appears in the browser dialog).\nOverride this default with the `filename` parameter.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Download(file string, filename ...string) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.Download(\"./files/report-12345.pdf\")\n  // => Download report-12345.pdf\n\n  return c.Download(\"./files/report-12345.pdf\", \"report.pdf\")\n  // => Download report.pdf\n})\n```\n\nFor filenames containing non-ASCII characters, a `filename*` parameter is added\naccording to [RFC 6266](https://www.rfc-editor.org/rfc/rfc6266) and\n[RFC 8187](https://www.rfc-editor.org/rfc/rfc8187):\n\n```go title=\"Example\"\napp.Get(\"/non-ascii\", func(c fiber.Ctx) error {\n  return c.Download(\"./files/文件.txt\")\n  // => Content-Disposition: attachment; filename=\"文件.txt\"; filename*=UTF-8''%E6%96%87%E4%BB%B6.txt\n})\n```\n\n### End\n\nEnd immediately flushes the current response and closes the underlying connection.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) End() error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    c.SendString(\"Hello World!\")\n    return c.End()\n})\n```\n\n:::caution\nCalling `c.End()` will disallow further writes to the underlying connection.\n:::\n\n:::warning\n`c.End()` does **not** work in streaming mode (e.g. when using `fasthttp`'s `HijackConn` or `SendStream`).\nIn streaming mode the connection is managed asynchronously and `ctx.Conn()` may return `nil`,\nso `c.End()` will return `nil` without flushing or closing the connection.\n:::\n\nEnd can be used to stop a middleware from modifying a response of a handler/other middleware down the method chain\nwhen they regain control after calling `c.Next()`.\n\n```go title=\"Example\"\n// Error Logging/Responding middleware\napp.Use(func(c fiber.Ctx) error {\n    err := c.Next()\n\n    // Log errors & write the error to the response\n    if err != nil {\n        log.Printf(\"Got error in middleware: %v\", err)\n        return c.Writef(\"(got error %v)\", err)\n    }\n\n    // No errors occurred\n    return nil\n})\n\n// Handler with simulated error\napp.Get(\"/\", func(c fiber.Ctx) error {\n    // Closes the connection instantly after writing from this handler\n    // and disallow further modification of its response\n    defer c.End()\n\n    c.SendString(\"Hello, ... I forgot what comes next!\")\n    return errors.New(\"some error\")\n})\n```\n\n### Format\n\nPerforms content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format from the supplied offers. A default handler can be provided by setting the `MediaType` to `\"default\"`. If no offers match and no default is provided, a 406 (Not Acceptable) response is sent. The Content-Type is automatically set when a handler is selected.\n\n:::info\nIf the Accept header is **not** specified, the first handler will be used.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Format(handlers ...ResFmt) error\n```\n\n```go title=\"Example\"\n// Accept: application/json => {\"command\":\"eat\",\"subject\":\"fruit\"}\n// Accept: text/plain => Eat Fruit!\n// Accept: application/xml => Not Acceptable\napp.Get(\"/no-default\", func(c fiber.Ctx) error {\n  return c.Format(\n    fiber.ResFmt{\"application/json\", func(c fiber.Ctx) error {\n      return c.JSON(fiber.Map{\n        \"command\": \"eat\",\n        \"subject\": \"fruit\",\n      })\n    }},\n    fiber.ResFmt{\"text/plain\", func(c fiber.Ctx) error {\n      return c.SendString(\"Eat Fruit!\")\n    }},\n  )\n})\n\n// Accept: application/json => {\"command\":\"eat\",\"subject\":\"fruit\"}\n// Accept: text/plain => Eat Fruit!\n// Accept: application/xml => Eat Fruit!\napp.Get(\"/default\", func(c fiber.Ctx) error {\n  textHandler := func(c fiber.Ctx) error {\n    return c.SendString(\"Eat Fruit!\")\n  }\n\n  handlers := []fiber.ResFmt{\n    {\"application/json\", func(c fiber.Ctx) error {\n      return c.JSON(fiber.Map{\n        \"command\": \"eat\",\n        \"subject\": \"fruit\",\n      })\n    }},\n    {\"text/plain\", textHandler},\n    {\"default\", textHandler},\n  }\n\n  return c.Format(handlers...)\n})\n```\n\n### JSON\n\nConverts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package.\n\n:::info\nJSON also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/json; charset=utf-8` by default.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) JSON(data any, ctype ...string) error\n```\n\n```go title=\"Example\"\ntype SomeStruct struct {\n  Name string\n  Age  uint8\n}\n\napp.Get(\"/json\", func(c fiber.Ctx) error {\n  // Create data struct:\n  data := SomeStruct{\n    Name: \"Grame\",\n    Age:  20,\n  }\n\n  return c.JSON(data)\n  // => Content-Type: application/json; charset=utf-8\n  // => {\"Name\": \"Grame\", \"Age\": 20}\n\n  return c.JSON(fiber.Map{\n    \"name\": \"Grame\",\n    \"age\":  20,\n  })\n  // => Content-Type: application/json; charset=utf-8\n  // => {\"name\": \"Grame\", \"age\": 20}\n\n  return c.JSON(fiber.Map{\n    \"type\":     \"https://example.com/probs/out-of-credit\",\n    \"title\":    \"You do not have enough credit.\",\n    \"status\":   403,\n    \"detail\":   \"Your current balance is 30, but that costs 50.\",\n    \"instance\": \"/account/12345/msgs/abc\",\n  }, \"application/problem+json\")\n  // => Content-Type: application/problem+json\n  // => \"{\n  // =>     \"type\": \"https://example.com/probs/out-of-credit\",\n  // =>     \"title\": \"You do not have enough credit.\",\n  // =>     \"status\": 403,\n  // =>     \"detail\": \"Your current balance is 30, but that costs 50.\",\n  // =>     \"instance\": \"/account/12345/msgs/abc\",\n  // => }\"\n})\n```\n\n### JSONP\n\nSends a JSON response with JSONP support. This method is identical to [JSON](ctx.md#json), except that it opts-in to JSONP callback support. By default, the callback name is simply `callback`.\n\nOverride this by passing a **named string** in the method.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) JSONP(data any, callback ...string) error\n```\n\n```go title=\"Example\"\ntype SomeStruct struct {\n  Name string\n  Age  uint8\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Create data struct:\n  data := SomeStruct{\n    Name: \"Grame\",\n    Age:  20,\n  }\n\n  return c.JSONP(data)\n  // => callback({\"Name\": \"Grame\", \"Age\": 20})\n\n  return c.JSONP(data, \"customFunc\")\n  // => customFunc({\"Name\": \"Grame\", \"Age\": 20})\n})\n```\n\n### Links\n\nJoins the links followed by the property to populate the response’s [Link HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) field.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Links(link ...string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Links(\n    \"http://api.example.com/users?page=2\", \"next\",\n    \"http://api.example.com/users?page=5\", \"last\",\n  )\n  // Link: <http://api.example.com/users?page=2>; rel=\"next\",\n  //       <http://api.example.com/users?page=5>; rel=\"last\"\n\n  // ...\n})\n```\n\n### Location\n\nSets the response [Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Location(path string)\n```\n\n```go title=\"Example\"\napp.Post(\"/\", func(c fiber.Ctx) error {\n  c.Location(\"http://example.com\")\n\n  c.Location(\"/foo/bar\")\n\n  return nil\n})\n```\n\n### MsgPack\n\n> **Note:** Before using any MsgPack-related features, make sure to follow the [MsgPack setup instructions](../guide/advance-format.md#msgpack).\n\nA compact binary alternative to [JSON](#json) for efficient data transfer between micro-services or from server to client. MessagePack serializes faster and yields smaller payloads than plain JSON.\n\nConverts any **interface** or **string** to MsgPack using the [shamaton/msgpack](https://pkg.go.dev/github.com/shamaton/msgpack/v3) package.\n\n:::info\nMsgPack also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/vnd.msgpack`.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) MsgPack(data any, ctype ...string) error\n```\n\n```go title=\"Example\"\ntype SomeStruct struct {\n  Name string\n  Age  uint8\n}\n\napp.Get(\"/msgpack\", func(c fiber.Ctx) error {\n  // Create data struct:\n  data := SomeStruct{\n    Name: \"Grame\",\n    Age:  20,\n  }\n\n  return c.MsgPack(data)\n  // => Content-Type: application/vnd.msgpack\n  // => 82 A4 4E 61 6D 65 A5 47 72 61 6D 65 A3 41 67 65 14\n\n  return c.MsgPack(fiber.Map{\n    \"name\": \"Grame\",\n    \"age\":  20,\n  })\n  // => Content-Type: application/vnd.msgpack\n  // => 82 A4 6E 61 6D 65 A5 47 72 61 6D 65 A3 61 67 65 14\n\n  return c.MsgPack(fiber.Map{\n    \"type\":     \"https://example.com/probs/out-of-credit\",\n    \"title\":    \"You do not have enough credit.\",\n    \"status\":   403,\n    \"detail\":   \"Your current balance is 30, but that costs 50.\",\n    \"instance\": \"/account/12345/msgs/abc\",\n  }, \"application/problem+msgpack\")\n})\n\n// => Content-Type: application/problem+msgpack\n// 85 A4 74 79 70 65 D9 27 68 74 74 70 73 3A 2F 2F 65 78 61 6D 70 6C 65 2E 63 6F 6D 2F 70 72 6F 62 73 2F 6F 75 74 2D 6F 66 2D 63 72 65 64 69 74 A5 74 69 74 6C 65 BE 59 6F 75 20 64 6F 20 6E 6F 74 20 68 61 76 65 20 65 6E 6F 75 67 68 20 63 72 65 64 69 74 2E A6 73 74 61 74 75 73 CD 01 93 A6 64 65 74 61 69 6C D9 2E 59 6F 75 72 20 63 75 72 72 65 6E 74 20 62 61 6C 61 6E 63 65 20 69 73 20 33 30 2C 20 62 75 74 20 74 68 61 74 20 63 6F 73 74 73 20 35 30 2E A8 69 6E 73 74 61 6E 63 65 B7 2F 61 63 63 6F 75 6E 74 2F 31 32 33 34 35 2F 6D 73 67 73 2F 61 62 63\n```\n\n### Render\n\nRenders 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).\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Render(name string, bind any, layouts ...string) error\n```\n\n### Send\n\nSets the HTTP response body.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Send(body []byte) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.Send([]byte(\"Hello, World!\")) // => \"Hello, World!\"\n})\n```\n\nFiber also provides `SendString` and `SendStream` methods for raw inputs.\n\n:::tip\nUse this if you **don't need** type assertion, recommended for **faster** performance.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SendString(body string) error\nfunc (c fiber.Ctx) SendStream(stream io.Reader, size ...int) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.SendString(\"Hello, World!\")\n  // => \"Hello, World!\"\n\n  return c.SendStream(bytes.NewReader([]byte(\"Hello, World!\")))\n  // => \"Hello, World!\"\n})\n```\n\n### SendEarlyHints\n\nSends an informational `103 Early Hints` response with one or more\n[`Link` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link)\nbefore the final response. This allows the browser to start preloading\nresources while the server prepares the full response.\n\n:::caution\nThis feature requires HTTP/2 or newer. Some legacy HTTP/1.1 clients may not support sendEarlyHints.\nEarly Hints (`103` responses) are supported in HTTP/2 and newer. Older HTTP/1.1 clients may ignore these interim responses or misbehave when receiving them.\nSee [Enabling HTTP/2](../guide/reverse-proxy#enabling-http2) for instructions on how to use a reverse proxy (e.g. Nginx or Traefik) to enable HTTP/2 support.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SendEarlyHints(hints []string) error\n```\n\n```go title=\"Example\"\nhints := []string{\"<https://cdn.com/app.js>; rel=preload; as=script\"}\napp.Get(\"/early\", func(c fiber.Ctx) error {\n  if err := c.SendEarlyHints(hints); err != nil {\n    return err\n  }\n  return c.SendString(\"done\")\n})\n```\n\n### SendFile\n\nTransfers the file from the given path. Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response HTTP header field based on the **file** extension or format.\n\n```go title=\"Config\" title=\"Config\"\n// SendFile defines configuration options when to transfer file with SendFile.\ntype SendFile struct {\n  // FS is the file system to serve the static files from.\n  // You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.\n  //\n  // Optional. Default: nil\n  FS fs.FS\n\n  // When set to true, the server tries minimizing CPU usage by caching compressed files.\n  // This works differently than the github.com/gofiber/compression middleware.\n  // You have to set Content-Encoding header to compress the file.\n  // Available compression methods are gzip, br, and zstd.\n  //\n  // Optional. Default: false\n  Compress bool `json:\"compress\"`\n\n  // When set to true, enables byte range requests.\n  //\n  // Optional. Default: false\n  ByteRange bool `json:\"byte_range\"`\n\n  // When set to true, enables direct download.\n  //\n  // Optional. Default: false\n  Download bool `json:\"download\"`\n\n  // Expiration duration for inactive file handlers.\n  // Use a negative time.Duration to disable it.\n  //\n  // Optional. Default: 10 * time.Second\n  CacheDuration time.Duration `json:\"cache_duration\"`\n\n  // The value for the Cache-Control HTTP-header\n  // that is set on the file response. MaxAge is defined in seconds.\n  //\n  // Optional. Default: 0\n  MaxAge int `json:\"max_age\"`\n}\n```\n\n```go title=\"Signature\" title=\"Signature\"\nfunc (c fiber.Ctx) SendFile(file string, config ...SendFile) error\n```\n\n```go title=\"Example\"\napp.Get(\"/not-found\", func(c fiber.Ctx) error {\n  return c.SendFile(\"./public/404.html\")\n\n  // Disable compression\n  return c.SendFile(\"./static/index.html\", fiber.SendFile{\n    Compress: false,\n  })\n})\n```\n\n:::info\nIf the file contains a URL-specific character, you have to escape it before passing the file path into the `SendFile` function.\n:::\n\n```go title=\"Example\"\napp.Get(\"/file-with-url-chars\", func(c fiber.Ctx) error {\n  return c.SendFile(url.PathEscape(\"hash_sign_#.txt\"))\n})\n```\n\n:::info\nYou can set the `CacheDuration` config property to `-1` to disable caching.\n:::\n\n```go title=\"Example\"\napp.Get(\"/file\", func(c fiber.Ctx) error {\n  return c.SendFile(\"style.css\", fiber.SendFile{\n    CacheDuration: -1,\n  })\n})\n```\n\n:::info\nYou can use multiple `SendFile` calls with different configurations in a single route. Fiber creates different filesystem handlers per config.\n:::\n\n```go title=\"Example\"\napp.Get(\"/file\", func(c fiber.Ctx) error {\n  switch c.Query(\"config\") {\n    case \"filesystem\":\n      return c.SendFile(\"style.css\", fiber.SendFile{\n        FS: os.DirFS(\".\")\n      })\n    case \"filesystem-compress\":\n      return c.SendFile(\"style.css\", fiber.SendFile{\n        FS: os.DirFS(\".\"),\n        Compress: true,\n      })\n    case \"compress\":\n      return c.SendFile(\"style.css\", fiber.SendFile{\n        Compress: true,\n      })\n    default:\n      return c.SendFile(\"style.css\")\n  }\n\n  return nil\n})\n```\n\n:::info\nFor sending multiple files from an embedded file system, [this functionality](../middleware/static.md#serving-files-using-embedfs) can be used.\n:::\n\n### SendStatus\n\nSets the status code and the correct status message in the body if the response body is **empty**.\n\n:::tip\nYou can find all used status codes and messages [in the Fiber source code](https://github.com/gofiber/fiber/blob/dffab20bcdf4f3597d2c74633a7705a517d2c8c2/utils.go#L183-L244).\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SendStatus(status int) error\n```\n\n```go title=\"Example\"\napp.Get(\"/not-found\", func(c fiber.Ctx) error {\n  return c.SendStatus(415)\n  // => 415 \"Unsupported Media Type\"\n\n  c.SendString(\"Hello, World!\")\n  return c.SendStatus(415)\n  // => 415 \"Hello, World!\"\n})\n```\n\n### SendStream\n\nSets the response body to a stream of data and adds an optional body size.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SendStream(stream io.Reader, size ...int) error\n```\n\n:::info\n`SendStream` operates asynchronously. The handler returns immediately after setting up the stream,\nbut the actual reading and sending of data happens **after** the handler completes. This is handled\nby the underlying `fasthttp` library.\n\nIf the provided stream implements `io.Closer`, it will be automatically closed by `fasthttp` after\nthe response is fully sent or if an error occurs.\n:::\n\n:::caution\nWhen passing `fiber.Ctx` as a `context.Context` to libraries that spawn goroutines (e.g., for streaming operations),\nthose goroutines may attempt to access the context after the handler returns. Since `fiber.Ctx` is recycled and\nreleased after the handler completes, this can cause issues.\n\n**Recommended approach**: Use `c.Context()` or `c.RequestCtx()` instead of passing `c` directly to such libraries.\nSee the [Context Guide](../guide/context.md) for more details.\n:::\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.SendStream(bytes.NewReader([]byte(\"Hello, World!\")))\n  // => \"Hello, World!\"\n})\n```\n\n```go title=\"Example with file streaming\"\napp.Get(\"/download\", func(c fiber.Ctx) error {\n  file, err := os.Open(\"large-file.zip\")\n  if err != nil {\n    return err\n  }\n  // File will be automatically closed by fasthttp after streaming completes\n  \n  stat, err := file.Stat()\n  if err != nil {\n    file.Close()\n    return err\n  }\n  \n  return c.SendStream(file, int(stat.Size()))\n})\n```\n\n### SendStreamWriter\n\nSets the response body stream writer.\n\n:::note\nThe argument `streamWriter` represents a function that populates\nthe response body using a buffered stream writer.\n:::\n\n```go title=\"Signature\"\nfunc (c Ctx) SendStreamWriter(streamWriter func(*bufio.Writer)) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func (c fiber.Ctx) error {\n  return c.SendStreamWriter(func(w *bufio.Writer) {\n    fmt.Fprintf(w, \"Hello, World!\\n\")\n  })\n  // => \"Hello, World!\"\n})\n```\n\n:::info\nTo send data before `streamWriter` returns, you can call `w.Flush()`\non the provided writer. Otherwise, the buffered stream flushes after\n`streamWriter` returns.\n:::\n\n:::note\n`w.Flush()` will return an error if the client disconnects before `streamWriter` finishes writing a response.\n:::\n\n```go title=\"Example\"\napp.Get(\"/wait\", func(c fiber.Ctx) error {\n  return c.SendStreamWriter(func(w *bufio.Writer) {\n    // Begin Work\n    fmt.Fprintf(w, \"Please wait for 10 seconds\\n\")\n    if err := w.Flush(); err != nil {\n      log.Print(\"Client disconnected!\")\n      return\n    }\n\n    // Send progress over time\n    time.Sleep(time.Second)\n    for i := 0; i < 9; i++ {\n      fmt.Fprintf(w, \"Still waiting...\\n\")\n      if err := w.Flush(); err != nil {\n        // If client disconnected, cancel work and finish\n        log.Print(\"Client disconnected!\")\n        return\n      }\n      time.Sleep(time.Second)\n    }\n\n    // Finish\n    fmt.Fprintf(w, \"Done!\\n\")\n  })\n})\n```\n\n### SendString\n\nSets the response body to a string.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) SendString(body string) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.SendString(\"Hello, World!\")\n  // => \"Hello, World!\"\n})\n```\n\n### Set\n\nSets the response’s HTTP header field to the specified `key`, `value`.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Set(key string, val string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Set(\"Content-Type\", \"text/plain\")\n  // => \"Content-Type: text/plain\"\n\n  // ...\n})\n```\n\n### Status\n\nSets the HTTP status for the response.\n\n:::info\nThis method is **chainable**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Status(status int) fiber.Ctx\n```\n\n```go title=\"Example\"\napp.Get(\"/fiber\", func(c fiber.Ctx) error {\n  c.Status(fiber.StatusOK)\n  return nil\n})\n\napp.Get(\"/hello\", func(c fiber.Ctx) error {\n  return c.Status(fiber.StatusBadRequest).SendString(\"Bad Request\")\n})\n\napp.Get(\"/world\", func(c fiber.Ctx) error {\n  return c.Status(fiber.StatusNotFound).SendFile(\"./public/gopher.png\")\n})\n```\n\n### Type\n\nSets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header to the MIME type listed [in the Nginx MIME types configuration](https://github.com/nginx/nginx/blob/master/conf/mime.types) specified by the file **extension**.\n\n:::info\nThis method is **chainable**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Type(ext string, charset ...string) fiber.Ctx\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Type(\".html\") // => \"text/html\"\n  c.Type(\"html\")  // => \"text/html\"\n  c.Type(\"png\")   // => \"image/png\"\n\n  c.Type(\"json\", \"utf-8\")  // => \"application/json; charset=utf-8\"\n\n  // ...\n})\n```\n\n### Vary\n\nAdds the given header field to the [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) response header. This will append the header if not already listed; otherwise, it leaves it listed in the current location.\n\n:::info\nMultiple fields are **allowed**.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Vary(fields ...string)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Vary(\"Origin\")     // => Vary: Origin\n  c.Vary(\"User-Agent\") // => Vary: Origin, User-Agent\n\n  // No duplicates\n  c.Vary(\"Origin\") // => Vary: Origin, User-Agent\n\n  c.Vary(\"Accept-Encoding\", \"Accept\")\n  // => Vary: Origin, User-Agent, Accept-Encoding, Accept\n\n  // ...\n})\n```\n\n### Write\n\nAdopts the `Writer` interface.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Write(p []byte) (n int, err error)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  c.Write([]byte(\"Hello, World!\")) // => \"Hello, World!\"\n\n  fmt.Fprintf(c, \"%s\\n\", \"Hello, World!\") // => \"Hello, World!\"\n})\n```\n\n### Writef\n\nWrites a formatted string using a format specifier.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) Writef(format string, a ...any) (n int, err error)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  world := \"World!\"\n  c.Writef(\"Hello, %s\", world) // => \"Hello, World!\"\n\n  fmt.Fprintf(c, \"%s\\n\", \"Hello, World!\") // => \"Hello, World!\"\n})\n```\n\n### WriteString\n\nWrites a string to the response body.\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) WriteString(s string) (n int, err error)\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.WriteString(\"Hello, World!\")\n  // => \"Hello, World!\"\n})\n```\n\n### XML\n\nConverts any **interface** or **string** to XML using the standard `encoding/xml` package.\n\n:::info\nXML also sets the content header to `application/xml; charset=utf-8`.\n:::\n\n```go title=\"Signature\"\nfunc (c fiber.Ctx) XML(data any) error\n```\n\n```go title=\"Example\"\ntype SomeStruct struct {\n  XMLName xml.Name `xml:\"Fiber\"`\n  Name    string   `xml:\"Name\"`\n  Age     uint8    `xml:\"Age\"`\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // Create data struct:\n  data := SomeStruct{\n    Name: \"Grame\",\n    Age:  20,\n  }\n\n  return c.XML(data)\n  // <Fiber>\n  //     <Name>Grame</Name>\n  //     <Age>20</Age>\n  // </Fiber>\n})\n```\n"
  },
  {
    "path": "docs/api/fiber.md",
    "content": "---\nid: fiber\ntitle: 📦 Fiber\ndescription: Fiber represents the fiber package where you start to create an instance.\nsidebar_position: 1\n---\n\nimport Reference from '@site/src/components/reference';\n\n## Server start\n\n### New\n\nThis method creates a new **App** named instance. You can pass optional [config](#config) when creating a new instance.\n\n```go title=\"Signature\"\nfunc New(config ...Config) *App\n```\n\n```go title=\"Example\"\n// Default config\napp := fiber.New()\n\n// ...\n```\n\n### Config\n\nYou can pass an optional Config when creating a new Fiber instance.\n\n```go title=\"Example\"\n// Custom config\napp := fiber.New(fiber.Config{\n    CaseSensitive: true,\n    StrictRouting: true,\n    ServerHeader:  \"Fiber\",\n    AppName: \"Test App v1.0.1\",\n})\n\n// ...\n```\n\n#### Config fields\n\n| Property                                                                              | Type                                                            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Default                                                                |\n|---------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|\n| <Reference id=\"appname\">AppName</Reference>                                           | `string`                                                        | Sets the application name used in logs and the Server header                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `\"\"`                                                                   |\n| <Reference id=\"bodylimit\">BodyLimit</Reference>                                       | `int`                                                           | Sets the maximum allowed size for a request body. Zero or negative values fall back to the default limit. If the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. This limit also applies when running Fiber through the adaptor middleware from `net/http`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `4 * 1024 * 1024`                                                      |\n| <Reference id=\"casesensitive\">CaseSensitive</Reference>                               | `bool`                                                          | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo` and `/foo` are treated the same.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                                                                |\n| <Reference id=\"cbordecoder\">CBORDecoder</Reference>                                   | `utils.CBORUnmarshal`                                           | Allowing for flexibility in using another cbor library for decoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `binder.UnimplementedCborUnmarshal`                                   |\n| <Reference id=\"cborencoder\">CBOREncoder</Reference>                                   | `utils.CBORMarshal`                                             | Allowing for flexibility in using another cbor library for encoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `binder.UnimplementedCborMarshal`                                     |\n| <Reference id=\"colorscheme\">ColorScheme</Reference>                                   | [`Colors`](https://github.com/gofiber/fiber/blob/main/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | [`DefaultColors`](https://github.com/gofiber/fiber/blob/main/color.go) |\n| <Reference id=\"compressedfilesuffixes\">CompressedFileSuffixes</Reference>             | `map[string]string`                                             | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `{\"gzip\": \".fiber.gz\", \"br\": \".fiber.br\", \"zstd\": \".fiber.zst\"}`       |\n| <Reference id=\"concurrency\">Concurrency</Reference>                                   | `int`                                                           | Maximum number of concurrent connections.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `256 * 1024`                                                           |\n| <Reference id=\"disabledefaultcontenttype\">DisableDefaultContentType</Reference>       | `bool`                                                          | When true, omits the default Content-Type header from the response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `false`                                                                |\n| <Reference id=\"disabledefaultdate\">DisableDefaultDate</Reference>                     | `bool`                                                          | When true, omits the Date header from the response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `false`                                                                |\n| <Reference id=\"disableheadautoregister\">DisableHeadAutoRegister</Reference>           | `bool`                          | Prevents Fiber from automatically registering `HEAD` routes for each `GET` route so you can supply custom `HEAD` handlers; manual `HEAD` routes still override the generated ones. | `false`                                                                |\n| <Reference id=\"disableheadernormalizing\">DisableHeaderNormalizing</Reference>         | `bool`                                                          | By default all header names are normalized: conteNT-tYPE -&gt; Content-Type                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                                                                |\n| <Reference id=\"disablekeepalive\">DisableKeepalive</Reference>                         | `bool`                                                          | Disables keep-alive connections so the server closes each connection after the first response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `false`                                                                |\n| <Reference id=\"disablepreparsemultipartform\">DisablePreParseMultipartForm</Reference> | `bool`                                                          | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `false`                                                                |\n| <Reference id=\"enableipvalidation\">EnableIPValidation</Reference>                     | `bool`                                                          | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma separated string.<br /><br />**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header.                                                                                                                                                                                                                                                                                                                                                                         | `false`                                                                |\n| <Reference id=\"enablesplittingonparsers\">EnableSplittingOnParsers</Reference>         | `bool`                                                          | Splits query, body, and header parameters on commas when enabled.<br /><br />For example, `/api?foo=bar,baz` becomes `foo[]=bar&foo[]=baz`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `false`                                                                |\n| <Reference id=\"errorhandler\">ErrorHandler</Reference>                                 | `ErrorHandler`                                                  | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `DefaultErrorHandler`                                                  |\n| <Reference id=\"getonly\">GETOnly</Reference>                                           | `bool`                                                          | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `false`                                                                |\n| <Reference id=\"idletimeout\">IdleTimeout</Reference>                                   | `time.Duration`                                                 | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `0`                                                                    |\n| <Reference id=\"immutable\">Immutable</Reference>                                       | `bool`                                                          | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\\#185](https://github.com/gofiber/fiber/issues/185).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `false`                                                                |\n| <Reference id=\"jsondecoder\">JSONDecoder</Reference>                                   | `utils.JSONUnmarshal`                                           | Allowing for flexibility in using another json library for decoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `json.Unmarshal`                                                       |\n| <Reference id=\"jsonencoder\">JSONEncoder</Reference>                                   | `utils.JSONMarshal`                                             | Allowing for flexibility in using another json library for encoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `json.Marshal`                                                         |\n| <Reference id=\"maxranges\">MaxRanges</Reference>                                       | `int`                                                           | Sets the maximum number of ranges parsed from a `Range` header. Zero or negative values fall back to the default limit. If the limit is exceeded, the request is rejected with `416 - Requested Range Not Satisfiable` and `Content-Range: bytes */<size>`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `16`                                                                   |\n| <Reference id=\"msgpackdecoder\">MsgPackDecoder</Reference>                             | `utils.MsgPackUnmarshal`                                        | Allowing for flexibility in using another msgpack library for decoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `binder.UnimplementedMsgpackUnmarshal`                                 |\n| <Reference id=\"msgpackencoder\">MsgPackEncoder</Reference>                             | `utils.MsgPackMarshal`                                          | Allowing for flexibility in using another msgpack library for encoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `binder.UnimplementedMsgpackMarshal`                                   |\n| <Reference id=\"passlocalstocontext\">PassLocalsToContext</Reference>                   | `bool`                                                          | Controls whether `StoreInContext` also propagates values into the request `context.Context` for Fiber-backed contexts. `StoreInContext` always writes to `c.Locals()`. `ValueFromContext` for Fiber-backed contexts always reads from `c.Locals()`. | `false`                                                                |\n| <Reference id=\"passlocalstoviews\">PassLocalsToViews</Reference>                       | `bool`                                                          | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `false`                                                                |\n| <Reference id=\"proxyheader\">ProxyHeader</Reference>                                   | `string`                                                        | Specifies the header name to read the client's real IP address from when behind a reverse proxy. Common values: `fiber.HeaderXForwardedFor`, `\"X-Real-IP\"`, `\"CF-Connecting-IP\"` (Cloudflare). <br /><br />**Important:** This setting **requires** `TrustProxy` to be enabled; `TrustProxyConfig` controls which proxy IPs are trusted for reading this header. Without `TrustProxy`, this setting has no effect and `c.IP()` will always return the remote IP from the TCP connection. <br /><br />**Behavior note:** `X-Forwarded-For` often contains a comma-separated chain of IP addresses. With the default `EnableIPValidation = false`, `c.IP()` will return the raw header value (the whole chain) rather than a single parsed client IP. With `EnableIPValidation = true`, `c.IP()` parses the header and returns the **first syntactically valid IP address** it finds; it does **not** walk the chain to find the first non-proxy hop. For a reliable client IP, configure your reverse proxy to overwrite or sanitize this header and/or to provide a single-IP header such as `\"X-Real-IP\"` or a provider-specific header like `\"CF-Connecting-IP\"`. <br /><br />**Security Warning:** Headers can be easily spoofed. Always configure `TrustProxyConfig` to validate the proxy IP address, otherwise malicious clients can forge headers to bypass IP-based access controls.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `\"\"`                                                                   |\n| <Reference id=\"readbuffersize\">ReadBufferSize</Reference>                             | `int`                                                           | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \\(for example, BIG cookies\\).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `4096`                                                                 |\n| <Reference id=\"readtimeout\">ReadTimeout</Reference>                                   | `time.Duration`                                                 | The amount of time allowed to read the full request, including the body. The default timeout is unlimited.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `0`                                                                    |\n| <Reference id=\"reducememoryusage\">ReduceMemoryUsage</Reference>                       | `bool`                                                          | Aggressively reduces memory usage at the cost of higher CPU usage if set to true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `false`                                                                |\n| <Reference id=\"requestmethods\">RequestMethods</Reference>                             | `[]string`                                                      | RequestMethods provides customizability for HTTP methods. You can add/remove methods as you wish.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `DefaultMethods`                                                       |\n| <Reference id=\"serverheader\">ServerHeader</Reference>                                 | `string`                                                        | Enables the `Server` HTTP header with the given value.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `\"\"`                                                                   |\n| <Reference id=\"streamrequestbody\">StreamRequestBody</Reference>                       | `bool`                                                          | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger than the current limit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `false`                                                                |\n| <Reference id=\"strictrouting\">StrictRouting</Reference>                               | `bool`                                                          | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `false`                                                                |\n| <Reference id=\"structvalidator\">StructValidator</Reference>                           | `StructValidator`                                               | If you want to validate header/form/query... automatically when to bind, you can define struct validator. Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `nil`                                                                  |\n| <Reference id=\"trustproxy\">TrustProxy</Reference>                                     | `bool` | Enables trust of reverse proxy headers. When enabled, Fiber will check if the request is coming from a trusted proxy (configured in `TrustProxyConfig`) before reading values from proxy headers. <br /><br />**Required for**: Using `ProxyHeader` to read client IP from headers like `X-Forwarded-For`. <br /><br />**Behavior when enabled:** If the remote IP is trusted (matches `TrustProxyConfig`), then `c.IP()` reads from `ProxyHeader` (when configured; otherwise it uses `RemoteIP()`), `c.Scheme()` first checks standard proxy scheme headers (`X-Forwarded-Proto`, `X-Forwarded-Protocol`, `X-Forwarded-Ssl`, `X-Url-Scheme`) and falls back to the actual connection scheme if none are set, and `c.Hostname()` prefers `X-Forwarded-Host` but falls back to the request Host header when the proxy header is not present. If the remote IP is NOT trusted, these methods ignore proxy headers and use the actual connection values instead. <br /><br />**Security:** This prevents header spoofing by validating the proxy's IP address. Always configure `TrustProxyConfig` when enabling this option and set `ProxyHeader` if you want `c.IP()` to use a specific header. | `false`                                                                |\n| <Reference id=\"trustproxyconfig\">TrustProxyConfig</Reference>                         | `TrustProxyConfig`                                              | Configures which proxy IP addresses or ranges to trust. Only effective when `TrustProxy` is enabled. <br /><br />**Fields:** <br />• `Proxies` - List of trusted proxy IPs or CIDR ranges (e.g., `[]string{\"10.10.0.58\", \"192.168.0.0/24\"}`) <br />• `Loopback` - Trust loopback addresses (127.0.0.0/8, ::1/128) <br />• `Private` - Trust all private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) <br />• `LinkLocal` - Trust link-local addresses (169.254.0.0/16, fe80::/10) <br />• `UnixSocket` - Trust Unix domain socket connections <br /><br />**Example:** For an app behind Nginx at 10.10.0.58, use `TrustProxyConfig{Proxies: []string{\"10.10.0.58\"}}` or `TrustProxyConfig{Private: true}` if using private network IPs.                                                                                                                                                                                                                                                                                                                                                | `{}`                                                                  |\n| <Reference id=\"unescapepath\">UnescapePath</Reference>                                 | `bool`                                                          | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `false`                                                                |\n| <Reference id=\"views\">Views</Reference>                                               | `Views`                                                         | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `nil`                                                                  |\n| <Reference id=\"viewslayout\">ViewsLayout</Reference>                                   | `string`                                                        | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `\"\"`                                                                   |\n| <Reference id=\"writebuffersize\">WriteBufferSize</Reference>                           | `int`                                                           | Per-connection buffer size for responses' writing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `4096`                                                                 |\n| <Reference id=\"writetimeout\">WriteTimeout</Reference>                                 | `time.Duration`                                                 | The maximum duration before timing out writes of the response. The default timeout is unlimited.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `0`                                                                    |\n| <Reference id=\"xmldecoder\">XMLDecoder</Reference>                                     | `utils.XMLUnmarshal`                                            | Allowing for flexibility in using another XML library for decoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `xml.Unmarshal`                                                        |\n| <Reference id=\"xmlencoder\">XMLEncoder</Reference>                                     | `utils.XMLMarshal`                                              | Allowing for flexibility in using another XML library for encoding.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `xml.Marshal`                                                          |\n\n## Server listening\n\n### Config\n\nYou can pass an optional ListenConfig when calling the [`Listen`](#listen) or [`Listener`](#listener) method.\n\n```go title=\"Example\"\n// Custom config\napp.Listen(\":8080\", fiber.ListenConfig{\n    EnablePrefork: true,\n    DisableStartupMessage: true,\n})\n```\n\n#### Config fields\n\n| Property                                                                | Type                          | Description                                                                                                                                                                                                                                                                                                                  | Default            |\n|-------------------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|\n| <Reference id=\"beforeservefunc\">BeforeServeFunc</Reference>             | `func(app *App) error`        | Allows customizing and accessing fiber app before serving the app.                                                                                                                                                                                                                                                           | `nil`              |\n| <Reference id=\"certclientfile\">CertClientFile</Reference>               | `string`                      | Path of the client certificate. If you want to use mTLS, you must enter this field.                                                                                                                                                                                                                                          | `\"\"`               |\n| <Reference id=\"certfile\">CertFile</Reference>                           | `string`                      | Path of the certificate file. If you want to use TLS, you must enter this field.                                                                                                                                                                                                                                             | `\"\"`               |\n| <Reference id=\"certkeyfile\">CertKeyFile</Reference>                     | `string`                      | Path of the certificate's private key. If you want to use TLS, you must enter this field.                                                                                                                                                                                                                                    | `\"\"`               |\n| <Reference id=\"disablestartupmessage\">DisableStartupMessage</Reference> | `bool`                        | When set to true, it will not print out the «Fiber» ASCII art and listening address.                                                                                                                                                                                                                                         | `false`            |\n| <Reference id=\"enableprefork\">EnablePrefork</Reference>                 | `bool`                        | When set to true, this will spawn multiple Go processes listening on the same port.                                                                                                                                                                                                                                          | `false`            |\n| <Reference id=\"enableprintroutes\">EnablePrintRoutes</Reference>         | `bool`                        | If set to true, will print all routes with their method, path, and handler.                                                                                                                                                                                                                                                  | `false`            |\n| <Reference id=\"gracefulcontext\">GracefulContext</Reference>             | `context.Context`             | Field to shutdown Fiber by given context gracefully.                                                                                                                                                                                                                                                                         | `nil`              |\n| <Reference id=\"ShutdownTimeout\">ShutdownTimeout</Reference>             | `time.Duration`               | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnPostShutdown` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` |\n| <Reference id=\"listeneraddrfunc\">ListenerAddrFunc</Reference>           | `func(addr net.Addr)`         | Allows accessing and customizing `net.Listener`.                                                                                                                                                                                                                                                                             | `nil`              |\n| <Reference id=\"listenernetwork\">ListenerNetwork</Reference>             | `string`                      | Known networks are \"tcp\", \"tcp4\" (IPv4-only), \"tcp6\" (IPv6-only), \"unix\" (Unix Domain Sockets). WARNING: When prefork is set to true, only \"tcp4\" and \"tcp6\" can be chosen.                                                                                                                                                  | `tcp4`             |\n| <Reference id=\"unixsocketfilemode\">UnixSocketFileMode</Reference>       | `os.FileMode`                 | FileMode to set for Unix Domain Socket (ListenerNetwork must be \"unix\")                                                                                                                                                                                                                                                      | `0770`             |\n| <Reference id=\"tlsconfigfunc\">TLSConfigFunc</Reference>                 | `func(tlsConfig *tls.Config)` | Allows customizing `tls.Config` as you want. Ignored when `TLSConfig` is set.                                                                                                                                                                                                                                                | `nil`              |\n| <Reference id=\"tlsconfig\">TLSConfig</Reference>                         | `*tls.Config`                 | Recommended base TLS configuration (cloned). Use for external certificate providers via `GetCertificate`. When set, other TLS fields are ignored.                                                                                                                                                                             | `nil`              |\n| <Reference id=\"autocertmanager\">AutoCertManager</Reference>             | `*autocert.Manager`           | Manages TLS certificates automatically using the ACME protocol. Enables integration with Let's Encrypt or other ACME-compatible providers.                                                                                                                                                                                   | `nil`              |\n| <Reference id=\"tlsminversion\">TLSMinVersion</Reference>                 | `uint16`                      | Allows customizing the TLS minimum version.                                                                                                                                                                                                                                                                                  | `tls.VersionTLS12` |\n\n### Listen\n\nListen serves HTTP requests from the given address.\n\n```go title=\"Signature\"\nfunc (app *App) Listen(addr string, config ...ListenConfig) error\n```\n\n```go title=\"Basic Listen usage\"\n// Listen on port :8080\napp.Listen(\":8080\")\n\n// Listen on port :8080 with Prefork\napp.Listen(\":8080\", fiber.ListenConfig{EnablePrefork: true})\n\n// Custom host\napp.Listen(\"127.0.0.1:8080\")\n```\n\n#### Prefork\n\nPrefork is a feature that allows you to spawn multiple Go processes listening on the same port. This can be useful for scaling across multiple CPU cores.\n\n```go title=\"Prefork listener\"\napp.Listen(\":8080\", fiber.ListenConfig{EnablePrefork: true})\n```\n\nThis distributes the incoming connections between the spawned processes and allows more requests to be handled simultaneously.\n\n#### TLS\n\nPrefer `TLSConfig` for TLS configuration so you can fully control certificates and settings. When `TLSConfig` is set, Fiber ignores `CertFile`, `CertKeyFile`, `CertClientFile`, `TLSMinVersion`, `AutoCertManager`, and `TLSConfigFunc`.\n\nTLS serves HTTPs requests from the given address using certFile and keyFile paths as TLS certificate and key file.\n\n```go title=\"TLS with cert and key files\"\napp.Listen(\":443\", fiber.ListenConfig{CertFile: \"./cert.pem\", CertKeyFile: \"./cert.key\"})\n```\n\n#### TLS with client CA certificate\n\n`CertClientFile` only configures the client CA for mTLS when using `CertFile`/`CertKeyFile`. If `TLSConfig` is set, `CertClientFile` is ignored, so configure client CAs in the provided `tls.Config` instead.\n\n```go title=\"TLS with client CA certificate\"\napp.Listen(\":443\", fiber.ListenConfig{\n    CertFile:       \"./cert.pem\",\n    CertKeyFile:    \"./cert.key\",\n    CertClientFile: \"./ca-chain-cert.pem\",\n})\n```\n\n#### TLS AutoCert support (ACME / Let's Encrypt)\n\nProvides automatic access to certificates management from Let's Encrypt and any other ACME-based providers.\n\n```go title=\"AutoCert (ACME) configuration\"\n// Certificate manager\ncertManager := &autocert.Manager{\n    Prompt: autocert.AcceptTOS,\n    // Replace with your domain name\n    HostPolicy: autocert.HostWhitelist(\"example.com\"),\n    // Folder to store the certificates\n    Cache: autocert.DirCache(\"./certs\"),\n}\n\napp.Listen(\":444\", fiber.ListenConfig{\n    AutoCertManager:    certManager,\n})\n```\n\n#### Precedence and conflicts\n\n- `TLSConfig` is preferred and ignores `CertFile`/`CertKeyFile`, `CertClientFile`, `AutoCertManager`, `TLSMinVersion`, and `TLSConfigFunc`.\n- `AutoCertManager` cannot be combined with `CertFile`/`CertKeyFile`.\n\n#### TLS with external certificate provider\n\nUse `TLSConfig` to supply a base `tls.Config` that can fetch certificates at runtime. `TLSConfig` is cloned and used as-is.\n\n```go title=\"TLSConfig with dynamic certificate provider\"\napp.Listen(\":443\", fiber.ListenConfig{\n    TLSConfig: &tls.Config{\n        GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {\n            return myProvider.Certificate(info.ServerName)\n        },\n    },\n})\n```\n\n#### Mutual TLS with TLSConfig\n\nUse `TLSConfig` to configure mutual TLS by setting `ClientAuth` and `ClientCAs`. This replaces `CertClientFile` when you manage TLS configuration directly.\n\n```go title=\"TLSConfig with client CA pool\"\ncertPEM := []byte(certPEMString)\nkeyPEM := []byte(keyPEMString)\ncaPEM := []byte(caPEMString)\n\ncert, err := tls.X509KeyPair(certPEM, keyPEM)\nif err != nil {\n    log.Fatal(err)\n}\n\nclientCAs := x509.NewCertPool()\nif ok := clientCAs.AppendCertsFromPEM(caPEM); !ok {\n    log.Fatal(\"failed to append client CA\")\n}\n\napp.Listen(\":443\", fiber.ListenConfig{\n    TLSConfig: &tls.Config{\n        Certificates: []tls.Certificate{cert},\n        ClientAuth:   tls.RequireAndVerifyClientCert,\n        ClientCAs:    clientCAs,\n    },\n})\n```\n\nLoad certificates from memory or environment variables and provide them via `TLSConfig`.\n\n```go title=\"TLSConfig with in-memory certificate\"\ncertPEM := []byte(certPEMString)\nkeyPEM := []byte(keyPEMString)\n\ncert, err := tls.X509KeyPair(certPEM, keyPEM)\nif err != nil {\n    log.Fatal(err)\n}\n\napp.Listen(\":443\", fiber.ListenConfig{\n    TLSConfig: &tls.Config{\n        Certificates: []tls.Certificate{cert},\n    },\n})\n```\n\n```go title=\"TLSConfig with certificate from environment\"\ncertPEM := []byte(os.Getenv(\"TLS_CERT_PEM\"))\nkeyPEM := []byte(os.Getenv(\"TLS_KEY_PEM\"))\n\ncert, err := tls.X509KeyPair(certPEM, keyPEM)\nif err != nil {\n    log.Fatal(err)\n}\n\napp.Listen(\":443\", fiber.ListenConfig{\n    TLSConfig: &tls.Config{\n        Certificates: []tls.Certificate{cert},\n    },\n})\n```\n\n### Listener\n\nYou can pass your own [`net.Listener`](https://pkg.go.dev/net/#Listener) using the `Listener` method. This method can be used to enable **TLS/HTTPS** with a custom tls.Config.\n\n```go title=\"Signature\"\nfunc (app *App) Listener(ln net.Listener, config ...ListenConfig) error\n```\n\n```go title=\"Examples\"\nln, _ := net.Listen(\"tcp\", \":3000\")\n\ncer, _:= tls.LoadX509KeyPair(\"server.crt\", \"server.key\")\n\nln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cer}})\n\napp.Listener(ln)\n```\n\n## Server\n\nServer returns the underlying [fasthttp server](https://godoc.org/github.com/valyala/fasthttp#Server)\n\n```go title=\"Signature\"\nfunc (app *App) Server() *fasthttp.Server\n```\n\n```go title=\"Examples\"\nfunc main() {\n    app := fiber.New()\n\n    app.Server().MaxConnsPerIP = 1\n\n    // ...\n}\n```\n\n## Server Shutdown\n\nShutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners and then waits indefinitely for all connections to return to idle before shutting down.\n\nShutdownWithTimeout will forcefully close any active connections after the timeout expires.\n\nShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors.\n\n```go\nfunc (app *App) Shutdown() error\nfunc (app *App) ShutdownWithTimeout(timeout time.Duration) error\nfunc (app *App) ShutdownWithContext(ctx context.Context) error\n```\n\n## Helper functions\n\n### NewError\n\nNewError creates a new HTTPError instance with an optional message.\n\n```go title=\"Signature\"\nfunc NewError(code int, message ...string) *Error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return fiber.NewError(782, \"Custom error message\")\n})\n```\n\n### NewErrorf\n\nNewErrorf creates a new HTTPError instance with an optional formatted message.\n\n```go title=\"Signature\"\nfunc NewErrorf(code int, message ...any) *Error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return fiber.NewErrorf(782, \"Custom error %s\", \"message\")\n})\n```\n\n### IsChild\n\nIsChild determines if the current process is a result of Prefork.\n\n```go title=\"Signature\"\nfunc IsChild() bool\n```\n\n```go title=\"Example\"\n// Config app\napp := fiber.New()\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    if !fiber.IsChild() {\n        fmt.Println(\"I'm the parent process\")\n    } else {\n        fmt.Println(\"I'm a child process\")\n    }\n    return c.SendString(\"Hello, World!\")\n})\n\n// ...\n\n// With prefork enabled, the parent process will spawn child processes\napp.Listen(\":8080\", fiber.ListenConfig{EnablePrefork: true})\n```\n"
  },
  {
    "path": "docs/api/hooks.md",
    "content": "---\nid: hooks\ntitle: 🎣 Hooks\nsidebar_position: 7\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nFiber lets you run custom callbacks at specific points in the routing lifecycle. Available hooks include:\n\n- [OnRoute](#onroute)\n- [OnName](#onname)\n- [OnGroup](#ongroup)\n- [OnGroupName](#ongroupname)\n- [OnListen](#onlisten)\n- [OnPreStartupMessage/OnPostStartupMessage](#onprestartupmessageonpoststartupmessage)\n  - [ListenData](#listendata)\n- [OnFork](#onfork)\n- [OnPreShutdown](#onpreshutdown)\n- [OnPostShutdown](#onpostshutdown)\n- [OnMount](#onmount)\n\n## Constants\n\n```go\n// Handlers define functions to create hooks for Fiber.\ntype OnRouteHandler = func(Route) error\ntype OnNameHandler = OnRouteHandler\ntype OnGroupHandler = func(Group) error\ntype OnGroupNameHandler = OnGroupHandler\ntype OnListenHandler = func(ListenData) error\ntype OnForkHandler = func(int) error\ntype OnPreStartupMessageHandler  = func(*PreStartupMessageData) error\ntype OnPostStartupMessageHandler = func(*PostStartupMessageData) error\ntype OnPreShutdownHandler  = func() error\ntype OnPostShutdownHandler = func(error) error\ntype OnMountHandler = func(*App) error\n```\n\n## OnRoute\n\nRuns after each route is registered. The callback receives the route so you can inspect its properties.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnRoute(handler ...OnRouteHandler)\n```\n\n## OnName\n\nRuns when a route is named. The callback receives the route.\n\n:::caution\n`OnName` only works with named routes, not groups.\n:::\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnName(handler ...OnNameHandler)\n```\n\n<Tabs>\n<TabItem value=\"onname-example\" label=\"OnName Example\">\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(c.Route().Name)\n    }).Name(\"index\")\n\n    app.Hooks().OnName(func(r fiber.Route) error {\n        fmt.Print(\"Name: \" + r.Name + \", \")\n        return nil\n    })\n\n    app.Hooks().OnName(func(r fiber.Route) error {\n        fmt.Print(\"Method: \" + r.Method + \"\\n\")\n        return nil\n    })\n\n    app.Get(\"/add/user\", func(c fiber.Ctx) error {\n        return c.SendString(c.Route().Name)\n    }).Name(\"addUser\")\n\n    app.Delete(\"/destroy/user\", func(c fiber.Ctx) error {\n        return c.SendString(c.Route().Name)\n    }).Name(\"destroyUser\")\n\n    app.Listen(\":5000\")\n}\n\n// Results:\n// Name: addUser, Method: GET\n// Name: destroyUser, Method: DELETE\n```\n\n</TabItem>\n</Tabs>\n\n## OnGroup\n\nRuns after each group is registered. The callback receives the group.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnGroup(handler ...OnGroupHandler)\n```\n\n## OnGroupName\n\nRuns when a group is named. The callback receives the group.\n\n:::caution\n`OnGroupName` only works with named groups, not routes.\n:::\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnGroupName(handler ...OnGroupNameHandler)\n```\n\n## OnListen\n\nRuns when the app starts listening via `Listen` or `Listener`.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnListen(handler ...OnListenHandler)\n```\n\n<Tabs>\n<TabItem value=\"onlisten-example\" label=\"OnListen Example\">\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"os\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/log\"\n)\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        DisableStartupMessage: true,\n    })\n\n    app.Hooks().OnListen(func(listenData fiber.ListenData) error {\n        if fiber.IsChild() {\n            return nil\n        }\n        scheme := \"http\"\n        if listenData.TLS {\n            scheme = \"https\"\n        }\n        log.Println(scheme + \"://\" + listenData.Host + \":\" + listenData.Port)\n        return nil\n    })\n\n    app.Listen(\":5000\")\n}\n```\n\n</TabItem>\n</Tabs>\n\n## OnPreStartupMessage/OnPostStartupMessage\n\nUse `OnPreStartupMessage` to tweak the banner before Fiber prints it, and `OnPostStartupMessage` to run logic after the banner is printed (or skipped). You can use some helper functions to customize the banner inside the `OnPreStartupMessage` hook.\n\n```go title=\"Signatures\"\n// AddInfo adds an informational entry to the startup message with \"INFO\" label.\nfunc (sm *PreStartupMessageData) AddInfo(key, title, value string, priority ...int)\n\n// AddWarning adds a warning entry to the startup message with \"WARNING\" label.\nfunc (sm *PreStartupMessageData) AddWarning(key, title, value string, priority ...int)\n\n// AddError adds an error entry to the startup message with \"ERROR\" label.\nfunc (sm *PreStartupMessageData) AddError(key, title, value string, priority ...int)\n\n// EntryKeys returns all entry keys currently present in the startup message.\nfunc (sm *PreStartupMessageData) EntryKeys() []string\n\n// ResetEntries removes all existing entries from the startup message.\nfunc (sm *PreStartupMessageData) ResetEntries()\n\n// DeleteEntry removes a specific entry from the startup message by its key.\nfunc (sm *PreStartupMessageData) DeleteEntry(key string)\n```\n\n- Assign `sm.BannerHeader` to override the ASCII art banner. Leave it empty to use the default banner provided by Fiber.\n- Set `sm.PreventDefault = true` to suppress the built-in banner without affecting other hooks.\n- `PostStartupMessageData` reports whether the banner was skipped via the `Disabled`, `IsChild`, and `Prevented` flags.\n\n### Startup Message Customization\n\n```go title=\"Customize the startup message\"\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Hooks().OnPreStartupMessage(func(sm *fiber.PreStartupMessageData) error {\n        sm.BannerHeader = \"FOOBER \" + sm.Version + \"\\n-------\"\n\n        // Optional: you can also remove old entries\n        // sm.ResetEntries()\n\n        sm.AddInfo(\"git-hash\", \"Git hash\", os.Getenv(\"GIT_HASH\"))\n        sm.AddInfo(\"prefork\", \"Prefork\", fmt.Sprintf(\"%v\", sm.Prefork), 15)\n        return nil\n    })\n\n    app.Hooks().OnPostStartupMessage(func(sm fiber.PostStartupMessageData) error {\n        if !sm.Disabled && !sm.IsChild && !sm.Prevented {\n            fmt.Println(\"startup completed\")\n        }\n        return nil\n    })\n\n    app.Listen(\":5000\")\n}\n```\n\n### ListenData\n\n`ListenData` exposes runtime metadata about the listener:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `Host` | `string` | Resolved hostname or IP address. |\n| `Port` | `string` | The bound port. |\n| `TLS` | `bool` | Indicates whether TLS is enabled. |\n| `Version` | `string` | Fiber version reported in the startup banner. |\n| `AppName` | `string` | Application name from the configuration. |\n| `HandlerCount` | `int` | Total registered handler count. |\n| `ProcessCount` | `int` | Number of processes Fiber will use. |\n| `PID` | `int` | Current process identifier. |\n| `Prefork` | `bool` | Whether prefork is enabled. |\n| `ChildPIDs` | `[]int` | Child process identifiers when preforking. |\n| `ColorScheme` | [`Colors`](https://github.com/gofiber/fiber/blob/main/color.go) | Active color scheme for the startup message. |\n\n## OnFork\n\nRuns in the child process after a fork.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnFork(handler ...OnForkHandler)\n```\n\n## OnPreShutdown\n\nRuns before the server shuts down.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler)\n```\n\n## OnPostShutdown\n\nRuns after the server shuts down.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler)\n```\n\n## OnMount\n\nFires after a sub-app is mounted on a parent. The parent app is passed to the callback and it works for both app and group mounts.\n\n```go title=\"Signature\"\nfunc (h *Hooks) OnMount(handler ...OnMountHandler)\n```\n\n<Tabs>\n<TabItem value=\"onmount-example\" label=\"OnMount Example\">\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n    app.Get(\"/\", testSimpleHandler).Name(\"x\")\n\n    subApp := fiber.New()\n    subApp.Get(\"/test\", testSimpleHandler)\n\n    subApp.Hooks().OnMount(func(parent *fiber.App) error {\n        fmt.Print(\"Mount path of parent app: \" + parent.MountPath())\n        // Additional custom logic...\n        return nil\n    })\n\n    app.Use(\"/sub\", subApp)\n}\n\nfunc testSimpleHandler(c fiber.Ctx) error {\n    return c.SendString(\"Hello, Fiber!\")\n}\n\n// Result:\n// Mount path of parent app: /sub\n```\n\n</TabItem>\n</Tabs>\n\n:::caution\nOnName, OnRoute, OnGroup, and OnGroupName are mount-sensitive. When you mount a sub-app that registers these hooks, route and group paths include the mount prefix.\n:::\n"
  },
  {
    "path": "docs/api/log.md",
    "content": "---\nid: log\ntitle: 📃 Log\ndescription: Fiber's built-in log package\nsidebar_position: 6\n---\n\nLogs help you observe program behavior, diagnose issues, and trigger alerts. Structured logs improve searchability and speed up troubleshooting.\n\nFiber logs to standard output by default and exposes global helpers such as `log.Info`, `log.Errorf`, and `log.Warnw`.\n\n## Log Levels\n\n```go\nconst (\n    LevelTrace Level = iota\n    LevelDebug\n    LevelInfo\n    LevelWarn\n    LevelError\n    LevelFatal\n    LevelPanic\n)\n```\n\n## Custom Log\n\nFiber provides the generic `AllLogger[T]` interface for adapting various log libraries.\n\n```go\ntype CommonLogger interface {\n    Logger\n    FormatLogger\n    WithLogger\n}\n\ntype ConfigurableLogger[T any] interface {\n    // SetLevel sets logging level.\n    SetLevel(level Level)\n\n    // SetOutput sets the logger output.\n    SetOutput(w io.Writer)\n\n    // Logger returns the logger instance.\n    Logger() T\n}\n\ntype AllLogger[T any] interface {\n    CommonLogger\n    ConfigurableLogger[T]\n    WithLogger\n}\n```\n\n## Print Log\n\n**Note:** The Fatal level method will terminate the program after printing the log message. Please use it with caution.\n\n### Basic Logging\n\nCall level-specific methods directly; entries use the `messageKey` (default `msg`).\n\n```go\nlog.Info(\"Hello, World!\")\nlog.Debug(\"Are you OK?\")\nlog.Info(\"42 is the answer to life, the universe, and everything\")\nlog.Warn(\"We are under attack!\")\nlog.Error(\"Houston, we have a problem.\")\nlog.Fatal(\"So Long, and Thanks for All the Fish.\")\nlog.Panic(\"The system is down.\")\n```\n\n### Formatted Logging\n\nAppend `f` to format the message.\n\n```go\nlog.Debugf(\"Hello %s\", \"boy\")\nlog.Infof(\"%d is the answer to life, the universe, and everything\", 42)\nlog.Warnf(\"We are under attack, %s!\", \"boss\")\nlog.Errorf(\"%s, we have a problem.\", \"John Smith\")\nlog.Fatalf(\"So Long, and Thanks for All the %s.\", \"fish\")\n```\n\n### Key-Value Logging\n\nKey-value helpers log structured fields; mismatched pairs emit `KEYVALS UNPAIRED`.\n\n```go\nlog.Debugw(\"\", \"greeting\", \"Hello\", \"target\", \"boy\")\nlog.Infow(\"\", \"number\", 42)\nlog.Warnw(\"\", \"job\", \"boss\")\nlog.Errorw(\"\", \"name\", \"John Smith\")\nlog.Fatalw(\"\", \"fruit\", \"fish\")\n```\n\n## Global Log\n\nFiber also exposes a global logger for quick messages.\n\n```go\nimport \"github.com/gofiber/fiber/v3/log\"\n\nlog.Info(\"info\")\nlog.Warn(\"warn\")\n```\n\nThe example uses `log.DefaultLogger`, which writes to stdout. The [contrib](https://github.com/gofiber/contrib) repo offers adapters like `fiberzap` and `fiberzerolog`, or you can register your own with `log.SetLogger`.\n\nHere's an example using a custom logger:\n\n```go\nimport (\n    \"log\"\n    fiberlog \"github.com/gofiber/fiber/v3/log\"\n)\n\nvar _ fiberlog.AllLogger[*log.Logger] = (*customLogger)(nil)\n\ntype customLogger struct {\n    stdlog *log.Logger\n}\n\n// Implement required methods for the AllLogger interface...\n\n// Inject your custom logger\nfiberlog.SetLogger[*log.Logger](&customLogger{\n    stdlog: log.New(os.Stdout, \"CUSTOM \", log.LstdFlags),\n})\n\n// Retrieve the underlying *log.Logger for direct use\nstd := fiberlog.DefaultLogger[*log.Logger]().Logger()\nstd.Println(\"custom logging\")\n```\n\n## Set Level\n\n`log.SetLevel` sets the minimum level that will be output. The default is `LevelTrace`.\n\n**Note:** This method is not concurrent safe.\n\n```go\nimport \"github.com/gofiber/fiber/v3/log\"\n\nlog.SetLevel(log.LevelInfo)\n```\n\nSetting the log level allows you to control the verbosity of the logs, filtering out messages below the specified level.\n\n## Set Output\n\n`log.SetOutput` sets where logs are written. By default, they go to the console.\n\n### Writing Logs to Stderr\n\n```go\nvar logger fiberlog.AllLogger[*log.Logger] = &defaultLogger{\n    stdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n    depth:  4,\n}\n```\n\nThis lets you route logs to a file, service, or any destination.\n\n### Writing Logs to a File\n\nTo write to a file such as `test.log`:\n\n```go\n// Output to ./test.log file\nf, err := os.OpenFile(\"test.log\", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)\nif err != nil {\n    log.Fatal(\"Failed to open log file:\", err)\n}\nlog.SetOutput(f)\n```\n\n### Writing Logs to Both Console and File\n\nWrite to both `test.log` and `stdout`:\n\n```go\n// Output to ./test.log file\nfile, err := os.OpenFile(\"test.log\", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)\nif err != nil {\n    log.Fatal(\"Failed to open log file:\", err)\n}\niw := io.MultiWriter(os.Stdout, file)\nlog.SetOutput(iw)\n```\n\n## Bind Context\n\nBind a logger to a context with `log.WithContext`, which returns a `CommonLogger` tied to that context.\n\n```go\ncommonLogger := log.WithContext(ctx)\ncommonLogger.Info(\"info\")\n```\n\nContext binding adds request-specific data for easier tracing.\n\n## Logger\n\nUse `Logger` to access the underlying logger and call its native methods:\n\n```go\nlogger := fiberlog.DefaultLogger[*log.Logger]() // Get the default logger instance\n\nstdlogger := logger.Logger() // stdlogger is *log.Logger\nstdlogger.SetFlags(0) // Hide timestamp by setting flags to 0\n```\n"
  },
  {
    "path": "docs/api/redirect.md",
    "content": "---\nid: redirect\ntitle: 🔄 Redirect\ndescription: Fiber's built-in redirect package\nsidebar_position: 5\ntoc_max_heading_level: 5\n---\n\nRedirect helpers send the client to another URL or route.\n\n## Redirect Methods\n\n### To\n\nRedirects to a URL built from the given path. Optionally set an HTTP [status](#status).\n\n:::info\nIf unspecified, status defaults to **303 See Other**.\n:::\n\n```go title=\"Signature\"\nfunc (r *Redirect) To(location string) error\n```\n\n```go title=\"Example\"\napp.Get(\"/coffee\", func(c fiber.Ctx) error {\n  // => HTTP - GET 301 /teapot\n  return c.Redirect().Status(fiber.StatusMovedPermanently).To(\"/teapot\")\n})\n\napp.Get(\"/teapot\", func(c fiber.Ctx) error {\n  return c.Status(fiber.StatusTeapot).Send(\"🍵 short and stout 🍵\")\n})\n```\n\n```go title=\"More examples\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // => HTTP - GET 303 /foo/bar\n  return c.Redirect().To(\"/foo/bar\")\n  // => HTTP - GET 303 ../login\n  return c.Redirect().To(\"../login\")\n  // => HTTP - GET 303 http://example.com\n  return c.Redirect().To(\"http://example.com\")\n  // => HTTP - GET 301 https://example.com\n  return c.Redirect().Status(301).To(\"http://example.com\")\n})\n```\n\n### Route\n\nRedirects to a named route with parameters and queries.\n\n:::info\nTo send params and queries to a route, use the [`RedirectConfig`](#redirectconfig) struct.\n:::\n\n```go title=\"Signature\"\nfunc (r *Redirect) Route(name string, config ...RedirectConfig) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // /user/fiber\n  return c.Redirect().Route(\"user\", fiber.RedirectConfig{\n    Params: fiber.Map{\n      \"name\": \"fiber\",\n    },\n  })\n})\n\napp.Get(\"/with-queries\", func(c fiber.Ctx) error {\n  // /user/fiber?data[0][name]=john&data[0][age]=10&test=doe\n  return c.Redirect().Route(\"user\", fiber.RedirectConfig{\n    Params: fiber.Map{\n      \"name\": \"fiber\",\n    },\n    Queries: map[string]string{\n      \"data[0][name]\": \"john\",\n      \"data[0][age]\":  \"10\",\n      \"test\":          \"doe\",\n    },\n  })\n})\n\napp.Get(\"/user/:name\", func(c fiber.Ctx) error {\n  return c.SendString(c.Params(\"name\"))\n}).Name(\"user\")\n```\n\n### Back\n\nRedirects to the referer. If it's missing, fall back to the provided URL. You can also set the status code.\n\n:::info\nIf unspecified, status defaults to **303 See Other**.\n:::\n\n```go title=\"Signature\"\nfunc (r *Redirect) Back(fallback string) error\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  return c.SendString(\"Home page\")\n})\n\napp.Get(\"/test\", func(c fiber.Ctx) error {\n  c.Set(\"Content-Type\", \"text/html\")\n  return c.SendString(`<a href=\"/back\">Back</a>`)\n})\n\napp.Get(\"/back\", func(c fiber.Ctx) error {\n  return c.Redirect().Back(\"/\")\n})\n```\n\n## Controls\n\n:::info\nMethods are **chainable**.\n:::\n\n### Status\n\nSets the HTTP status code for the redirect.\n\n:::info\nIt is used in conjunction with [**To**](#to), [**Route**](#route), and [**Back**](#back) methods.\n:::\n\n```go title=\"Signature\"\nfunc (r *Redirect) Status(status int) *Redirect\n```\n\n```go title=\"Example\"\napp.Get(\"/coffee\", func(c fiber.Ctx) error {\n  // => HTTP - GET 301 /teapot\n  return c.Redirect().Status(fiber.StatusMovedPermanently).To(\"/teapot\")\n})\n```\n\n### RedirectConfig\n\nSets the configuration for the redirect.\n\n:::info\nIt is used in conjunction with the [**Route**](#route) method.\n:::\n\n```go title=\"Definition\"\n// RedirectConfig is a config to use with Redirect().Route()\ntype RedirectConfig struct {\n  Params  fiber.Map         // Route parameters\n  Queries map[string]string // Query map\n}\n```\n\n### Flash Message\n\nSimilar to [Laravel](https://laravel.com/docs/11.x/redirects#redirecting-with-flashed-session-data), we can flash a message and retrieve it in the next request.\n\n#### Messages\n\nRetrieve all flash messages. See [With](#with) for details.\n\n```go title=\"Signature\"\nfunc (r *Redirect) Messages() map[string]string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  messages := c.Redirect().Messages()\n  return c.JSON(messages)\n})\n```\n\n#### Message\n\nGet a flash message by key; see [With](#with).\n\n```go title=\"Signature\"\nfunc (r *Redirect) Message(key string) *Redirect\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  message := c.Redirect().Message(\"status\")\n  return c.SendString(message)\n})\n```\n\n#### OldInputs\n\nRetrieve stored input data. See [WithInput](#withinput).\n\n```go title=\"Signature\"\nfunc (r *Redirect) OldInputs() map[string]string\n```\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n  oldInputs := c.Redirect().OldInputs()\n  return c.JSON(oldInputs)\n})\n```\n\n#### OldInput\n\nGet stored input data by key; see [WithInput](#withinput).\n\n```go title=\"Signature\"\nfunc (r *Redirect) OldInput(key string) string\n```\n\n```go title=\"Example\"\napp.Get(\"/name\", func(c fiber.Ctx) error {\n  oldInput := c.Redirect().OldInput(\"name\")\n  return c.SendString(oldInput)\n})\n```\n\n#### With\n\nSend flash messages with `With`.\n\n```go title=\"Signature\"\nfunc (r *Redirect) With(key, value string) *Redirect\n```\n\n```go title=\"Example\"\napp.Get(\"/login\", func(c fiber.Ctx) error {\n  return c.Redirect().With(\"status\", \"Logged in successfully\").To(\"/\")\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n  // => Logged in successfully\n  return c.SendString(c.Redirect().Message(\"status\"))\n})\n```\n\n#### WithInput\n\nSend input data with `WithInput`, which stores them in a cookie.\n\nIt captures form, multipart, or query data depending on the request content type.\n\n```go title=\"Signature\"\nfunc (r *Redirect) WithInput() *Redirect\n```\n\n```go title=\"Example\"\n// curl -X POST http://localhost:3000/login -d \"name=John\"\napp.Post(\"/login\", func(c fiber.Ctx) error {\n  return c.Redirect().WithInput().Route(\"name\")\n})\n\napp.Get(\"/name\", func(c fiber.Ctx) error {\n  // => John\n  return c.SendString(c.Redirect().OldInput(\"name\"))\n}).Name(\"name\")\n```\n"
  },
  {
    "path": "docs/api/services.md",
    "content": "---\nid: services\ntitle: 🧩 Services\nsidebar_position: 9\n---\n\nServices wrap external dependencies. Register them in the application's state, and Fiber starts and stops them automatically—useful during development and testing.\n\nAfter adding a service to the app configuration, Fiber starts it on launch and stops it during shutdown. Retrieve a service from state with `GetService` or `MustGetService` (see [State Management](./state)).\n\n## Service Interface\n\nThe `Service` interface defines methods a service must implement.\n\n### Definition\n\n```go\ntype Service interface {\n    // Start starts the service, returning an error if it fails.\n    Start(ctx context.Context) error\n\n    // String returns a string representation of the service.\n    // It is used to print a human-readable name of the service in the startup message.\n    String() string\n\n    // State returns the current state of the service.\n    State(ctx context.Context) (string, error)\n\n    // Terminate terminates the service, returning an error if it fails.\n    Terminate(ctx context.Context) error\n}\n```\n\n## Service Methods\n\n### Start\n\nStarts the service. Fiber calls this when the application starts.\n\n```go\nfunc (s *SomeService) Start(ctx context.Context) error\n```\n\n### String\n\nReturns a string representation of the service, used to print the service in the startup message.\n\n```go\nfunc (s *SomeService) String() string\n```\n\n### State\n\nReports the current state of the service for the startup message.\n\n```go\nfunc (s *SomeService) State(ctx context.Context) (string, error)\n```\n\n### Terminate\n\nStops the service after the application shuts down using a post-shutdown hook.\n\n```go\nfunc (s *SomeService) Terminate(ctx context.Context) error\n```\n\n## Comprehensive Examples\n\n### Example: Adding a Service\n\nThis example demonstrates how to add a Redis store as a service to the application, backed by the Testcontainers Redis Go module.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/redis/go-redis/v9\"\n    tcredis \"github.com/testcontainers/testcontainers-go/modules/redis\"\n)\n\nconst redisServiceName = \"redis-store\"\n\ntype redisService struct {\n    ctr *tcredis.RedisContainer\n}\n\n// Start initializes and starts the service. It implements the [fiber.Service] interface.\nfunc (s *redisService) Start(ctx context.Context) error {\n    // start the service\n    c, err := tcredis.Run(ctx, \"redis:latest\")\n    if err != nil {\n        return err\n    }\n\n    s.ctr = c\n    return nil\n}\n\n// String returns a string representation of the service.\n// It is used to print a human-readable name of the service in the startup message.\n// It implements the [fiber.Service] interface.\nfunc (s *redisService) String() string {\n    return redisServiceName\n}\n\n// State returns the current state of the service.\n// It implements the [fiber.Service] interface.\nfunc (s *redisService) State(ctx context.Context) (string, error) {\n    state, err := s.ctr.State(ctx)\n    if err != nil {\n        return \"\", fmt.Errorf(\"container state: %w\", err)\n    }\n\n    return state.Status, nil\n}\n\n// Terminate stops and removes the service. It implements the [fiber.Service] interface.\nfunc (s *redisService) Terminate(ctx context.Context) error {\n    // stop the service\n    return s.ctr.Terminate(ctx)\n}\n\nfunc main() {\n    cfg := &fiber.Config{}\n\n    // Initialize service.\n    cfg.Services = append(cfg.Services, &redisService{})\n\n    // Define a context provider for the services startup.\n    // This is useful to cancel the startup of the services if the context is canceled.\n    // Default is context.Background().\n    startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n    cfg.ServicesStartupContextProvider = func() context.Context {\n        return startupCtx\n    }\n\n    // Define a context provider for the services shutdown.\n    // This is useful to cancel the shutdown of the services if the context is canceled.\n    // Default is context.Background().\n    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n    cfg.ServicesShutdownContextProvider = func() context.Context {\n        return shutdownCtx\n    }\n\n    app := fiber.New(*cfg)\n\n    ctx := context.Background()\n\n    // Obtain the Redis service from the application's State.\n    redisSrv, ok := fiber.GetService[*redisService](app.State(), redisServiceName)\n    if !ok || redisSrv == nil {\n        log.Printf(\"Redis service not found\")\n        return\n    }\n\n    // Obtain the connection string from the service.\n    connString, err := redisSrv.ctr.ConnectionString(ctx)\n    if err != nil {\n        log.Printf(\"Could not get connection string: %v\", err)\n        return\n    }\n\n    // Parse the connection string to create a Redis client.\n    options, err := redis.ParseURL(connString)\n    if err != nil {\n        log.Printf(\"failed to parse connection string: %s\", err)\n        return\n    }\n\n    // Initialize the Redis client.\n    rdb := redis.NewClient(options)\n\n    // Check the Redis connection.\n    if err := rdb.Ping(ctx).Err(); err != nil {\n        log.Fatalf(\"Could not connect to Redis: %v\", err)\n    }\n\n    app.Listen(\":3000\")\n}\n\n```\n\n### Example: Add a service with the Store middleware\n\nThis example shows how to use services with the Store middleware for dependency injection. It uses a Redis store backed by the Testcontainers Redis module.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n    redisStore \"github.com/gofiber/storage/redis/v3\"\n    \"github.com/redis/go-redis/v9\"\n    tcredis \"github.com/testcontainers/testcontainers-go/modules/redis\"\n)\n\nconst (\n    redisServiceName = \"redis-store\"\n)\n\ntype User struct {\n    ID    int    `json:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\ntype redisService struct {\n    ctr *tcredis.RedisContainer\n}\n\n// Start initializes and starts the service. It implements the [fiber.Service] interface.\nfunc (s *redisService) Start(ctx context.Context) error {\n    // start the service\n    c, err := tcredis.Run(ctx, \"redis:latest\")\n    if err != nil {\n        return err\n    }\n\n    s.ctr = c\n    return nil\n}\n\n// String returns a string representation of the service.\n// It is used to print a human-readable name of the service in the startup message.\n// It implements the [fiber.Service] interface.\nfunc (s *redisService) String() string {\n    return redisServiceName\n}\n\n// State returns the current state of the service.\n// It implements the [fiber.Service] interface.\nfunc (s *redisService) State(ctx context.Context) (string, error) {\n    state, err := s.ctr.State(ctx)\n    if err != nil {\n        return \"\", fmt.Errorf(\"container state: %w\", err)\n    }\n\n    return state.Status, nil\n}\n\n// Terminate stops and removes the service. It implements the [fiber.Service] interface.\nfunc (s *redisService) Terminate(ctx context.Context) error {\n    // stop the service\n    return s.ctr.Terminate(ctx)\n}\n\nfunc main() {\n    cfg := &fiber.Config{}\n\n    // Initialize service.\n    cfg.Services = append(cfg.Services, &redisService{})\n\n    // Define a context provider for the services startup.\n    // This is useful to cancel the startup of the services if the context is canceled.\n    // Default is context.Background().\n    startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n    cfg.ServicesStartupContextProvider = func() context.Context {\n        return startupCtx\n    }\n\n    // Define a context provider for the services shutdown.\n    // This is useful to cancel the shutdown of the services if the context is canceled.\n    // Default is context.Background().\n    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n    cfg.ServicesShutdownContextProvider = func() context.Context {\n        return shutdownCtx\n    }\n\n    app := fiber.New(*cfg)\n\n    // Initialize default config\n    app.Use(logger.New())\n\n    ctx := context.Background()\n\n    // Obtain the Redis service from the application's State.\n    redisSrv, ok := fiber.GetService[*redisService](app.State(), redisServiceName)\n    if !ok || redisSrv == nil {\n        log.Printf(\"Redis service not found\")\n        return\n    }\n\n    // Obtain the connection string from the service.\n    connString, err := redisSrv.ctr.ConnectionString(ctx)\n    if err != nil {\n        log.Printf(\"Could not get connection string: %v\", err)\n        return\n    }\n\n    // define a GoFiber session store, backed by the Redis service\n    store := redisStore.New(redisStore.Config{\n        URL: connString,\n    })\n\n    app.Post(\"/user/create\", func(c fiber.Ctx) error {\n        var user User\n        if err := c.Bind().JSON(&user); err != nil {\n            return c.Status(fiber.StatusBadRequest).SendString(err.Error())\n        }\n\n        json, err := json.Marshal(user)\n        if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n\n        // Save the user to the database.\n        err = store.Set(user.Email, json, time.Hour*24)\n        if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n\n        return c.JSON(user)\n    })\n\n    app.Get(\"/user/:id\", func(c fiber.Ctx) error {\n        id := c.Params(\"id\")\n\n        user, err := store.Get(id)\n        if err == redis.Nil {\n            return c.Status(fiber.StatusNotFound).SendString(\"User not found\")\n        } else if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n\n        return c.JSON(string(user))\n    })\n\n    app.Listen(\":3000\")\n}\n\n```\n"
  },
  {
    "path": "docs/api/state.md",
    "content": "---\nid: state\ntitle: 🗂️ State Management\nsidebar_position: 8\n---\n\nState management provides a global key–value store for application dependencies and runtime data. The store is shared across the entire application and persists between requests. It's commonly used to store [Services](../api/services), which you can retrieve with the `GetService` or `MustGetService` functions.\n\n:::warning\nWhen prefork is enabled, each worker process has an independent state store, meaning state is not shared between them.\n:::\n\n## State Type\n\n`State` is a key–value store built on top of `sync.Map` to ensure safe concurrent access. It allows storage and retrieval of dependencies and configurations in a Fiber application as well as thread–safe access to runtime data.\n\n### Definition\n\n```go\n// State is a key–value store for Fiber's app, used as a global storage for the app's dependencies.\n// It is a thread–safe implementation of a map[string]any, using sync.Map.\ntype State struct {\n    dependencies sync.Map\n}\n```\n\n## Methods on State\n\n### Set\n\nSet adds or updates a key–value pair in the State.\n\n```go\n// Set adds or updates a key–value pair in the State.\nfunc (s *State) Set(key string, value any)\n```\n\n**Usage Example:**\n\n```go\napp.State().Set(\"appName\", \"My Fiber App\")\n```\n\n### Get\n\nGet retrieves a value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) Get(key string) (any, bool)\n```\n\n**Usage Example:**\n\n```go\nvalue, ok := app.State().Get(\"appName\")\nif ok {\n    fmt.Println(\"App Name:\", value)\n}\n```\n\n### MustGet\n\nMustGet retrieves a value from the State and panics if the key is not found.\n\n```go title=\"Signature\"\nfunc (s *State) MustGet(key string) any\n```\n\n**Usage Example:**\n\n```go\nappName := app.State().MustGet(\"appName\")\nfmt.Println(\"App Name:\", appName)\n```\n\n### Has\n\nHas checks if a key exists in the State.\n\n```go title=\"Signature\"\nfunc (s *State) Has(key string) bool\n```\n\n**Usage Example:**\n\n```go\nif app.State().Has(\"appName\") {\n    fmt.Println(\"App Name is set.\")\n}\n```\n\n### Delete\n\nDelete removes a key–value pair from the State.\n\n```go title=\"Signature\"\nfunc (s *State) Delete(key string)\n```\n\n**Usage Example:**\n\n```go\napp.State().Delete(\"obsoleteKey\")\n```\n\n### Reset\n\nReset removes all keys from the State, including those related to Services.\n\n```go title=\"Signature\"\nfunc (s *State) Reset()\n```\n\n**Usage Example:**\n\n```go\napp.State().Reset()\n```\n\n### Keys\n\nKeys returns a slice containing all keys present in the State.\n\n```go title=\"Signature\"\nfunc (s *State) Keys() []string\n```\n\n**Usage Example:**\n\n```go\nkeys := app.State().Keys()\nfmt.Println(\"State Keys:\", keys)\n```\n\n### Len\n\nLen returns the number of keys in the State.\n\n```go\n// Len returns the number of keys in the State.\nfunc (s *State) Len() int\n```\n\n**Usage Example:**\n\n```go\nfmt.Printf(\"Total State Entries: %d\\n\", app.State().Len())\n```\n\n### GetString\n\nGetString retrieves a string value from the State. It returns the string and a boolean indicating a successful type assertion.\n\n```go title=\"Signature\"\nfunc (s *State) GetString(key string) (string, bool)\n```\n\n**Usage Example:**\n\n```go\nif appName, ok := app.State().GetString(\"appName\"); ok {\n    fmt.Println(\"App Name:\", appName)\n}\n```\n\n### GetInt\n\nGetInt retrieves an integer value from the State. It returns the int and a boolean indicating a successful type assertion.\n\n```go title=\"Signature\"\nfunc (s *State) GetInt(key string) (int, bool)\n```\n\n**Usage Example:**\n\n```go\nif count, ok := app.State().GetInt(\"userCount\"); ok {\n    fmt.Printf(\"User Count: %d\\n\", count)\n}\n```\n\n### GetBool\n\nGetBool retrieves a boolean value from the State. It returns the bool and a boolean indicating a successful type assertion.\n\n```go title=\"Signature\"\nfunc (s *State) GetBool(key string) (value, bool)\n```\n\n**Usage Example:**\n\n```go\nif debug, ok := app.State().GetBool(\"debugMode\"); ok {\n    fmt.Printf(\"Debug Mode: %v\\n\", debug)\n}\n```\n\n### GetFloat64\n\nGetFloat64 retrieves a float64 value from the State. It returns the float64 and a boolean indicating a successful type assertion.\n\n```go title=\"Signature\"\nfunc (s *State) GetFloat64(key string) (float64, bool)\n```\n\n**Usage Example:**\n\n```go title=\"Signature\"\nif ratio, ok := app.State().GetFloat64(\"scalingFactor\"); ok {\n    fmt.Printf(\"Scaling Factor: %f\\n\", ratio)\n}\n```\n\n### GetUint\n\nGetUint retrieves a `uint` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUint(key string) (uint, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUint(\"maxConnections\"); ok {\n    fmt.Printf(\"Max Connections: %d\\n\", val)\n}\n```\n\n### GetInt8\n\nGetInt8 retrieves an `int8` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetInt8(key string) (int8, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetInt8(\"threshold\"); ok {\n    fmt.Printf(\"Threshold: %d\\n\", val)\n}\n```\n\n### GetInt16\n\nGetInt16 retrieves an `int16` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetInt16(key string) (int16, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetInt16(\"minValue\"); ok {\n    fmt.Printf(\"Minimum Value: %d\\n\", val)\n}\n```\n\n### GetInt32\n\nGetInt32 retrieves an `int32` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetInt32(key string) (int32, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetInt32(\"portNumber\"); ok {\n    fmt.Printf(\"Port Number: %d\\n\", val)\n}\n```\n\n### GetInt64\n\nGetInt64 retrieves an `int64` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetInt64(key string) (int64, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetInt64(\"fileSize\"); ok {\n    fmt.Printf(\"File Size: %d\\n\", val)\n}\n```\n\n### GetUint8\n\nGetUint8 retrieves a `uint8` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUint8(key string) (uint8, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUint8(\"byteValue\"); ok {\n    fmt.Printf(\"Byte Value: %d\\n\", val)\n}\n```\n\n### GetUint16\n\nGetUint16 retrieves a `uint16` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUint16(key string) (uint16, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUint16(\"limit\"); ok {\n    fmt.Printf(\"Limit: %d\\n\", val)\n}\n```\n\n### GetUint32\n\nGetUint32 retrieves a `uint32` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUint32(key string) (uint32, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUint32(\"timeout\"); ok {\n    fmt.Printf(\"Timeout: %d\\n\", val)\n}\n```\n\n### GetUint64\n\nGetUint64 retrieves a `uint64` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUint64(key string) (uint64, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUint64(\"maxSize\"); ok {\n    fmt.Printf(\"Max Size: %d\\n\", val)\n}\n```\n\n### GetUintptr\n\nGetUintptr retrieves a `uintptr` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetUintptr(key string) (uintptr, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetUintptr(\"pointerValue\"); ok {\n    fmt.Printf(\"Pointer Value: %d\\n\", val)\n}\n```\n\n### GetFloat32\n\nGetFloat32 retrieves a `float32` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetFloat32(key string) (float32, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetFloat32(\"scalingFactor32\"); ok {\n    fmt.Printf(\"Scaling Factor (float32): %f\\n\", val)\n}\n```\n\n### GetComplex64\n\nGetComplex64 retrieves a `complex64` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetComplex64(key string) (complex64, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetComplex64(\"complexVal\"); ok {\n    fmt.Printf(\"Complex Value (complex64): %v\\n\", val)\n}\n```\n\n### GetComplex128\n\nGetComplex128 retrieves a `complex128` value from the State.\n\n```go title=\"Signature\"\nfunc (s *State) GetComplex128(key string) (complex128, bool)\n```\n\n**Usage Example:**\n\n```go\nif val, ok := app.State().GetComplex128(\"complexVal128\"); ok {\n    fmt.Printf(\"Complex Value (complex128): %v\\n\", val)\n}\n```\n\n## Generic Functions\n\nFiber provides generic functions to retrieve state values with type safety and fallback options.\n\n### GetState\n\nGetState retrieves a value from the State and casts it to the desired type. It returns the cast value and a boolean indicating if the cast was successful.\n\n```go title=\"Signature\"\nfunc GetState[T any](s *State, key string) (T, bool)\n```\n\n**Usage Example:**\n\n```go\n// Retrieve an integer value safely.\nuserCount, ok := GetState[int](app.State(), \"userCount\")\nif ok {\n    fmt.Printf(\"User Count: %d\\n\", userCount)\n}\n```\n\n### MustGetState\n\nMustGetState retrieves a value from the State and casts it to the desired type. It panics if the key is not found or if the type assertion fails.\n\n```go title=\"Signature\"\nfunc MustGetState[T any](s *State, key string) T\n```\n\n**Usage Example:**\n\n```go\n// Retrieve the value or panic if it is not present.\nconfig := MustGetState[string](app.State(), \"configFile\")\nfmt.Println(\"Config File:\", config)\n```\n\n### GetStateWithDefault\n\nGetStateWithDefault retrieves a value from the State, casting it to the desired type. If the key is not present, it returns the provided default value.\n\n```go title=\"Signature\"\nfunc GetStateWithDefault[T any](s *State, key string, defaultVal T) T\n```\n\n**Usage Example:**\n\n```go\n// Retrieve a value with a fallback.\nrequestCount := GetStateWithDefault[int](app.State(), \"requestCount\", 0)\nfmt.Printf(\"Request Count: %d\\n\", requestCount)\n```\n\n### GetService\n\nGetService retrieves a Service from the State and casts it to the desired type. It returns the cast value and a boolean indicating if the cast was successful.\n\n```go title=\"Signature\"\nfunc GetService[T Service](s *State, key string) (T, bool) {\n```\n\n**Usage Example:**\n\n```go\nif srv, ok := fiber.GetService[*redisService](app.State(), \"someService\")\n    fmt.Printf(\"Some Service: %s\\n\", srv.String())\n}\n```\n\n### MustGetService\n\nMustGetService retrieves a Service from the State and casts it to the desired type. It panics if the key is not found or if the type assertion fails.\n\n```go title=\"Signature\"\nfunc MustGetService[T Service](s *State, key string) T\n```\n\n**Usage Example:**\n\n```go\nsrv := fiber.MustGetService[*SomeService](app.State(), \"someService\")\n```\n\n## Comprehensive Examples\n\n### Example: Request Counter\n\nThis example demonstrates how to track the number of requests using the State.\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Initialize state with a counter.\n    app.State().Set(\"requestCount\", 0)\n\n    // Middleware: Increase counter for every request.\n    app.Use(func(c fiber.Ctx) error {\n        count, _ := c.App().State().GetInt(\"requestCount\")\n        app.State().Set(\"requestCount\", count+1)\n        return c.Next()\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello World!\")\n    })\n\n    app.Get(\"/stats\", func(c fiber.Ctx) error {\n        count, _ := c.App().State().Get(\"requestCount\")\n        return c.SendString(fmt.Sprintf(\"Total requests: %d\", count))\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n### Example: Environment–Specific Configuration\n\nThis example shows how to configure different settings based on the environment.\n\n```go\npackage main\n\nimport (\n    \"os\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Determine environment.\n    environment := os.Getenv(\"ENV\")\n    if environment == \"\" {\n        environment = \"development\"\n    }\n    app.State().Set(\"environment\", environment)\n\n    // Set environment-specific configurations.\n    if environment == \"development\" {\n        app.State().Set(\"apiUrl\", \"http://localhost:8080/api\")\n        app.State().Set(\"debug\", true)\n    } else {\n        app.State().Set(\"apiUrl\", \"https://api.production.com\")\n        app.State().Set(\"debug\", false)\n    }\n\n    app.Get(\"/config\", func(c fiber.Ctx) error {\n        config := map[string]any{\n            \"environment\": environment,\n            \"apiUrl\":      fiber.GetStateWithDefault(c.App().State(), \"apiUrl\", \"\"),\n            \"debug\":       fiber.GetStateWithDefault(c.App().State(), \"debug\", false),\n        }\n        return c.JSON(config)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n### Example: Dependency Injection with State Management\n\nThis example demonstrates how to use the State for dependency injection in a Fiber application.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/redis/go-redis/v9\"\n)\n\ntype User struct {\n    ID    int    `query:\"id\"`\n    Name  string `query:\"name\"`\n    Email string `query:\"email\"`\n}\n\nfunc main() {\n    app := fiber.New()\n    ctx := context.Background()\n\n    // Initialize Redis client.\n    rdb := redis.NewClient(&redis.Options{\n        Addr:     \"localhost:6379\",\n        Password: \"\",\n        DB:       0,\n    })\n\n    // Check the Redis connection.\n    if err := rdb.Ping(ctx).Err(); err != nil {\n        log.Fatalf(\"Could not connect to Redis: %v\", err)\n    }\n\n    // Inject the Redis client into Fiber's State for dependency injection.\n    app.State().Set(\"redis\", rdb)\n\n    app.Get(\"/user/create\", func(c fiber.Ctx) error {\n        var user User\n        if err := c.Bind().Query(&user); err != nil {\n            return c.Status(fiber.StatusBadRequest).SendString(err.Error())\n        }\n\n        // Retrieve the Redis client from the global state.\n        rdb, ok := fiber.GetState[*redis.Client](c.App().State(), \"redis\")\n        if !ok {\n            return c.Status(fiber.StatusInternalServerError).SendString(\"Redis client not found\")\n        }\n\n        // Save the user to the database.\n        key := fmt.Sprintf(\"user:%d\", user.ID)\n        err := rdb.HSet(ctx, key, \"name\", user.Name, \"email\", user.Email).Err()\n        if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n\n        return c.JSON(user)\n    })\n\n    app.Get(\"/user/:id\", func(c fiber.Ctx) error {\n        id := c.Params(\"id\")\n\n        rdb, ok := fiber.GetState[*redis.Client](c.App().State(), \"redis\")\n        if !ok {\n            return c.Status(fiber.StatusInternalServerError).SendString(\"Redis client not found\")\n        }\n\n        key := fmt.Sprintf(\"user:%s\", id)\n        user, err := rdb.HGetAll(ctx, key).Result()\n        if err == redis.Nil {\n            return c.Status(fiber.StatusNotFound).SendString(\"User not found\")\n        } else if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n\n        return c.JSON(user)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n"
  },
  {
    "path": "docs/client/_category_.json",
    "content": "{\n  \"label\": \"\\uD83C\\uDF0E Client\",\n  \"position\": 6,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"HTTP client for Fiber.\"\n  }\n}\n"
  },
  {
    "path": "docs/client/examples.md",
    "content": "---\nid: examples\ntitle: 🍳 Examples\ndescription: >-\n  Client usage examples.\nsidebar_position: 5\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Basic Auth\n\nClients send credentials via the `Authorization` header, while the server\nstores hashed passwords as shown in the middleware example.\n\n<Tabs>\n<TabItem value=\"client\" label=\"Client\">\n\n```go\npackage main\n\nimport (\n    \"encoding/base64\"\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    cc := client.New()\n\n    out := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n    resp, err := cc.Get(\"http://localhost:3000\", client.Config{\n        Header: map[string]string{\n            \"Authorization\": \"Basic \" + out,\n        },\n    })\n    if err != nil {\n        panic(err)\n    }\n\n    fmt.Print(string(resp.Body()))\n}\n```\n\n</TabItem>\n<TabItem value=\"server\" label=\"Server\">\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/basicauth\"\n)\n\nfunc main() {\n    app := fiber.New()\n    app.Use(\n        basicauth.New(basicauth.Config{\n            Users: map[string]string{\n                // \"doe\" hashed using SHA-256\n                \"john\": \"{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=\",\n            },\n        }),\n    )\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n</TabItem>\n</Tabs>\n\n## TLS\n\n<Tabs>\n<TabItem value=\"client\" label=\"Client\">\n\n```go\npackage main\n\nimport (\n    \"crypto/tls\"\n    \"crypto/x509\"\n    \"fmt\"\n    \"os\"\n\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    cc := client.New()\n\n    certPool, err := x509.SystemCertPool()\n    if err != nil {\n        panic(err)\n    }\n\n    cert, err := os.ReadFile(\"ssl.cert\")\n    if err != nil {\n        panic(err)\n    }\n\n    certPool.AppendCertsFromPEM(cert)\n    cc.SetTLSConfig(&tls.Config{\n        RootCAs: certPool,\n    })\n\n    resp, err := cc.Get(\"https://localhost:3000\")\n    if err != nil {\n        panic(err)\n    }\n\n    fmt.Print(string(resp.Body()))\n}\n```\n\n</TabItem>\n<TabItem value=\"server\" label=\"Server\">\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    err := app.Listen(\":3000\", fiber.ListenConfig{\n        CertFile:    \"ssl.cert\",\n        CertKeyFile: \"ssl.key\",\n    })\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n</TabItem>\n</Tabs>\n\n## Reusing fasthttp transports\n\nThe Fiber client can wrap existing `fasthttp` clients so that you can reuse\nconnection pools, custom dialers, or load-balancing logic that is already tuned\nfor your infrastructure.\n\n### HostClient\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3/client\"\n    \"github.com/valyala/fasthttp\"\n)\n\nfunc main() {\n    hc := &fasthttp.HostClient{\n        Addr:              \"api.internal:443\",\n        IsTLS:             true,\n        MaxConnDuration:   30 * time.Second,\n        MaxIdleConnDuration: 10 * time.Second,\n    }\n\n    cc := client.NewWithHostClient(hc)\n\n    resp, err := cc.Get(\"https://api.internal:443/status\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    log.Printf(\"status=%d body=%s\", resp.StatusCode(), resp.Body())\n}\n```\n\n### LBClient\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3/client\"\n    \"github.com/valyala/fasthttp\"\n)\n\nfunc main() {\n    lb := &fasthttp.LBClient{\n        Timeout: 2 * time.Second,\n        Clients: []fasthttp.BalancingClient{\n            &fasthttp.HostClient{Addr: \"edge-1.internal:8080\"},\n            &fasthttp.HostClient{Addr: \"edge-2.internal:8080\"},\n        },\n    }\n\n    cc := client.NewWithLBClient(lb)\n\n    // Per-request overrides such as redirects, retries, TLS, and proxy dialers\n    // are shared across every host client managed by the load balancer.\n    resp, err := cc.Get(\"http://service.internal/api\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    log.Printf(\"status=%d body=%s\", resp.StatusCode(), resp.Body())\n}\n```\n\n## Cookie jar\n\nThe client can store and reuse cookies between requests by attaching a cookie jar.\n\n### Request\n\n```go\nfunc main() {\n    jar := client.AcquireCookieJar()\n    defer client.ReleaseCookieJar(jar)\n\n    cc := client.New()\n    cc.SetCookieJar(jar)\n\n    jar.SetKeyValueBytes(\"httpbin.org\", []byte(\"john\"), []byte(\"doe\"))\n\n    resp, err := cc.Get(\"https://httpbin.org/cookies\")\n    if err != nil {\n        panic(err)\n    }\n\n    fmt.Println(string(resp.Body()))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n{\n  \"cookies\": {\n    \"john\": \"doe\"\n  }\n}\n```\n\n</details>\n\n### Response\n\nRead cookies set by the server directly from the jar.\n\n```go\nfunc main() {\n    jar := client.AcquireCookieJar()\n    defer client.ReleaseCookieJar(jar)\n\n    cc := client.New()\n    cc.SetCookieJar(jar)\n\n    _, err := cc.Get(\"https://httpbin.org/cookies/set/john/doe\")\n    if err != nil {\n        panic(err)\n    }\n\n    uri := fasthttp.AcquireURI()\n    defer fasthttp.ReleaseURI(uri)\n\n    uri.SetHost(\"httpbin.org\")\n    uri.SetPath(\"/cookies\")\n    fmt.Println(jar.Get(uri))\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```plaintext\n[john=doe; path=/]\n```\n\n</details>\n\n### Response (follow-up request)\n\n```go\nfunc main() {\n    jar := client.AcquireCookieJar()\n    defer client.ReleaseCookieJar(jar)\n\n    cc := client.New()\n    cc.SetCookieJar(jar)\n\n    _, err := cc.Get(\"https://httpbin.org/cookies/set/john/doe\")\n    if err != nil {\n        panic(err)\n    }\n\n    resp, err := cc.Get(\"https://httpbin.org/cookies\")\n    if err != nil {\n        panic(err)\n    }\n\n    fmt.Println(resp.String())\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n{\n  \"cookies\": {\n    \"john\": \"doe\"\n  }\n}\n```\n\n</details>\n"
  },
  {
    "path": "docs/client/hooks.md",
    "content": "---\nid: hooks\ntitle: 🎣 Hooks\ndescription: >-\n  Hooks are used to manipulate the request/response process of the Fiber client.\nsidebar_position: 4\n---\n\nHooks let you intercept and modify the request or response flow of the Fiber client. They are useful for:\n\n- Changing request parameters (e.g., URL, headers) before sending the request.\n- Logging request and response details.\n- Integrating complex tracing or monitoring tools.\n- Handling authentication, retries, or other custom logic.\n\nThere are two kinds of hooks:\n\n## Request Hooks\n\n**Request hooks** are functions executed before the HTTP request is sent. They follow the signature:\n\n```go\ntype RequestHook func(*Client, *Request) error\n```\n\nA request hook receives both the `Client` and the `Request` objects, allowing you to modify the request before it leaves your application. For example, you could:\n\n- Change the host URL.\n- Log request details (method, URL, headers).\n- Add or modify headers or query parameters.\n- Intercept and apply custom authentication logic.\n\n**Example:**\n\n```go\ntype Repository struct {\n    Name        string `json:\"name\"`\n    FullName    string `json:\"full_name\"`\n    Description string `json:\"description\"`\n    Homepage    string `json:\"homepage\"`\n\n    Owner struct {\n        Login string `json:\"login\"`\n    } `json:\"owner\"`\n}\n\nfunc main() {\n    cc := client.New()\n\n    // Add a request hook that modifies the request URL before sending.\n    cc.AddRequestHook(func(c *client.Client, r *client.Request) error {\n        r.SetURL(\"https://api.github.com/\" + r.URL())\n        return nil\n    })\n\n    resp, err := cc.Get(\"repos/gofiber/fiber\")\n    if err != nil {\n        panic(err)\n    }\n\n    var repo Repository\n    if err := resp.JSON(&repo); err != nil {\n        panic(err)\n    }\n\n    fmt.Printf(\"Status code: %d\\n\", resp.StatusCode())\n    fmt.Printf(\"Repository: %s\\n\", repo.FullName)\n    fmt.Printf(\"Description: %s\\n\", repo.Description)\n    fmt.Printf(\"Homepage: %s\\n\", repo.Homepage)\n    fmt.Printf(\"Owner: %s\\n\", repo.Owner.Login)\n    fmt.Printf(\"Name: %s\\n\", repo.Name)\n    fmt.Printf(\"Full Name: %s\\n\", repo.FullName)\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```plaintext\nStatus code: 200\nRepository: gofiber/fiber\nDescription: ⚡️ Express inspired web framework written in Go\nHomepage: https://gofiber.io\nOwner: gofiber\nName: fiber\nFull Name: gofiber/fiber\n```\n\n</details>\n\n### Built-in Request Hooks\n\nFiber includes built-in request hooks:\n\n- **parserRequestURL**: Normalizes and customizes the URL based on path and query parameters. Required for `PathParam` and `QueryParam` methods.\n- **parserRequestHeader**: Sets request headers, cookies, content type, referer, and user agent based on client and request properties.\n- **parserRequestBody**: Automatically serializes the request body (JSON, XML, form, file uploads, etc.).\n\n:::info\nIf a request hook returns an error, Fiber stops the request and returns the error immediately.\n:::\n\n**Example with Multiple Hooks:**\n\n```go\nfunc main() {\n    cc := client.New()\n\n    cc.AddRequestHook(func(c *client.Client, r *client.Request) error {\n        fmt.Println(\"Hook 1\")\n        return errors.New(\"error\")\n    })\n\n    cc.AddRequestHook(func(c *client.Client, r *client.Request) error {\n        fmt.Println(\"Hook 2\")\n        return nil\n    })\n\n    _, err := cc.Get(\"https://example.com/\")\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```shell\nHook 1.\npanic: error\n\ngoroutine 1 [running]:\nmain.main()\n        main.go:25 +0xaa\nexit status 2\n```\n\n</details>\n\n## Response Hooks\n\n**Response hooks** are functions executed after the HTTP response is received. They follow the signature:\n\n```go\ntype ResponseHook func(*Client, *Response, *Request) error\n```\n\nA response hook receives the `Client`, `Response`, and `Request` objects, allowing you to inspect and modify the response or perform additional actions such as logging, tracing, or processing response data.\n\n**Example:**\n\n```go\nfunc main() {\n    cc := client.New()\n\n    cc.AddResponseHook(func(c *client.Client, resp *client.Response, req *client.Request) error {\n        fmt.Printf(\"Response Status Code: %d\\n\", resp.StatusCode())\n        fmt.Printf(\"HTTP protocol: %s\\n\\n\", resp.Protocol())\n\n        fmt.Println(\"Response Headers:\")\n       for key, value := range resp.RawResponse.Header.All() {\n           fmt.Printf(\"%s: %s\\n\", key, value)\n       }\n\n        return nil\n    })\n\n    _, err := cc.Get(\"https://example.com/\")\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```plaintext\nResponse Status Code: 200\nHTTP protocol: HTTP/1.1\n\nResponse Headers:\nContent-Length: 1256\nContent-Type: text/html; charset=UTF-8\nServer: ECAcc (dcd/7D5A)\nAge: 216114\nCache-Control: max-age=604800\nDate: Fri, 10 May 2024 10:49:10 GMT\nEtag: \"3147526947+gzip+ident\"\nExpires: Fri, 17 May 2024 10:49:10 GMT\nLast-Modified: Thu, 17 Oct 2019 07:18:26 GMT\nVary: Accept-Encoding\nX-Cache: HIT\n```\n\n</details>\n\n### Built-in Response Hooks\n\nFiber includes built-in response hooks:\n\n- **parserResponseCookie**: Parses cookies from the response and stores them in the response object and cookie jar if available.\n- **logger**: Logs information about the raw request and response. It uses the `log.CommonLogger` interface.\n\n:::info\nIf a response hook returns an error, Fiber skips the remaining hooks and returns that error.\n:::\n\n**Example with Multiple Response Hooks:**\n\n```go\nfunc main() {\n    cc := client.New()\n\n    cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error {\n        fmt.Println(\"Hook 1\")\n        return nil\n    })\n\n    cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error {\n        fmt.Println(\"Hook 2\")\n        return errors.New(\"error\")\n    })\n\n    cc.AddResponseHook(func(c *client.Client, r1 *client.Response, r2 *client.Request) error {\n        fmt.Println(\"Hook 3\")\n        return nil\n    })\n\n    _, err := cc.Get(\"https://example.com/\")\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```shell\nHook 1\nHook 2\npanic: error\n\ngoroutine 1 [running]:\nmain.main()\n        main.go:30 +0xd6\nexit status 2\n```\n\n</details>\n\n## Hook Execution Order\n\nHooks run in FIFO order (first in, first out), so they're executed in the order you add them. Keep this in mind when adding multiple hooks, as the order can affect the outcome.\n\n**Example:**\n\n```go\nfunc main() {\n    cc := client.New()\n\n    cc.AddRequestHook(func(c *client.Client, r *client.Request) error {\n        fmt.Println(\"Hook 1\")\n        return nil\n    })\n\n    cc.AddRequestHook(func(c *client.Client, r *client.Request) error {\n        fmt.Println(\"Hook 2\")\n        return nil\n    })\n\n    _, err := cc.Get(\"https://example.com/\")\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```plaintext\nHook 1\nHook 2\n```\n\n</details>\n"
  },
  {
    "path": "docs/client/request.md",
    "content": "---\nid: request\ntitle: 📤 Request\ndescription: >-\n  Request methods of Gofiber HTTP client.\nsidebar_position: 2\n---\n\nThe `Request` struct in Fiber's HTTP client represents an HTTP request. It encapsulates the data required to send a request, including:\n\n- **URL**: The endpoint to which the request is sent.\n- **Method**: The HTTP method (GET, POST, PUT, DELETE, etc.).\n- **Headers**: Key-value pairs that provide additional information about the request or guide how the response should be processed.\n- **Body**: The data sent with the request, commonly used with methods like POST and PUT.\n- **Query Parameters**: Parameters appended to the URL to pass additional data or modify the request's behavior.\n\nThis structure is designed to be both flexible and efficient, allowing you to easily build and modify HTTP requests as needed.\n\n```go\ntype Request struct {\n    url       string\n    method    string\n    userAgent string\n    boundary  string\n    referer   string\n    ctx       context.Context\n    header    *Header\n    params    *QueryParam\n    cookies   *Cookie\n    path      *PathParam\n\n    timeout      time.Duration\n    maxRedirects int\n\n    client *Client\n\n    body     any\n    formData *FormData\n    files    []*File\n    bodyType bodyType\n\n    RawRequest *fasthttp.Request\n}\n```\n\n## REST Methods\n\n### Get\n\n**Get** sends a GET request to the specified URL. It sets the URL and HTTP method, then dispatches the request to the server.\n\n```go title=\"Signature\"\nfunc (r *Request) Get(url string) (*Response, error)\n```\n\n### Post\n\n**Post** sends a POST request. It sets the URL and method to POST, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Post(url string) (*Response, error)\n```\n\n### Put\n\n**Put** sends a PUT request. It sets the URL and method to PUT, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Put(url string) (*Response, error)\n```\n\n### Patch\n\n**Patch** sends a PATCH request. It sets the URL and method to PATCH, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Patch(url string) (*Response, error)\n```\n\n### Delete\n\n**Delete** sends a DELETE request. It sets the URL and method to DELETE, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Delete(url string) (*Response, error)\n```\n\n### Head\n\n**Head** sends a HEAD request. It sets the URL and method to HEAD, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Head(url string) (*Response, error)\n```\n\n### Options\n\n**Options** sends an OPTIONS request. It sets the URL and method to OPTIONS, then sends the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Options(url string) (*Response, error)\n```\n\n### Custom\n\n**Custom** sends a request using a custom HTTP method. For example, you can use this to send a TRACE or CONNECT request.\n\n```go title=\"Signature\"\nfunc (r *Request) Custom(url, method string) (*Response, error)\n```\n\n## AcquireRequest\n\n**AcquireRequest** returns a new pooled `Request`. Call `ReleaseRequest` when you're finished to return it to the pool and limit allocations.\n\n```go title=\"Signature\"\nfunc AcquireRequest() *Request\n```\n\n## ReleaseRequest\n\n**ReleaseRequest** returns the `Request` to the pool. Do not use it after releasing; doing so may cause data races.\n\n```go title=\"Signature\"\nfunc ReleaseRequest(req *Request)\n```\n\n## Method\n\n**Method** returns the current HTTP method set for the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Method() string\n```\n\n## SetMethod\n\n**SetMethod** sets the HTTP method for the `Request` object. Typically, you should use the specialized request methods (`Get`, `Post`, etc.) instead of calling `SetMethod` directly.\n\n```go title=\"Signature\"\nfunc (r *Request) SetMethod(method string) *Request\n```\n\n## URL\n\n**URL** returns the current URL set in the `Request`.\n\n```go title=\"Signature\"\nfunc (r *Request) URL() string\n```\n\n## SetURL\n\n**SetURL** sets the URL for the `Request` object.\n\n```go title=\"Signature\"\nfunc (r *Request) SetURL(url string) *Request\n```\n\n## Client\n\n**Client** retrieves the `Client` instance associated with the `Request`.\n\n```go title=\"Signature\"\nfunc (r *Request) Client() *Client\n```\n\n## SetClient\n\n**SetClient** assigns a `Client` to the `Request`. If the provided client is `nil`, it will panic.\n\n```go title=\"Signature\"\nfunc (r *Request) SetClient(c *Client) *Request\n```\n\n## Context\n\n**Context** returns the `context.Context` of the request, or `context.Background()` if none is set.\n\n```go title=\"Signature\"\nfunc (r *Request) Context() context.Context\n```\n\n## SetContext\n\n**SetContext** sets the `context.Context` for the request, allowing you to cancel or time out the request. See the [Go blog](https://blog.golang.org/context) and [context](https://pkg.go.dev/context) docs for more details.\n\n```go title=\"Signature\"\nfunc (r *Request) SetContext(ctx context.Context) *Request\n```\n\n## Header\n\n**Header** returns all values for the specified header key. It searches all header fields stored in the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Header(key string) []string\n```\n\n### Headers\n\n**Headers** returns an iterator over all headers in the request. Use `maps.Collect()` to transform them into a map if needed. The returned values are valid only until the request is released. Make copies as required.\n\n```go title=\"Signature\"\nfunc (r *Request) Headers() iter.Seq2[string, []string]\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\n\nreq.AddHeader(\"Golang\", \"Fiber\")\nreq.AddHeader(\"Test\", \"123456\")\nreq.AddHeader(\"Test\", \"654321\")\n\nfor k, v := range req.Headers() {\n  fmt.Printf(\"Header Key: %s, Header Value: %v\\n\", k, v)\n}\n```\n\n```sh\nHeader Key: Golang, Header Value: [Fiber]\nHeader Key: Test, Header Value: [123456 654321]\n```\n\n</details>\n\n<details>\n<summary>Example with maps.Collect()</summary>\n\n```go title=\"Example with maps.Collect()\"\nreq := client.AcquireRequest()\n\nreq.AddHeader(\"Golang\", \"Fiber\")\nreq.AddHeader(\"Test\", \"123456\")\nreq.AddHeader(\"Test\", \"654321\")\n\nheaders := maps.Collect(req.Headers()) // Collect all headers into a map\nfor k, v := range headers {\n  fmt.Printf(\"Header Key: %s, Header Value: %v\\n\", k, v)\n}\n```\n\n```sh\nHeader Key: Golang, Header Value: [Fiber]\nHeader Key: Test, Header Value: [123456 654321]\n```\n\n</details>\n\n### AddHeader\n\n**AddHeader** adds a single header field and its value to the request.\n\n```go title=\"Signature\"\nfunc (r *Request) AddHeader(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.AddHeader(\"Golang\", \"Fiber\")\nreq.AddHeader(\"Test\", \"123456\")\nreq.AddHeader(\"Test\", \"654321\")\n\nresp, err := req.Get(\"https://httpbin.org/headers\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(resp.String())\n```\n\n```json\n{\n  \"headers\": {\n    \"Golang\": \"Fiber\", \n    \"Host\": \"httpbin.org\", \n    \"Referer\": \"\", \n    \"Test\": \"123456,654321\", \n    \"User-Agent\": \"fiber\", \n    \"X-Amzn-Trace-Id\": \"Root=1-664105d2-033cf7173457adb56d9e7193\"\n  }\n}\n```\n\n</details>\n\n### SetHeader\n\n**SetHeader** sets a single header field and its value, overriding any previously set header with the same key.\n\n```go title=\"Signature\"\nfunc (r *Request) SetHeader(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetHeader(\"Test\", \"123456\")\nreq.SetHeader(\"Test\", \"654321\")\n\nresp, err := req.Get(\"https://httpbin.org/headers\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(resp.String())\n```\n\n```json\n{\n  \"headers\": {\n    \"Golang\": \"Fiber\", \n    \"Host\": \"httpbin.org\", \n    \"Referer\": \"\", \n    \"Test\": \"654321\", \n    \"User-Agent\": \"fiber\", \n    \"X-Amzn-Trace-Id\": \"Root=1-664105e5-5d676ba348450cdb62847f04\"\n  }\n}\n```\n\n</details>\n\n### AddHeaders\n\n**AddHeaders** adds multiple headers at once from a map of string slices.\n\n```go title=\"Signature\"\nfunc (r *Request) AddHeaders(h map[string][]string) *Request\n```\n\n### SetHeaders\n\n**SetHeaders** sets multiple headers at once from a map of strings, overriding any previously set headers.\n\n```go title=\"Signature\"\nfunc (r *Request) SetHeaders(h map[string]string) *Request\n```\n\n## Param\n\n**Param** returns all values associated with a given query parameter key.\n\n```go title=\"Signature\"\nfunc (r *Request) Param(key string) []string\n```\n\n### Params\n\n**Params** returns an iterator over all query parameters. Use `maps.Collect()` if you need them in a map. The returned values are valid only until the request is released.\n\n```go title=\"Signature\"\nfunc (r *Request) Params() iter.Seq2[string, []string]\n```\n\n### AddParam\n\n**AddParam** adds a single query parameter key-value pair.\n\n```go title=\"Signature\"\nfunc (r *Request) AddParam(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.AddParam(\"name\", \"john\")\nreq.AddParam(\"hobbies\", \"football\")\nreq.AddParam(\"hobbies\", \"basketball\")\n\nresp, err := req.Get(\"https://httpbin.org/response-headers\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"Content-Length\": \"145\", \n  \"Content-Type\": \"application/json\", \n  \"hobbies\": [\n    \"football\", \n    \"basketball\"\n  ], \n  \"name\": \"joe\"\n}\n```\n\n</details>\n\n### SetParam\n\n**SetParam** sets a single query parameter key-value pair, overriding any previously set values for that key.\n\n```go title=\"Signature\"\nfunc (r *Request) SetParam(key, val string) *Request\n```\n\n### AddParams\n\n**AddParams** adds multiple query parameters from a map of string slices.\n\n```go title=\"Signature\"\nfunc (r *Request) AddParams(m map[string][]string) *Request\n```\n\n### SetParams\n\n**SetParams** sets multiple query parameters from a map of strings, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (r *Request) SetParams(m map[string]string) *Request\n```\n\n### SetParamsWithStruct\n\n**SetParamsWithStruct** sets multiple query parameters from a struct. Nested structs are not supported.\n\n```go title=\"Signature\"\nfunc (r *Request) SetParamsWithStruct(v any) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetParamsWithStruct(struct {\n    Name    string   `json:\"name\"`\n    Hobbies []string `json:\"hobbies\"`\n}{\n    Name: \"John Doe\",\n    Hobbies: []string{\n        \"Football\",\n        \"Basketball\",\n    },\n})\n\nresp, err := req.Get(\"https://httpbin.org/response-headers\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"Content-Length\": \"147\", \n  \"Content-Type\": \"application/json\", \n  \"Hobbies\": [\n    \"Football\", \n    \"Basketball\"\n  ], \n  \"Name\": \"John Doe\"\n}\n```\n\n</details>\n\n### DelParams\n\n**DelParams** removes one or more query parameters by their keys.\n\n```go title=\"Signature\"\nfunc (r *Request) DelParams(key ...string) *Request\n```\n\n## UserAgent\n\n**UserAgent** returns the user agent currently set in the request.\n\n```go title=\"Signature\"\nfunc (r *Request) UserAgent() string\n```\n\n## SetUserAgent\n\n**SetUserAgent** sets the user agent header for the request, overriding the one set at the client level if any.\n\n```go title=\"Signature\"\nfunc (r *Request) SetUserAgent(ua string) *Request\n```\n\n## Boundary\n\n**Boundary** returns the multipart boundary used by the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Boundary() string\n```\n\n## SetBoundary\n\n**SetBoundary** sets the multipart boundary for file uploads.\n\n```go title=\"Signature\"\nfunc (r *Request) SetBoundary(b string) *Request\n```\n\n## Referer\n\n**Referer** returns the Referer header value currently set in the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Referer() string\n```\n\n## SetReferer\n\n**SetReferer** sets the Referer header for the request, overriding the one set at the client level if any.\n\n```go title=\"Signature\"\nfunc (r *Request) SetReferer(referer string) *Request\n```\n\n## Cookie\n\n**Cookie** returns the value of the specified cookie. If the cookie does not exist, it returns an empty string.\n\n```go title=\"Signature\"\nfunc (r *Request) Cookie(key string) string\n```\n\n### Cookies\n\n**Cookies** returns an iterator over all cookies set in the request. Use `maps.Collect()` to gather them into a map.\n\n```go title=\"Signature\"\nfunc (r *Request) Cookies() iter.Seq2[string, string]\n```\n\n### SetCookie\n\n**SetCookie** sets a single cookie key-value pair, overriding any previously set cookie with the same key.\n\n```go title=\"Signature\"\nfunc (r *Request) SetCookie(key, val string) *Request\n```\n\n### SetCookies\n\n**SetCookies** sets multiple cookies from a map, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (r *Request) SetCookies(m map[string]string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetCookies(map[string]string{\n    \"cookie1\": \"value1\",\n    \"cookie2\": \"value2\",\n})\n\nresp, err := req.Get(\"https://httpbin.org/cookies\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"cookies\": {\n    \"test\": \"123\"\n  }\n}\n```\n\n</details>\n\n### SetCookiesWithStruct\n\n**SetCookiesWithStruct** sets multiple cookies from a struct.\n\n```go title=\"Signature\"\nfunc (r *Request) SetCookiesWithStruct(v any) *Request\n```\n\n### DelCookies\n\n**DelCookies** removes one or more cookies by their keys.\n\n```go title=\"Signature\"\nfunc (r *Request) DelCookies(key ...string) *Request\n```\n\n## PathParam\n\n**PathParam** returns the value of a named path parameter. If not found, returns an empty string.\n\n```go title=\"Signature\"\nfunc (r *Request) PathParam(key string) string\n```\n\n### PathParams\n\n**PathParams** returns an iterator over all path parameters in the request. Use `maps.Collect()` to convert them into a map.\n\n```go title=\"Signature\"\nfunc (r *Request) PathParams() iter.Seq2[string, string]\n```\n\n### SetPathParam\n\n**SetPathParam** sets a single path parameter key-value pair, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (r *Request) SetPathParam(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetPathParam(\"base64\", \"R29maWJlcg==\")\n\nresp, err := req.Get(\"https://httpbin.org/base64/:base64\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```plaintext\nGofiber\n```\n\n</details>\n\n### SetPathParams\n\n**SetPathParams** sets multiple path parameters at once, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (r *Request) SetPathParams(m map[string]string) *Request\n```\n\n### SetPathParamsWithStruct\n\n**SetPathParamsWithStruct** sets multiple path parameters from a struct.\n\n```go title=\"Signature\"\nfunc (r *Request) SetPathParamsWithStruct(v any) *Request\n```\n\n### DelPathParams\n\n**DelPathParams** deletes one or more path parameters by their keys.\n\n```go title=\"Signature\"\nfunc (r *Request) DelPathParams(key ...string) *Request\n```\n\n### ResetPathParams\n\n**ResetPathParams** deletes all path parameters.\n\n```go title=\"Signature\"\nfunc (r *Request) ResetPathParams() *Request\n```\n\n## SetJSON\n\n**SetJSON** sets the request body to a JSON-encoded payload.\n\n```go title=\"Signature\"\nfunc (r *Request) SetJSON(v any) *Request\n```\n\n## SetXML\n\n**SetXML** sets the request body to an XML-encoded payload.\n\n```go title=\"Signature\"\nfunc (r *Request) SetXML(v any) *Request\n```\n\n## SetCBOR\n\n**SetCBOR** sets the request body to a CBOR-encoded payload. It automatically sets the `Content-Type` to `application/cbor`.\n\n```go title=\"Signature\"\nfunc (r *Request) SetCBOR(v any) *Request\n```\n\n## SetRawBody\n\n**SetRawBody** sets the request body to raw bytes.\n\n```go title=\"Signature\"\nfunc (r *Request) SetRawBody(v []byte) *Request\n```\n\n## FormData\n\n**FormData** returns all values associated with the given form data field.\n\n```go title=\"Signature\"\nfunc (r *Request) FormData(key string) []string\n```\n\n### AllFormData\n\n**AllFormData** returns an iterator over all form data fields. Use `maps.Collect()` if needed.\n\n```go title=\"Signature\"\nfunc (r *Request) AllFormData() iter.Seq2[string, []string]\n```\n\n### AddFormData\n\n**AddFormData** adds a single form data key-value pair.\n\n```go title=\"Signature\"\nfunc (r *Request) AddFormData(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.AddFormData(\"points\", \"80\")\nreq.AddFormData(\"points\", \"90\")\nreq.AddFormData(\"points\", \"100\")\n\nresp, err := req.Post(\"https://httpbin.org/post\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {}, \n  \"form\": {\n    \"points\": [\n      \"80\", \n      \"90\", \n      \"100\"\n    ]\n  }, \n  // ...\n}\n```\n\n</details>\n\n### SetFormData\n\n**SetFormData** sets a single form data field, overriding any previously set values.\n\n```go title=\"Signature\"\nfunc (r *Request) SetFormData(key, val string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetFormData(\"name\", \"john\")\nreq.SetFormData(\"email\", \"john@doe.com\")\n\nresp, err := req.Post(\"https://httpbin.org/post\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {}, \n  \"form\": {\n    \"email\": \"john@doe.com\", \n    \"name\": \"john\"\n  }, \n  // ...\n}\n```\n\n</details>\n\n### AddFormDataWithMap\n\n**AddFormDataWithMap** adds multiple form data fields and values from a map of string slices.\n\n```go title=\"Signature\"\nfunc (r *Request) AddFormDataWithMap(m map[string][]string) *Request\n```\n\n### SetFormDataWithMap\n\n**SetFormDataWithMap** sets multiple form data fields from a map of strings.\n\n```go title=\"Signature\"\nfunc (r *Request) SetFormDataWithMap(m map[string]string) *Request\n```\n\n### SetFormDataWithStruct\n\n**SetFormDataWithStruct** sets multiple form data fields from a struct.\n\n```go title=\"Signature\"\nfunc (r *Request) SetFormDataWithStruct(v any) *Request\n```\n\n### DelFormData\n\n**DelFormData** deletes one or more form data fields by their keys.\n\n```go title=\"Signature\"\nfunc (r *Request) DelFormData(key ...string) *Request\n```\n\n## File\n\n**File** returns a file from the request by its name. If no name was provided, it attempts to match by path.\n\n```go title=\"Signature\"\nfunc (r *Request) File(name string) *File\n```\n\n### Files\n\n**Files** returns all files in the request as a slice. The returned slice is valid only until the request is released.\n\n```go title=\"Signature\"\nfunc (r *Request) Files() []*File\n```\n\n### FileByPath\n\n**FileByPath** returns a file from the request by its file path.\n\n```go title=\"Signature\"\nfunc (r *Request) FileByPath(path string) *File\n```\n\n### AddFile\n\n**AddFile** adds a single file to the request from a file path.\n\n```go title=\"Signature\"\nfunc (r *Request) AddFile(path string) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.AddFile(\"test.txt\")\n\nresp, err := req.Post(\"https://httpbin.org/post\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {\n    \"file1\": \"This is an empty file!\\n\"\n  }, \n  \"form\": {}, \n  // ...\n}\n```\n\n</details>\n\n### AddFileWithReader\n\n**AddFileWithReader** adds a single file to the request from an `io.ReadCloser`.\n\n```go title=\"Signature\"\nfunc (r *Request) AddFileWithReader(name string, reader io.ReadCloser) *Request\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nbuf := bytes.NewBuffer([]byte(\"Hello, World!\"))\nreq.AddFileWithReader(\"test.txt\", io.NopCloser(buf))\n\nresp, err := req.Post(\"https://httpbin.org/post\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {\n    \"file1\": \"Hello, World!\"\n  }, \n  \"form\": {}, \n  // ...\n}\n```\n\n</details>\n\n### AddFiles\n\n**AddFiles** adds multiple files to the request at once.\n\n```go title=\"Signature\"\nfunc (r *Request) AddFiles(files ...*File) *Request\n```\n\n## Timeout\n\n**Timeout** returns the timeout duration set in the request.\n\n```go title=\"Signature\"\nfunc (r *Request) Timeout() time.Duration\n```\n\n## SetTimeout\n\n**SetTimeout** sets a timeout for the request, overriding any timeout set at the client level.\n\n```go title=\"Signature\"\nfunc (r *Request) SetTimeout(t time.Duration) *Request\n```\n\n<details>\n<summary>Example 1</summary>\n\n```go title=\"Example 1\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetTimeout(5 * time.Second)\n\nresp, err := req.Get(\"https://httpbin.org/delay/4\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```json\n{\n  \"args\": {}, \n  \"data\": \"\", \n  \"files\": {}, \n  \"form\": {}, \n  // ...\n}\n```\n\n</details>\n\n<details>\n<summary>Example 2</summary>\n\n```go title=\"Example 2\"\nreq := client.AcquireRequest()\ndefer client.ReleaseRequest(req)\n\nreq.SetTimeout(5 * time.Second)\n\nresp, err := req.Get(\"https://httpbin.org/delay/6\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n```shell\npanic: timeout or cancel\n\ngoroutine 1 [running]:\nmain.main()\n        main.go:18 +0xeb\nexit status 2\n```\n\n</details>\n\n## MaxRedirects\n\n**MaxRedirects** returns the maximum number of redirects allowed for the request.\n\n```go title=\"Signature\"\nfunc (r *Request) MaxRedirects() int\n```\n\n## SetMaxRedirects\n\n**SetMaxRedirects** sets the maximum number of redirects for the request, overriding the client's setting.\n\n```go title=\"Signature\"\nfunc (r *Request) SetMaxRedirects(count int) *Request\n```\n\n## Send\n\n**Send** executes the HTTP request and returns a `Response`.\n\n```go title=\"Signature\"\nfunc (r *Request) Send() (*Response, error)\n```\n\n## Reset\n\n**Reset** clears the `Request` object, making it ready for reuse. This is used by `ReleaseRequest`.\n\n```go title=\"Signature\"\nfunc (r *Request) Reset()\n```\n\n## Header\n\n**Header** is a wrapper around `fasthttp.RequestHeader`, storing headers for both the client and request.\n\n```go\ntype Header struct {\n    *fasthttp.RequestHeader\n}\n```\n\n### PeekMultiple\n\n**PeekMultiple** returns multiple values associated with the same header key.\n\n```go title=\"Signature\"\nfunc (h *Header) PeekMultiple(key string) []string\n```\n\n### AddHeaders\n\n**AddHeaders** adds multiple headers from a map of string slices.\n\n```go title=\"Signature\"\nfunc (h *Header) AddHeaders(r map[string][]string)\n```\n\n### SetHeaders\n\n**SetHeaders** sets multiple headers from a map of strings, overriding previously set headers.\n\n```go title=\"Signature\"\nfunc (h *Header) SetHeaders(r map[string]string)\n```\n\n## QueryParam\n\n**QueryParam** is a wrapper around `fasthttp.Args`, storing query parameters.\n\n```go\ntype QueryParam struct {\n    *fasthttp.Args\n}\n```\n\n### Keys\n\n**Keys** returns all keys in the query parameters.\n\n```go title=\"Signature\"\nfunc (p *QueryParam) Keys() []string\n```\n\n### AddParams\n\n**AddParams** adds multiple query parameters from a map of string slices.\n\n```go title=\"Signature\"\nfunc (p *QueryParam) AddParams(r map[string][]string)\n```\n\n### SetParams\n\n**SetParams** sets multiple query parameters from a map of strings, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (p *QueryParam) SetParams(r map[string]string)\n```\n\n### SetParamsWithStruct\n\n**SetParamsWithStruct** sets multiple query parameters from a struct. Nested structs are not supported.\n\n```go title=\"Signature\"\nfunc (p *QueryParam) SetParamsWithStruct(v any)\n```\n\n## Cookie\n\n**Cookie** is a map that stores cookies.\n\n```go\ntype Cookie map[string]string\n```\n\n### Add\n\n**Add** adds a cookie key-value pair.\n\n```go title=\"Signature\"\nfunc (c Cookie) Add(key, val string)\n```\n\n### Del\n\n**Del** removes a cookie by its key.\n\n```go title=\"Signature\"\nfunc (c Cookie) Del(key string)\n```\n\n### SetCookie\n\n**SetCookie** sets a single cookie key-value pair, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (c Cookie) SetCookie(key, val string)\n```\n\n### SetCookies\n\n**SetCookies** sets multiple cookies from a map of strings.\n\n```go title=\"Signature\"\nfunc (c Cookie) SetCookies(m map[string]string)\n```\n\n### SetCookiesWithStruct\n\n**SetCookiesWithStruct** sets multiple cookies from a struct.\n\n```go title=\"Signature\"\nfunc (c Cookie) SetCookiesWithStruct(v any)\n```\n\n### DelCookies\n\n**DelCookies** deletes one or more cookies by their keys.\n\n```go title=\"Signature\"\nfunc (c Cookie) DelCookies(key ...string)\n```\n\n### All\n\n**All** returns an iterator over all cookies. The key and value returned\nshould not be retained after the loop ends.\n\n```go title=\"Signature\"\nfunc (c Cookie) All() iter.Seq2[string, string]\n```\n\n### Reset\n\n**Reset** clears all cookies.\n\n```go title=\"Signature\"\nfunc (c Cookie) Reset()\n```\n\n## PathParam\n\n**PathParam** is a map that stores path parameters.\n\n```go\ntype PathParam map[string]string\n```\n\n### Add\n\n**Add** adds a path parameter key-value pair.\n\n```go title=\"Signature\"\nfunc (p PathParam) Add(key, val string)\n```\n\n### Del\n\n**Del** removes a path parameter by its key.\n\n```go title=\"Signature\"\nfunc (p PathParam) Del(key string)\n```\n\n### SetParam\n\n**SetParam** sets a single path parameter key-value pair, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (p PathParam) SetParam(key, val string)\n```\n\n### SetParams\n\n**SetParams** sets multiple path parameters from a map of strings.\n\n```go title=\"Signature\"\nfunc (p PathParam) SetParams(m map[string]string)\n```\n\n### SetParamsWithStruct\n\n**SetParamsWithStruct** sets multiple path parameters from a struct.\n\n```go title=\"Signature\"\nfunc (p PathParam) SetParamsWithStruct(v any)\n```\n\n### DelParams\n\n**DelParams** deletes one or more path parameters by their keys.\n\n```go title=\"Signature\"\nfunc (p PathParam) DelParams(key ...string)\n```\n\n### All\n\n**All** returns an iterator over all path parameters. The key and value returned\nshould not be retained after the loop ends.\n\n```go title=\"Signature\"\nfunc (p PathParam) All() iter.Seq2[string, string]\n```\n\n### Reset\n\n**Reset** clears all path parameters.\n\n```go title=\"Signature\"\nfunc (p PathParam) Reset()\n```\n\n## FormData\n\n**FormData** is a wrapper around `fasthttp.Args`, used to handle URL-encoded and form-data (multipart) request bodies.\n\n```go\ntype FormData struct {\n    *fasthttp.Args\n}\n```\n\n### Keys\n\n**Keys** returns all form data keys.\n\n```go title=\"Signature\"\nfunc (f *FormData) Keys() []string\n```\n\n### Add\n\n**Add** adds a single form field key-value pair.\n\n```go title=\"Signature\"\nfunc (f *FormData) Add(key, val string)\n```\n\n### Set\n\n**Set** sets a single form field key-value pair, overriding any previously set values.\n\n```go title=\"Signature\"\nfunc (f *FormData) Set(key, val string)\n```\n\n### AddWithMap\n\n**AddWithMap** adds multiple form fields from a map of string slices.\n\n```go title=\"Signature\"\nfunc (f *FormData) AddWithMap(m map[string][]string)\n```\n\n### SetWithMap\n\n**SetWithMap** sets multiple form fields from a map of strings.\n\n```go title=\"Signature\"\nfunc (f *FormData) SetWithMap(m map[string]string)\n```\n\n### SetWithStruct\n\n**SetWithStruct** sets multiple form fields from a struct.\n\n```go title=\"Signature\"\nfunc (f *FormData) SetWithStruct(v any)\n```\n\n### DelData\n\n**DelData** deletes one or more form fields by their keys.\n\n```go title=\"Signature\"\nfunc (f *FormData) DelData(key ...string)\n```\n\n### Reset\n\n**Reset** clears all form data fields.\n\n```go title=\"Signature\"\nfunc (f *FormData) Reset()\n```\n\n## File\n\n**File** represents a file to be uploaded. It can be specified by name, path, or an `io.ReadCloser`.\n\n```go\ntype File struct {\n    name      string\n    fieldName string\n    path      string\n    reader    io.ReadCloser\n}\n```\n\n### AcquireFile\n\n**AcquireFile** returns a `File` from the pool and applies any provided `SetFileFunc` functions to it. Release it with `ReleaseFile` when done.\n\n```go title=\"Signature\"\nfunc AcquireFile(setter ...SetFileFunc) *File\n```\n\n### ReleaseFile\n\n**ReleaseFile** returns the `File` to the pool. Do not use the file afterward.\n\n```go title=\"Signature\"\nfunc ReleaseFile(f *File)\n```\n\n### SetName\n\n**SetName** sets the file's name.\n\n```go title=\"Signature\"\nfunc (f *File) SetName(n string)\n```\n\n### SetFieldName\n\n**SetFieldName** sets the field name of the file in the multipart form.\n\n```go title=\"Signature\"\nfunc (f *File) SetFieldName(n string)\n```\n\n### SetPath\n\n**SetPath** sets the file's path.\n\n```go title=\"Signature\"\nfunc (f *File) SetPath(p string)\n```\n\n### SetReader\n\n**SetReader** sets the file's `io.ReadCloser`. The reader is closed automatically when the request body is parsed.\n\n```go title=\"Signature\"\nfunc (f *File) SetReader(r io.ReadCloser)\n```\n\n### Reset\n\n**Reset** clears the file's fields.\n\n```go title=\"Signature\"\nfunc (f *File) Reset()\n```\n"
  },
  {
    "path": "docs/client/response.md",
    "content": "---\nid: response\ntitle: 📥 Response\ndescription: >-\n  Response methods of Gofiber HTTP client.\nsidebar_position: 3\n---\n\nThe `Response` struct in Fiber's HTTP client represents the server's reply and exposes:\n\n- **Status Code**: The HTTP status code returned by the server (e.g., `200 OK`, `404 Not Found`).\n- **Headers**: All HTTP headers returned by the server, providing additional response-related information.\n- **Body**: The response body content, which can be JSON, XML, plain text, or other formats.\n- **Cookies**: Any cookies the server sent along with the response.\n\nIt makes it easy to inspect and handle data returned by the server.\n\n```go\ntype Response struct {\n    client      *Client\n    request     *Request\n    cookie      []*fasthttp.Cookie\n    RawResponse *fasthttp.Response\n}\n```\n\n## AcquireResponse\n\n**AcquireResponse** returns a new pooled `Response`. Call `ReleaseResponse` when you're done to return it to the pool and limit allocations.\n\n```go title=\"Signature\"\nfunc AcquireResponse() *Response\n```\n\n## ReleaseResponse\n\n**ReleaseResponse** puts the `Response` back into the pool. Do not use it after releasing; doing so can trigger data races.\n\n```go title=\"Signature\"\nfunc ReleaseResponse(resp *Response)\n```\n\n## Status\n\n**Status** returns the HTTP status message (e.g., `OK`, `Not Found`) associated with the response.\n\n```go title=\"Signature\"\nfunc (r *Response) Status() string\n```\n\n## StatusCode\n\n**StatusCode** returns the numeric HTTP status code of the response.\n\n```go title=\"Signature\"\nfunc (r *Response) StatusCode() int\n```\n\n## Protocol\n\n**Protocol** returns the HTTP protocol used (e.g., `HTTP/1.1`, `HTTP/2`) for the response.\n\n```go title=\"Signature\"\nfunc (r *Response) Protocol() string\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nresp, err := client.Get(\"https://httpbin.org/get\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(resp.Protocol())\n```\n\n**Output:**\n\n```text\nHTTP/1.1\n```\n\n</details>\n\n## Header\n\n**Header** retrieves the value of a specific response header by key. If multiple values exist for the same header, this returns the first one.\n\n```go title=\"Signature\"\nfunc (r *Response) Header(key string) string\n```\n\n## Headers\n\n**Headers** returns an iterator over all response headers. Use `maps.Collect()` to convert them into a map if desired. The returned values are only valid until the response is released, so make copies if needed.\n\n```go title=\"Signature\"\nfunc (r *Response) Headers() iter.Seq2[string, []string]\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nresp, err := client.Get(\"https://httpbin.org/get\")\nif err != nil {\n    panic(err)\n}\n\nfor key, values := range resp.Headers() {\n    fmt.Printf(\"%s => %s\\n\", key, strings.Join(values, \", \"))\n}\n```\n\n**Output:**\n\n```text\nDate => Wed, 04 Dec 2024 15:28:29 GMT\nConnection => keep-alive\nAccess-Control-Allow-Origin => *\nAccess-Control-Allow-Credentials => true\n```\n\n</details>\n\n<details>\n<summary>Example with maps.Collect()</summary>\n\n```go title=\"Example with maps.Collect()\"\nresp, err := client.Get(\"https://httpbin.org/get\")\nif err != nil {\n    panic(err)\n}\n\nheaders := maps.Collect(resp.Headers())\nfor key, values := range headers {\n    fmt.Printf(\"%s => %s\\n\", key, strings.Join(values, \", \"))\n}\n```\n\n**Output:**\n\n```text\nDate => Wed, 04 Dec 2024 15:28:29 GMT\nConnection => keep-alive\nAccess-Control-Allow-Origin => *\nAccess-Control-Allow-Credentials => true\n```\n\n</details>\n\n## Cookies\n\n**Cookies** returns a slice of all cookies set by the server in this response. The slice is only valid until the response is released.\n\n```go title=\"Signature\"\nfunc (r *Response) Cookies() []*fasthttp.Cookie\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\nresp, err := client.Get(\"https://httpbin.org/cookies/set/go/fiber\")\nif err != nil {\n    panic(err)\n}\n\ncookies := resp.Cookies()\nfor _, cookie := range cookies {\n    fmt.Printf(\"%s => %s\\n\", string(cookie.Key()), string(cookie.Value()))\n}\n```\n\n**Output:**\n\n```text\ngo => fiber\n```\n\n</details>\n\n## Body\n\n**Body** returns the raw response body as a byte slice.\n\n```go title=\"Signature\"\nfunc (r *Response) Body() []byte\n```\n\n## BodyStream\n\n**BodyStream** returns the response body as an `io.Reader`, allowing incremental reading without loading the entire body into memory. This is particularly useful when `Client.SetStreamResponseBody(true)` is enabled.\n\nWhen streaming is enabled, the underlying stream from fasthttp is returned directly. When streaming is not enabled, a `bytes.Reader` wrapping the body is returned as a fallback.\n\n:::note\nWhen using `BodyStream()`, the response body is consumed as you read. Calling `Body()` afterward may return an empty slice if the stream has been fully read.\n:::\n\n```go title=\"Signature\"\nfunc (r *Response) BodyStream() io.Reader\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\ncc := client.New()\ncc.SetStreamResponseBody(true)\n\nresp, err := cc.Get(\"https://httpbin.org/bytes/1024\")\nif err != nil {\n    panic(err)\n}\ndefer resp.Close()\n\nbuf := make([]byte, 256)\ntotal, err := io.CopyBuffer(io.Discard, resp.BodyStream(), buf)\nif err != nil {\n    panic(err)\n}\n\nfmt.Printf(\"Read %d bytes\\n\", total)\n```\n\n**Output:**\n\n```text\nRead 1024 bytes\n```\n\n</details>\n\n## IsStreaming\n\n**IsStreaming** returns `true` if the response body is being streamed (i.e., when `Client.SetStreamResponseBody(true)` was set and the underlying transport provided a stream).\n\n```go title=\"Signature\"\nfunc (r *Response) IsStreaming() bool\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\ncc := client.New()\ncc.SetStreamResponseBody(true)\n\nresp, err := cc.Get(\"https://httpbin.org/get\")\nif err != nil {\n    panic(err)\n}\ndefer resp.Close()\n\nif resp.IsStreaming() {\n    fmt.Println(\"Response is streaming\")\n    // Use resp.BodyStream() to read incrementally\n} else {\n    fmt.Println(\"Response is buffered\")\n    // Use resp.Body() for direct access\n}\n```\n\n</details>\n\n## String\n\n**String** returns the response body as a trimmed string.\n\n```go title=\"Signature\"\nfunc (r *Response) String() string\n```\n\n## JSON\n\n**JSON** unmarshal the response body into the provided variable `v` using JSON. `v` should be a pointer to a struct or a type compatible with JSON unmarshal.\n\n```go title=\"Signature\"\nfunc (r *Response) JSON(v any) error\n```\n\n<details>\n<summary>Example</summary>\n\n```go title=\"Example\"\ntype Body struct {\n    Slideshow struct {\n        Author string `json:\"author\"`\n        Date   string `json:\"date\"`\n        Title  string `json:\"title\"`\n    } `json:\"slideshow\"`\n}\nvar out Body\n\nresp, err := client.Get(\"https://httpbin.org/json\")\nif err != nil {\n    panic(err)\n}\n\nif err = resp.JSON(&out); err != nil {\n    panic(err)\n}\n\nfmt.Printf(\"%+v\\n\", out)\n```\n\n**Output:**\n\n```text\n{Slideshow:{Author:Yours Truly Date:date of publication Title:Sample Slide Show}}\n```\n\n</details>\n\n## XML\n\n**XML** unmarshal the response body into the provided variable `v` using XML decoding.\n\n```go title=\"Signature\"\nfunc (r *Response) XML(v any) error\n```\n\n## CBOR\n\n**CBOR** unmarshal the response body into `v` using CBOR decoding.\n\n```go title=\"Signature\"\nfunc (r *Response) CBOR(v any) error\n```\n\n## Save\n\n**Save** writes the response body to a file or an `io.Writer`. If `v` is a string, it interprets it as a file path, creates the file (and directories if needed), and writes the response to it. If `v` is an `io.Writer`, it writes directly to it.\n\n```go title=\"Signature\"\nfunc (r *Response) Save(v any) error\n```\n\n## Reset\n\n**Reset** clears the `Response` object, making it ready for reuse by `ReleaseResponse`.\n\n```go title=\"Signature\"\nfunc (r *Response) Reset()\n```\n\n## Close\n\n**Close** releases both the associated `Request` and `Response` objects back to their pools.\n\n:::warning\nAfter calling `Close`, any attempt to use the request or response may result in data races or undefined behavior. Ensure all processing is complete before closing.\n:::\n\n```go title=\"Signature\"\nfunc (r *Response) Close()\n```\n"
  },
  {
    "path": "docs/client/rest.md",
    "content": "---\nid: rest\ntitle: 🖥️ REST\ndescription: >-\n  HTTP client for Fiber.\nsidebar_position: 1\ntoc_max_heading_level: 5\n---\n\nThe Fiber Client is a high-performance HTTP client built on FastHTTP. It handles both internal service calls and external requests with minimal overhead.\n\n## Features\n\n- **Lightweight and fast**: built on FastHTTP for minimal overhead.\n- **Flexible configuration**: set global defaults like timeouts or headers and override them per request.\n- **Connection pooling**: reuses persistent connections instead of opening new ones.\n- **Timeouts and retries**: supports per-request deadlines and retry policies for transient errors.\n\n## Usage\n\nCreate a client with any required configuration, then send requests:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    cc := client.New()\n    cc.SetTimeout(10 * time.Second)\n\n    // Send a GET request\n    resp, err := cc.Get(\"https://httpbin.org/get\")\n    if err != nil {\n        panic(err)\n    }\n\n    fmt.Printf(\"Status: %d\\n\", resp.StatusCode())\n    fmt.Printf(\"Body: %s\\n\", string(resp.Body()))\n}\n```\n\nSee [examples](examples.md) for more detailed usage.\n\n```go\ntype Client struct {\n    mu sync.RWMutex\n\n    fasthttp *fasthttp.Client\n\n    baseURL   string\n    userAgent string\n    referer   string\n    header    *Header\n    params    *QueryParam\n    cookies   *Cookie\n    path      *PathParam\n\n    debug bool\n\n    timeout time.Duration\n\n    // user-defined request hooks\n    userRequestHooks []RequestHook\n\n    // client package-defined request hooks\n    builtinRequestHooks []RequestHook\n\n    // user-defined response hooks\n    userResponseHooks []ResponseHook\n\n    // client package-defined response hooks\n    builtinResponseHooks []ResponseHook\n\n    jsonMarshal   utils.JSONMarshal\n    jsonUnmarshal utils.JSONUnmarshal\n    xmlMarshal    utils.XMLMarshal\n    xmlUnmarshal  utils.XMLUnmarshal\n    cborMarshal   utils.CBORMarshal\n    cborUnmarshal utils.CBORUnmarshal\n\n    cookieJar *CookieJar\n\n    // proxy\n    proxyURL string\n\n    // retry\n    retryConfig *RetryConfig\n\n    // logger\n    logger log.CommonLogger\n}\n```\n\n### New\n\n**New** creates and returns a new Client object.\n\n```go title=\"Signature\"\nfunc New() *Client\n```\n\n### NewWithClient\n\n**NewWithClient** creates and returns a new Client object from an existing `fasthttp.Client`.\n\n```go title=\"Signature\"\nfunc NewWithClient(c *fasthttp.Client) *Client\n```\n\n## REST Methods\n\nThese helpers mirror axios-style method names and send HTTP requests using the configured client:\n\n### Get\n\nSends a GET request.\n\n```go title=\"Signature\"\nfunc (c *Client) Get(url string, cfg ...Config) (*Response, error)\n```\n\n### Post\n\nSends a POST request.\n\n```go title=\"Signature\"\nfunc (c *Client) Post(url string, cfg ...Config) (*Response, error)\n```\n\n### Put\n\nSends a PUT request.\n\n```go title=\"Signature\"\nfunc (c *Client) Put(url string, cfg ...Config) (*Response, error)\n```\n\n### Patch\n\nSends a PATCH request.\n\n```go title=\"Signature\"\nfunc (c *Client) Patch(url string, cfg ...Config) (*Response, error)\n```\n\n### Delete\n\nSends a DELETE request.\n\n```go title=\"Signature\"\nfunc (c *Client) Delete(url string, cfg ...Config) (*Response, error)\n```\n\n### Head\n\nSends a HEAD request.\n\n```go title=\"Signature\"\nfunc (c *Client) Head(url string, cfg ...Config) (*Response, error)\n```\n\n### Options\n\nSends an OPTIONS request.\n\n```go title=\"Signature\"\nfunc (c *Client) Options(url string, cfg ...Config) (*Response, error)\n```\n\n### Custom\n\nSends a request with any HTTP method.\n\n```go title=\"Signature\"\nfunc (c *Client) Custom(url, method string, cfg ...Config) (*Response, error)\n```\n\n## Request Configuration\n\nThe `Config` type holds per-request parameters. JSON is used to serialize the body by default. If multiple body sources are set, precedence is:\n\n1. Body\n2. FormData\n3. File\n\n```go\ntype Config struct {\n    Ctx context.Context\n\n    UserAgent string\n    Referer   string\n    Header    map[string]string\n    Param     map[string]string\n    Cookie    map[string]string\n    PathParam map[string]string\n\n    Timeout      time.Duration\n    MaxRedirects int\n\n    Body     any\n    FormData map[string]string\n    File     []*File\n}\n```\n\n## R\n\n**R** gets a `Request` object from the pool. Call `ReleaseRequest` when finished.\n\n```go title=\"Signature\"\nfunc (c *Client) R() *Request\n```\n\n## Hooks\n\nHooks allow you to add custom logic before a request is sent or after a response is received.\n\n### RequestHook\n\n**RequestHook** returns user-defined request hooks.\n\n```go title=\"Signature\"\nfunc (c *Client) RequestHook() []RequestHook\n```\n\n### ResponseHook\n\n**ResponseHook** returns user-defined response hooks.\n\n```go title=\"Signature\"\nfunc (c *Client) ResponseHook() []ResponseHook\n```\n\n### AddRequestHook\n\nAdds one or more user-defined request hooks.\n\n```go title=\"Signature\"\nfunc (c *Client) AddRequestHook(h ...RequestHook) *Client\n```\n\n### AddResponseHook\n\nAdds one or more user-defined response hooks.\n\n```go title=\"Signature\"\nfunc (c *Client) AddResponseHook(h ...ResponseHook) *Client\n```\n\n## JSON\n\n### JSONMarshal\n\nReturns the JSON marshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) JSONMarshal() utils.JSONMarshal\n```\n\n### JSONUnmarshal\n\nReturns the JSON unmarshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) JSONUnmarshal() utils.JSONUnmarshal\n```\n\n### SetJSONMarshal\n\nSets a custom JSON marshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetJSONMarshal(f utils.JSONMarshal) *Client\n```\n\n### SetJSONUnmarshal\n\nSets a custom JSON unmarshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetJSONUnmarshal(f utils.JSONUnmarshal) *Client\n```\n\n## XML\n\n### XMLMarshal\n\nReturns the XML marshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) XMLMarshal() utils.XMLMarshal\n```\n\n### XMLUnmarshal\n\nReturns the XML unmarshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) XMLUnmarshal() utils.XMLUnmarshal\n```\n\n### SetXMLMarshal\n\nSets a custom XML marshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetXMLMarshal(f utils.XMLMarshal) *Client\n```\n\n### SetXMLUnmarshal\n\nSets a custom XML unmarshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetXMLUnmarshal(f utils.XMLUnmarshal) *Client\n```\n\n## CBOR\n\n### CBORMarshal\n\nReturns the CBOR marshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) CBORMarshal() utils.CBORMarshal\n```\n\n### CBORUnmarshal\n\nReturns the CBOR unmarshaler function used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) CBORUnmarshal() utils.CBORUnmarshal\n```\n\n### SetCBORMarshal\n\nSets a custom CBOR marshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client\n```\n\n### SetCBORUnmarshal\n\nSets a custom CBOR unmarshaler.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCBORUnmarshal(f utils.CBORUnmarshal) *Client\n```\n\n## TLS\n\n### TLSConfig\n\nReturns the client's TLS configuration. If none is set, it initializes a new\nconfiguration with `MinVersion` defaulting to TLS 1.2.\n\n```go title=\"Signature\"\nfunc (c *Client) TLSConfig() *tls.Config\n```\n\n### SetTLSConfig\n\nSets the TLS configuration for the client.\n\n```go title=\"Signature\"\nfunc (c *Client) SetTLSConfig(config *tls.Config) *Client\n```\n\n### SetCertificates\n\nAdds client certificates to the TLS configuration.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCertificates(certs ...tls.Certificate) *Client\n```\n\n### SetRootCertificate\n\nAdds one or more root certificates to the client's trust store.\n\n```go title=\"Signature\"\nfunc (c *Client) SetRootCertificate(path string) *Client\n```\n\n### SetRootCertificateFromString\n\nAdds one or more root certificates from a string.\n\n```go title=\"Signature\"\nfunc (c *Client) SetRootCertificateFromString(pem string) *Client\n```\n\n## SetProxyURL\n\nSets a proxy URL for the client. All subsequent requests will use this proxy.\n\n```go title=\"Signature\"\nfunc (c *Client) SetProxyURL(proxyURL string) error\n```\n\n## Response Streaming\n\n### StreamResponseBody\n\nReturns whether response body streaming is enabled. When enabled, the response body is not fully loaded into memory and can be read as a stream using `Response.BodyStream()`. This is useful for handling large responses or server-sent events (SSE).\n\n```go title=\"Signature\"\nfunc (c *Client) StreamResponseBody() bool\n```\n\n### SetStreamResponseBody\n\nEnables or disables response body streaming. When enabled, responses can be consumed incrementally without loading the entire body into memory.\n\n```go title=\"Signature\"\nfunc (c *Client) SetStreamResponseBody(enable bool) *Client\n```\n\n**Example:**\n\n```go title=\"Example\"\ncc := client.New()\ncc.SetStreamResponseBody(true)\n\nresp, err := cc.Get(\"https://example.com/large-file\")\nif err != nil {\n    panic(err)\n}\ndefer resp.Close()\n\n// Check if response is streaming\nif resp.IsStreaming() {\n    // Read body incrementally\n    reader := resp.BodyStream()\n    buf := make([]byte, 4096)\n    for {\n        n, err := reader.Read(buf)\n        if n > 0 {\n            // Process chunk...\n        }\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            panic(err)\n        }\n    }\n} else {\n    // Regular body access\n    body := resp.Body()\n    fmt.Println(string(body))\n}\n```\n\n**Server-Sent Events Example:**\n\n```go title=\"SSE Example\"\ncc := client.New()\ncc.SetStreamResponseBody(true)\n\nresp, err := cc.Get(\"https://example.com/events\")\nif err != nil {\n    panic(err)\n}\ndefer resp.Close()\n\nreader := bufio.NewReader(resp.BodyStream())\nfor {\n    line, err := reader.ReadString('\\n')\n    if err == io.EOF {\n        break\n    }\n    if err != nil {\n        panic(err)\n    }\n    fmt.Print(line) // Process SSE event\n}\n```\n\n## RetryConfig\n\nReturns the retry configuration of the client.\n\n```go title=\"Signature\"\nfunc (c *Client) RetryConfig() *RetryConfig\n```\n\n## SetRetryConfig\n\nSets the retry configuration for the client.\n\n```go title=\"Signature\"\nfunc (c *Client) SetRetryConfig(config *RetryConfig) *Client\n```\n\n## BaseURL\n\n### BaseURL\n\n**BaseURL** returns the base URL currently set in the client.\n\n```go title=\"Signature\"\nfunc (c *Client) BaseURL() string\n```\n\n### SetBaseURL\n\nSets a base URL prefix for all requests made by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) SetBaseURL(url string) *Client\n```\n\n**Example:**\n\n```go title=\"Example\"\ncc := client.New()\ncc.SetBaseURL(\"https://httpbin.org/\")\n\nresp, err := cc.Get(\"/get\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n{\n  \"args\": {},\n  ...\n}\n```\n\n</details>\n\n## Headers\n\n### Header\n\nRetrieves all values of a header key at the client level. The returned values apply to all requests.\n\n```go title=\"Signature\"\nfunc (c *Client) Header(key string) []string\n```\n\n### AddHeader\n\nAdds a single header to all requests initiated by this client.\n\n```go title=\"Signature\"\nfunc (c *Client) AddHeader(key, val string) *Client\n```\n\n### SetHeader\n\nSets a single header, overriding any existing headers with the same key.\n\n```go title=\"Signature\"\nfunc (c *Client) SetHeader(key, val string) *Client\n```\n\n### AddHeaders\n\nAdds multiple headers at once, all applying to all future requests from this client.\n\n```go title=\"Signature\"\nfunc (c *Client) AddHeaders(h map[string][]string) *Client\n```\n\n### SetHeaders\n\nSets multiple headers at once, overriding previously set headers.\n\n```go title=\"Signature\"\nfunc (c *Client) SetHeaders(h map[string]string) *Client\n```\n\n## Query Parameters\n\n### Param\n\nReturns the values for a given query parameter key.\n\n```go title=\"Signature\"\nfunc (c *Client) Param(key string) []string\n```\n\n### AddParam\n\nAdds a single query parameter for all requests.\n\n```go title=\"Signature\"\nfunc (c *Client) AddParam(key, val string) *Client\n```\n\n### SetParam\n\nSets a single query parameter, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (c *Client) SetParam(key, val string) *Client\n```\n\n### AddParams\n\nAdds multiple query parameters from a map of string slices.\n\n```go title=\"Signature\"\nfunc (c *Client) AddParams(m map[string][]string) *Client\n```\n\n### SetParams\n\nSets multiple query parameters from a map, overriding previously set values.\n\n```go title=\"Signature\"\nfunc (c *Client) SetParams(m map[string]string) *Client\n```\n\n### SetParamsWithStruct\n\nSets multiple query parameters from a struct. Nested structs are not currently supported.\n\n```go title=\"Signature\"\nfunc (c *Client) SetParamsWithStruct(v any) *Client\n```\n\n### DelParams\n\nDeletes one or more query parameters.\n\n```go title=\"Signature\"\nfunc (c *Client) DelParams(key ...string) *Client\n```\n\n## UserAgent & Referer\n\n### SetUserAgent\n\nSets the user agent header for all requests.\n\n```go title=\"Signature\"\nfunc (c *Client) SetUserAgent(ua string) *Client\n```\n\n### SetReferer\n\nSets the referer header for all requests.\n\n```go title=\"Signature\"\nfunc (c *Client) SetReferer(r string) *Client\n```\n\n## Path Parameters\n\n### PathParam\n\nReturns the value of a named path parameter, if set.\n\n```go title=\"Signature\"\nfunc (c *Client) PathParam(key string) string\n```\n\n### SetPathParam\n\nSets a single path parameter.\n\n```go title=\"Signature\"\nfunc (c *Client) SetPathParam(key, val string) *Client\n```\n\n### SetPathParams\n\nSets multiple path parameters at once.\n\n```go title=\"Signature\"\nfunc (c *Client) SetPathParams(m map[string]string) *Client\n```\n\n### SetPathParamsWithStruct\n\nSets multiple path parameters from a struct.\n\n```go title=\"Signature\"\nfunc (c *Client) SetPathParamsWithStruct(v any) *Client\n```\n\n### DelPathParams\n\nDeletes one or more path parameters.\n\n```go title=\"Signature\"\nfunc (c *Client) DelPathParams(key ...string) *Client\n```\n\n## Cookies\n\n### Cookie\n\nReturns the value of a named cookie if set at the client level.\n\n```go title=\"Signature\"\nfunc (c *Client) Cookie(key string) string\n```\n\n### SetCookie\n\nSets a single cookie for all requests.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCookie(key, val string) *Client\n```\n\n**Example:**\n\n```go title=\"Example\"\ncc := client.New()\ncc.SetCookie(\"john\", \"doe\")\n\nresp, err := cc.Get(\"https://httpbin.org/cookies\")\nif err != nil {\n    panic(err)\n}\n\nfmt.Println(string(resp.Body()))\n```\n\n<details>\n<summary>Click here to see the result</summary>\n\n```json\n{\n  \"cookies\": {\n    \"john\": \"doe\"\n  }\n}\n```\n\n</details>\n\n### SetCookies\n\nSets multiple cookies at once.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCookies(m map[string]string) *Client\n```\n\n### SetCookiesWithStruct\n\nSets multiple cookies from a struct.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCookiesWithStruct(v any) *Client\n```\n\n### DelCookies\n\nDeletes one or more cookies.\n\n```go title=\"Signature\"\nfunc (c *Client) DelCookies(key ...string) *Client\n```\n\n## Timeout\n\n### SetTimeout\n\nSets a default timeout for all requests, which can be overridden per request.\n\n```go title=\"Signature\"\nfunc (c *Client) SetTimeout(t time.Duration) *Client\n```\n\n## Debugging\n\n### Debug\n\nEnables debug-level logging output.\n\n```go title=\"Signature\"\nfunc (c *Client) Debug() *Client\n```\n\n### DisableDebug\n\nDisables debug-level logging output.\n\n```go title=\"Signature\"\nfunc (c *Client) DisableDebug() *Client\n```\n\n## Cookie Jar\n\n### SetCookieJar\n\nAssigns a cookie jar to the client to store and manage cookies across requests.\n\n```go title=\"Signature\"\nfunc (c *Client) SetCookieJar(cookieJar *CookieJar) *Client\n```\n\n## Dial & Logger\n\n### SetDial\n\nSets a custom dial function.\n\n```go title=\"Signature\"\nfunc (c *Client) SetDial(dial fasthttp.DialFunc) *Client\n```\n\n### SetLogger\n\nSets the logger instance used by the client.\n\n```go title=\"Signature\"\nfunc (c *Client) SetLogger(logger log.CommonLogger) *Client\n```\n\n### Logger\n\nReturns the current logger instance.\n\n```go title=\"Signature\"\nfunc (c *Client) Logger() log.CommonLogger\n```\n\n## Reset\n\n### Reset\n\nClears and resets the client to its default state and reinstates the default\n`fasthttp.Client` transport.\n\n```go title=\"Signature\"\nfunc (c *Client) Reset()\n```\n\n## Default Client\n\nFiber provides a default client (created with `New()`). You can configure it or replace it as needed.\n\n### C\n\n**C** returns the default client.\n\n```go title=\"Signature\"\nfunc C() *Client\n```\n\n### Get\n\nGet is a convenience method that sends a GET request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Get(url string, cfg ...Config) (*Response, error)\n```\n\n### Post\n\nPost is a convenience method that sends a POST request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Post(url string, cfg ...Config) (*Response, error)\n```\n\n### Put\n\nPut is a convenience method that sends a PUT request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Put(url string, cfg ...Config) (*Response, error)\n```\n\n### Patch\n\nPatch is a convenience method that sends a PATCH request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Patch(url string, cfg ...Config) (*Response, error)\n```\n\n### Delete\n\nDelete is a convenience method that sends a DELETE request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Delete(url string, cfg ...Config) (*Response, error)\n```\n\n### Head\n\nHead sends a HEAD request using the `defaultClient`, a convenience method.\n\n```go title=\"Signature\"\nfunc Head(url string, cfg ...Config) (*Response, error)\n```\n\n### Options\n\nOptions is a convenience method that sends an OPTIONS request using the `defaultClient`.\n\n```go title=\"Signature\"\nfunc Options(url string, cfg ...Config) (*Response, error)\n```\n\n### Replace\n\n**Replace** replaces the default client with a new one. It returns a function that can restore the old client.\n\n:::caution\nDo not modify the default client concurrently.\n:::\n\n```go title=\"Signature\"\nfunc Replace(c *Client) func()\n```\n"
  },
  {
    "path": "docs/extra/_category_.json",
    "content": "{\n  \"label\": \"\\uD83E\\uDDE9 Extra\",\n  \"position\": 8,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"Extra contents for Fiber.\"\n  }\n}\n"
  },
  {
    "path": "docs/extra/benchmarks.md",
    "content": "---\nid: benchmarks\ntitle: 📊 Benchmarks\ndescription: >-\n  These benchmarks aim to compare the performance of Fiber and other web\n  frameworks.\nsidebar_position: 2\n---\n\n## TechEmpower\n\n[TechEmpower](https://www.techempower.com/benchmarks/#section=test&runid=1d5bfc8a-5c4a-4fb2-a792-ad967f1eb138) provides a performance comparison of many web application frameworks that execute fundamental tasks such as JSON serialization, database access, and server-side template rendering.\n\nEach framework runs under a realistic production configuration. Results are recorded on both cloud instances and physical hardware. The test implementations are community contributed and live in the [FrameworkBenchmarks repository](https://github.com/TechEmpower/FrameworkBenchmarks).\n\n* Fiber `v3.0.0`\n* 56 Cores Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz (Three homogeneous ProLiant DL360 Gen10 Plus)\n* 64GB RAM\n* Enterprise SSD\n* Ubuntu\n* Mellanox Technologies MT28908 Family ConnectX-6 40Gbps Ethernet\n\n### Plaintext\n\nThe Plaintext test measures basic request routing and demonstrates the capacity of high-performance platforms. Requests are pipelined, and the tiny response body demands high throughput to saturate the benchmark's gigabit Ethernet.\n\nSee [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#plaintext)\n\n**Fiber** - **11,987,976** responses per second with an average latency of **1.0** ms.\n**Express** - **1,204,969** responses per second with an average latency of **8.8** ms.\n\n![](/img/v3/plaintext.png)\n\n![Fiber vs Express](/img/v3/plaintext_express.png)\n\n### Data Updates\n\n**Fiber** handled **29,984** responses per second with an average latency of **16.9** ms.\n**Express** handled **54,887** responses per second with an average latency of **9.2** ms.\n\n![](/img/v3/data_updates.png)\n\n![Fiber vs Express](/img/v3/data_updates_express.png)\n\n### Multiple Queries\n\n**Fiber** handled **54,002** responses per second with an average latency of **9.4** ms.\n**Express** handled **85,011** responses per second with an average latency of **6.0** ms.\n\n![](/img/v3/multiple_queries.png)\n\n![Fiber vs Express](/img/v3/multiple_queries_express.png)\n\n### Single Query\n\n**Fiber** handled **953,016** responses per second with an average latency of **0.6** ms.\n**Express** handled **441,543** responses per second with an average latency of **1.3** ms.\n\n![](/img/v3/single_query.png)\n\n![Fiber vs Express](/img/v3/single_query_express.png)\n\n### JSON Serialization\n\n**Fiber** handled **2,363,294** responses per second with an average latency of **0.2** ms.\n**Express** handled **949,717** responses per second with an average latency of **0.5** ms.\n\n![](/img/v3/json.png)\n\n![Fiber vs Express](/img/v3/json_express.png)\n"
  },
  {
    "path": "docs/extra/faq.md",
    "content": "---\nid: faq\ntitle: 🤔 FAQ\ndescription: >-\n  Frequently asked questions. Open an issue if you have another question to add.\nsidebar_position: 1\n---\n\n## How should I structure my application?\n\nThere's no single answer; the ideal structure depends on your application's scale and team. Fiber makes no assumptions about project layout.\n\nRoutes and other application logic can live in any files or directories. For inspiration, see:\n\n* [gofiber/boilerplate](https://github.com/gofiber/boilerplate)\n* [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate)\n* [Youtube - Building a REST API using Gorm and Fiber](https://www.youtube.com/watch?v=Iq2qT0fRhAA)\n* [embedmode/fiberseed](https://github.com/embedmode/fiberseed)\n\n## How do I handle custom 404 responses?\n\nIf you're using v2.32.0 or later, implement a custom error handler as shown below or read more at [Error Handling](../guide/error-handling.md#custom-error-handler).\n\nIf you're using v2.31.0 or earlier, the error handler will not capture 404 errors. Instead, add a middleware function at the very bottom of the stack \\(below all other functions\\) to handle a 404 response:\n\n```go title=\"Example\"\napp.Use(func(c fiber.Ctx) error {\n    return c.Status(fiber.StatusNotFound).SendString(\"Sorry can't find that!\")\n})\n```\n\n## How can I use live reload?\n\n[Air](https://github.com/air-verse/air) automatically restarts your Go application when source files change, speeding development.\n\nTo use Air in a Fiber project, follow these steps:\n\n* Install Air by downloading the appropriate binary for your operating system from the GitHub release page or by building the tool from source.\n* Create a configuration file for Air in your project directory, such as `.air.toml` or `air.conf`. Here's a sample configuration file that works with Fiber:\n\n```toml\n# .air.toml\nroot = \".\"\ntmp_dir = \"tmp\"\n[build]\n  cmd = \"go build -o ./tmp/main .\"\n  bin = \"./tmp/main\"\n  delay = 1000 # ms\n  exclude_dir = [\"assets\", \"tmp\", \"vendor\"]\n  include_ext = [\"go\", \"tpl\", \"tmpl\", \"html\"]\n  exclude_regex = [\"_test\\\\.go\"]\n```\n\n* Start your Fiber application with Air by running the following command:\n\n```sh\nair\n```\n\nAs you edit source files, Air detects the changes and restarts the application.\n\nA complete example is available in the [Fiber Recipes repository](https://github.com/gofiber/recipes/tree/master/air) and shows how to configure Air for a Fiber project.\n\n## How do I set up an error handler?\n\nTo override the default error handler, provide a custom one in the [Config](../api/fiber.md#errorhandler) when creating a new [Fiber instance](../api/fiber.md#new).\n\n```go title=\"Example\"\napp := fiber.New(fiber.Config{\n    ErrorHandler: func(c fiber.Ctx, err error) error {\n        return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n    },\n})\n```\n\nWe have a dedicated page explaining how error handling works in Fiber, see [Error Handling](../guide/error-handling.md).\n\n## Which template engines does Fiber support?\n\nFiber currently supports 9 template engines in our [gofiber/template](https://docs.gofiber.io/template/) middleware:\n\n* [ace](https://docs.gofiber.io/template/ace/)\n* [amber](https://docs.gofiber.io/template/amber/)\n* [django](https://docs.gofiber.io/template/django/)\n* [handlebars](https://docs.gofiber.io/template/handlebars/)\n* [html](https://docs.gofiber.io/template/html/)\n* [jet](https://docs.gofiber.io/template/jet/)\n* [mustache](https://docs.gofiber.io/template/mustache/)\n* [pug](https://docs.gofiber.io/template/pug/)\n* [slim](https://docs.gofiber.io/template/slim/)\n\nTo learn more about using Templates in Fiber, see [Templates](../guide/templates.md).\n\n## Does Fiber have a community chat?\n\nYes, we have a [Discord](https://gofiber.io/discord) server with rooms for every topic.\nIf you have questions or just want to chat, join us via this [invite link](https://gofiber.io/discord).\n\n![](/img/support-discord.png)\n\n## Does Fiber support subdomain routing?\n\nYes, we do. Here are some examples:\n\n<details>\n<summary>Example</summary>\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n)\n\ntype Host struct {\n    Fiber *fiber.App\n}\n\nfunc main() {\n    // Hosts\n    hosts := map[string]*Host{}\n    //-----\n    // API\n    //-----\n    api := fiber.New()\n    api.Use(logger.New(logger.Config{\n        Format: \"[${ip}]:${port} ${status} - ${method} ${path}\\n\",\n    }))\n    hosts[\"api.localhost:3000\"] = &Host{api}\n    api.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"API\")\n    })\n    //------\n    // Blog\n    //------\n    blog := fiber.New()\n    blog.Use(logger.New(logger.Config{\n        Format: \"[${ip}]:${port} ${status} - ${method} ${path}\\n\",\n    }))\n    hosts[\"blog.localhost:3000\"] = &Host{blog}\n    blog.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Blog\")\n    })\n    //---------\n    // Website\n    //---------\n    site := fiber.New()\n    site.Use(logger.New(logger.Config{\n        Format: \"[${ip}]:${port} ${status} - ${method} ${path}\\n\",\n    }))\n\n    hosts[\"localhost:3000\"] = &Host{site}\n    site.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Website\")\n    })\n    // Server\n    app := fiber.New()\n    app.Use(func(c fiber.Ctx) error {\n        host := hosts[c.Hostname()]\n        if host == nil {\n            return c.SendStatus(fiber.StatusNotFound)\n        } else {\n            host.Fiber.Handler()(c.Context())\n            return nil\n        }\n    })\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n</details>\n\nFor more information, see issue [#750](https://github.com/gofiber/fiber/issues/750).\n\n## How can I handle conversions between Fiber and net/http?\n\nFiber can register common `net/http` handlers directly—just pass an\n`http.Handler`, `http.HandlerFunc`, compatible function, or even a native\n`fasthttp.RequestHandler` to your routing method. For other interoperability scenarios, the `adaptor` middleware provides\nutilities for converting between Fiber and `net/http`. It allows seamless\nintegration of `net/http` handlers, middleware, and requests into Fiber\napplications, and vice versa.\n\n:::caution Performance trade-offs\nConverted `net/http` handlers run through a compatibility layer. They won't expose\n`fiber.Ctx` or Fiber-specific helpers, and the extra adaptation work makes them slower\nthan native Fiber handlers. Use them when interoperability matters, but prefer Fiber\nhandlers for maximum performance.\n:::\n\nFor details on how to:\n\n* Convert `net/http` handlers to Fiber handlers\n* Convert Fiber handlers to `net/http` handlers\n* Convert `fiber.Ctx` to `http.Request`\n\nSee the dedicated documentation: [Adaptor Documentation](../middleware/adaptor.md).\n"
  },
  {
    "path": "docs/extra/internal.md",
    "content": "---\ntitle: 🏗️ Internal Architecture\ndescription: >-\n  Learn about the internal architecture of Fiber, including the overall structure, request handling flow, routing, and path parsing.\nsidebar_position: 3\n---\n## Overall Architecture\n\nAt the heart of Fiber is the **App** struct. It is responsible for configuring the server, managing a pool of Contexts (either our default implementation, **DefaultCtx**, or a user‑supplied **CustomCtx**), and holding the router stack with all registered routes and groups. In addition, the App contains mount fields to support sub‑applications and hooks that allow developers to run custom code at key stages (e.g. when registering routes or starting the server).\n\n```mermaid\nflowchart TD\n    A[App]\n    B[\"Configuration (Config)\"]\n    C[Context Pool]\n    D[\"DefaultCtx \\/ CustomCtx\"]\n    E[Router Stack]\n    F[\"Groups & Routes\"]\n    G[\"MountFields (Sub‑Apps)\"]\n    H[Hooks]\n\n    A --> B\n    A --> C\n    C --> D\n    A --> E\n    E --> F\n    A --> G\n    A --> H\n```\n\n### Explanation\n\n- App: The central object that bootstraps and runs the Fiber server.\n- Configuration (Config): Contains settings for body limits, timeouts, TLS options, routing behavior (e.g. case‑sensitivity, strict routing), and more.\n- Context Pool: A synchronized pool from which Contexts are acquired per request. This design minimizes allocations by recycling DefaultCtx (or CustomCtx) instances.\n- Router Stack: Organizes all registered routes. It is later processed into a tree structure for fast route‑matching.\n- MountFields: Support for mounting sub‑applications so that large APIs can be segmented into independent routers.\n- Hooks: Allow for custom behavior at critical points (e.g., on route registration, route naming, on listen, on shutdown, etc.).\n\n## Request Processing Flow\n\nFiber’s request processing is designed for performance and minimal overhead. When an HTTP request is received by the underlying fasthttp server, the flow is as follows:\n\n1. Request Arrival: The fasthttp server receives the HTTP request.\n2. Context Acquisition: The App calls AcquireCtx() to fetch a Context from the pool.\n3. Context Reset: The acquired Context is reset (via DefaultCtx.Reset()) with the new request’s data.\n4. Request Handling: The request handler (default or custom) is invoked.\n5. Route Matching: The framework uses the next() (or nextCustom()) function to traverse the pre‑built route tree and find a matching route based on the URL and HTTP method.\n6. Middleware Chain Execution: The matched route’s handler chain is executed in sequence.\n7. Error Handling (if required): Any errors encountered trigger the registered error handler.\n8. Response Generation: The response is sent back to the client.\n9. Context Release: Finally, the Context is cleaned up and returned to the pool.\n\n```mermaid\nflowchart LR\n    R[\"HTTP Request (fasthttp)\"]\n    A[\"App.RequestHandler<br/>(default or custom)\"]\n    C[\"Acquire Context<br/>(from Pool)\"]\n    X[\"Reset Context<br/>(DefaultCtx.Reset())\"]\n    N[\"Route Matching<br/>(next() \\/ nextCustom())\"]\n    M[\"Handler Chain Execution\"]\n    EH[\"Error Handling<br/>(if needed)\"]\n    S[\"HTTP Response\"]\n    RC[\"Release Context<br/>(to Pool)\"]\n\n    R --> A\n    A --> C\n    C --> X\n    X --> N\n    N --> M\n    M --> EH\n    EH --> S\n    S --> RC\n```\n\n### Additional Note\n\nFiber minimizes memory allocations by reusing Context objects and uses an optimized route‑matching algorithm to rapidly determine the correct handler chain.\n\n## Routing & Path Parsing\n\nFiber allows you to register routes using helper methods (e.g. Get(), Post()) or by creating groups and sub‑routers. Internally, the route pattern is parsed by the parseRoute() function. This function decomposes the route string into segments:\n\n- Constant Segments: Fixed parts of the path (e.g. /api).\n- Parameter Segments: Dynamic parts that begin with a colon. For example, a route may be defined as:\n  /api/\\:userId&lt;int&gt;\n  Here, the segment \\:userId&lt;int&gt; is a parameter segment with a type constraint (an integer).\n- Constraints: Constraints (such as int, bool, datetime, or even regular expressions) are extracted from the parameter part and stored in the route’s metadata for validation at runtime.\n\n```mermaid\nflowchart TD\n    P[\"Route Pattern String<br/>(e.g., '/api/\\\\:userId\\\\&lt;int&gt;')\"]\n    PA[\"parseRoute()\"]\n    RP[routeParser]\n    RS[\"routeSegment(s)\"]\n    C[\"Constraints<br/>(e.g., int, datetime, regex)\"]\n    PARAM[Extracted Parameter Names]\n\n    P --> PA\n    PA --> RP\n    RP --> RS\n    RS --> C\n    RP --> PARAM\n```\n\n### Explanation\n\n- parseRoute(): Takes a route string and returns a routeParser struct that includes a list of routeSegment objects.\n- routeSegment: Represents a portion of the route. If it is a parameter segment, it may include constraints that determine the allowed format (for example, ensuring that a parameter is an integer).\n- Extracted Parameter Names: These are later used to populate the request’s Context with the actual values parsed from the URL.\n\n## Route Matching and Parameter Extraction\n\nWhen a request is processed, Fiber uses its pre‑computed route tree (the treeStack) to efficiently match the incoming URL against registered routes.\n\n1. Normalization: The URL is normalized (converted to lowercase, trailing slashes trimmed) to create a “detection path.”\n2. Tree Traversal: The route tree, grouped by common prefixes, is traversed based on the HTTP method.\n3. Matching: Constant segments are compared exactly, while parameter segments extract dynamic values.\n4. Constraint Validation: Extracted parameter values are validated against any defined constraints.\n\n```mermaid\nflowchart TD\n    A[\"Incoming Request URL<br/>(e.g., '/api/john')\"]\n    B[\"Normalize URL<br/>(lowercase, trim trailing slashes)\"]\n    C[\"Detection Path\"]\n    D[\"Traverse Route Tree<br/>(treeStack based on method)\"]\n    E[\"Match Constant Segments\"]\n    F[\"Identify Parameter Segments<br/>(e.g., ':userId')\"]\n    G[\"Extract Parameter Values\"]\n    H[\"Validate Constraints<br/>(e.g., 'int', 'datetime', 'regex')\"]\n    I[\"Route Found\"]\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n    F --> G\n    G --> H\n    H --> I\n```\n\n### Insight\n\nThis efficient matching mechanism leverages pre‑grouped routes to minimize comparisons, while dynamic segments allow for flexible URL structures and runtime validation.\n\n## Middleware Chain Execution\n\nOnce a matching route is found, Fiber executes the chain of middleware and route handlers sequentially. The process is as follows:\n\n1. Initial Handler Execution: The first handler of the matched route is invoked.\n2. Calling Next(): Each handler calls Ctx.Next() to pass control to the next handler in the chain.\n3. Termination: When no further handlers remain, the chain terminates and the response is sent.\n\n```mermaid\nflowchart TD\n    A[Matched Route]\n    B[Handler 1]\n    C[Handler 2]\n    D[Handler 3]\n    E[Response Generation]\n\n    A --> B\n    B -- \"Calls C via Next()\" --> C\n    C -- \"Calls D via Next()\" --> D\n    D -- \"No Next() available\" --> E\n```\n\n### Explanation\n\n- Each handler in the chain can perform operations (e.g. authentication, logging, transformation) before calling Next() to forward control.\n- This sequential processing ensures that middleware are executed in the order they were registered.\n- If an error occurs or a handler does not call Next(), the chain may be terminated early, and an error handler may be invoked.\n\n### Observations\n\nMiddleware are executed in the order they are registered. This sequential design allows each handler to perform tasks such as authentication, logging, or transformation before delegating to the next handler.\n\n## Sub-Application Mounting & Grouping\n\nFiber allows mounting sub‑applications (or sub‑routers) under specific path prefixes. This enables modular design of large APIs. The mounting process works as follows:\n\n1. Defining a Mount Point: A parent application (or group) calls `Use` with a sub-app, which triggers the internal mount path logic.\n2. Merging Mount Fields: The sub‑app’s mount fields are updated with the prefix of the parent, and its routes are integrated into the parent’s routing structure.\n3. Processing Sub‑App Routes: During startup, the parent app collects routes from mounted sub‑apps and builds a unified route tree.\n\n```mermaid\nflowchart TD\n    A[Parent App]\n    B[\"Sub-App (Mounted)\"]\n    C[\"Define Mount Point<br/>(e.g. \\'/admin\\')\"]\n    D[\"Update MountFields<br/>(assign mount path)\"]\n    E[\"Merge Sub-App Routes<br/>(append to Router Stack)\"]\n    F[Generate Unified Route Tree]\n\n    A --> C\n    C --> B\n    B --> D\n    D --> E\n    E --> F\n```\n\n### Impact\n\nThis mechanism enables large APIs to be broken down into smaller, maintainable modules while still benefiting from Fiber’s optimized routing and request handling.\n\n## Route Tree Building\n\nFiber builds a route tree (the treeStack) to optimize route matching. This involves grouping routes based on a prefix (usually the first few characters) to reduce the number of comparisons during a request.\n\n1. Iterating Over the Router Stack: Each registered route is examined.\n2. Computing the Tree Key: A key is computed from the route’s normalized path (e.g. the first 3 characters).\n3. Grouping Routes: Routes are added to the appropriate branch of the tree.\n4. Sorting: Within each group, routes are sorted based on their registration order (or position) to ensure the correct match is found.\n\n```mermaid\nflowchart TD\n    A[\"Router Stack<br/>(All Registered Routes)\"]\n    B[\"Compute Tree Key<br/>(e.g. first 3 characters)\"]\n    C[\"Group Routes by Key<br/>(treeStack)\"]\n    D[\"Merge Global Routes<br/>(key \\'\\' for global matches)\"]\n    E[Sort Routes within Groups]\n    F[Optimized Route Tree]\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n```\n\n### Explanation\n\n- Building a route tree is an optimization step that reduces the matching overhead by limiting the search space to a subset of routes that share a common prefix.\n- The tree is rebuilt whenever new routes are registered, ensuring that the latest routing configuration is always used for matching.\n\n## Context Lifecycle Management\n\nFiber minimizes allocations by pooling Context objects. The lifecycle of a Context is as follows:\n\n1. **Acquisition:** When a new HTTP request arrives, a Context is retrieved from the pool via `App.AcquireCtx()`.\n2. **Reset:** The acquired Context is reset with the current `fasthttp.RequestCtx` to clear previous data and initialize new request‑specific values.\n3. **Processing:** The Context is passed along the middleware and handler chain.\n4. **Release:** After processing the request (or when an error occurs), the Context is released back to the pool via `App.ReleaseCtx()`, making it available for reuse.\n\n```mermaid\nflowchart TD\n    A[\"HTTP Request<br/>(fasthttp)\"]\n    B[\"Acquire Context<br/>(App.AcquireCtx())\"]\n    C[\"Reset Context<br/>(DefaultCtx.Reset())\"]\n    D[\"Process Request<br/>(Handlers & Middleware)\"]\n    E[\"Error Handling<br/>(if needed)\"]\n    F[\"Release Context<br/>(App.ReleaseCtx())\"]\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n```\n\n### Key Benefit\n\nReusing Context objects significantly reduces garbage collection overhead, ensuring Fiber remains fast and memory‑efficient even under heavy load.\n\n## Preforking Mechanism\n\nTo take full advantage of multi‑core systems, Fiber offers a prefork mode. In this mode, the master process spawns several child processes that listen on the same port using OS features such as SO_REUSEPORT (or fall back to SO_REUSEADDR).\n\n```mermaid\nflowchart LR\n    M[\"Master Process (App)\"]\n    C[Child Processes]\n    GOMAX[\"Set GOMAXPROCS(1)\"]\n    REQ[Handle HTTP Requests]\n    WM[\"watchMaster()\"]\n\n    M -->|Spawns| C\n    C --> GOMAX\n    C -->|Processes| REQ\n    C --> WM\n```\n\n### Explanation\n\n- Master Process: The main process determines the number of available CPU cores and spawns that many child processes.\n- Child Processes: Each child sets GOMAXPROCS(1) to run on a single CPU core and listens on the shared port.\n- watchMaster(): Each child process runs a watchdog routine to monitor the master process; if the master exits (or its parent process ID becomes 1 on Unix‑like systems), the child terminates gracefully.\n\n### Detailed Preforking Workflow\n\nFiber’s prefork mode uses OS‑level mechanisms to allow multiple processes to listen on the same port. Here’s a more detailed look:\n\n1. Master Process Spawning: The master process detects the number of CPU cores and spawns that many child processes.\n2. Child Process Initialization: Each child process sets GOMAXPROCS(1) so that it runs on a single core.\n3. Binding to Port: Child processes use packages like reuseport to bind to the same address and port.\n4. Parent Monitoring: Each child runs a watchdog function (watchMaster()) to monitor the master process; if the master terminates, children exit.\n5. Request Handling: Each child independently handles incoming HTTP requests.\n\n```mermaid\nflowchart TD\n    A[Master Process]\n    B[Determine CPU Cores]\n    C[Spawn Child Processes]\n    D[\"Child Process Initialization<br/>(GOMAXPROCS(1))\"]\n    E[\"Bind to Port<br/>(reuseport)\"]\n    F[\"Run watchMaster()<br/>(Monitor Parent)\"]\n    G[Handle HTTP Requests]\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n    F --> G\n```\n\n#### Explanation\n\n- Preforking improves performance by allowing multiple processes to handle requests concurrently.\n- Using reuseport (or a fallback) ensures that all child processes can listen on the same port without conflicts.\n- The watchdog routine in each child ensures that they exit if the master process is no longer running, maintaining process integrity.\n\n## Redirection & Flash Messages\n\nFiber’s redirection mechanism is implemented via the Redirect struct. This structure allows not only setting a new location for redirection but also passing along flash messages and old input data via a special cookie.\n\n```mermaid\nflowchart LR\n    R[Redirect Struct]\n    RP[redirectPool]\n    FM[\"Flash Messages \\/ Old Inputs\"]\n    M[\"Methods:<br/>To(), Route(), Back()\"]\n    LH[Set Location Header]\n    CK[\"Flash Cookie<br/>(fiber\\_flash)\"]\n\n    R -->|Acquired from| RP\n    R --> FM\n    R --> M\n    M --> LH\n    FM -->|Serialized| CK\n```\n\n### Explanation\n\n- Redirect Struct: Retrieved from a pool (to minimize allocations), it stores redirection settings such as the HTTP status code (defaulting to 303 See Other) and any flash messages.\n- Flash Messages & Old Inputs: These are collected via methods like With() or WithInput() and then serialized and stored in a cookie named fiber_flash.\n- Redirection Methods: The To(), Route(), and Back() methods determine the target URL and set the Location header accordingly.\n\n### Flash Message Handling in Redirection\n\nWhen performing redirections, Fiber can send flash messages or preserve old input data. This process involves:\n\n1. Collecting Flash Data: When a redirect is initiated, developers can add flash messages via Redirect.With() or old input data via Redirect.WithInput().\n2. Serialization: The flash messages and input data are serialized (using a fast marshalling method) into a byte sequence.\n3. Setting a Cookie: The serialized data is stored in a special cookie (named fiber_flash) that will be sent to the client.\n4. Retrieval & Clearing: On the subsequent request, the flash data is read from the cookie, deserialized, and then cleared.\n\n```mermaid\nflowchart TD\n    A[Initiate Redirect]\n    B[\"Add Flash Messages<br/>(With(), WithInput())\"]\n    C[Serialize Flash Data]\n    D[\"Set Flash Cookie<br/>(\\'fiber\\_flash\\')\"]\n    E[Client Receives Redirect]\n    F[Next Request Reads Flash Cookie]\n    G[\"Deserialize & Clear Flash Data\"]\n\n    A --> B\n    B --> C\n    C --> D\n    D --> E\n    E --> F\n    F --> G\n```\n\n#### Explanation\n\n- Flash messages provide a way to pass transient data (such as notifications or error messages) to the next request after a redirect.\n- The data is stored temporarily in a cookie, which is then read and cleared upon processing the next request.\n- This mechanism is essential for implementing post‑redirect‑get patterns and ensuring a smooth user experience.\n\n## Hooks, Error Handling & Context Lifecycle\n\n### Hooks\n\nFiber provides a comprehensive hook system that allows you to run custom functions at key moments:\n\n- OnRoute: Called when a route is registered.\n- OnName: Invoked when a route is assigned a name.\n- OnGroup: Triggered when a group is created.\n- OnListen: Runs when the server starts listening.\n- OnShutdown: Called during graceful shutdown.\n- OnFork: Invoked when a child process is forked.\n- OnMount: Used when a sub‑application is mounted.\n\n```mermaid\nflowchart TD\n    H[Hooks]\n    OR[OnRoute]\n    ON[OnName]\n    OG[OnGroup]\n    OL[OnListen]\n    OS[OnShutdown]\n    OF[OnFork]\n    OM[OnMount]\n\n    H --> OR\n    H --> ON\n    H --> OG\n    H --> OL\n    H --> OS\n    H --> OF\n    H --> OM\n```\n\n#### Explanation\n\n- Hooks provide extension points for developers and maintainers to inject custom logic without modifying the core Fiber code.\n- They are executed at various stages (for example, every time a new route is registered, the OnRoute hooks are executed to allow for logging, validation, or transformation of the route).\n\n### Error Handling & Context Lifecycle\n\nFiber’s DefaultCtx (or CustomCtx) represents the per‑request context. The lifecycle is as follows:\n\n- Acquire: A Context is obtained from the pool at the beginning of a request.\n- Processing: The context is passed along to the route handlers and middleware.\n- Error Handling: If an error occurs (e.g., route not found, method not allowed, or a panic in the handler), Fiber calls the registered error handler. Errors such as ErrMethodNotAllowed or StatusNotFound are generated as needed.\n- Release: Once the request is processed, the Context is released back into the pool for reuse.\n\n```mermaid\nflowchart LR\n    AC[\"Acquire Context<br/>(from Pool)\"]\n    HP[\"Handle Request<br/>(Handlers & Middleware)\"]\n    EH[\"Error Handling<br/>(if needed)\"]\n    RC[\"Release Context<br/>(to Pool)\"]\n\n    AC --> HP\n    HP --> EH\n    EH --> RC\n```\n\n#### Explanation\n\n- This lifecycle ensures that Fiber minimizes allocations by reusing Context objects.\n- Errors are propagated and handled consistently, and the context is properly reset after every request.\n"
  },
  {
    "path": "docs/extra/learning-resources.md",
    "content": "---\nid: learning-resources\ntitle: 📚 Learning Resources\ndescription: >-\n  Interactive learning platforms and community resources to help you learn Fiber concepts through hands-on practice.\nsidebar_position: 3\n---\n\n## Interactive Learning Platforms\n\nLooking to practice Fiber concepts through hands-on exercises? Here are some community-driven learning resources:\n\n### Go Interview Practice - Fiber Challenges\n\nA comprehensive platform offering progressive Fiber challenges that complement the official documentation.\n\n![Learning Path Overview](/img/learning-resources/fiber-learning-path.png)\n\n**What You'll Learn:**\n\n- **High-Performance APIs** - Build ultra-fast RESTful APIs with zero-allocation routing\n- **Middleware & Security** - Implement custom middleware, rate limiting, CORS, and authentication\n- **Request Validation** - Input validation, error handling, and data transformation\n- **Authentication & JWT** - Secure authentication systems with JWT tokens and API key validation\n\n![Challenge Interface](/img/learning-resources/fiber-challenge-interface.png)\n\n**Challenge Roadmap:**\n\n1. **Basic Routing** - Setup Fiber, routes, and handlers (Beginner)\n2. **Middleware & CORS** - Custom middleware and rate limiting (Intermediate)\n3. **Validation & Errors** - Input validation and error handling (Intermediate)\n4. **Authentication** - JWT tokens and API key validation (Advanced)\n\n![Fiber Framework Overview](/img/learning-resources/fiber-framework-overview.png)\n\n![Interactive Learning Experience](/img/learning-resources/fiber-learning-experience.png)\n\n[Explore Fiber Challenges →](https://rezasi.github.io/go-interview-practice/fiber) | [GitHub Repository →](https://github.com/RezaSi/go-interview-practice)\n"
  },
  {
    "path": "docs/guide/_category_.json",
    "content": "{\n  \"label\": \"\\uD83D\\uDCD6 Guide\",\n  \"position\": 7,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"Guides for Fiber.\"\n  }\n}\n"
  },
  {
    "path": "docs/guide/advance-format.md",
    "content": "---\nid: advance-format\ntitle: 🐛 Advanced Format\ndescription: >-\n  Learn how to use MessagePack (MsgPack) and CBOR for efficient binary serialization in Fiber applications.\nsidebar_position: 9\n---\n\n## MsgPack\n\nFiber lets you use MessagePack for efficient binary serialization. Use one of the popular Go libraries below to encode and decode data in handlers.\n\n- Fiber can bind requests with the `application/vnd.msgpack` content type out of the box. See the [Binding documentation](../api/bind.md#msgpack) for details.\n- Use `Bind().MsgPack()` to bind data to structs, similar to JSON. `Ctx.AutoFormat()` responds with MsgPack when the `Accept` header is `application/vnd.msgpack`. See the [AutoFormat documentation](../api/ctx.md#autoformat) for more.\n\n### Recommended Libraries\n\n- [github.com/vmihailenco/msgpack](https://pkg.go.dev/github.com/vmihailenco/msgpack) — A widely used, feature-rich MsgPack library.\n- [github.com/shamaton/msgpack/v3](https://pkg.go.dev/github.com/shamaton/msgpack/v3) — High-performance MsgPack library.\n\n### Installation\n\nInstall either library using:\n\n```bash\ngo get github.com/vmihailenco/msgpack\n# or\ngo get github.com/shamaton/msgpack/v3\n```\n\n> **Note:** Fiber doesn't bundle a MsgPack implementation because it's outside the Go standard library. Pick one of the popular libraries in the ecosystem; the two below are widely used and well maintained.\n\n### Example: Using `shamaton/msgpack/v3`\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/shamaton/msgpack/v3\"\n)\n\ntype User struct {\n    Name string `msgpack:\"name\"` // tag may vary depending on your MsgPack library\n    Age  int   `msgpack:\"age\"`\n}\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        // Optional: Set custom MsgPack encoder/decoder\n        MsgPackEncoder: msgpack.Marshal,\n        MsgPackDecoder: msgpack.Unmarshal,\n    })\n\n    app.Post(\"/msgpack\", func(c fiber.Ctx) error {\n        var user User\n        if err := c.Bind().MsgPack(&user); err != nil {\n            return err\n        }\n        // Content type will be set automatically to application/vnd.msgpack\n        return c.MsgPack(user)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n## CBOR\n\nFiber doesn't ship with a CBOR implementation. Use a library such as [fxamacker/cbor](https://github.com/fxamacker/cbor) to add encoding and decoding.\n\n- Use `Bind().CBOR()` to bind CBOR to structs. `Ctx.AutoFormat()` replies with CBOR when the `Accept` header is `application/cbor`. See the [AutoFormat documentation](../api/ctx.md#autoformat) for details.\n\n```bash\ngo get github.com/fxamacker/cbor/v2\n```\n\nConfigure Fiber with the chosen library:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/fxamacker/cbor/v2\"\n)\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        CBOREncoder: cbor.Marshal,\n        CBORDecoder: cbor.Unmarshal,\n    })\n\n    type User struct {\n        Name string `cbor:\"name\"`\n        Age  int    `cbor:\"age\"`\n    }\n\n    app.Post(\"/cbor\", func(c fiber.Ctx) error {\n        var user User\n        if err := c.Bind().CBOR(&user); err != nil {\n            return err\n        }\n\n        // Content type will be set automatically to application/cbor\n        return c.CBOR(user)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n"
  },
  {
    "path": "docs/guide/context.md",
    "content": "---\nid: go-context\ntitle: \"\\U0001F9E0 Go Context\"\ndescription: >-\n  Learn how Fiber's Ctx integrates with Go's context.Context,\n  how to interact with the underlying fasthttp RequestCtx,\n  and how to use the available context helpers.\nsidebar_position: 6\ntoc_max_heading_level: 4\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Fiber Context as `context.Context`\n\nFiber's [`Ctx`](../api/ctx.md) implements Go's\n[`context.Context`](https://pkg.go.dev/context#Context) interface.\nYou can pass `c` directly to functions that expect a `context.Context`\nwithout adapters.\nHowever, `fasthttp` doesn't support cancellation yet, so\n`Deadline`, `Done`, and `Err` are no-ops.\n\n:::caution\nThe `fiber.Ctx` instance is only valid within the lifetime of the handler.\nIt is reused for subsequent requests, so avoid storing `c` or using it in\ngoroutines that outlive the handler. For asynchronous work, call\n`c.Context()` inside the handler to obtain a `context.Context` that can safely\nbe used after the handler returns. By default, this returns `context.Background()`\nunless a custom context was provided with `c.SetContext`.\n:::\n\n```go title=\"Example\"\nfunc doSomething(ctx context.Context) {\n    // ... your logic here\n}\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    doSomething(c) // c satisfies context.Context\n    return nil\n})\n```\n\n### Using context outside the handler\n\n`fiber.Ctx` is recycled after each request. If you need a context that lives\nlonger—for example, for work performed in a new goroutine—obtain it with\n`c.Context()` before returning from the handler.\n\n```go title=\"Async work\"\napp.Get(\"/job\", func(c fiber.Ctx) error {\n    ctx := c.Context()\n    go performAsync(ctx)\n    return c.SendStatus(fiber.StatusAccepted)\n})\n```\n\nYou can customize the base context by calling `c.SetContext` before\nrequesting it:\n\n```go\napp.Get(\"/job\", func(c fiber.Ctx) error {\n    c.SetContext(context.WithValue(context.Background(), \"requestID\", \"123\"))\n    ctx := c.Context()\n    go performAsync(ctx)\n    return nil\n})\n```\n\n### Retrieving Values\n\n`Ctx.Value` is backed by [Locals](../api/ctx.md#locals).\nValues stored with `c.Locals` are accessible through `Value` or\nstandard `context.WithValue` helpers.\n\n```go title=\"Locals and Value\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    c.Locals(\"role\", \"admin\")\n    role := c.Value(\"role\") // returns \"admin\"\n    return c.SendString(role.(string))\n})\n```\n\n## Working with `RequestCtx` and `fasthttpctx`\n\nThe underlying [`fasthttp.RequestCtx`](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx)\ncan be accessed via `c.RequestCtx()`.\nThis exposes low-level APIs and the extra context support provided by\n`fasthttpctx`.\n\n```go title=\"Accessing RequestCtx\"\napp.Get(\"/raw\", func(c fiber.Ctx) error {\n    fctx := c.RequestCtx()\n    // use fasthttp APIs directly\n    fctx.Response.Header.Set(\"X-Engine\", \"fasthttp\")\n    return nil\n})\n```\n\n`fasthttpctx` enables `fasthttp` to satisfy the `context.Context` interface.\n`Deadline` always reports no deadline, `Done` is closed when the client\nconnection ends, and once it fires `Err` reports `context.Canceled`. This\nmeans handlers can detect client disconnects while still passing\n`c.RequestCtx()` into APIs that expect a `context.Context`.\n\n## Context Helpers\n\nFiber and its middleware expose a number of helper functions that\nretrieve request-scoped values from the context.\n\n### Request ID\n\nThe RequestID middleware stores the generated identifier in the context.\nUse `requestid.FromContext` to read it later.\n\n```go\napp.Use(requestid.New())\napp.Get(\"/\", func(c fiber.Ctx) error {\n    id := requestid.FromContext(c)\n    return c.SendString(id)\n})\n```\n\n### CSRF\n\nThe CSRF middleware provides helpers to fetch the token or the handler\nattached to the current context.\n\n```go\napp.Use(csrf.New())\napp.Get(\"/form\", func(c fiber.Ctx) error {\n    token := csrf.TokenFromContext(c)\n    return c.SendString(token)\n})\n```\n\n```go title=\"Deleting a token\"\napp.Post(\"/logout\", func(c fiber.Ctx) error {\n    handler := csrf.HandlerFromContext(c)\n    if handler != nil {\n        // Invalidate the token on logout\n        _ = handler.DeleteToken(c)\n    }\n    // ... other logout logic\n    return c.SendString(\"Logged out\")\n})\n```\n\n### Session\n\nSessions are stored on the context and can be retrieved via\n`session.FromContext`.\n\n```go\napp.Use(session.New())\napp.Get(\"/\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n    count := sess.Get(\"visits\")\n    return c.JSON(fiber.Map{\"visits\": count})\n})\n```\n\n### Basic Authentication\n\nAfter successful authentication, the username is available with\n`basicauth.UsernameFromContext`. Passwords in `Users` must be pre-hashed.\n\n```go\napp.Use(basicauth.New(basicauth.Config{\n    Users: map[string]string{\n        // \"secret\" hashed using SHA-256\n        \"admin\": \"{SHA256}K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=\",\n    },\n}))\napp.Get(\"/\", func(c fiber.Ctx) error {\n    user := basicauth.UsernameFromContext(c)\n    return c.SendString(user)\n})\n```\n\n### Key Authentication\n\nFor API key authentication, the extracted token is stored in the\ncontext and accessible via `keyauth.TokenFromContext`.\n\n```go\napp.Use(keyauth.New())\napp.Get(\"/\", func(c fiber.Ctx) error {\n    token := keyauth.TokenFromContext(c)\n    return c.SendString(token)\n})\n```\n\n## Using `context.WithValue` and Friends\n\nSince `fiber.Ctx` conforms to `context.Context`, standard helpers such as\n`context.WithValue`, `context.WithTimeout`, or `context.WithCancel`\ncan wrap the request context when needed.\n\n```go\napp.Get(\"/job\", func(c fiber.Ctx) error {\n    ctx, cancel := context.WithTimeout(c, 5*time.Second)\n    defer cancel()\n\n    // pass ctx to async operations that honor cancellation\n    if err := doWork(ctx); err != nil {\n        return err\n    }\n    return c.SendStatus(fiber.StatusOK)\n})\n```\n\n### Context Cancellation with Goroutines in Fiber\n\nWhen starting asynchronous work inside a handler, Fiber does not cancel the base `fiber.Ctx` automatically.\nBy wrapping the request context with `context.WithTimeout`, you can create a derived context that honors deadlines and cancellation signals.\n\nThe goroutine checks `ctx.Done()` before sending a result.\nIf the request times out or the client disconnects the goroutine exits early and avoids leaking resources.\n\nThe handler then waits for either:\n\n- a result from the goroutine, or\n- the `context timeout` (which returns a 504 Gateway Timeout)\n\nThis pattern ensures that long-running operations (database queries, external API calls, background tasks) do not continue running after the request has ended.\n\n```go\nfunc Handler(c fiber.Ctx) error {\n    ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)\n    defer cancel()\n\n    resultChan := make(chan string, 1)\n\n    go func() {\n        select {\n        case <-time.After(3 * time.Second):\n            select {\n            case <-ctx.Done():\n                return\n            case resultChan <- \"done\":\n            }\n        case <-ctx.Done():\n            return\n        }\n    }()\n\n    select {\n    case res := <-resultChan:\n        return c.SendString(res)\n    case <-ctx.Done():\n        return c.Status(fiber.StatusGatewayTimeout).SendString(\"timeout\")\n    }\n}\n```\n\nThis approach provides safe cancellation semantics for goroutine-based work while allowing you to integrate Fiber handlers with context-aware APIs.\n\n## Summary\n\n- `fiber.Ctx` satisfies `context.Context` but its `Deadline`, `Done`, and `Err`\n  methods are currently no-ops.\n- `RequestCtx` exposes the raw `fasthttp` context, whose `Done` channel closes\n  when the client connection ends.\n- Use `fiber.StoreInContext(c, key, value)` to store request-scoped values in both\n  `c.Locals()` and `c.Context()` when values must be available through either API.\n- Middleware helpers like `requestid.FromContext` or `session.FromContext`\n  make it easy to retrieve request-scoped data.\n- Standard helpers such as `context.WithTimeout` can wrap `fiber.Ctx` to create\n  fully featured derived contexts inside handlers.\n- `fiber.Config.PassLocalsToContext` controls whether Fiber context helpers\n  also propagate values into the request `context.Context` for Fiber-backed\n  contexts when using `StoreInContext`. It defaults to `false` for backward\n  compatibility, while `ValueFromContext` keeps reading from `c.Locals()`.\n- Use `c.Context()` to obtain a `context.Context` that can outlive the handler,\n  and `c.SetContext()` to customize it with additional values or deadlines.\n\nWith these tools, you can seamlessly integrate Fiber applications with\nGo's context-based APIs and manage request-scoped data effectively.\n"
  },
  {
    "path": "docs/guide/error-handling.md",
    "content": "---\nid: error-handling\ntitle: 🐛 Error Handling\ndescription: >-\n  Fiber supports centralized error handling: handlers return errors so you can\n  log them or send a custom HTTP response to the client.\nsidebar_position: 4\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n## Catching Errors\n\nReturn errors from route handlers and middleware so Fiber can handle them centrally.\n\n<Tabs>\n<TabItem value=\"example\" label=\"Example\">\n\n```go\napp.Get(\"/\", func(c fiber.Ctx) error {\n    // Pass error to Fiber\n    return c.SendFile(\"file-does-not-exist\")\n})\n```\n\n</TabItem>\n</Tabs>\n\nFiber does not recover from [panics](https://go.dev/blog/defer-panic-and-recover) by default. Add the `Recover` middleware to catch panics in any handler:\n\n```go title=\"Example\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/recover\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(recover.New())\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        panic(\"This panic is caught by fiber\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nUse `fiber.NewError()` to create an error with a status code. If you omit the message, Fiber uses the standard status text (for example, `404` becomes `Not Found`).\n\n```go title=\"Example\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    // 503 Service Unavailable\n    return fiber.ErrServiceUnavailable\n\n    // 503 On vacation!\n    return fiber.NewError(fiber.StatusServiceUnavailable, \"On vacation!\")\n})\n```\n\n## Default Error Handler\n\nFiber ships with a default error handler that sends **500 Internal Server Error** for generic errors. If the error is a [fiber.Error](https://godoc.org/github.com/gofiber/fiber#Error), the response uses the embedded status code and message.\n\n```go title=\"Example\"\n// Default error handler\nvar DefaultErrorHandler = func(c fiber.Ctx, err error) error {\n    // Status code defaults to 500\n    code := fiber.StatusInternalServerError\n\n    // Retrieve the custom status code if it's a *fiber.Error\n    var e *fiber.Error\n    if errors.As(err, &e) {\n        code = e.Code\n    }\n\n    // Set Content-Type: text/plain; charset=utf-8\n    c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\n    // Return status code with error message\n    return c.Status(code).SendString(err.Error())\n}\n```\n\n## Custom Error Handler\n\nSet a custom error handler in [`fiber.Config`](../api/fiber.md#errorhandler) when creating a new app.\n\nThe default handler covers most cases, but a custom handler lets you react to specific error types—for example, by logging to a service or sending a tailored JSON or HTML response.\n\nThe following example shows how to display error pages for different types of errors.\n\n```go title=\"Example\"\n// Create a new fiber instance with custom config\napp := fiber.New(fiber.Config{\n    // Override default error handler\n    ErrorHandler: func(ctx fiber.Ctx, err error) error {\n        // Status code defaults to 500\n        code := fiber.StatusInternalServerError\n\n        // Retrieve the custom status code if it's a *fiber.Error\n        var e *fiber.Error\n        if errors.As(err, &e) {\n            code = e.Code\n        }\n\n        // Send custom error page\n        err = ctx.Status(code).SendFile(fmt.Sprintf(\"./%d.html\", code))\n        if err != nil {\n            // In case the SendFile fails\n            return ctx.Status(fiber.StatusInternalServerError).SendString(\"Internal Server Error\")\n        }\n\n        // Return from handler\n        return nil\n    },\n})\n\n// ...\n```\n\n> Special thanks to the [Echo](https://echo.labstack.com/) and [Express](https://expressjs.com/) frameworks for inspiring parts of this error-handling approach.\n"
  },
  {
    "path": "docs/guide/extractors.md",
    "content": "---\nid: extractors\ntitle: 🔬 Extractors\ndescription: Learn how to use extractors in Fiber middleware\nsidebar_position: 8.5\ntoc_max_heading_level: 4\n---\n\nThe extractors package provides shared value extraction utilities for Fiber middleware packages. It helps reduce code duplication across middleware packages while ensuring consistent behavior and security practices.\n\n## Overview\n\nThe `github.com/gofiber/fiber/v3/extractors` module provides standardized value extraction utilities integrated into Fiber's middleware ecosystem. This approach:\n\n- **Reduces Code Duplication**: Eliminates redundant extractor implementations across middleware packages\n- **Ensures Consistency**: Maintains identical behavior and security practices across all extractors\n- **Simplifies Maintenance**: Changes to extraction logic only need to be made in one place\n- **Enables Direct Usage**: Middleware can import and use extractors directly\n- **Improves Performance**: Shared, optimized extraction functions reduce overhead\n\n## What Are Extractors?\n\nExtractors are utilities that middleware uses to get values from different parts of HTTP requests:\n\n### Available Extractors\n\n- `FromAuthHeader(authScheme string)`: Extract from Authorization header with optional scheme\n- `FromCookie(key string)`: Extract from HTTP cookies\n- `FromParam(param string)`: Extract from URL path parameters\n- `FromForm(param string)`: Extract from form data\n- `FromHeader(header string)`: Extract from custom HTTP headers\n- `FromQuery(param string)`: Extract from URL query parameters\n- `FromCustom(key string, fn func(fiber.Ctx) (string, error))`: Define custom extraction logic with metadata\n- `Chain(extractors ...Extractor)`: Chain multiple extractors with fallback logic\n\n### Extractor Structure\n\nEach `Extractor` contains:\n\n```go\ntype Extractor struct {\n    Extract    func(fiber.Ctx) (string, error)  // Extraction function\n    Key        string                           // Parameter/header name\n    Source     Source                           // Source type for inspection\n    AuthScheme string                           // Auth scheme (FromAuthHeader)\n    Chain      []Extractor                      // Chained extractors\n}\n```\n\n- **Headers**: `Authorization`, `X-API-Key`, custom headers\n- **Cookies**: Session cookies, authentication tokens\n- **Query Parameters**: URL parameters like `?token=abc123`\n- **Form Data**: POST body form fields\n- **URL Parameters**: Route parameters like `/users/:id`\n\n### Chain Behavior\n\nThe `Chain` function creates extractors that try multiple sources in order:\n\n- Returns the first successful extraction (non-empty value with no error)\n- If all extractors fail, returns the last error encountered or `ErrNotFound`\n- **Robust error handling**: Skips extractors with `nil` Extract functions\n- Preserves the source and key from the first extractor for metadata\n- Stores a defensive copy of all chained extractors for introspection via the `Chain` field\n\n## Why Middleware Uses Extractors\n\nMiddleware needs to extract values from requests for authentication, authorization, and other purposes. Extractors provide:\n\n- **Security Awareness**: Different sources have different security implications\n- **Fallback Support**: Try multiple sources if the first one doesn't have the value\n- **Consistency**: Same extraction logic across all middleware packages\n- **Source Tracking**: Know where values came from for security decisions\n\n## Usage Examples\n\n### Basic Usage\n\n```go\n// KeyAuth middleware extracts key from header\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromHeader(\"Middleware-Key\"),\n}))\n```\n\n### Fallback Chains\n\n```go\n// Try multiple sources in order\ntokenExtractor := extractors.Chain(\n    extractors.FromHeader(\"Middleware-Key\"),  // Try header first\n    extractors.FromCookie(\"middleware_key\"),  // Then cookie\n    extractors.FromQuery(\"middleware_key\"),   // Finally query param\n)\n\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: tokenExtractor,\n}))\n```\n\n## Configuring Middleware That Uses Extractors\n\n### Authentication Middleware\n\n```go\n// KeyAuth middleware (default: FromAuthHeader)\napp.Use(keyauth.New(keyauth.Config{\n    // Default extracts from Authorization header\n    // Extractor: extractors.FromAuthHeader(\"Bearer\"),\n}))\n\n// Custom header extraction\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromHeader(\"X-API-Key\"),\n}))\n\n// Multiple sources with secure fallback\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.Chain(\n        extractors.FromAuthHeader(\"Bearer\"),  // Secure first\n        extractors.FromHeader(\"X-API-Key\"),   // Then custom header\n        extractors.FromQuery(\"api_key\"),      // Least secure last\n    ),\n}))\n```\n\n### Session Middleware\n\n```go\n// Session middleware (default: FromCookie)\napp.Use(session.New(session.Config{\n    // Default extracts from session_id cookie\n    // Extractor: extractors.FromCookie(\"session_id\"),\n}))\n\n// Custom cookie name\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCookie(\"my_session\"),\n}))\n```\n\n### CSRF Middleware\n\n```go\n// CSRF middleware (default: FromHeader)\napp.Use(csrf.New(csrf.Config{\n    // Default extracts from X-CSRF-Token header\n    // Extractor: extractors.FromHeader(\"X-CSRF-Token\"),\n}))\n\n// Form-based CSRF (less secure, use only if needed)\napp.Use(csrf.New(csrf.Config{\n    Extractor: extractors.Chain(\n        extractors.FromHeader(\"X-CSRF-Token\"), // Secure first\n        extractors.FromForm(\"_csrf\"),          // Form fallback\n    ),\n}))\n```\n\n## Security Considerations\n\n### Source Characteristics\n\nDifferent extraction sources have different security properties and use cases:\n\n#### Headers (Generally Preferred)\n\n- **Authorization Header**: Standard for authentication tokens, widely supported\n- **Custom Headers**: Application-specific, less likely to be logged by default\n- **Considerations**: Can be intercepted without HTTPS, may be stripped by proxies\n\n#### Cookies (Good for Sessions)\n\n- **Session Cookies**: Designed for secure client-side storage\n- **Considerations**: Require proper `Secure`, `HttpOnly`, and `SameSite` flags\n- **Best for**: Session management, remember-me tokens\n\n#### Query Parameters (Use Sparingly)\n\n- **Query parameters**: Convenient for simple APIs and debugging\n- **Considerations**: Always visible in URLs, logged by servers/proxies, stored in browser history\n- **Best for**: Non-sensitive parameters, public identifiers\n\n#### Form Data (Context Dependent)\n\n- **POST Bodies**: Suitable for form submissions and API requests\n- **Considerations**: Avoid putting sensitive data in query strings; ensure request bodies aren’t logged and use the correct content type\n- **Best for**: User-generated content, file uploads\n\n### Security Best Practices\n\n1. **Use HTTPS**: Encrypt all traffic to protect extracted values in transit\n2. **Validate Input**: Always validate and sanitize extracted values\n3. **Log Carefully**: Avoid logging sensitive values from any source\n4. **Choose Appropriate Sources**: Match the source to your security requirements\n5. **Test Thoroughly**: Verify extraction works in your environment\n6. **Monitor Security**: Watch for extraction failures or unusual patterns\n\n### Chain Ordering Strategy\n\nWhen using multiple sources, order them by your security preferences:\n\n```go\n// Example: Prefer headers, fall back to cookies, then query\nextractors.Chain(\n    extractors.FromAuthHeader(\"Bearer\"),    // Standard auth\n    extractors.FromCookie(\"auth_token\"),    // Secure storage\n    extractors.FromQuery(\"token\"),          // Public fallback\n)\n```\n\nThe \"best\" source depends on your specific use case, security requirements, and application architecture.\n\n### Common Security Issues\n\n#### Leaky URLs\n\n```go\n// ❌ DON'T: API keys in URLs (visible in logs, history, bookmarks)\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromQuery(\"api_key\"), // PROBLEMATIC\n}))\n\n// ✅ DO: API keys in headers (not visible in URLs)\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromHeader(\"X-API-Key\"), // BETTER\n}))\n```\n\n#### Session Tokens in Query Parameters\n\n```go\n// ❌ DON'T: Session tokens in URLs (can be bookmarked, leaked)\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromQuery(\"session\"), // PROBLEMATIC\n}))\n\n// ✅ DO: Session tokens in cookies (designed for this purpose)\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCookie(\"session_id\"), // BETTER\n}))\n```\n\n#### Form-Only CSRF Tokens\n\nWhile the default extractor uses headers, some implementations use form fields, which is fine if you don't have AJAX or API clients:\n\n```go\n// ❌ DON'T: CSRF tokens only in forms (breaks AJAX, API calls)\napp.Use(csrf.New(csrf.Config{\n    Extractor: extractors.FromForm(\"_csrf\"), // LIMITED\n}))\n\n// ✅ DO: Header-first with form fallback (works everywhere)\napp.Use(csrf.New(csrf.Config{\n    Extractor: extractors.Chain(\n        extractors.FromHeader(\"X-CSRF-Token\"), // PREFERRED\n        extractors.FromForm(\"_csrf\"),          // FALLBACK\n    ),\n}))\n```\n\n### Understanding Trade-offs\n\n**No extractor is universally \"secure\" - security depends on:**\n\n- Whether you're using HTTPS\n- How you configure cookies (Secure, HttpOnly, SameSite flags)\n- Your logging and monitoring setup\n- The sensitivity of the data being extracted\n- Your threat model and security requirements\n\nChoose extractors based on your specific use case and security needs, not blanket \"secure\" vs \"insecure\" labels.\n\n## Standards Compliance\n\n### Authorization Header (RFC 9110 & RFC 7235)\n\nThe `FromAuthHeader` extractor provides comprehensive RFC compliance with strict security validation:\n\n#### RFC 9110 Compliance (Authorization Header Format)\n\n- **Section 11.6.2 Format**: Enforces `credentials = auth-scheme 1*SP token68` structure\n- **1*SP Requirement**: Validates exactly one or more spaces between auth-scheme and token\n- **Case-insensitive scheme matching**: `Bearer`, `bearer`, `BEARER` all work correctly\n- **Proper whitespace handling**: Rejects tabs between scheme and token (only spaces allowed)\n\n#### RFC 7235 Token68 Validation\n\nThe extractor implements strict token68 character validation per RFC 7235:\n\n- **Allowed characters**: `A-Z`, `a-z`, `0-9`, `-`, `.`, `_`, `~`, `+`, `/`, `=`\n- **Padding rules**: `=` characters only allowed at the end of tokens\n- **Security validation**: Prevents tokens starting with `=` or having non-padding characters after `=`\n- **Whitespace rejection**: Rejects tokens containing spaces, tabs, or any other whitespace\n\n#### Security Features\n\n- **Header injection prevention**: Strict parsing prevents malformed authorization headers from bypassing authentication\n- **Token validation**: Ensures extracted tokens conform to standards, preventing authentication bypass\n- **Consistent error handling**: Returns `ErrNotFound` for all invalid cases\n\n#### Examples\n\n```go\n// Standard usage - strict validation\nextractor := extractors.FromAuthHeader(\"Bearer\")\n\n// ✅ Valid cases:\n// \"Bearer abc123\" -> \"abc123\"\n// \"bearer ABC123\" -> \"ABC123\" (case-insensitive scheme)\n// \"Bearer token123=\" -> \"token123=\" (valid padding)\n// \"Bearer token==\" -> \"token==\" (valid multiple padding)\n\n// ❌ Invalid cases (all return ErrNotFound):\n// \"Bearer abc def\" -> rejected (space in token)\n// \"Bearer abc\\tdef\" -> rejected (tab in token)\n// \"Bearer =abc\" -> rejected (padding at start)\n// \"Bearer ab=cd\" -> rejected (padding in middle)\n// \"Bearer  token\" -> rejected (multiple spaces after scheme)\n// \"Bearer\\ttoken\" -> rejected (tab after scheme)\n// \"Bearertoken\" -> rejected (no space after scheme)\n\n// Raw header extraction (no validation)\nrawExtractor := extractors.FromAuthHeader(\"\")\n// \"CustomAuth anything goes here\" -> \"CustomAuth anything goes here\"\n```\n\n#### Benefits\n\n- **Standards Compliance**: Full adherence to HTTP authentication RFCs\n- **Security Hardening**: Prevents common authentication bypass vulnerabilities\n- **Consistent Behavior**: Reliable parsing across different client implementations\n- **Developer Confidence**: Clear validation rules reduce authentication bugs\n\n## Troubleshooting\n\n### Extraction Fails\n\n**Problem**: Middleware returns \"value not found\" or authentication fails\n\n**Solutions**:\n\n1. Check if the expected header/cookie/query parameter is present\n2. Verify the key name matches exactly (headers are case-insensitive; params/cookies/query keys are case-sensitive)\n3. Ensure the request uses the correct HTTP method (GET vs POST)\n4. Check if middleware is configured with the right extractor\n\n**Debug Example**:\n\n```go\n// Add simple debug logging (avoid logging secrets in production)\napp.Use(func(c fiber.Ctx) error {\n    hdr := c.Get(\"X-API-Key\")\n    cookie := c.Cookies(\"session_id\")\n    if hdr != \"\" || cookie != \"\" {\n        log.Printf(\"debug: X-API-Key present=%t, session_id present=%t\", hdr != \"\", cookie != \"\")\n    }\n    return c.Next()\n})\n```\n\n### Wrong Source Used\n\n**Problem**: Values extracted from unexpected sources\n\n**Solutions**:\n\n1. Check middleware configuration order\n2. Verify chain order (first successful extraction wins)\n3. Use more specific extractors when needed\n\n### Security Warnings\n\n**Problem**: Getting security warnings in logs\n\n**Solutions**:\n\n1. Switch to more secure sources (headers/cookies)\n2. Use HTTPS to encrypt traffic\n3. Review if sensitive data should be in that source\n\n## Advanced Usage\n\n### Custom Extraction Logic\n\nExtractors support custom extractors for complex scenarios:\n\n```go\n// Extract from custom logic (rarely needed)\ncustomExtractor := extractors.FromCustom(\"my-source\", func(c fiber.Ctx) (string, error) {\n    // Complex extraction logic\n    if value := c.Locals(\"computed_token\"); value != nil {\n        return value.(string), nil\n    }\n    return \"\", extractors.ErrNotFound\n})\n```\n\n:::warning\n**Custom extractors break source awareness.** When you use `FromCustom`, middleware cannot determine where the value came from, which means:\n\n- **No automatic security warnings** for potentially insecure sources\n- **No source-based logging** or monitoring capabilities\n- **Developer responsibility** for ensuring the extraction is secure and appropriate\n\n**Only use `FromCustom` when:**\n\n- Standard extractors don't meet your needs\n- You've carefully evaluated the security implications\n- You're confident in the security of your custom extraction logic\n- You understand that middleware cannot provide source-aware security guidance\n\n**Note:** If you pass `nil` as the function parameter, `FromCustom` will return an extractor that always fails with `ErrNotFound`.\n:::\n\n### Multiple Middleware Coordination\n\nWhen using multiple middleware that extract values, ensure they don't conflict:\n\n```go\n// Good: Different sources for different purposes\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromHeader(\"X-API-Key\"),\n}))\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCookie(\"session_id\"),\n}))\n\n// Avoid: Same source for different middleware\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromCookie(\"token\"), // API auth\n}))\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCookie(\"token\"), // Session - CONFLICT!\n}))\n```\n"
  },
  {
    "path": "docs/guide/faster-fiber.md",
    "content": "---\nid: faster-fiber\ntitle: ⚡ Make Fiber Faster\nsidebar_position: 7\n---\n\n## Custom JSON Encoder/Decoder\n\nFiber defaults to the standard `encoding/json` for stability and reliability. If you need more speed, consider these libraries:\n\n- [goccy/go-json](https://github.com/goccy/go-json)\n- [bytedance/sonic](https://github.com/bytedance/sonic)\n- [segmentio/encoding](https://github.com/segmentio/encoding)\n- [minio/simdjson-go](https://github.com/minio/simdjson-go)\n\n```go title=\"Example\"\npackage main\n\nimport \"github.com/gofiber/fiber/v3\"\nimport \"github.com/goccy/go-json\"\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        JSONEncoder: json.Marshal,\n        JSONDecoder: json.Unmarshal,\n    })\n\n    // ...\n}\n```\n\n### References\n\n- [Set custom JSON encoder for client](../client/rest.md#setjsonmarshal)\n- [Set custom JSON decoder for client](../client/rest.md#setjsonunmarshal)\n- [Set custom JSON encoder for application](../api/fiber.md#jsonencoder)\n- [Set custom JSON decoder for application](../api/fiber.md#jsondecoder)\n"
  },
  {
    "path": "docs/guide/grouping.md",
    "content": "---\nid: grouping\ntitle: 🎭 Grouping\nsidebar_position: 2\n---\n\n:::info\nGrouping works like Express.js. Groups are virtual; routes are flattened with the group's prefix and executed in declaration order, mirroring Express.js.\n:::\n\n## Paths\n\nGroups can use path prefixes to organize related routes.\n\n```go\nfunc main() {\n    app := fiber.New()\n\n    api := app.Group(\"/api\", middleware) // /api\n\n    v1 := api.Group(\"/v1\", middleware)   // /api/v1\n    v1.Get(\"/list\", handler)             // /api/v1/list\n    v1.Get(\"/user\", handler)             // /api/v1/user\n\n    v2 := api.Group(\"/v2\", middleware)   // /api/v2\n    v2.Get(\"/list\", handler)             // /api/v2/list\n    v2.Get(\"/user\", handler)             // /api/v2/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n:::note\nGroup prefixes follow the same slash-boundary rule as `app.Use`. A prefix must either match the full path or stop at a `/`, so `/api` applies to `/api` and `/api/v1` but not `/apiv1`. Parameter markers (for example `:id`, `:id?`, `*`, and `+`) are processed before checking the boundary.\n:::\n\nGroups can also include an optional handler.\n\n```go\nfunc main() {\n    app := fiber.New()\n\n    api := app.Group(\"/api\")      // /api\n\n    v1 := api.Group(\"/v1\")        // /api/v1\n    v1.Get(\"/list\", handler)      // /api/v1/list\n    v1.Get(\"/user\", handler)      // /api/v1/user\n\n    v2 := api.Group(\"/v2\")        // /api/v2\n    v2.Get(\"/list\", handler)      // /api/v2/list\n    v2.Get(\"/user\", handler)      // /api/v2/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n:::caution\nAccessing `/api`, `/v1`, or `/v2` directly returns a **404**, so add error handlers as needed.\n:::\n\n## Group Handlers\n\nGroup handlers can act as routing paths but must call `Next` to continue the flow.\n\n```go\nfunc main() {\n    app := fiber.New()\n\n    handler := func(c fiber.Ctx) error {\n        return c.SendStatus(fiber.StatusOK)\n    }\n    api := app.Group(\"/api\") // /api\n\n    v1 := api.Group(\"/v1\", func(c fiber.Ctx) error { // middleware for /api/v1\n        c.Set(\"Version\", \"v1\")\n        return c.Next()\n    })\n    v1.Get(\"/list\", handler) // /api/v1/list\n    v1.Get(\"/user\", handler) // /api/v1/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n"
  },
  {
    "path": "docs/guide/reverse-proxy.md",
    "content": "---\nid: reverse-proxy\ntitle: 🔄 Reverse Proxy Configuration\ndescription: >-\n  Learn how to set up reverse proxies like Nginx or Traefik to enable modern\n  HTTP capabilities in your Fiber application, including HTTP/2 and\n  HTTP/3 (QUIC) support. This guide also covers basic reverse\n  proxy configuration and links to external documentation.\nsidebar_position: 4\n---\n\n## Reverse Proxies\n\nRunning Fiber behind a reverse proxy is a common production setup.\nReverse proxies can handle:\n\n- **HTTPS/TLS termination** (offloading SSL certificates)\n- **Protocol upgrades** (HTTP/2, HTTP/3 support)\n- **Request routing & load balancing**\n- **Caching & compression**\n- **Security features** (rate limiting, WAF, DDoS mitigation)\n\nSome Fiber features (like [`SendEarlyHints`](../api/ctx.md#sendearlyhints)) require **HTTP/2 or newer**, which is easiest to enable using a reverse proxy.\n\n### Popular Reverse Proxies\n\n- [Nginx](https://nginx.org/)\n- [Traefik](https://traefik.io/)\n- [HA PROXY](https://www.haproxy.com/)\n- [Caddy](https://caddyserver.com/)\n\n## Getting the Real Client IP Address\n\nWhen your Fiber application is behind a reverse proxy, the TCP connection comes from the proxy server, not the actual client. To get the real client IP address, you need to configure Fiber to read it from proxy headers like `X-Forwarded-For`.\n\n:::warning Security Warning\nProxy headers can be easily spoofed by malicious clients. **Always** configure `TrustProxyConfig` to validate the proxy IP address, otherwise attackers can forge headers to bypass IP-based access controls, rate limiting, or geolocation features.\n\nIn addition, your reverse proxy should be configured to **set or overwrite** the forwarding header you choose (for example, `X-Forwarded-For`) based on the real client connection, or to use its real IP / PROXY protocol features. Do not simply pass through client-supplied forwarding headers, or `c.IP()` may still be controlled by an attacker even when `TrustProxyConfig` is correct.\n:::\n\n### Configuration\n\nTo enable reading the client IP from proxy headers, you must configure **three settings**:\n\n1. **`TrustProxy`** - Enable proxy header trust (must be `true`)\n2. **`ProxyHeader`** - Specify which header contains the client IP\n3. **`TrustProxyConfig`** - Define which proxy IPs to trust\n\n```go title=\"Example - App Behind Nginx\"\napp := fiber.New(fiber.Config{\n    // Enable proxy support\n    TrustProxy: true,\n\n    // Read client IP from X-Forwarded-For header\n    ProxyHeader: fiber.HeaderXForwardedFor,\n\n    // Trust requests from your Nginx proxy\n    TrustProxyConfig: fiber.TrustProxyConfig{\n        // Option 1: Trust specific proxy IPs\n        Proxies: []string{\"10.10.0.58\", \"192.168.1.0/24\"},\n\n        // Option 2: Or trust all private IPs (useful for internal load balancers)\n        // Private: true,\n    },\n})\n```\n\n### Common Proxy Headers\n\nDifferent proxies use different headers:\n\n| Proxy/Service | Recommended Header | Config Value |\n|---------------|-------------------|--------------|\n| Nginx, HAProxy, Apache | X-Forwarded-For | `fiber.HeaderXForwardedFor` |\n| Cloudflare | CF-Connecting-IP | `\"CF-Connecting-IP\"` |\n| Fastly | Fastly-Client-IP | `\"Fastly-Client-IP\"` |\n| Generic | X-Real-IP | `\"X-Real-IP\"` |\n\n### TrustProxyConfig Options\n\nThe `TrustProxyConfig` struct provides multiple ways to specify trusted proxies:\n\n```go\nTrustProxyConfig: fiber.TrustProxyConfig{\n    // Specific IPs or CIDR ranges\n    Proxies: []string{\n        \"10.10.0.58\",           // Single IP\n        \"192.168.0.0/24\",       // CIDR range\n        \"2001:db8::/32\",        // IPv6 range\n    },\n\n    // Or use convenience flags:\n    Loopback:   true,  // Trust 127.0.0.0/8, ::1/128\n    LinkLocal:  true,  // Trust 169.254.0.0/16, fe80::/10\n    Private:    true,  // Trust 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7\n    UnixSocket: true,  // Trust Unix domain socket connections\n},\n```\n\n### Complete Example with Nginx\n\n```nginx title=\"nginx.conf\"\nserver {\n    listen 443 ssl;\n    http2 on;\n    server_name example.com;\n\n    ssl_certificate     /etc/ssl/certs/example.crt;\n    ssl_certificate_key /etc/ssl/private/example.key;\n\n    location / {\n        proxy_pass http://127.0.0.1:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Connection \"\";\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n```\n\n```go title=\"main.go\"\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New(fiber.Config{\n        TrustProxy:        true,\n        ProxyHeader:       fiber.HeaderXForwardedFor,\n        EnableIPValidation: true,\n        TrustProxyConfig: fiber.TrustProxyConfig{\n            // Trust localhost since Nginx is on the same machine\n            Loopback: true,\n        },\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        // This will now return the real client IP from X-Forwarded-For\n        // instead of 127.0.0.1\n        return c.SendString(\"Your IP: \" + c.IP())\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n### Testing Your Configuration\n\nYou can verify your configuration is working:\n\n```go\napp.Get(\"/debug\", func(c fiber.Ctx) error {\n    return c.JSON(fiber.Map{\n        \"c.IP()\":           c.IP(),                    // Should show real client IP\n        \"X-Forwarded-For\":  c.Get(\"X-Forwarded-For\"),  // Raw header value\n        \"IsProxyTrusted\":   c.IsProxyTrusted(),        // Should be true\n        \"RemoteIP\":         c.RequestCtx().RemoteIP().String(), // Proxy IP\n    })\n})\n```\n\n## Enabling HTTP/2\n\nPopular choices include Nginx and Traefik.\n\n<details>\n<summary>Nginx Example</summary>\n\nSee the [Complete Example with Nginx](#complete-example-with-nginx) above for a full configuration with HTTP/2 enabled.\n</details>\n<details>\n<summary>Traefik Example</summary>\n\n```yaml title=\"traefik.yaml\"\nentryPoints:\n  websecure:\n    address: \":443\"\n\nhttp:\n  routers:\n    app:\n      rule: \"Host(`example.com`)\"\n      entryPoints:\n        - websecure\n      service: app\n      tls: {}\n\n  services:\n    app:\n      loadBalancer:\n        servers:\n          - url: \"http://127.0.0.1:3000\"\n```\n\nWith this configuration, Traefik terminates TLS and serves your app over HTTP/2.\n</details>\n\n## HTTP/3 (QUIC) Support\n\nEarly Hints (103 responses) are defined for HTTP and can be delivered over HTTP/1.1 and HTTP/2/3. In practice, browsers process 103 most reliably over HTTP/2/3. Many reverse proxies also support HTTP/3 (QUIC):\n\n- **Nginx**\n- **Traefik**\n\nEnabling HTTP/3 is optional but can provide lower latency and improved performance for clients that support it. If you enable HTTP/3, your Early Hints responses will still work as expected.\nFor more details, see the official documentation:\n\n- [Nginx QUIC / HTTP/3](https://nginx.org/en/docs/quic.html)\n- [Traefik HTTP/3](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/#http3)\n"
  },
  {
    "path": "docs/guide/routing.md",
    "content": "---\nid: routing\ntitle: 🔌 Routing\ndescription: >-\n  Routing refers to how an application's endpoints (URIs) respond to client\n  requests.\nsidebar_position: 1\ntoc_max_heading_level: 4\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\nimport RoutingHandler from './../partials/routing/handler.md';\n\n## Handlers\n\n<RoutingHandler />\n\n## Automatic HEAD routes\n\nFiber automatically registers a `HEAD` route for every `GET` route you add. The generated handler chain mirrors the `GET` chain, so `HEAD` requests reuse middleware, status codes, and headers while the response body is suppressed.\n\n```go title=\"GET handlers automatically expose HEAD\"\napp := fiber.New()\n\napp.Get(\"/users/:id\", func(c fiber.Ctx) error {\n    c.Set(\"X-User\", c.Params(\"id\"))\n    return c.SendStatus(fiber.StatusOK)\n})\n\n// HEAD /users/:id now returns the same headers and status without a body.\n```\n\nYou can still register dedicated `HEAD` handlers—even with auto-registration enabled—and Fiber replaces the generated route so your implementation wins:\n\n```go title=\"Override the generated HEAD handler\"\napp.Head(\"/users/:id\", func(c fiber.Ctx) error {\n    return c.SendStatus(fiber.StatusNoContent)\n})\n```\n\nTo opt out globally, start the app with `DisableHeadAutoRegister`:\n\n```go title=\"Disable automatic HEAD registration\"\nhandler := func(c fiber.Ctx) error {\n    c.Set(\"X-User\", c.Params(\"id\"))\n    return c.SendStatus(fiber.StatusOK)\n}\n\napp := fiber.New(fiber.Config{DisableHeadAutoRegister: true})\napp.Get(\"/users/:id\", handler) // HEAD /users/:id now returns 405 unless you add it manually.\n```\n\nAuto-generated `HEAD` routes participate in every router scope, including `Group` hierarchies, mounted sub-apps, parameterized and wildcard paths, and static file helpers. They also appear in route listings such as `app.Stack()` so tooling sees both the `GET` and `HEAD` entries.\n\n## Paths\n\nA route path paired with an HTTP method defines an endpoint. It can be a plain **string** or a **pattern**.\n\n### Examples of route paths based on strings\n\n```go\n// This route path will match requests to the root route, \"/\":\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"root\")\n})\n\n// This route path will match requests to \"/about\":\napp.Get(\"/about\", func(c fiber.Ctx) error {\n    return c.SendString(\"about\")\n})\n\n// This route path will match requests to \"/random.txt\":\napp.Get(\"/random.txt\", func(c fiber.Ctx) error {\n    return c.SendString(\"random.txt\")\n})\n```\n\nAs with the Express.js framework, the order in which routes are declared matters.\nRoutes are evaluated sequentially, so more specific paths should appear before those with variables.\n\n:::info\nPlace routes with variable parameters after fixed paths to avoid unintended matches.\n:::\n\n## Parameters\n\nRoute parameters are dynamic segments in a path, either named or unnamed, used to capture values from the URL. Retrieve them with the [Params](../api/ctx.md#params) function using the parameter name or, for unnamed parameters, the wildcard (`*`) or plus (`+`) symbol with an index.\n\nThe characters `:`, `+`, and `*` introduce parameters.\n\nUse `*` or `+` to capture segments greedily.\n\nYou can define optional parameters by appending `?` to a named segment. The `+` sign is greedy and required, while `*` acts as an optional greedy wildcard.\n\n### Example of defining routes with route parameters\n\n```go\n// Parameters\napp.Get(\"/user/:name/books/:title\", func(c fiber.Ctx) error {\n    fmt.Fprintf(c, \"%s\\n\", c.Params(\"name\"))\n    fmt.Fprintf(c, \"%s\\n\", c.Params(\"title\"))\n    return nil\n})\n// Plus - greedy - not optional\napp.Get(\"/user/+\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"+\"))\n})\n\n// Optional parameter\napp.Get(\"/user/:name?\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"name\"))\n})\n\n// Wildcard - greedy - optional\napp.Get(\"/user/*\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"*\"))\n})\n\n// This route path will match requests to \"/v1/some/resource/name:customVerb\", since the parameter character is escaped\napp.Get(`/v1/some/resource/name\\:customVerb`, func(c fiber.Ctx) error {\n    return c.SendString(\"Hello, Community\")\n})\n```\n\n:::info\nThe hyphen \\(`-`\\) and dot \\(`.`\\) are treated literally, so you can combine them with route parameters.\n:::\n\n:::info\nEscape special parameter characters with `\\\\` to treat them literally. This technique is useful for custom methods like those in the [Google API Design Guide](https://cloud.google.com/apis/design/custom_methods). Wrap routes in backticks to keep escape sequences clear.\n:::\n\n```go\n// http://localhost:3000/plantae/prunus.persica\napp.Get(\"/plantae/:genus.:species\", func(c fiber.Ctx) error {\n    fmt.Fprintf(c, \"%s.%s\\n\", c.Params(\"genus\"), c.Params(\"species\"))\n    return nil // prunus.persica\n})\n```\n\n```go\n// http://localhost:3000/flights/LAX-SFO\napp.Get(\"/flights/:from-:to\", func(c fiber.Ctx) error {\n    fmt.Fprintf(c, \"%s-%s\\n\", c.Params(\"from\"), c.Params(\"to\"))\n    return nil // LAX-SFO\n})\n```\n\nFiber's router detects when these characters belong to the literal path and handles them accordingly.\n\n```go\n// http://localhost:3000/shop/product/color:blue/size:xs\napp.Get(\"/shop/product/color::color/size::size\", func(c fiber.Ctx) error {\n    fmt.Fprintf(c, \"%s:%s\\n\", c.Params(\"color\"), c.Params(\"size\"))\n    return nil // blue:xs\n})\n```\n\nYou can chain multiple named or unnamed parameters—including wildcard and plus segments—giving the router greater flexibility.\n\n```go\n// GET /@v1\n// Params: \"sign\" -> \"@\", \"param\" -> \"v1\"\napp.Get(\"/:sign:param\", handler)\n\n// GET /api-v1\n// Params: \"name\" -> \"v1\"\napp.Get(\"/api-:name\", handler)\n\n// GET /customer/v1/cart/proxy\n// Params: \"*1\" -> \"customer/\", \"*2\" -> \"/cart\"\napp.Get(\"/*v1*/proxy\", handler)\n\n// GET /v1/brand/4/shop/blue/xs\n// Params: \"*1\" -> \"brand/4\", \"*2\" -> \"blue/xs\"\napp.Get(\"/v1/*/shop/*\", handler)\n```\n\nFiber's routing is inspired by Express but intentionally omits regular expression routes due to their performance cost. You can try similar patterns using the Express route tester (v0.1.7).\n\n### Constraints\n\nRoute 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).\n\n:::caution\nConstraints aren't validation for parameters. If constraints aren't valid for a parameter value, Fiber returns **404 handler**.\n:::\n\n| Constraint        | Example                          | Example matches                                                                             |\n| ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------- |\n| int               | `:id<int>`                       | 123456789, -123456789                                                                       |\n| bool              | `:active<bool>`                  | true,false                                                                                  |\n| guid              | `:id<guid>`                      | CD2C1638-1638-72D5-1638-DEADBEEF1638                                                        |\n| float             | `:weight<float>`                 | 1.234, -1,001.01e8                                                                          |\n| minLen(value)     | `:username<minLen(4)>`           | Test (must be at least 4 characters)                                                        |\n| maxLen(value)     | `:filename<maxLen(8)>`           | MyFile (must be no more than 8 characters                                                   |\n| len(length)       | `:filename<len(12)>`             | somefile.txt (exactly 12 characters)                                                        |\n| min(value)        | `:age<min(18)>`                  | 19 (Integer value must be at least 18)                                                      |\n| max(value)        | `:age<max(120)>`                 | 91 (Integer value must be no more than 120)                                                 |\n| range(min,max)    | `:age<range(18,120)>`            | 91 (Integer value must be at least 18 but no more than 120)                                 |\n| alpha             | `:name<alpha>`                   | Rick (String must consist of one or more alphabetical characters, a-z and case-insensitive) |\n| datetime          | `:dob<datetime(2006\\\\-01\\\\-02)>` | 2005-11-01                                                                                  |\n| regex(expression) | `:date<regex(\\d{4}-\\d{2}-\\d{2})>` | 2022-08-27 (Must match regular expression)                                                  |\n\n#### Examples\n\n<Tabs>\n<TabItem value=\"single-constraint\" label=\"Single Constraint\">\n\n```go\napp.Get(\"/:test<min(5)>\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"test\"))\n})\n\n// curl -X GET http://localhost:3000/12\n// 12\n\n// curl -X GET http://localhost:3000/1\n// Not Found\n```\n\n</TabItem>\n<TabItem value=\"multiple-constraints\" label=\"Multiple Constraints\">\n\nYou can use `;` for multiple constraints.\n\n```go\napp.Get(\"/:test<min(100);maxLen(5)>\", func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"test\"))\n})\n\n// curl -X GET http://localhost:3000/120000\n// Not Found\n\n// curl -X GET http://localhost:3000/1\n// Not Found\n\n// curl -X GET http://localhost:3000/250\n// 250\n```\n\n</TabItem>\n<TabItem value=\"regex-constraint\" label=\"Regex Constraint\">\n\nFiber precompiles the regex when registering routes, so regex constraints add no runtime overhead.\n\n```go\napp.Get(`/:date<regex(\\d{4}-\\d{2}-\\d{2})>`, func(c fiber.Ctx) error {\n    return c.SendString(c.Params(\"date\"))\n})\n\n// curl -X GET http://localhost:3000/125\n// Not Found\n\n// curl -X GET http://localhost:3000/test\n// Not Found\n\n// curl -X GET http://localhost:3000/2022-08-27\n// 2022-08-27\n```\n\n</TabItem>\n</Tabs>\n\n:::caution\nPrefix routing characters with `\\\\` when using the datetime constraint (`*`, `+`, `?`, `:`, `/`, `<`, `>`, `;`, `(`, `)`), to avoid misparsing.\n:::\n\n#### Optional Parameter Example\n\nYou can impose constraints on optional parameters as well.\n\n```go\napp.Get(\"/:test<int>?\", func(c fiber.Ctx) error {\n  return c.SendString(c.Params(\"test\"))\n})\n// curl -X GET http://localhost:3000/42\n// 42\n// curl -X GET http://localhost:3000/\n//\n// curl -X GET http://localhost:3000/7.0\n// Not Found\n```\n\n#### Custom Constraint\n\nCustom constraints can be added to Fiber using the `app.RegisterCustomConstraint` method. Your constraints have to be compatible with the `CustomConstraint` interface.\n\n:::caution\nAttention, custom constraints can now override built-in constraints. If a custom constraint has the same name as a built-in constraint, the custom constraint will be used instead. This allows for more flexibility in defining route parameter constraints.\n:::\n\nAdd external constraints when you need stricter rules, such as verifying that a parameter is a valid ULID.\n\n```go\n// CustomConstraint is an interface for custom constraints\ntype CustomConstraint interface {\n    // Name returns the name of the constraint.\n    // This name is used in the constraint matching.\n    Name() string\n\n    // Execute executes the constraint.\n    // It returns true if the constraint is matched and right.\n    // param is the parameter value to check.\n    // args are the constraint arguments.\n    Execute(param string, args ...string) bool\n}\n```\n\nYou can check the example below:\n\n```go\ntype UlidConstraint struct {\n    fiber.CustomConstraint\n}\n\nfunc (*UlidConstraint) Name() string {\n    return \"ulid\"\n}\n\nfunc (*UlidConstraint) Execute(param string, args ...string) bool {\n    _, err := ulid.Parse(param)\n    return err == nil\n}\n\nfunc main() {\n    app := fiber.New()\n    app.RegisterCustomConstraint(&UlidConstraint{})\n\n    app.Get(\"/login/:id<ulid>\", func(c fiber.Ctx) error {\n        return c.SendString(\"...\")\n    })\n\n    app.Listen(\":3000\")\n\n    // /login/01HK7H9ZE5BFMK348CPYP14S0Z -> 200\n    // /login/12345 -> 404\n}\n```\n\n## Middleware\n\nFunctions that are designed to make changes to the request or response are called **middleware functions**. The [Next](../api/ctx.md#next) is a **Fiber** router function, when called, executes the **next** function that **matches** the current route.\n\n### Example of a middleware function\n\n```go\napp.Use(func(c fiber.Ctx) error {\n    // Set a custom header on all responses:\n    c.Set(\"X-Custom-Header\", \"Hello, World\")\n\n    // Go to next middleware:\n    return c.Next()\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Hello, World!\")\n})\n```\n\n`Use` method path is a **mount**, or **prefix** path, and limits middleware to only apply to any paths requested that begin with it.\n\n:::note\nPrefix matches must now end at a slash boundary (or be an exact match). For example, `/api` runs for `/api` and `/api/users` but no longer for `/apiv2`. Parameter tokens such as `:name`, `:name?`, `*`, and `+` are still expanded before this boundary check runs.\n:::\n\n### Constraints on Adding Routes Dynamically\n\n:::caution\nAdding 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.\n:::\n\n## Grouping\n\nIf you have many endpoints, you can organize your routes using `Group`.\n\n```go\nfunc main() {\n    app := fiber.New()\n\n    api := app.Group(\"/api\", middleware) // /api\n\n    v1 := api.Group(\"/v1\", middleware)   // /api/v1\n    v1.Get(\"/list\", handler)             // /api/v1/list\n    v1.Get(\"/user\", handler)             // /api/v1/user\n\n    v2 := api.Group(\"/v2\", middleware)   // /api/v2\n    v2.Get(\"/list\", handler)             // /api/v2/list\n    v2.Get(\"/user\", handler)             // /api/v2/user\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nMore information about this in our [Grouping Guide](./grouping.md)\n"
  },
  {
    "path": "docs/guide/templates.md",
    "content": "---\nid: templates\ntitle: 📝 Templates\ndescription: Fiber supports server-side template engines.\nsidebar_position: 3\n---\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\nTemplates render dynamic content without requiring a separate frontend framework.\n\n## Template Engines\n\nFiber accepts a custom template engine during app initialization.\n\n```go\napp := fiber.New(fiber.Config{\n    // Provide a template engine\n    Views: engine,\n\n    // Default path for views, overridden when calling Render()\n    ViewsLayout: \"layouts/main\",\n\n    // Enables/Disables access to `ctx.Locals()` entries in rendered views\n    // (defaults to false)\n    PassLocalsToViews: false,\n})\n```\n\n### Supported Engines\n\nFiber maintains a [templates](https://docs.gofiber.io/template) package that wraps several engines:\n\n* [ace](https://docs.gofiber.io/template/ace/)\n* [amber](https://docs.gofiber.io/template/amber/)\n* [django](https://docs.gofiber.io/template/django/)\n* [handlebars](https://docs.gofiber.io/template/handlebars)\n* [html](https://docs.gofiber.io/template/html)\n* [jet](https://docs.gofiber.io/template/jet)\n* [mustache](https://docs.gofiber.io/template/mustache)\n* [pug](https://docs.gofiber.io/template/pug)\n* [slim](https://docs.gofiber.io/template/slim)\n\n:::info\nCustom engines implement the `Views` interface to work with Fiber.\n:::\n\n```go title=\"Views interface\"\ntype Views interface {\n    // Fiber executes Load() on app initialization to load/parse the templates\n    Load() error\n\n    // Outputs a template to the provided buffer using the provided template,\n    // template name, and bound data\n    Render(io.Writer, string, interface{}, ...string) error\n}\n```\n\n:::note\nThe `Render` method powers [**ctx.Render\\(\\)**](../api/ctx.md#render), which accepts a template name and data to bind.\n:::\n\n## Rendering Templates\n\nAfter configuring an engine, handlers call [**ctx.Render\\(\\)**](../api/ctx.md#render) with a template name and data to send the rendered output.\n\n```go title=\"Signature\"\nfunc (c Ctx) Render(name string, bind Map, layouts ...string) error\n```\n\n:::info\nBy default, [**ctx.Render\\(\\)**](../api/ctx.md#render) searches for the template in the `ViewsLayout` path. Pass alternate paths in the `layouts` argument to override this behavior.\n:::\n\n<Tabs>\n<TabItem value=\"example\" label=\"Example\">\n\n```go\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.Render(\"index\", fiber.Map{\n        \"Title\": \"Hello, World!\",\n    })\n\n})\n```\n\n</TabItem>\n\n<TabItem value=\"index\" label=\"layouts/index.html\">\n\n```html\n<!DOCTYPE html>\n<html>\n    <body>\n        <h1>{{.Title}}</h1>\n    </body>\n</html>\n```\n\n</TabItem>\n\n</Tabs>\n\n:::caution\nWhen `PassLocalsToViews` is enabled, all values set using `ctx.Locals(key, value)` are passed to the template. Use unique keys to avoid collisions.\n:::\n\n## Advanced Templating\n\n### Custom Functions\n\nFiber supports adding custom functions to templates.\n\n#### AddFunc\n\nAdds a global function to all templates.\n\n```go title=\"Signature\"\nfunc (e *Engine) AddFunc(name string, fn interface{}) IEngineCore\n```\n\n<Tabs>\n<TabItem value=\"add-func-example\" label=\"AddFunc Example\">\n\n```go\n// Add `ToUpper` to engine\nengine := html.New(\"./views\", \".html\")\nengine.AddFunc(\"ToUpper\", func(s string) string {\n    return strings.ToUpper(s)\n}\n\n// Initialize Fiber App\napp := fiber.New(fiber.Config{\n    Views: engine,\n})\n\napp.Get(\"/\", func (c fiber.Ctx) error {\n    return c.Render(\"index\", fiber.Map{\n        \"Content\": \"hello, World!\"\n    })\n})\n```\n\n</TabItem>\n<TabItem value=\"add-func-template\" label=\"views/index.html\">\n\n```html\n<!DOCTYPE html>\n<html>\n    <body>\n        <p>This will be in {{ToUpper \"all caps\"}}:</p>\n        <p>{{ToUpper .Content}}</p>\n    </body>\n</html>\n```\n\n</TabItem>\n</Tabs>\n\n#### AddFuncMap\n\nAdds a Map of functions (keyed by name) to all templates.\n\n```go title=\"Signature\"\nfunc (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore\n```\n\n<Tabs>\n<TabItem value=\"add-func-map-example\" label=\"AddFuncMap Example\">\n\n```go\n// Add `ToUpper` to engine\nengine := html.New(\"./views\", \".html\")\nengine.AddFuncMap(map[string]interface{}{\n    \"ToUpper\": func(s string) string {\n        return strings.ToUpper(s)\n    },\n})\n\n// Initialize Fiber App\napp := fiber.New(fiber.Config{\n    Views: engine,\n})\n\napp.Get(\"/\", func (c fiber.Ctx) error {\n    return c.Render(\"index\", fiber.Map{\n        \"Content\": \"hello, world!\"\n    })\n})\n```\n\n</TabItem>\n<TabItem value=\"add-func-map-template\" label=\"views/index.html\">\n\n```html\n<!DOCTYPE html>\n<html>\n    <body>\n        <p>This will be in {{ToUpper \"all caps\"}}:</p>\n        <p>{{ToUpper .Content}}</p>\n    </body>\n</html>\n```\n\n</TabItem>\n</Tabs>\n\n* For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template).\n\n## Full Example\n\n<Tabs>\n<TabItem value=\"example\" label=\"Example\">\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/template/html/v2\"\n)\n\nfunc main() {\n    // Initialize standard Go html template engine\n    engine := html.New(\"./views\", \".html\")\n    // If you want to use another engine,\n    // just replace with following:\n    // Create a new engine with django\n    // engine := django.New(\"./views\", \".django\")\n\n    app := fiber.New(fiber.Config{\n        Views: engine,\n    })\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        // Render index template\n        return c.Render(\"index\", fiber.Map{\n            \"Title\": \"Go Fiber Template Example\",\n            \"Description\": \"An example template\",\n            \"Greeting\": \"Hello, World!\",\n        });\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n</TabItem>\n<TabItem value=\"index\" label=\"views/index.html\">\n\n```html\n<!DOCTYPE html>\n<html>\n    <head>\n        <title>{{.Title}}</title>\n        <meta name=\"description\" content=\"{{.Description}}\">\n    </head>\n<body>\n    <h1>{{.Title}}</h1>\n        <p>{{.Greeting}}</p>\n</body>\n</html>\n```\n\n</TabItem>\n</Tabs>\n"
  },
  {
    "path": "docs/guide/utils.md",
    "content": "---\nid: utils\ntitle: 🧰 Utils\nsidebar_position: 8\ntoc_max_heading_level: 4\n---\n\n## Generics\n\n### Convert\n\nConverts a string to a specific type while handling errors and optional defaults.\nIt wraps conversion and fallback logic to keep your code clean and consistent.\n\n```go title=\"Signature\"\nfunc Convert[T any](value string, converter func(string) (T, error), defaultValue ...T) (T, error)\n```\n\n```go title=\"Example\"\n// GET http://example.com/id/bb70ab33-d455-4a03-8d78-d3c1dacae9ff\napp.Get(\"/id/:id\", func(c fiber.Ctx) error {\n    fiber.Convert(c.Params(\"id\"), uuid.Parse)                   // UUID(bb70ab33-d455-4a03-8d78-d3c1dacae9ff), nil\n})\n\n// GET http://example.com/search?id=65f6f54221fb90e6a6b76db7\napp.Get(\"/search\", func(c fiber.Ctx) error {\n    fiber.Convert(c.Query(\"id\"), mongo.ParseObjectID)           // objectid(65f6f54221fb90e6a6b76db7), nil\n    fiber.Convert(c.Query(\"id\"), uuid.Parse)                    // uuid.Nil, error(cannot parse given uuid)\n    fiber.Convert(c.Query(\"id\"), uuid.Parse, mongo.NewObjectID) // new object id generated and return nil as error.\n    return nil\n})\n\n// ...\n```\n\n### GetReqHeader\n\nRetrieves an HTTP request header as a specific type using generics.\n\n```go title=\"Signature\"\nfunc GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V\n```\n\n```go title=\"Example\"\napp.Get(\"/search\", func(c fiber.Ctx) error {\n    // curl -X GET http://example.com/search -H \"X-Request-ID: 12345\" -H \"X-Request-Name: John\"\n    fiber.GetReqHeader[int](c, \"X-Request-ID\")               // => returns 12345 as integer.\n    fiber.GetReqHeader[string](c, \"X-Request-Name\")          // => returns \"John\" as string.\n    fiber.GetReqHeader[string](c, \"unknownParam\", \"default\") // => returns \"default\" as string.\n    // ...\n})\n```\n\n### Locals\n\nReads or writes local values in the request context using generics.\n\n```go title=\"Signature\"\n// Set a value\nfunc Locals[V any](c Ctx, key any, value ...V) V\n// Get a value\nfunc Locals[V any](c Ctx, key any) V\n```\n\n```go title=\"Example\"\napp.Use(\"/user/:user/:id\", func(c fiber.Ctx) error {\n    // set local values\n    fiber.Locals[string](c, \"user\", \"john\")\n    fiber.Locals[int](c, \"id\", 25)\n    // ...\n    \n    return c.Next()\n})\n\n\napp.Get(\"/user/*\", func(c fiber.Ctx) error {\n    // get local values\n    name := fiber.Locals[string](c, \"user\") // john\n    age := fiber.Locals[int](c, \"id\")       // 25\n    // ...\n})\n```\n\n### Params\n\nRetrieves route parameters as a specific type.\n\n```go title=\"Signature\"\nfunc Params[V GenericType](c Ctx, key string, defaultValue ...V) V\n```\n\n```go title=\"Example\"\napp.Get(\"/user/:user/:id\", func(c fiber.Ctx) error {\n    // http://example.com/user/john/25\n    fiber.Params[int](c, \"id\")               // => returns 25 as integer.\n    fiber.Params[int](c, \"unknownParam\", 99) // => returns the default 99 as integer.\n    // ...\n    return c.SendString(\"Hello, \" + fiber.Params[string](c, \"user\"))\n})\n```\n\n### Query\n\nRetrieves query parameters as a specific type.\n\n```go title=\"Signature\"\nfunc Query[V GenericType](c Ctx, key string, defaultValue ...V) V\n```\n\n```go title=\"Example\"\napp.Get(\"/search\", func(c fiber.Ctx) error {\n    // http://example.com/search?name=john&age=25\n    fiber.Query[string](c, \"name\")                    // => returns \"john\"\n    fiber.Query[int](c, \"age\")                        // => returns 25 as integer.\n    fiber.Query[string](c, \"unknownParam\", \"default\") // => returns \"default\" as string.\n    // ...\n})\n```\n\n### RoutePatternMatch\n\nChecks whether a given path matches a Fiber route pattern. Useful for testing\npatterns without registering them. Patterns may contain parameters, wildcards\nand optional segments. An optional `Config` allows control over case sensitivity\nand strict routing.\n\n```go title=\"Signature\"\nfunc RoutePatternMatch(path, pattern string, cfg ...Config) bool\n```\n\n```go title=\"Example\"\nfiber.RoutePatternMatch(\"/user/john\", \"/user/:name\") // true\n\nfiber.RoutePatternMatch(\n    \"/User/john\",\n    \"/user/:name\",\n    fiber.Config{CaseSensitive: true},\n) // false\n```\n"
  },
  {
    "path": "docs/guide/validation.md",
    "content": "---\nid: validation\ntitle: 🔎 Validation\nsidebar_position: 5\n---\n\n## Validator package\n\nFiber's [Bind](../api/bind.md#validation) function binds request data to a struct and validates it.\n\n```go title=\"Basic Example\"\nimport \"github.com/go-playground/validator/v10\"\n\ntype structValidator struct {\n    validate *validator.Validate\n}\n\n// Validator needs to implement the Validate method\nfunc (v *structValidator) Validate(out any) error {\n    return v.validate.Struct(out)\n}\n\n// Set up your validator in the config\napp := fiber.New(fiber.Config{\n    StructValidator: &structValidator{validate: validator.New()},\n})\n\n// Note:\n// StructValidator runs only for struct destinations (or pointers to structs).\n// Binding into maps and other non-struct types skips validation.\n\ntype User struct {\n    Name string `json:\"name\" form:\"name\" query:\"name\" validate:\"required\"`\n    Age  int    `json:\"age\" form:\"age\" query:\"age\" validate:\"gte=0,lte=100\"`\n}\n\napp.Post(\"/\", func(c fiber.Ctx) error {\n    user := new(User)\n    \n    // Works with all bind methods—Body, Query, Form, etc.\n    if err := c.Bind().Body(user); err != nil { // validation errors are returned here\n        return err\n    }\n    \n    return c.JSON(user)\n})\n```\n\n```go title=\"Advanced Validation Example\"\ntype User struct {\n    Name     string `json:\"name\" validate:\"required,min=3,max=32\"`\n    Email    string `json:\"email\" validate:\"required,email\"`\n    Age      int    `json:\"age\" validate:\"gte=0,lte=100\"`\n    Password string `json:\"password\" validate:\"required,min=8\"`\n    Website  string `json:\"website\" validate:\"url\"`\n}\n\n// Custom validation error messages\ntype UserWithCustomMessages struct {\n    Name     string `json:\"name\" validate:\"required,min=3,max=32\" message:\"Name is required and must be between 3 and 32 characters\"`\n    Email    string `json:\"email\" validate:\"required,email\" message:\"Valid email is required\"`\n    Age      int    `json:\"age\" validate:\"gte=0,lte=100\" message:\"Age must be between 0 and 100\"`\n}\n\napp.Post(\"/user\", func(c fiber.Ctx) error {\n    user := new(User)\n    \n    if err := c.Bind().Body(user); err != nil {\n        // Handle validation errors\n        if validationErrors, ok := err.(validator.ValidationErrors); ok {\n            for _, e := range validationErrors {\n                // e.Field() - field name\n                // e.Tag() - validation tag\n                // e.Value() - invalid value\n                // e.Param() - validation parameter\n                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\n                    \"field\": e.Field(),\n                    \"error\": e.Error(),\n                })\n            }\n        }\n        return err\n    }\n    \n    return c.JSON(user)\n})\n```\n\n```go title=\"Custom Validator Example\"\n// Custom validator for password strength\ntype PasswordValidator struct {\n    validate *validator.Validate\n}\n\nfunc (v *PasswordValidator) Validate(out any) error {\n    if err := v.validate.Struct(out); err != nil {\n        return err\n    }\n    \n    // Custom password validation logic\n    if user, ok := out.(*User); ok {\n        if len(user.Password) < 8 {\n            return errors.New(\"password must be at least 8 characters\")\n        }\n        // Add more password validation rules here\n    }\n    \n    return nil\n}\n\n// Usage\napp := fiber.New(fiber.Config{\n    StructValidator: &PasswordValidator{validate: validator.New()},\n})\n```\n"
  },
  {
    "path": "docs/intro.md",
    "content": "---\nslug: /\nid: welcome\ntitle: 👋 Welcome\nsidebar_position: 1\n---\nWelcome to Fiber's online API documentation, complete with examples to help you start building web applications right away!\n\n**Fiber** is an [Express](https://github.com/expressjs/express)-inspired **web framework** built on top of [Fasthttp](https://github.com/valyala/fasthttp), the **fastest** HTTP engine for [Go](https://go.dev/doc/). It is designed to facilitate rapid development with **zero memory allocations** and a strong focus on **performance**.\n\nThese docs cover **Fiber v3**.\n\nLooking to practice Fiber concepts hands-on? Check out our [Learning Resources](./extra/learning-resources) for interactive challenges and tutorials.\n\n### Installation\n\nFirst, [download](https://go.dev/dl/) and install Go. Version `1.25` or higher is required.\n\nInstall Fiber using the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command:\n\n```bash\ngo get github.com/gofiber/fiber/v3\n```\n\n### Zero Allocation\n\nFiber is optimized for **high performance**, meaning values returned from **fiber.Ctx** are **not** immutable by default and **will** be reused across requests. As a rule of thumb, you should use context values only within the handler and **must not** keep any references. Once you return from the handler, any values obtained from the context will be reused in future requests. Here is an example:\n\n```go\nfunc handler(c fiber.Ctx) error {\n    // Variable is only valid within this handler\n    result := c.Params(\"foo\")\n\n    // ...\n}\n```\n\nIf you need to persist such values outside the handler, make copies of their **underlying buffer** using the [copy](https://pkg.go.dev/builtin/#copy) builtin. Here is an example of persisting a string:\n\n```go\nfunc handler(c fiber.Ctx) error {\n    // Variable is only valid within this handler\n    result := c.Params(\"foo\")\n\n    // Make a copy\n    buffer := make([]byte, len(result))\n    copy(buffer, result)\n    resultCopy := string(buffer)\n    // Variable is now valid indefinitely\n\n    // ...\n}\n```\n\nFiber provides `GetString` and `GetBytes` methods on the app that detach values when `Immutable` is enabled and the data isn't already read-only. If it's disabled, use `utils.CopyString` and `utils.CopyBytes` to allocate only when necessary.\n\n```go\napp.Get(\"/:foo\", func(c fiber.Ctx) error {\n    // Detach if necessary when Immutable is enabled\n    result := c.App().GetString(c.Params(\"foo\"))\n\n    // ...\n})\n```\n\nAlternatively, you can enable the `Immutable` setting. This makes all values returned from the context immutable, allowing you to persist them anywhere. Note that this comes at the cost of performance.\n\n```go\napp := fiber.New(fiber.Config{\n    Immutable: true,\n})\n```\n\nFor more information, please refer to [#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).\n\n### Hello, World\n\nHere is the simplest **Fiber** application you can create:\n\n```go\npackage main\n\nimport \"github.com/gofiber/fiber/v3\"\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```bash\ngo run server.go\n```\n\nBrowse to `http://localhost:3000` and you should see `Hello, World!` displayed on the page.\n\n### Basic Routing\n\nRouting determines how an application responds to a client request at a particular endpoint—a combination of path and HTTP request method (`GET`, `PUT`, `POST`, etc.).\n\nEach route can have **multiple handler functions** that are executed when the route is matched.\n\nRoute definitions follow the structure below:\n\n```go\n// Function signature\napp.Method(path string, ...func(fiber.Ctx) error)\n```\n\n- `app` is an instance of **Fiber**\n- `Method` is an [HTTP request method](./api/app#route-handlers): `GET`, `PUT`, `POST`, etc.\n- `path` is a virtual path on the server\n- `func(fiber.Ctx) error` is a callback function containing the [Context](./api/ctx) executed when the route is matched\n\n#### Simple Route\n\n```go\n// Respond with \"Hello, World!\" on root path \"/\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Hello, World!\")\n})\n```\n\n#### Parameters\n\n```go\n// GET http://localhost:8080/hello%20world\n\napp.Get(\"/:value\", func(c fiber.Ctx) error {\n    return c.SendString(\"value: \" + c.Params(\"value\"))\n    // => Response: \"value: hello world\"\n})\n```\n\n#### Optional Parameter\n\n```go\n// GET http://localhost:3000/john\n\napp.Get(\"/:name?\", func(c fiber.Ctx) error {\n    if c.Params(\"name\") != \"\" {\n        return c.SendString(\"Hello \" + c.Params(\"name\"))\n        // => Response: \"Hello john\"\n    }\n    return c.SendString(\"Where is john?\")\n    // => Response: \"Where is john?\"\n})\n```\n\n#### Wildcards\n\n```go\n// GET http://localhost:3000/api/user/john\n\napp.Get(\"/api/*\", func(c fiber.Ctx) error {\n    return c.SendString(\"API path: \" + c.Params(\"*\"))\n    // => Response: \"API path: user/john\"\n})\n```\n\n### Static Files\n\nTo serve static files such as **images**, **CSS**, and **JavaScript** files, use the [static middleware](./middleware/static.md).\n\nUse the following code to serve files in a directory named `./public`:\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/static\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(\"/\", static.New(\"./public\"))\n\n    app.Listen(\":3000\")\n}\n```\n\nNow, you can access the files in the `./public` directory via your browser:\n\n```bash\nhttp://localhost:3000/hello.html\nhttp://localhost:3000/js/jquery.js\nhttp://localhost:3000/css/style.css\n```\n"
  },
  {
    "path": "docs/middleware/_category_.json",
    "content": "{\n  \"label\": \"\\uD83E\\uDDEC Middleware\",\n  \"position\": 4,\n  \"collapsed\": true,\n  \"link\": {\n    \"type\": \"generated-index\",\n    \"description\": \"Middleware is a function chained in the HTTP request cycle with access to the Context which it uses to perform a specific action, for example, logging every request or enabling CORS.\"\n  }\n}\n"
  },
  {
    "path": "docs/middleware/adaptor.md",
    "content": "---\nid: adaptor\n---\n\n# Adaptor\n\nThe `adaptor` package converts between Fiber and `net/http`, letting you reuse handlers, middleware, and requests across both frameworks.\n\n:::tip\nFiber can register plain `net/http` handlers directly—just pass an `http.Handler`,\n`http.HandlerFunc`, or `func(http.ResponseWriter, *http.Request)` to any router\nmethod and it will be adapted automatically. The adaptor helpers remain valuable\nwhen you need to convert middleware, swap handler directions, or transform\nrequests explicitly.\n:::\n\n:::caution Fiber features are unavailable\nEven when you register them directly, adapted `net/http` handlers still run with standard\nlibrary semantics. They don't have access to `fiber.Ctx`, and the compatibility layer comes\nwith additional overhead compared to native Fiber handlers. Use them for interop and legacy\nscenarios, but prefer Fiber handlers when performance or Fiber-specific APIs matter.\n:::\n\n## Features\n\n- Convert `net/http` handlers and middleware to Fiber handlers\n- Convert Fiber handlers to `net/http` handlers\n- Convert a Fiber context (`fiber.Ctx`) into an `http.Request`\n- Copy values stored in a `context.Context` onto a `fasthttp.RequestCtx`\n\n:::note Body size limits when running Fiber from net/http\nWhen Fiber is executed from a `net/http` server through `FiberHandler`, `FiberHandlerFunc`,\nor `FiberApp`, the adaptor enforces the app's configured `BodyLimit`. The app's `BodyLimit` defaults to **4 MiB** if a non-positive value is provided during configuration. Requests exceeding the active limit receive `413 Request Entity Too Large`.\n:::\n\n## API Reference\n\n| Name                          | Signature                                                                     | Description                                                                   |\n|-------------------------------|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------|\n| `HTTPHandler`                 | `HTTPHandler(h http.Handler) fiber.Handler`                                   | Converts `http.Handler` to `fiber.Handler`                                    |\n| `HTTPHandlerWithContext`      | `HTTPHandlerWithContext(h http.Handler) fiber.Handler`                        | Converts `http.Handler` to `fiber.Handler`, propagating Fiber's local context |\n| `HTTPHandlerFunc`             | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler`                           | Converts `http.HandlerFunc` to `fiber.Handler`                                |\n| `HTTPMiddleware`              | `HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler`            | Converts `http.Handler` middleware to `fiber.Handler` middleware              |\n| `FiberHandler`                | `FiberHandler(h fiber.Handler) http.Handler`                                  | Converts `fiber.Handler` to `http.Handler`                                    |\n| `FiberHandlerFunc`            | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc`                          | Converts `fiber.Handler` to `http.HandlerFunc`                                |\n| `FiberApp`                    | `FiberApp(app *fiber.App) http.HandlerFunc`                                   | Converts an entire Fiber app to a `http.HandlerFunc`                          |\n| `ConvertRequest`              | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)`          | Converts `fiber.Ctx` into a `http.Request`                                    |\n| `LocalContextFromHTTPRequest` | `LocalContextFromHTTPRequest(r *http.Request) (context.Context, bool)`        | Extracts the propagated `context.Context` from an adapted `http.Request`      |\n| `CopyContextToFiberContext`   | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | Copies `context.Context` to `fasthttp.RequestCtx`                             |\n\n---\n\n## Usage Examples\n\n### 1. Using `net/http` handlers in Fiber (`HTTPHandler`, `HTTPHandlerFunc`)\n\nRun standard `net/http` handlers inside Fiber. Fiber can auto-adapt them, or you can\nexplicitly convert them when you want to cache or share the converted handler.\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Fiber adapts net/http handlers for you during registration.\n    app.Get(\"/\", http.HandlerFunc(helloHandler))\n\n    // You can also convert and reuse the handler manually.\n    cached := adaptor.HTTPHandler(http.HandlerFunc(helloHandler))\n    app.Get(\"/cached\", cached)\n\n    // When you already have an http.HandlerFunc, convert it directly.\n    app.Get(\"/func\", adaptor.HTTPHandlerFunc(helloHandler))\n\n    app.Listen(\":3000\")\n}\n\nfunc helloHandler(w http.ResponseWriter, r *http.Request) {\n    fmt.Fprint(w, \"Hello from net/http!\")\n}\n```\n\n### 2. Using `net/http` middleware with Fiber (`HTTPMiddleware`)\n\nMiddleware written for `net/http` can run inside Fiber:\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Apply an http middleware in Fiber\n    app.Use(adaptor.HTTPMiddleware(loggingMiddleware))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello Fiber!\")\n    })\n\n    app.Listen(\":3000\")\n}\n\nfunc loggingMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        log.Println(\"Request received\")\n        next.ServeHTTP(w, r)\n    })\n}\n```\n\n### 3. Using Fiber handlers in `net/http` (`FiberHandler`)\n\nYou can use Fiber handlers from `net/http`:\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    // Convert a Fiber handler to an http.Handler\n    http.Handle(\"/\", adaptor.FiberHandler(helloFiber))\n    \n    // Convert a Fiber handler to an http.HandlerFunc\n    http.HandleFunc(\"/func\", adaptor.FiberHandlerFunc(helloFiber))\n    \n    http.ListenAndServe(\":3000\", nil)\n}\n\nfunc helloFiber(c fiber.Ctx) error {\n    return c.SendString(\"Hello from Fiber!\")\n}\n```\n\n### 4. Converting Fiber handlers to `http.HandlerFunc` (`FiberHandlerFunc`)\n\nWhen you specifically need an `http.HandlerFunc`, wrap the Fiber handler directly:\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    http.HandleFunc(\"/func-only\", adaptor.FiberHandlerFunc(helloFiber))\n    http.ListenAndServe(\":3000\", nil)\n}\n\nfunc helloFiber(c fiber.Ctx) error {\n    return c.SendString(\"Hello from Fiber!\")\n}\n```\n\n### 5. Running a full Fiber app inside `net/http` (`FiberApp`)\n\nYou can wrap a full Fiber app inside `net/http`:\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    app := fiber.New()\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello from Fiber!\")\n    })\n\n    // Run Fiber inside an http server\n    http.ListenAndServe(\":3000\", adaptor.FiberApp(app))\n}\n```\n\n### 6. Converting `fiber.Ctx` to `*http.Request` (`ConvertRequest`)\n\nCreate an `*http.Request` from a `fiber.Ctx`. The `forServer` parameter determines how\nserver-oriented fields are populated:\n\n- Use `forServer = true` when the converted request will be passed into a `net/http` handler\n  (sets `RequestURI`, `RemoteAddr`, and `TLS` fields for server-side handling)\n- Use `forServer = false` when creating a request for client-side use (e.g., making an\n  outbound HTTP request with `http.Client`)\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"net/http/httptest\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc main() {\n    app := fiber.New()\n    app.Get(\"/request\", handleRequest)\n    app.Listen(\":3000\")\n}\n\nfunc handleRequest(c fiber.Ctx) error {\n    // Use forServer = true when passing to a net/http handler\n    httpReq, err := adaptor.ConvertRequest(c, true)\n    if err != nil {\n        return err\n    }\n\n    // Pass the request to a net/http handler.\n    recorder := httptest.NewRecorder()\n    http.DefaultServeMux.ServeHTTP(recorder, httpReq)\n\n    return c.SendString(\"Converted Request URL: \" + httpReq.URL.String())\n}\n```\n\n### 7. Passing Fiber user context into `net/http`\n\nThis example shows a realistic flow: a Fiber middleware sets a request-scoped `context.Context` (with a `request_id`) on the Fiber context, then an adapted `net/http` handler retrieves it via `LocalContextFromHTTPRequest`.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"net/http\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\ntype ctxKey string\nconst requestIDKey ctxKey = \"request_id\"\n\nfunc main() {\n    app := fiber.New()\n\n    // Create a request-scoped context in Fiber (e.g., request id, auth claims, trace span).\n    app.Use(func(c fiber.Ctx) error {\n        reqID := c.Get(\"X-Request-ID\")\n\n        ctx := context.WithValue(context.Background(), requestIDKey, reqID)\n\n        // Fiber stores request-scoped context as \"user context\".\n        c.SetContext(ctx)\n        return c.Next()\n    })\n\n    // 2) Run a standard net/http handler that includes Fiber's user context propagated.\n    app.Get(\"/hello\", adaptor.HTTPHandlerWithContext(http.HandlerFunc(handleRequest)))\n\n    app.Listen(\":3000\")\n}\n\nfunc handleRequest(w http.ResponseWriter, r *http.Request) {\n    ctx, ok := adaptor.LocalContextFromHTTPRequest(r)\n    if !ok || ctx == nil {\n        http.Error(w, \"missing propagated context\", http.StatusInternalServerError)\n        return\n    }\n\n    reqID, _ := ctx.Value(requestIDKey).(string)\n    fmt.Fprintf(w, \"Hello from net/http (request_id=%s)\\n\", reqID)\n}\n```\n\n### 8. Copying context values onto `fasthttp.RequestCtx` (`CopyContextToFiberContext`)\n\n`CopyContextToFiberContext` copies values stored in a `context.Context` onto a\n`fasthttp.RequestCtx`. The function is marked deprecated in code because it uses\nreflection and unsafe operations—prefer explicit parameter passing when possible.\nWhen you do need it, call it immediately after you add values to the `net/http`\ncontext so Fiber can read them via `c.Context()`:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"net/http\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\ntype contextKey string\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(func(c fiber.Ctx) error {\n        // Convert the Fiber context to an http.Request so we can attach context values.\n        httpReq, err := adaptor.ConvertRequest(c, true)\n        if err != nil {\n            return err\n        }\n\n        // Add context data and push it back to the Fiber context.\n        enriched := httpReq.WithContext(context.WithValue(httpReq.Context(), contextKey(\"requestID\"), \"req-123\"))\n        adaptor.CopyContextToFiberContext(enriched.Context(), c.RequestCtx())\n\n        return c.Next()\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        if id, ok := c.Context().Value(contextKey(\"requestID\")).(string); ok {\n            return c.SendString(\"Request ID: \" + id)\n        }\n        return c.SendStatus(fiber.StatusNotFound)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n---\n\n## Summary\n\nThe `adaptor` package lets Fiber and `net/http` interoperate so you can:\n\n- Convert handlers and middleware in both directions\n- Run Fiber apps inside `net/http`\n- Convert `fiber.Ctx` to `http.Request`\n- Propagate Fiber's user context into adapted `net/http` handlers\n\nThis makes it straightforward to integrate Fiber with existing Go projects or migrate between frameworks.\n"
  },
  {
    "path": "docs/middleware/basicauth.md",
    "content": "---\nid: basicauth\n---\n\n# BasicAuth\n\nBasic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides HTTP basic auth. It calls the next handler for valid credentials and returns [`401 Unauthorized`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) for missing or invalid credentials, [`400 Bad Request`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400) for malformed `Authorization` headers, or [`431 Request Header Fields Too Large`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431) when the header exceeds size limits. Credentials may omit Base64 padding as permitted by RFC 7235's `token68` syntax.\n\nThe default unauthorized response includes the header `WWW-Authenticate: Basic realm=\"Restricted\", charset=\"UTF-8\"`, sets `Cache-Control: no-store`, and adds a `Vary: Authorization` header. Only the `UTF-8` charset is supported; any other value will panic.\n\n## Signatures\n\n```go\nfunc New(config Config) fiber.Handler\nfunc UsernameFromContext(ctx any) string\n```\n\n`UsernameFromContext` accepts a `fiber.CustomCtx`, `fiber.Ctx`, a `*fasthttp.RequestCtx`, or a `context.Context`.\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/basicauth\"\n)\n```\n\nOnce your Fiber app is initialized, choose one of the following approaches:\n\n```go\n// Provide a minimal config\napp.Use(basicauth.New(basicauth.Config{\n    Users: map[string]string{\n        // \"doe\" hashed using SHA-256\n        \"john\":  \"{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=\",\n        // \"123456\" hashed using bcrypt\n        \"admin\": \"$2a$10$gTYwCN66/tBRoCr3.TXa1.v1iyvwIF7GRBqxzv7G.AHLMt/owXrp.\",\n    },\n}))\n\n// Or extend your config for customization\napp.Use(basicauth.New(basicauth.Config{\n    Users: map[string]string{\n        // \"doe\" hashed using SHA-256\n        \"john\":  \"{SHA256}eZ75KhGvkY4/t0HfQpNPO1aO0tk6wd908bjUGieTKm8=\",\n        // \"123456\" hashed using bcrypt\n        \"admin\": \"$2a$10$gTYwCN66/tBRoCr3.TXa1.v1iyvwIF7GRBqxzv7G.AHLMt/owXrp.\",\n    },\n    Realm: \"Forbidden\",\n    Authorizer: func(user, pass string, c fiber.Ctx) bool {\n        // custom validation logic\n        return (user == \"john\" || user == \"admin\")\n    },\n    Unauthorized: func(c fiber.Ctx) error {\n        return c.SendFile(\"./unauthorized.html\")\n    },\n}))\n```\n\n### Password hashes\n\nPasswords must be supplied in pre-hashed form. The middleware detects the\nhashing algorithm from a prefix:\n\n- `\"{SHA512}\"` or `\"{SHA256}\"` followed by a base64-encoded digest\n- standard bcrypt strings beginning with `$2`\n\nIf no prefix is present, the value is interpreted as a SHA-256 digest encoded in\nhex or base64. Plaintext passwords are rejected.\n\n#### Generating SHA-256 and SHA-512 passwords\n\nCreate a digest, encode it in base64, and prefix it with `{SHA256}` or\n`{SHA512}` before adding it to `Users`:\n\n```bash\n# SHA-256\nprintf 'secret' | openssl dgst -binary -sha256 | base64\n\n# SHA-512\nprintf 'secret' | openssl dgst -binary -sha512 | base64\n```\n\nInclude the prefix in your config:\n\n```go\nUsers: map[string]string{\n    \"john\":  \"{SHA256}K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=\",\n    \"admin\": \"{SHA512}vSsar3708Jvp9Szi2NWZZ02Bqp1qRCFpbcTZPdBhnWgs5WtNZKnvCXdhztmeD2cmW192CF5bDufKRpayrW/isg==\",\n}\n```\n\n## Config\n\n| Property        | Type                        | Description                                                                                                                                                           | Default               |\n|:----------------|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------|\n| Next            | `func(fiber.Ctx) bool`     | Next defines a function to skip this middleware when it returns true.                                                                                                   | `nil`                 |\n| Users           | `map[string]string`         | Users maps usernames to **hashed** passwords (e.g. bcrypt, `{SHA256}`). | `map[string]string{}` |\n| Realm           | `string`                    | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `\"Restricted\"`        |\n| Charset         | `string`                    | Charset sent in the `WWW-Authenticate` header. Only `\"UTF-8\"` is supported (case-insensitive). | `\"UTF-8\"` |\n| HeaderLimit     | `int`                       | Maximum allowed length of the `Authorization` header. Requests exceeding this limit are rejected. | `8192` |\n| Authorizer      | `func(string, string, fiber.Ctx) bool` | Authorizer defines a function to check the credentials. It will be called with a username, password, and the current context and is expected to return true or false to indicate approval.  | `nil`                 |\n| Unauthorized    | `fiber.Handler`             | Unauthorized defines the response body for unauthorized responses.                                                                                                    | `nil`                 |\n| BadRequest      | `fiber.Handler`             | BadRequest defines the response for malformed `Authorization` headers.                                                                                     | `nil`                 |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:            nil,\n    Users:           map[string]string{},\n    Realm:           \"Restricted\",\n    Charset:         \"UTF-8\",\n    HeaderLimit:     8192,\n    Authorizer:      nil,\n    Unauthorized:    nil,\n    BadRequest:      nil,\n}\n```\n"
  },
  {
    "path": "docs/middleware/cache.md",
    "content": "---\nid: cache\n---\n\n# Cache\n\nCache middleware for [Fiber](https://github.com/gofiber/fiber) that intercepts responses and stores the body, `Content-Type`, and status code under a key derived from the request path and method. Special thanks to [@codemicro](https://github.com/codemicro/fiber-cache) for contributing this middleware to Fiber core.\n\nBy default, cached responses expire after five minutes and the middleware stores up to 1 MB of response bodies.\n\nRequest directives\n\n- `Cache-Control: no-cache` returns the latest response while still caching it, so the status is always `miss`.\n- `Cache-Control: no-store` skips caching and always forwards a fresh response.\n\nIf the response includes a `Cache-Control: max-age` directive, its value sets the cache entry's expiration.\n\nCacheable status codes\n\nThe middleware caches these RFC 7231 status codes:\n\n- `200: OK`\n- `203: Non-Authoritative Information`\n- `204: No Content`\n- `206: Partial Content`\n- `300: Multiple Choices`\n- `301: Moved Permanently`\n- `404: Not Found`\n- `405: Method Not Allowed`\n- `410: Gone`\n- `414: URI Too Long`\n- `501: Not Implemented`\n\nResponses with other status codes result in an `unreachable` cache status.\n\nFor more about cacheable status codes and RFC 7231, see:\n\n- [Cacheable - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Glossary/Cacheable)\n\n- [RFC7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content](https://datatracker.ietf.org/doc/html/rfc7231)\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/cache\"\n    \"github.com/gofiber/utils/v2\"\n)\n```\n\nOnce your Fiber app is initialized, register the middleware:\n\n```go\n// Initialize default config\napp.Use(cache.New())\n\n// Or extend the config for customization\napp.Use(cache.New(cache.Config{\n    Next: func(c fiber.Ctx) bool {\n        return fiber.Query[bool](c, \"noCache\")\n    },\n    Expiration: 30 * time.Minute,\n    DisableCacheControl: true,\n}))\n```\n\nCustomize the cache key and expiration; the HTTP method is appended automatically:\n\n```go\napp.Use(cache.New(cache.Config{\n    ExpirationGenerator: func(c fiber.Ctx, cfg *cache.Config) time.Duration {\n        newCacheTime, _ := strconv.Atoi(c.GetRespHeader(\"Cache-Time\", \"600\"))\n        return time.Second * time.Duration(newCacheTime)\n    },\n    KeyGenerator: func(c fiber.Ctx) string {\n        return utils.CopyString(c.Path())\n    },\n}))\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    c.Response().Header.Add(\"Cache-Time\", \"6000\")\n    return c.SendString(\"hi\")\n})\n```\n\nUse `CacheInvalidator` to invalidate entries programmatically:\n\n```go\napp.Use(cache.New(cache.Config{\n    CacheInvalidator: func(c fiber.Ctx) bool {\n        return fiber.Query[bool](c, \"invalidateCache\")\n    },\n}))\n```\n\n`CacheInvalidator` defines custom invalidation rules. Return `true` to bypass the cache. In the example above, setting the `invalidateCache` query parameter to `true` invalidates the entry.\n\nCache keys are masked in logs and error messages by default. Set `DisableValueRedaction` to `true` if you explicitly need the raw key for debugging.\n\n## Config\n\n| Property             | Type                                           | Description                                                                                                                                                                                                                                                                                                    | Default                                                          |\n| :------------------- | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |\n| Next                 | `func(fiber.Ctx) bool`                         | Next defines a function that is executed before creating the cache entry and can be used to execute the request without cache creation. If an entry already exists, it will be used. If you want to completely bypass the cache functionality in certain cases, you should use the [skip middleware](skip.md). | `nil`                                                            |\n| Expiration           | `time.Duration`                                | Expiration is the time that a cached response will live. | `5 * time.Minute`                                                |\n| CacheHeader          | `string`                                       | CacheHeader is the header on the response header that indicates the cache status, with the possible return values \"hit,\" \"miss,\" or \"unreachable.\"                                                                                                                                                             | `X-Cache`                                                        |\n| DisableCacheControl  | `bool`                                          | DisableCacheControl omits the `Cache-Control` header when set to `true`. | `false`                                                         |\n| CacheInvalidator     | `func(fiber.Ctx) bool`                         | CacheInvalidator defines a function that is executed before checking the cache entry. It can be used to invalidate the existing cache manually by returning true. | `nil`                                                            |\n| DisableValueRedaction | `bool`                                        | Turns off cache key redaction in logs and error messages when set to `true`. | `false`                                             |\n| KeyGenerator         | `func(fiber.Ctx) string`                       | KeyGenerator allows you to generate custom keys. The HTTP method is appended automatically. | `func(c fiber.Ctx) string { return utils.CopyString(c.Path()) }` |\n| ExpirationGenerator  | `func(fiber.Ctx, *cache.Config) time.Duration` | ExpirationGenerator allows you to generate custom expiration keys based on the request.                                                                                                                                                                                                                        | `nil`                                                            |\n| Storage              | `fiber.Storage`                                | Storage is used to store the state of the middleware.                                                                                                                                                                                                                                                            | In-memory store                                                  |\n| StoreResponseHeaders | `bool`                                         | StoreResponseHeaders allows you to store additional headers generated by next middlewares & handler.                                                                                                                                                                                                           | `false`                                                          |\n| MaxBytes             | `uint`                                         | MaxBytes is the maximum number of bytes of response bodies simultaneously stored in cache. | `1 * 1024 * 1024` (~1 MB)                                                  |\n| Methods              | `[]string`                                     | Methods specifies the HTTP methods to cache.                                                                                                                                                                                                                                                                   | `[]string{fiber.MethodGet, fiber.MethodHead}`                    |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:         nil,\n    Expiration:   5 * time.Minute,\n    CacheHeader:  \"X-Cache\",\n    DisableCacheControl: false,\n    CacheInvalidator: nil,\n    DisableValueRedaction: false,\n    KeyGenerator: func(c fiber.Ctx) string {\n        return utils.CopyString(c.Path())\n    },\n    ExpirationGenerator:  nil,\n    StoreResponseHeaders: false,\n    Storage:              nil,\n    MaxBytes:             1 * 1024 * 1024,\n    Methods: []string{fiber.MethodGet, fiber.MethodHead},\n}\n```\n"
  },
  {
    "path": "docs/middleware/compress.md",
    "content": "---\nid: compress\n---\n\n# Compress\n\nCompression middleware for [Fiber](https://github.com/gofiber/fiber) that automatically compresses responses with `gzip`, `deflate`, `brotli`, or `zstd` based on the client's [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header.\n\n:::note\nBodies smaller than 200 bytes remain uncompressed because compression would likely increase their size and waste CPU cycles. [See the fasthttp source](https://github.com/valyala/fasthttp/blob/497922a21ef4b314f393887e9c6147b8c3e3eda4/http.go#L1713-L1715).\n:::\n\n## Behavior\n\n- Skips compression for responses that already define `Content-Encoding`, for range requests, `206` responses, status codes without bodies, or when either side sends `Cache-Control: no-transform`.\n- `HEAD` requests negotiate compression so `Content-Encoding`, `Content-Length`, `ETag`, and `Vary` reflect the encoded representation, but the body is removed before sending.\n- When compression runs, strong `ETag` values are recomputed from the compressed bytes; when skipped, `Accept-Encoding` is still merged into `Vary` unless the header is `*` or already present.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/compress\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(compress.New())\n\n// Or extend your config for customization\napp.Use(compress.New(compress.Config{\n    Level: compress.LevelBestSpeed, // 1\n}))\n\n// Skip middleware for specific routes\napp.Use(compress.New(compress.Config{\n    Next:  func(c fiber.Ctx) bool {\n      return c.Path() == \"/dont_compress\"\n    },\n    Level: compress.LevelBestSpeed, // 1\n}))\n```\n\n## Config\n\n| Property | Type                   | Description                                                 | Default            |\n|:-------- |:-----------------------|:------------------------------------------------------------|:-------------------|\n| Next     | `func(fiber.Ctx) bool` | Skips this middleware when the function returns `true`.     | `nil`              |\n| Level    | `Level`                | Compression level to use.                                   | `LevelDefault (0)` |\n\nPossible values for the \"Level\" field are:\n\n- `LevelDisabled (-1)`: Compression is disabled.\n- `LevelDefault (0)`: Default compression level.\n- `LevelBestSpeed (1)`: Best compression speed.\n- `LevelBestCompression (2)`: Best compression.\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:  nil,\n    Level: LevelDefault,\n}\n```\n\n## Constants\n\n```go\n// Compression levels\nconst (\n    LevelDisabled        = -1\n    LevelDefault         = 0\n    LevelBestSpeed       = 1\n    LevelBestCompression = 2\n)\n```\n"
  },
  {
    "path": "docs/middleware/cors.md",
    "content": "---\nid: cors\n---\n\n# CORS\n\nCORS (Cross-Origin Resource Sharing) middleware for [Fiber](https://github.com/gofiber/fiber) lets servers control who can access resources and how. It isn't a security feature; it merely relaxes the browser's same-origin policy so cross-origin requests can succeed. Learn more on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).\n\nIt adds CORS headers to responses, listing allowed origins, methods, and headers, and handles preflight checks.\n\nUse the `AllowOrigins` option to define which origins may send cross-origin requests. It accepts single origins, lists, subdomain patterns, wildcards, and supports dynamic validation with `AllowOriginsFunc`.\n\nThe middleware normalizes `AllowOrigins`, verifies HTTP/HTTPS schemes, and strips trailing slashes. Invalid origins cause a panic. Panic messages and logs redact misconfigured origins by default; set `DisableValueRedaction` to `true` if you need the raw value for troubleshooting.\n\nAvoid [common pitfalls](#common-pitfalls) such as using wildcard origins with credentials, overly permissive origin lists, or skipping validation with `AllowOriginsFunc`, as misconfiguration can create security risks.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/cors\"\n)\n```\n\nOnce your Fiber app is initialized, apply the middleware in one of the following ways:\n\n### Basic usage\n\nTo use the default configuration, simply use `cors.New()`. This will allow wildcard origins '*', all methods, no credentials, and no headers or exposed headers.\n\n```go\napp.Use(cors.New())\n```\n\n### Custom configuration (specific origins, headers, etc.)\n\n```go\n// Initialize default config\napp.Use(cors.New())\n\n// Or extend your config for customization\napp.Use(cors.New(cors.Config{\n    AllowOrigins: []string{\"https://gofiber.io\", \"https://gofiber.net\"},\n    AllowHeaders: []string{\"Origin\", \"Content-Type\", \"Accept\"},\n}))\n```\n\n### Dynamic origin validation\n\nYou can use `AllowOriginsFunc` to programmatically determine whether to allow a request based on its origin. This is useful when you need to validate origins against a database or other dynamic sources. The function should return `true` if the origin is allowed, and `false` otherwise.\n\nBe sure to review the [security considerations](#security-considerations) when using `AllowOriginsFunc`.\n\n:::caution\nNever allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats.\n\nIf you need to allow wildcard origins, use `AllowOrigins` with a wildcard `\"*\"` instead of `AllowOriginsFunc`.\n:::\n\n```go\n// dbCheckOrigin checks if the origin is in the list of allowed origins in the database.\nfunc dbCheckOrigin(db *sql.DB, origin string) bool {\n    // Placeholder query - adjust according to your database schema and query needs\n    query := \"SELECT COUNT(*) FROM allowed_origins WHERE origin = $1\"\n    \n    var count int\n    err := db.QueryRow(query, origin).Scan(&count)\n    if err != nil {\n      // Handle error (e.g., log it); for simplicity, we return false here\n      return false\n    }\n    \n    return count > 0\n}\n\n// ...\n\napp.Use(cors.New(cors.Config{\n    AllowOriginsFunc: func(origin string) bool {\n      return dbCheckOrigin(db, origin)\n    },\n}))\n```\n\n### Prohibited usage\n\nThe following example is prohibited because it can expose your application to security risks. It sets `AllowOrigins` to `\"*\"` (a wildcard) and `AllowCredentials` to `true`.\n\n```go\napp.Use(cors.New(cors.Config{\n    AllowOrigins: []string{\"*\"},\n    AllowCredentials: true,\n}))\n```\n\nThis will result in the following panic:\n\n```text\npanic: [CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin '*'. Please specify allowed origins explicitly or adjust 'AllowCredentials' setting.\n```\n\n## Config\n\n| Property             | Type                        | Description                                                                                                                                                                                                                                                                                                                                                          | Default                                 |\n|:---------------------|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------|\n| AllowCredentials     | `bool`                      | 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 a wildcard (`\"*\"`) to prevent security vulnerabilities. | `false`                                 |\n| AllowHeaders         | `[]string`                  | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request.                                                                                                                                                                                                                          | `[]`                                    |\n| AllowMethods         | `[]string`                  | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request.                                                                                                                                                                                                                                         | `\"GET, POST, HEAD, PUT, DELETE, PATCH\"` |\n| AllowOrigins         | `[]string`                  | AllowOrigins defines a list of origins that may access the resource. This supports subdomain matching, so you can use a value like \"https://*.example.com\" to allow any subdomain of example.com to submit requests. If the special wildcard `\"*\"` is present in the list, all origins will be allowed.                                                              | `[\"*\"]`                                 |\n| AllowOriginsFunc     | `func(origin string) bool`  | `AllowOriginsFunc` is a function that dynamically determines whether to allow a request based on its origin. If this function returns `true`, the 'Access-Control-Allow-Origin' response header will be set to the request's 'origin' header. This function is only used if the request's origin doesn't match any origin in `AllowOrigins`.                         | `nil`                                   |\n| AllowPrivateNetwork  | `bool`                      | Indicates whether the `Access-Control-Allow-Private-Network` response header should be set to `true`, allowing requests from private networks. This aligns with modern security practices for web applications interacting with private networks.                                                                                                                    | `false`                                 |\n| DisableValueRedaction | `bool`                    | Disables redaction of misconfigured origins and settings in panics and logs. | `false`                                 |\n| ExposeHeaders        | `[]string`                    | ExposeHeaders defines an allowlist of headers that clients are allowed to access.                                                                                                                                                                                                                                                                                    | `[]`                                    |\n| MaxAge               | `int`                       | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, the Access-Control-Max-Age header will not be added and the browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header to 0.                                     | `0`                                     |\n| Next                 | `func(fiber.Ctx) bool`      | Next defines a function to skip this middleware when it returns true.                                                                                                                                                                                                                                                                                                  | `nil`                                   |\n\n:::note\nIf AllowOrigins is a zero value `[]string{}`, and AllowOriginsFunc is provided, the middleware will not default to allowing all origins with the wildcard value \"*\". Instead, it will rely on the AllowOriginsFunc to dynamically determine whether to allow a request based on its origin. This provides more flexibility and control over which origins are allowed.\n:::\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:             nil,\n    AllowOriginsFunc: nil,\n    AllowOrigins:     []string{\"*\"},\n    DisableValueRedaction: false,\n    AllowMethods: []string{\n        fiber.MethodGet,\n        fiber.MethodPost,\n        fiber.MethodHead,\n        fiber.MethodPut,\n        fiber.MethodDelete,\n        fiber.MethodPatch,\n    },\n    AllowHeaders:        []string{},\n    AllowCredentials:    false,\n    ExposeHeaders:       []string{},\n    MaxAge:              0,\n    AllowPrivateNetwork: false,\n}\n```\n\n## Subdomain Matching\n\nThe `AllowOrigins` configuration supports matching subdomains at any level. This means you can use a value like `\"https://*.example.com\"` to allow any subdomain of `example.com` to submit requests, including multiple subdomain levels such as `\"https://sub.sub.example.com\"`.\n\n### Example\n\nIf you want to allow CORS requests from any subdomain of `example.com`, including nested subdomains, you can configure the `AllowOrigins` like so:\n\n```go\napp.Use(cors.New(cors.Config{\n    AllowOrigins: []string{\"https://*.example.com\"},\n}))\n```\n\n## How It Works\n\nThe 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.\n\nWhen a request arrives, the middleware first checks whether it is a preflight request—a CORS mechanism that determines if the actual request is safe to send. Preflight requests are HTTP OPTIONS requests with specific CORS headers. If the request is preflight, the middleware responds with the appropriate CORS headers and ends the request.\n\n:::note\nPreflight requests are typically sent by browsers before making actual cross-origin requests, especially for methods other than GET or POST, or when custom headers are used.\n\nA preflight request is an HTTP OPTIONS request that includes the `Origin`, `Access-Control-Request-Method`, and optionally `Access-Control-Request-Headers` headers. The browser sends this request to check if the server allows the actual request method and headers.\n:::\n\nIf the request is not preflight, the middleware adds the CORS headers to the response and passes the request to the next handler. The actual CORS headers added depend on the configuration of the middleware.\n\nThe `AllowOrigins` option controls which origins can make cross-origin requests. The middleware handles different `AllowOrigins` configurations as follows:\n\n- **Single origin:** If `AllowOrigins` is set to a single origin like `\"http://www.example.com\"`, and that origin matches the origin of the incoming request, the middleware adds the header `Access-Control-Allow-Origin: http://www.example.com` to the response.\n\n- **Multiple origins:** If `AllowOrigins` is set to multiple origins like `\"https://example.com, https://www.example.com\"`, the middleware picks the origin that matches the origin of the incoming request.\n\n- **Subdomain matching:** If `AllowOrigins` includes `\"https://*.example.com\"`, a subdomain like `https://sub.example.com` will be matched and `\"https://sub.example.com\"` will be the header. This will also match `https://sub.sub.example.com` and so on, but not `https://example.com`.\n\n- **Wildcard origin:** If `AllowOrigins` is set to `\"*\"`, the middleware uses that and adds the header `Access-Control-Allow-Origin: *` to the response.\n\nIn all cases above, except the **Wildcard origin**, the middleware will either add the `Access-Control-Allow-Origin` header to the response matching the origin of the incoming request, or it will not add the header at all if the origin is not allowed.\n\n- **Programmatic origin validation:**: The middleware also handles the `AllowOriginsFunc` option, which allows you to programmatically determine if an origin is allowed. If `AllowOriginsFunc` returns `true` for an origin, the middleware sets the `Access-Control-Allow-Origin` header to that origin.\n\n- **Null origin handling:** The middleware accepts the special literal value `\"null\"` as a valid origin. According to the [CORS specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#origin), browsers send `\"null\"` as the origin for certain privacy-sensitive contexts, such as:\n  - Requests from sandboxed iframes\n  - Requests from `file://` URLs\n  - Requests from `data:` URLs\n  - Cross-origin redirects\n\n  When using `AllowOriginsFunc`, if the function returns `true` for the literal string `\"null\"`, the middleware will set `Access-Control-Allow-Origin: null` in the response. The `\"null\"` origin is case-sensitive and must be lowercase.\n\nThe `AllowMethods` option controls which HTTP methods are allowed. For example, if `AllowMethods` is set to `\"GET, POST\"`, the middleware adds the header `Access-Control-Allow-Methods: GET, POST` to the response.\n\nThe `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.\n\nThe `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 (`*`).\n\nThe `ExposeHeaders` option defines an allowlist 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.\n\nThe `MaxAge` option indicates how long the results of a preflight request can be cached. If `MaxAge` is set to `3600`, the middleware adds the header `Access-Control-Max-Age: 3600` to the response.\n\nThe `Vary` header helps caches store the correct response. For simple requests the middleware sets `Vary: Origin` unless all origins are allowed. Preflight responses add `Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers` (and `Access-Control-Request-Private-Network` when enabled and requested). This ensures caches know when to reuse a response and when to revalidate with the server.\n\n## Infrastructure Considerations\n\nWhen deploying Fiber applications behind infrastructure components like CDNs, API gateways, load balancers, or reverse proxies, you have two main options for handling CORS:\n\n### Option 1: Use Infrastructure-Level CORS (Recommended)\n\n**For most production deployments, it is often preferable to handle CORS at the infrastructure level** rather than in your Fiber application. This approach offers several advantages:\n\n- **Better Performance**: CORS headers are added at the edge, closer to the client\n- **Reduced Server Load**: Preflight requests are handled without reaching your application\n- **Centralized Configuration**: Manage CORS policies alongside other infrastructure settings\n- **Built-in Caching**: Infrastructure providers optimize CORS response caching\n\n**Common infrastructure CORS solutions:**\n\n- **CDNs**: CloudFront, CloudFlare, Azure CDN - handle CORS at edge locations\n- **API Gateways**: AWS API Gateway, Google Cloud API Gateway - centralized CORS management\n- **Load Balancers**: Application Load Balancers with CORS rules\n- **Reverse Proxies**: Nginx, Apache with CORS modules\n\nIf using infrastructure-level CORS, **disable Fiber's CORS middleware** to avoid conflicts:\n\n```go\n// Don't use both - choose one approach\n// app.Use(cors.New()) // Remove this line when using infrastructure CORS\n```\n\n### Option 2: Application-Level CORS (Fiber Middleware)\n\nUse Fiber's CORS middleware when you need:\n\n- **Dynamic origin validation** based on application logic\n- **Fine-grained control** over CORS policies per route\n- **Integration with application state** (database-driven origins, etc.)\n- **Development environments** where infrastructure CORS isn't available\n\nIf choosing this approach, ensure that **all CORS headers reach your Fiber application unchanged**.\n\n### Required Headers for CORS Preflight Requests\n\nFor CORS preflight requests to work correctly, these headers **must not be stripped or modified by caching layers**:\n\n- `Origin` - Required to identify the requesting origin\n- `Access-Control-Request-Method` - Required to identify the HTTP method for the actual request\n- `Access-Control-Request-Headers` - Optional, contains custom headers the actual request will use\n- `Access-Control-Request-Private-Network` - Optional, for private network access requests\n\n:::warning Critical Preflight Requirement\nIf the `Access-Control-Request-Method` header is missing from an OPTIONS request, Fiber will not recognize them as CORS preflight requests. Instead, they'll be treated as regular OPTIONS requests, which typically return `405 Method Not Allowed` since most applications don't define explicit OPTIONS handlers.\n:::\n\n### CORS Response Headers (Set by Fiber)\n\nThe middleware sets these response headers based on your configuration:\n\n**For all CORS requests:**\n\n- `Access-Control-Allow-Origin` - Set to the allowed origin or \"*\"\n- `Access-Control-Allow-Credentials` - Set to \"true\" when `AllowCredentials: true`\n- `Access-Control-Expose-Headers` - Lists headers the client can access\n- `Vary` - Set to \"Origin\" (unless wildcard origins are used)\n\n**For preflight responses only:**\n\n- `Access-Control-Allow-Methods` - Lists allowed HTTP methods\n- `Access-Control-Allow-Headers` - Lists allowed request headers (or echoes the request)\n- `Access-Control-Max-Age` - Cache duration for preflight results (if MaxAge > 0)\n- `Access-Control-Allow-Private-Network` - Set to \"true\" when private network access is allowed\n- `Vary` - Set to \"Access-Control-Request-Method, Access-Control-Request-Headers, Origin\"\n\n### Common Infrastructure Issues\n\n**CDNs (CloudFront, CloudFlare, etc.)**:\n\n- Configure cache policies to forward all CORS headers\n- Ensure OPTIONS requests are not cached inappropriately or cache them correctly with proper Vary headers\n- Don't strip or modify CORS request headers\n\n**API Gateways**:\n\n- Choose either gateway-level CORS OR application-level CORS, not both\n- If using gateway CORS, disable Fiber's CORS middleware\n- If forwarding to Fiber, ensure all headers pass through unchanged\n\n**Load Balancers/Reverse Proxies**:\n\n- Preserve all HTTP headers, especially CORS-related ones\n- Don't modify or strip `Origin`, `Access-Control-Request-*` headers\n\n**WAFs/Security Services**:\n\n- Whitelist CORS headers in security rules\n- Ensure OPTIONS requests with CORS headers aren't blocked\n\n### Debugging CORS Issues\n\nAdd this middleware **before** your CORS configuration to debug what headers Fiber receives:\n\n```go\n// Debug middleware to log CORS preflight requests\n// Only use in development or testing environments\napp.Use(func(c *fiber.Ctx) error {\n    if c.Method() == \"OPTIONS\" {\n        fmt.Printf(\"OPTIONS %s\\n\", c.Path())\n        fmt.Printf(\"  Origin: %s\\n\", c.Get(\"Origin\"))\n        fmt.Printf(\"  Access-Control-Request-Method: %s\\n\", c.Get(\"Access-Control-Request-Method\"))\n        fmt.Printf(\"  Access-Control-Request-Headers: %s\\n\", c.Get(\"Access-Control-Request-Headers\"))\n    }\n    return c.Next()\n})\n\napp.Use(cors.New(cors.Config{\n    AllowOrigins: []string{\"https://yourdomain.com\"},\n    AllowMethods: []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"},\n}))\n```\n\nTest CORS preflight directly with curl:\n\n```bash\n# Test preflight request\ncurl -X OPTIONS https://your-app.com/api/test \\\n  -H \"Origin: https://yourdomain.com\" \\\n  -H \"Access-Control-Request-Method: POST\" \\\n  -H \"Access-Control-Request-Headers: Content-Type\" \\\n  -v\n\n# Test simple CORS request\ncurl -X GET https://your-app.com/api/test \\\n  -H \"Origin: https://yourdomain.com\" \\\n  -v\n```\n\n### Caching Considerations\n\nThe middleware sets appropriate `Vary` headers to ensure proper caching:\n\n- **Non-wildcard origins**: `Vary: Origin` is set to cache responses per origin\n- **Preflight requests**: `Vary: Access-Control-Request-Method, Access-Control-Request-Headers, Origin`\n- **OPTIONS without preflight headers**: `Vary: Origin` to avoid cache poisoning\n\nEnsure your infrastructure respects these `Vary` headers for correct caching behavior.\n\n### Choosing the Right Approach\n\n| Scenario | Recommended Approach |\n|----------|---------------------|\n| Production with CDN/API Gateway | Infrastructure-level CORS |\n| Dynamic origin validation needed | Application-level CORS |\n| Microservices with different CORS policies | Application-level CORS |\n| Simple static origins | Infrastructure-level CORS |\n| Development/testing | Application-level CORS |\n| High traffic applications | Infrastructure-level CORS |\n\n:::tip Infrastructure CORS Configuration\nMost cloud providers offer comprehensive CORS documentation:\n\n- [AWS CloudFront CORS](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors)\n- [Google Cloud CORS](https://cloud.google.com/storage/docs/cross-origin)\n- [Azure CDN CORS](https://docs.microsoft.com/en-us/azure/cdn/cdn-cors)\n- [CloudFlare CORS](https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/#cf-connecting-ip)\n\nConfigure CORS at the infrastructure level when possible for optimal performance and reduced complexity.\n:::\n\n## Security Considerations\n\nWhen configuring CORS, misconfiguration can potentially expose your application to various security risks. Here are some secure configurations and common pitfalls to avoid:\n\n### Secure Configurations\n\n- **Specify Allowed Origins**: Instead of using a wildcard (`\"*\"`), specify the exact domains allowed to make requests. For example, `AllowOrigins: \"https://www.example.com, https://api.example.com\"` ensures only these domains can make cross-origin requests to your application.\n\n- **Use Credentials Carefully**: If your application needs to support credentials in cross-origin requests, ensure `AllowCredentials` is set to `true` and specify exact origins in `AllowOrigins`. Do not use a wildcard origin in this case.\n\n- **Limit Exposed Headers**: Only allowlist headers that are necessary for the client-side application by setting `ExposeHeaders` appropriately. This minimizes the risk of exposing sensitive information.\n\n### Common Pitfalls\n\n- **Wildcard Origin with Credentials**: Setting `AllowOrigins` to `\"*\"` (a wildcard) and `AllowCredentials` to `true` is a common misconfiguration. This combination is prohibited because it can expose your application to security risks.\n\n- **Overly Permissive Origins**: Specifying too many origins or using overly broad patterns (e.g., `https://*.example.com`) can inadvertently allow malicious sites to interact with your application. Be as specific as possible with allowed origins.\n\n- **Inadequate `AllowOriginsFunc` Validation**: When using `AllowOriginsFunc` for dynamic origin validation, ensure the function includes robust checks to prevent unauthorized origins from being accepted. Overly permissive validation can lead to security vulnerabilities. Never allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats. If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `\"*\"` instead of `AllowOriginsFunc`.\n\nRemember, the key to secure CORS configuration is specificity and caution. By carefully selecting which origins, methods, and headers are allowed, you can help protect your application from cross-origin attacks.\n"
  },
  {
    "path": "docs/middleware/csrf.md",
    "content": "---\nid: csrf\n---\n\n# CSRF\n\nThe CSRF middleware protects against [Cross-Site Request Forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) attacks by validating tokens on unsafe HTTP methods such as POST, PUT, and DELETE. It responds with 403 Forbidden when validation fails.\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Best Practices & Production Requirements](#best-practices--production-requirements)\n- [Configuration by Application Type](#configuration-by-application-type)\n- [Recipes for Common Use Cases](#recipes-for-common-use-cases)\n- [Using CSRF Tokens](#using-csrf-tokens)\n- [Security Model](#security-model)\n- [Token Extractors](#token-extractors)\n- [Advanced Configuration](#advanced-configuration)\n- [API Reference](#api-reference)\n- [Config Properties](#config-properties)\n- [Error Types](#error-types)\n- [Constants](#constants)\n\n## Quick Start\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n    \"github.com/gofiber/fiber/v3/middleware/csrf\"\n)\n\n// Default config (development only)\napp.Use(csrf.New())\n\n// Production config\napp.Use(csrf.New(csrf.Config{\n    CookieName:        \"__Host-csrf_\",\n    CookieSecure:      true,\n    CookieHTTPOnly:    true,  // false for SPAs\n    CookieSameSite:    \"Lax\",\n    CookieSessionOnly: true,\n    Extractor:         extractors.FromHeader(\"X-Csrf-Token\"),\n    Session:           sessionStore,\n    // Redaction is enabled by default. Set DisableValueRedaction when you must expose tokens or storage keys in diagnostics.\n    // DisableValueRedaction: true,\n}))\n```\n\n## Best Practices & Production Requirements\n\n:::danger Production Requirements\n\n- `CookieSecure: true` (HTTPS only)\n- `CookieSameSite: \"Lax\"` or `\"Strict\"`\n- Use `Session` store for better security\n\n:::\n\n1. **Always use HTTPS** in production\n2. **Use sessions** for authenticated applications\n3. **Set `CookieSecure: true`** and appropriate SameSite values\n4. **Implement XSS protection** alongside CSRF\n5. **Regenerate tokens** after auth changes\n6. **Use `__Host-` cookie prefix** when possible\n\n:::warning BREACH Protection\nTo mitigate BREACH attacks, ensure your pages are served over HTTPS, disable HTTP compression, and implement rate limiting for requests. The CSRF token is sent as a header on every request, so if you include the token in a page that is vulnerable to BREACH, an attacker may be able to extract the token.\n:::\n\n## Configuration by Application Type\n\n### Server-Side Rendered Apps\n\n```go\napp.Use(csrf.New(csrf.Config{\n    CookieName:        \"__Host-csrf_\",\n    CookieSecure:      true,\n    CookieHTTPOnly:    true,        // Secure - blocks JavaScript\n    CookieSameSite:    \"Lax\",\n    CookieSessionOnly: true,\n    Extractor:         extractors.FromForm(\"_csrf\"),\n    Session:           sessionStore,\n}))\n```\n\n### Single Page Applications (SPAs)\n\n```go\napp.Use(csrf.New(csrf.Config{\n    CookieName:        \"__Host-csrf_\",\n    CookieSecure:      true,\n    CookieHTTPOnly:    false,       // Required for JavaScript access to tokens\n    CookieSameSite:    \"Lax\",\n    CookieSessionOnly: true,\n    Extractor:         extractors.FromHeader(\"X-Csrf-Token\"),\n    Session:           sessionStore,\n}))\n```\n\n:::warning SPA Security Trade-off\nSPAs require `CookieHTTPOnly: false` to access tokens via JavaScript. This slightly increases XSS risk but is necessary for SPA functionality.\n:::\n\n## Recipes for Common Use Cases\n\n- **Without Sessions**: [CSRF Recipe](https://github.com/gofiber/recipes/tree/master/csrf) - Simple Double Submit Cookie pattern\n- **With Sessions**: [CSRF with Session Recipe](https://github.com/gofiber/recipes/tree/master/csrf-with-session) - More secure Synchronizer Token pattern\n\n## Using CSRF Tokens\n\n### Server-Side Forms\n\n```go\nfunc formHandler(c fiber.Ctx) error {\n    token := csrf.TokenFromContext(c)\n\n    return c.SendString(fmt.Sprintf(`\n        <form method=\"POST\" action=\"/submit\">\n            <input type=\"hidden\" name=\"_csrf\" value=\"%s\">\n            <input type=\"text\" name=\"message\" required>\n            <button type=\"submit\">Submit</button>\n        </form>\n    `, token))\n}\n```\n\n### Single Page Applications\n\n```go\nfunc apiHandler(c fiber.Ctx) error {\n    token := csrf.TokenFromContext(c)\n\n    return c.JSON(fiber.Map{\n        \"csrf_token\": token,\n        \"data\":       \"your data\",\n    })\n}\n```\n\n```javascript\n// Get CSRF token from cookie\nfunction getCsrfToken() {\n    const value = `; ${document.cookie}`;\n    const parts = value.split(`; __Host-csrf_=`);\n    if (parts.length === 2) return parts.pop().split(';').shift();\n}\n\n// Use with fetch API\nasync function makeRequest(url, data) {\n    const csrfToken = getCsrfToken();\n\n    const response = await fetch(url, {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json',\n            'X-Csrf-Token': csrfToken\n        },\n        body: JSON.stringify(data)\n    });\n\n    if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    return response.json();\n}\n```\n\n## Security Model\n\nThe middleware employs a robust, defense-in-depth strategy to protect against CSRF attacks. The primary defense is token-based validation, which operates in one of two modes depending on your configuration. This is supplemented by a mandatory secondary check on the request's origin.\n\n### Fetch Metadata Guardrails\n\n- **Sec-Fetch-Site**: For unsafe methods, the middleware inspects the [`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site) header when present. If the header value is not one of \"same-origin\", \"none\", \"same-site\", or \"cross-site\", the request is rejected with `ErrFetchSiteInvalid`. If the header is valid or absent, the request proceeds to the standard origin and token validation checks. This provides an early check to block requests with invalid `Sec-Fetch-Site` values, while allowing legitimate same-site and cross-site requests to be validated by the existing mechanisms.\n\n### 1. Token Validation Patterns\n\n#### Double Submit Cookie (Default Mode)\n\nThis is the default pattern, used when a `Session` store is **not** configured. It is a \"semi-stateless\" approach; while it doesn't tie tokens to a specific user session, the server still maintains a record of all validly issued tokens.\n\n- **How it Works:**\n  1. On a user's first visit (or a safe request like `GET`), the middleware generates a unique token.\n  2. This token is sent to the client in a `Set-Cookie` header.\n  3. The server also stores this token (in memory by default or in the configured `Storage`). It confirms the token is server-generated and still valid, but it is not tied to a specific user.\n  4. For subsequent unsafe requests (e.g., `POST`, `PUT`), the client must read the token from the cookie and echo it in a different location, such as the `X-Csrf-Token` header.\n\n- **Validation:** The middleware validates three things: that the token from the header/form **exactly matches** the token from the cookie, that the token **exists** in the server-side storage, and that it **has not expired**.\n- **Why it is secure:** Attackers on a malicious domain cannot read the victim's cookie to forge a matching header. They also cannot invent a token because it wouldn't exist in the server's storage registry.\n\n#### Synchronizer Token (Session-Based Mode)\n\nThis is a more secure, stateful pattern that is **automatically enabled** when you provide a `Session` store in the configuration.\n\n- **How it Works:**\n  1. A unique token is generated and stored directly within the user's session data on the server.\n  2. The token is also sent to the client as a cookie.\n  3. For unsafe requests, the client sends the token back in a header or form field.\n\n- **Validation:** The middleware performs a multi-step validation:\n  1. It first performs the standard **Double Submit Cookie check**: the token from the header/form must exactly match the token from the cookie. This is a fast and efficient first line of defense, and there is little benefit of skipping it.\n  2. It then validates that this token exists and is valid within the user's **server-side session**. This is the authoritative check that ties the token to the authenticated user.\n\n- **Why it is more secure:** Tying the token to the server-side session provides the strongest CSRF protection, as the token is then guaranteed to have been generated for the specific user. While browsers automatically send the required cookie, custom API clients must remember to include the cookie with their requests for validation to succeed.\n\n```go\n// Enable the more secure Synchronizer Token pattern\napp.Use(csrf.New(csrf.Config{\n    Session: sessionStore, // Providing a session store activates this mode\n}))\n```\n\n### 2. Origin & Referer Validation\n\nAs a crucial second layer of defense, the middleware **always** performs `Origin` and `Referer` header checks for unsafe requests (when the connection is HTTPS).\n\n- The request's `Origin` (for cross-origin requests) or `Referer` (for same-origin requests) header **must** match the application's `Host` header or be explicitly allowed in the `TrustedOrigins` list.\n- This check is performed *in addition* to token validation and provides strong protection because these headers are reliably set by browsers and cannot be programmatically controlled by an attacker from a malicious site.\n\n## Token Extractors\n\nThis middleware uses the shared `extractors` package for token extraction. For full details on extractor types, chaining, security, and advanced usage, see the [Extractors Guide](../guide/extractors).\n\n**Extractor Source Constants:**\nExtractor source constants (such as `SourceHeader`, `SourceForm`, etc.) are defined in the shared extractors package, not in the CSRF middleware itself. Refer to the Extractors Guide for their definitions and usage.\n\n### CSRF-Specific Extractor Notes\n\nFor CSRF protection, prefer secure extraction methods:\n\n- **Headers** (`extractors.FromHeader(\"X-Csrf-Token\")`) – Most secure, not logged in URLs\n- **Form data** (`extractors.FromForm(\"_csrf\")`) – Secure for form submissions\n- **Avoid URL parameters** – Query/param extractors expose tokens in logs and browser history\n\n:::note What about cookies?\n**Cookies are generally not a secure source for CSRF tokens.** The middleware will panic if you configure an extractor that reads from cookies with the same name as your CSRF cookie. This is because reading the CSRF token from a cookie with the same name as the CSRF cookie defeats CSRF protection entirely, as the extracted token will always match the cookie value, allowing any CSRF attack to succeed.\n\n**Advanced usage:**\nIn rare cases, you may securely extract a CSRF token from a cookie if:\n\n- You read from a different cookie (not the CSRF cookie itself)\n- You use multiple cookies for custom validation\n- You implement custom logic across different cookie sources\n\nIf you do this, set the extractor’s `Source` to `SourceCookie` and allow the middleware to check that the cookie name is different from your CSRF cookie. It will panic if this is the case.\n\n**Warning:**\nCookie-based extraction is strongly discouraged, as it is easy to misconfigure and creates security risks. Prefer extracting tokens from headers or form fields for robust CSRF protection. See the [Extractors Guide](../guide/extractors#security-considerations) for more details.\n:::\n\n### Route-Specific Configuration\n\nYou can configure different extraction methods for different routes:\n\n```go\n// API routes - header extraction for AJAX/fetch requests\napi := app.Group(\"/api\")\napi.Use(csrf.New(csrf.Config{\n    Extractor: extractors.FromHeader(\"X-Csrf-Token\"),\n}))\n\n// Form routes - form field extraction for traditional forms\nforms := app.Group(\"/forms\")\nforms.Use(csrf.New(csrf.Config{\n    Extractor: extractors.FromForm(\"_csrf\"),\n}))\n```\n\n### Custom CSRF Extractors\n\nFor specialized CSRF token extraction needs, you can create custom extractors. See the [Extractors Guide](../guide/extractors#custom-extraction-logic) for advanced patterns and security notes.\n\n:::danger Never Extract from Cookies\n**NEVER create custom extractors that read from cookies using the same `CookieName` as your CSRF configuration.** This completely defeats CSRF protection by making the extracted token always match the cookie value, allowing any CSRF attack to succeed.\n\n```go\n// ❌ NEVER DO THIS - Completely defeats CSRF protection\nbadExtractor := csrf.Extractor{\n    Extract: func(c fiber.Ctx) (string, error) {\n        return c.Cookies(\"csrf_\"), nil  // Always passes validation!\n    },\n    Source: csrf.SourceCustom, // See extractors.SourceCustom in shared package\n    Key:    \"csrf_\",\n}\n\n// ✅ DO THIS - Extract from different source than cookie\napp.Use(csrf.New(csrf.Config{\n    CookieName: \"csrf_\",\n    Extractor: extractors.FromHeader(\"X-Csrf-Token\"), // Header vs cookie comparison\n}))\n```\n\nThe middleware uses the **Double Submit Cookie** pattern – it compares the extracted token against the cookie value. If you configure an extractor that reads from the same cookie, it will panic because they will always match and provide zero CSRF protection.\n:::\n\n#### Bearer Token Embedding & Custom Extractors\n\nYou can create advanced extractors for use cases like JWT embedding or JSON body parsing. See the [Extractors Guide](../guide/extractors#custom-extraction-logic) for secure implementation patterns and more examples.\n\n### Fallback Extraction\n\nFor applications that need to support both AJAX and form submissions:\n\n```go\n// Try header first (AJAX), fall back to form (traditional forms)\napp.Use(csrf.New(csrf.Config{\n    Extractor: extractors.Chain(\n        extractors.FromHeader(\"X-Csrf-Token\"),\n        extractors.FromForm(\"_csrf\"),\n    ),\n}))\n```\n\n:::warning\nChaining extractors increases complexity. Use only when you need to support multiple client types. See the [Extractors Guide](../guide/extractors#chain-ordering-strategy) for details and security notes.\n:::\n\n## Advanced Configuration\n\n### Trusted Origins\n\n```go\napp.Use(csrf.New(csrf.Config{\n    TrustedOrigins: []string{\n        \"https://trusted.example.com\",\n        \"https://*.example.com\", // Wildcard subdomains\n    },\n}))\n```\n\n### Custom Error Handler\n\n```go\napp.Use(csrf.New(csrf.Config{\n    ErrorHandler: func(c fiber.Ctx, err error) error {\n        accepts := c.Accepts(\"html\", \"json\")\n        path := c.Path()\n        if accepts == \"json\" || strings.HasPrefix(path, \"/api/\") {\n            return c.Status(fiber.StatusForbidden).JSON(fiber.Map{\n                \"error\": \"Forbidden\",\n            })\n        }\n        return c.Status(fiber.StatusForbidden).Render(\"error\", fiber.Map{\n            \"Title\": \"Forbidden\",\n            \"Status\": fiber.StatusForbidden,\n        }, \"layouts/main\")\n    },\n}))\n```\n\n### Custom Storage/Database\n\nYou can use any storage from our [storage](https://github.com/gofiber/storage/) package.\n\n```go\nstorage := sqlite3.New() // From github.com/gofiber/storage/sqlite3/v2\napp.Use(csrf.New(csrf.Config{\n    Storage: storage,\n}))\n```\n\n### Token Management\n\n```go\n// Delete token (e.g., on logout)\nhandler := csrf.HandlerFromContext(c)\nif handler != nil {\n    if err := handler.DeleteToken(c); err != nil {\n        // handle error, e.g. log it\n    }\n}\n\n// With session middleware\n// Destroying the session will also remove the CSRF token if using session-based CSRF.\nsession.Destroy()\n```\n\n## API Reference\n\n```go\n// Create middleware\nfunc New(config ...csrf.Config) fiber.Handler\n\n// Get token from context\nfunc TokenFromContext(ctx any) string\n\n// Get handler from context\nfunc HandlerFromContext(ctx any) *csrf.Handler\n\n// Delete token\nfunc (h *csrf.Handler) DeleteToken(c fiber.Ctx) error\n```\n\n`TokenFromContext` and `HandlerFromContext` accept a `fiber.CustomCtx`, `fiber.Ctx`, a `*fasthttp.RequestCtx`, or a `context.Context`.\n\n## Config Properties\n\n| Property          | Type                               | Description                                                                                                                   | Default                      |\n|:------------------|:-----------------------------------|:------------------------------------------------------------------------------------------------------------------------------|:-----------------------------|\n| Next              | `func(fiber.Ctx) bool`             | Skip middleware when returns true                                                                                             | `nil`                        |\n| CookieName        | `string`                           | CSRF cookie name                                                                                                              | `\"csrf_\"`                    |\n| CookieDomain      | `string`                           | CSRF cookie domain                                                                                                            | `\"\"`                         |\n| CookiePath        | `string`                           | CSRF cookie path                                                                                                              | `\"\"`                         |\n| CookieSecure      | `bool`                             | HTTPS only cookie (**required for production**)                                                                               | `false`                      |\n| CookieHTTPOnly    | `bool`                             | Prevent JavaScript access (**use `false` for SPAs**)                                                                          | `false`                      |\n| CookieSameSite    | `string`                           | SameSite attribute (**use \"Lax\" or \"Strict\"**)                                                                                | `\"Lax\"`                      |\n| CookieSessionOnly | `bool`                             | Session-only cookie (expires on browser close)                                                                                | `false`                      |\n| IdleTimeout       | `time.Duration`                    | Token expiration time                                                                                                         | `30 * time.Minute`           |\n| KeyGenerator      | `func() string`                    | Token generation function                                                                                                     | `utils.SecureToken`               |\n| ErrorHandler      | `fiber.ErrorHandler`               | Custom error handler                                                                                                          | `defaultErrorHandler`        |\n| Extractor         | `extractors.Extractor`             | Token extraction method with metadata                                                                                         | `extractors.FromHeader(\"X-Csrf-Token\")` |\n| DisableValueRedaction | `bool`                         | Disables redaction of tokens and storage keys in logs and error messages. | `false`                      |\n| Session           | `*session.Store`                   | Session store (**recommended for production**)                                                                                | `nil`                        |\n| Storage           | `fiber.Storage`                    | Token storage (overridden by Session)                                                                                         | `nil`                        |\n| TrustedOrigins    | `[]string`                         | Trusted origins for cross-origin requests                                                                                     | `[]`                         |\n| SingleUseToken    | `bool`                             | Generate new token after each use                                                                                             | `false`                      |\n\n## Error Types\n\n```go\nvar (\n    ErrTokenNotFound   = errors.New(\"csrf: token not found\")\n    ErrTokenInvalid    = errors.New(\"csrf: token invalid\")\n    ErrRefererNotFound = errors.New(\"csrf: referer header missing\")\n    ErrRefererInvalid  = errors.New(\"csrf: referer header invalid\")\n    ErrRefererNoMatch  = errors.New(\"csrf: referer does not match host or trusted origins\")\n    ErrOriginInvalid   = errors.New(\"csrf: origin header invalid\")\n    ErrOriginNoMatch   = errors.New(\"csrf: origin does not match host or trusted origins\")\n)\n```\n\n## Constants\n\n```go\nconst (\n    HeaderName = \"X-Csrf-Token\"\n)\n```\n"
  },
  {
    "path": "docs/middleware/earlydata.md",
    "content": "---\nid: earlydata\n---\n\n# EarlyData\n\nThe Early Data middleware adds TLS 1.3 \"0-RTT\" support to [Fiber](https://github.com/gofiber/fiber). When the client and server share a PSK, TLS 1.3 lets the client send data with the first flight and skip the initial round trip.\n\nEnable Fiber's `TrustProxy` option before using this middleware to avoid spoofed client headers.\nWhen `TrustProxy` is disabled (the default) or the remote address is not trusted by your proxy configuration, requests carrying the `Early-Data` header are rejected with `425 Too Early` to prevent 0-RTT spoofing from direct clients.\n\nEnabling early data in a reverse proxy (for example, `ssl_early_data on;` in nginx) makes requests replayable. Review these resources before proceeding:\n\n- [datatracker](https://datatracker.ietf.org/doc/html/rfc8446#section-8)\n- [trailofbits](https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt)\n\nBy default, the middleware permits early data only for safe methods (`GET`, `HEAD`, `OPTIONS`, `TRACE`) and rejects other requests before your handler runs. Override this behavior with the `AllowEarlyData` option.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\nfunc IsEarly(c fiber.Ctx) bool\n```\n\n`IsEarly` returns `true` when a request used early data and the middleware allowed it to proceed.\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/earlydata\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(earlydata.New())\n\n// Or extend your config for customization\napp.Use(earlydata.New(earlydata.Config{\n    Error: fiber.ErrTooEarly,\n    // ...\n}))\n```\n\n## Config\n\n| Property       | Type                    | Description | Default                                                |\n|:---------------|:------------------------|:-----------|:-------------------------------------------------------|\n| Next           | `func(fiber.Ctx) bool` | Skip this middleware when the function returns true. | `nil` |\n| IsEarlyData    | `func(fiber.Ctx) bool` | Reports whether the request used early data. | Function checking if \"Early-Data\" header equals \"1\" |\n| AllowEarlyData | `func(fiber.Ctx) bool` | Decides if an early-data request should be allowed. | Function rejecting on unsafe and allowing safe methods |\n| Error          | `error`                 | Returned when an early-data request is rejected. | `fiber.ErrTooEarly` |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    IsEarlyData: func(c fiber.Ctx) bool {\n        return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue\n    },\n    AllowEarlyData: func(c fiber.Ctx) bool {\n        return fiber.IsMethodSafe(c.Method())\n    },\n    Error: fiber.ErrTooEarly,\n}\n```\n\n## Constants\n\n```go\nconst (\n    DefaultHeaderName      = \"Early-Data\"\n    DefaultHeaderTrueValue = \"1\"\n)\n```\n"
  },
  {
    "path": "docs/middleware/encryptcookie.md",
    "content": "---\nid: encryptcookie\n---\n\n# Encrypt Cookie\n\nThe Encrypt Cookie middleware for [Fiber](https://github.com/gofiber/fiber) encrypts cookie values for secure storage.\n\n:::note\nThis middleware encrypts cookie values but not cookie names.\n:::\n\n## Signatures\n\n```go\n// Initializes the middleware\nfunc New(config ...Config) fiber.Handler\n\n// GenerateKey returns a random string of 16, 24, or 32 bytes.\n// The length of the key determines the AES encryption algorithm used:\n// 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM.\nfunc GenerateKey(length int) string\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/encryptcookie\"\n)\n```\n\nOnce your Fiber app is initialized, register the middleware:\n\n```go\n// Provide a minimal configuration\napp.Use(encryptcookie.New(encryptcookie.Config{\n    Key: \"secret-32-character-string\",\n}))\n\n// Retrieve the encrypted cookie value\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"value=\" + c.Cookies(\"test\"))\n})\n\n// Create an encrypted cookie\napp.Post(\"/\", func(c fiber.Ctx) error {\n    c.Cookie(&fiber.Cookie{\n        Name:  \"test\",\n        Value: \"SomeThing\",\n    })\n    return nil\n})\n```\n\n:::note\nUse an encoded key of 16, 24, or 32 bytes to select AES‑128, AES‑192, or AES‑256‑GCM. Generate a stable key with `openssl rand -base64 32` or `encryptcookie.GenerateKey(32)` and store it securely. Generating a new key on each startup renders existing cookies unreadable.\n:::\n\n## Config\n\n| Property  | Type                                                | Description                                                                                           | Default                      |\n|:----------|:----------------------------------------------------|:------------------------------------------------------------------------------------------------------|:-----------------------------|\n| Next      | `func(fiber.Ctx) bool`                             | A function to skip this middleware when it returns true.                                                | `nil`                        |\n| Except    | `[]string`                                          | Array of cookie keys that should not be encrypted.                                                    | `[]`                         |\n| Key       | `string`                                            | A base64-encoded unique key to encode & decode cookies. Required. Key length should be 16, 24, or 32 bytes. | (No default, required field) |\n| Encryptor | `func(name, decryptedString, key string) (string, error)` | A custom function to encrypt cookies.                                                                 | `EncryptCookie`              |\n| Decryptor | `func(name, encryptedString, key string) (string, error)` | A custom function to decrypt cookies.                                                                 | `DecryptCookie`              |\n\n### Encryptor and Decryptor parameters\n\nCustom encryptor and decryptor functions receive three arguments:\n\n- `name`: The cookie name. The default helpers bind this value as additional authenticated data (AAD) so encrypted values can only be decrypted for the same cookie.\n- `string`: The cookie payload. `EncryptCookie` accepts the decrypted value and returns ciphertext, while `DecryptCookie` receives ciphertext and must return the decrypted value.\n- `key`: The base64-encoded key pulled from the middleware configuration. Use it to derive or validate any encryption keys your implementation requires.\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:      nil,\n    Except:    []string{},\n    Key:       \"\",\n    Encryptor: EncryptCookie,\n    Decryptor: DecryptCookie,\n}\n```\n\n## Use with Other Middleware That Reads or Modifies Cookies\n\nPlace `encryptcookie` before middleware that reads or writes cookies. If you use the CSRF middleware, register `encryptcookie` first so it can read the token.\n\nExclude cookies from encryption by listing them in `Except`. If a frontend framework such as Angular reads the CSRF token from a cookie, add that name to the `Except` array:\n\n```go\napp.Use(encryptcookie.New(encryptcookie.Config{\n    Key:    \"secret-thirty-2-character-string\",\n    Except: []string{csrf.ConfigDefault.CookieName}, // exclude CSRF cookie\n}))\napp.Use(csrf.New(csrf.Config{\n    Extractor:      csrf.FromHeader(csrf.HeaderName),\n    CookieSameSite: \"Lax\",\n    CookieSecure:   true,\n    CookieHTTPOnly: false,\n}))\n```\n\n## Encryption Algorithms\n\nThe 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:\n\n- AES-128 requires a 16-byte key.\n- AES-192 requires a 24-byte key.\n- AES-256 requires a 32-byte key.\n\nFor example, to generate a key for AES-128:\n\n```go\nkey := encryptcookie.GenerateKey(16)\n```\n\nAnd for AES-192:\n\n```go\nkey := encryptcookie.GenerateKey(24)\n```\n"
  },
  {
    "path": "docs/middleware/envvar.md",
    "content": "---\nid: envvar\n---\n\n# EnvVar\n\nEnvVar middleware for [Fiber](https://github.com/gofiber/fiber) exposes environment variables with configurable options.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/envvar\"\n)\n```\n\nOnce your Fiber app is initialized, configure the middleware as shown:\n\n```go\n// Initialize default config (exports no variables)\napp.Use(\"/expose/envvars\", envvar.New())\n\n// Or extend your config for customization\napp.Use(\"/expose/envvars\", envvar.New(\n    envvar.Config{\n        ExportVars: map[string]string{\"testKey\": \"\", \"testDefaultKey\": \"testDefaultVal\"},\n    }),\n)\n```\n\n:::note\nMount the middleware on a path; it cannot be used without one.\n:::\n\n## Response\n\nSample response:\n\n```json\n{\n  \"vars\": {\n    \"someEnvVariable\": \"someValue\",\n    \"anotherEnvVariable\": \"anotherValue\"\n  }\n}\n\n```\n\n## Config\n\n| Property    | Type                | Description                                                                  | Default |\n|:------------|:--------------------|:-----------------------------------------------------------------------------|:--------|\n| ExportVars  | `map[string]string` | ExportVars lists the environment variables to expose. | `nil` |\n\n## Default Config\n\n```go\nConfig{}\n// Exports no environment variables\n```\n"
  },
  {
    "path": "docs/middleware/etag.md",
    "content": "---\nid: etag\n---\n\n# ETag\n\nETag middleware for [Fiber](https://github.com/gofiber/fiber) that helps caches validate responses and saves bandwidth by avoiding full retransmits when content is unchanged.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/etag\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(etag.New())\n\n// GET / -> ETag: \"13-1831710635\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Hello, World!\")\n})\n\n// Or extend your config for customization\napp.Use(etag.New(etag.Config{\n    Weak: true,\n}))\n\n// GET / -> ETag: W/\"13-1831710635\"\napp.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Hello, World!\")\n})\n```\n\nEntity tags in requests must be quoted per RFC 9110. For example:\n\n```text\nIf-None-Match: \"example-etag\"\n```\n\n## Config\n\n| Property | Type                    | Description                                                                                                        | Default |\n|:---------|:------------------------|:-------------------------------------------------------------------------------------------------------------------|:--------|\n| Weak     | `bool`                  | Enables weak validators. Weak ETags are easier to generate but less reliable for comparisons. | `false` |\n| Next     | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true.                                                | `nil`   |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next: nil,\n    Weak: false,\n}\n```\n"
  },
  {
    "path": "docs/middleware/expvar.md",
    "content": "---\nid: expvar\n---\n\n# ExpVar\n\nThe ExpVar middleware exposes runtime variables over HTTP in JSON. Using it (e.g., `app.Use(expvarmw.New())`) registers handlers on `/debug/vars`.\n\n## Signatures\n\n```go\nfunc New() fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    expvarmw \"github.com/gofiber/fiber/v3/middleware/expvar\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware as shown:\n\n```go\nvar count = expvar.NewInt(\"count\")\n\napp.Use(expvarmw.New())\napp.Get(\"/\", func(c fiber.Ctx) error {\n    count.Add(1)\n\n    return c.SendString(fmt.Sprintf(\"hello expvar count %d\", count.Value()))\n})\n```\n\nVisit `/debug/vars` to see all variables, and append `?r=key` to filter the output.\n\n```bash\ncurl 127.0.0.1:3000\nhello expvar count 1\n\ncurl 127.0.0.1:3000/debug/vars\n{\n    \"cmdline\": [\"xxx\"],\n    \"count\": 1,\n    \"expvarHandlerCalls\": 33,\n    \"expvarRegexpErrors\": 0,\n    \"memstats\": {...}\n}\n\ncurl 127.0.0.1:3000/debug/vars?r=c\n{\n    \"cmdline\": [\"xxx\"],\n    \"count\": 1\n}\n```\n\n## Config\n\n| Property | Type                    | Description                                                         | Default |\n|:---------|:------------------------|:--------------------------------------------------------------------|:--------|\n| Next     | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true. | `nil`   |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next: nil,\n}\n```\n"
  },
  {
    "path": "docs/middleware/favicon.md",
    "content": "---\nid: favicon\n---\n\n# Favicon\n\nFavicon middleware for [Fiber](https://github.com/gofiber/fiber) that drops repeated `/favicon.ico` requests or serves a cached icon from memory. Mount it before your logger to suppress noisy requests and avoid disk reads.\n\nIt handles only `GET`, `HEAD`, and `OPTIONS` to the configured URL; other methods return `405 Method Not Allowed`.\n\n:::note\nThis middleware only serves the default `/favicon.ico` (or a [custom URL](#config)). For multiple icons, use the Static middleware.\n:::\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/favicon\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(favicon.New())\n\n// Or extend your config for customization\napp.Use(favicon.New(favicon.Config{\n    File: \"./favicon.ico\",\n    URL: \"/favicon.ico\",\n}))\n```\n\n## Config\n\n| Property     | Type                    | Description                                                                      | Default                    |\n|:-------------|:------------------------|:---------------------------------------------------------------------------------|:---------------------------|\n| Next         | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true.              | `nil`                      |\n| Data         | `[]byte`                | Raw data of the favicon file. This can be used instead of `File`.                | `nil`                      |\n| File         | `string`                | File holds the path to an actual favicon that will be cached.                    | \"\"                         |\n| URL          | `string`                | URL for favicon handler.                                                         | \"/favicon.ico\"             |\n| FileSystem   | `fs.FS`                 | FileSystem is an optional alternate filesystem from which to load the favicon file (e.g. using `os.DirFS` or an `embed.FS`). | `nil`                      |\n| CacheControl | `string`                | CacheControl defines how the Cache-Control header in the response should be set. | \"public, max-age=31536000\" |\n| MaxBytes     | `int64`                 | MaxBytes limits the maximum size of the cached favicon asset.                    | `1048576`                  |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:         nil,\n    File:         \"\",\n    URL:          fPath,\n    CacheControl: \"public, max-age=31536000\",\n    MaxBytes:     1024 * 1024,\n}\n```\n"
  },
  {
    "path": "docs/middleware/healthcheck.md",
    "content": "---\nid: healthcheck\n---\n\n# Health Check\n\nMiddleware that adds liveness, readiness, and startup probes to [Fiber](https://github.com/gofiber/fiber) apps. It provides a generic handler you can mount on any route, with constants for the conventional `/livez`, `/readyz`, and `/startupz` endpoints.\n\n## Overview\n\nRegister the middleware on any endpoint you want to expose a probe on. The package exports constants for the conventional liveness, readiness, and startup endpoints:\n\n```go\napp.Get(healthcheck.LivenessEndpoint, healthcheck.New())\napp.Get(healthcheck.ReadinessEndpoint, healthcheck.New())\napp.Get(healthcheck.StartupEndpoint, healthcheck.New())\n```\n\nBy default the probe returns `true`, so each endpoint responds with `200 OK`; returning `false` yields `503 Service Unavailable`.\n\n- **Liveness**: Checks if the server is running.\n- **Readiness**: Checks if the application is ready to handle requests.\n- **Startup**: Checks if the application has completed its startup sequence.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/healthcheck\"\n)\n```\n\nAfter your app is initialized, register the middleware on the endpoints you want to expose:\n\n```go\n// Use the default probe on the conventional endpoints\napp.Get(healthcheck.LivenessEndpoint, healthcheck.New())\napp.Get(healthcheck.ReadinessEndpoint, healthcheck.New(healthcheck.Config{\n    Probe: func(c fiber.Ctx) bool {\n        return serviceA.Ready() && serviceB.Ready()\n    },\n}))\napp.Get(healthcheck.StartupEndpoint, healthcheck.New())\n\n// Register a custom endpoint\napp.Get(\"/healthz\", healthcheck.New())\n```\n\nThe middleware responds only to GET. Use `app.All` to expose a probe on every method; other methods fall through to the next handler:\n\n```go\napp.All(\"/healthz\", healthcheck.New())\n```\n\n## Config\n\n```go\ntype Config struct {\n    // Next defines a function to skip this middleware when it returns true. If this function returns true\n    // and no other handlers are defined for the route, Fiber will return a status 404 Not Found, since\n    // no other handlers were defined to return a different status.\n    //\n    // Optional. Default: nil\n    Next func(fiber.Ctx) bool\n\n    // Probe is executed to determine the current health state. It can be used for\n    // liveness, readiness or startup checks. Returning true indicates the application\n    // is healthy.\n    //\n    // Optional. Default: func(c fiber.Ctx) bool { return true }\n    Probe func(fiber.Ctx) bool\n}\n```\n\n## Default Config\n\nThe default configuration used by this middleware is defined as follows:\n\n```go\nfunc defaultProbe(_ fiber.Ctx) bool { return true }\n\nvar ConfigDefault = Config{\n    Next:  nil,\n    Probe: defaultProbe,\n}\n```\n"
  },
  {
    "path": "docs/middleware/helmet.md",
    "content": "---\nid: helmet\n---\n\n# Helmet\n\nHelmet secures your app by adding common security headers.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nOnce your Fiber app is initialized, add the middleware:\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/helmet\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(helmet.New())\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n      return c.SendString(\"Welcome!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n## Test\n\n```bash\ncurl -I http://localhost:3000\n```\n\n## Config\n\n| Property                  | Type                    | Description                                 | Default          |\n|:--------------------------|:------------------------|:--------------------------------------------|:-----------------|\n| Next                      | `func(fiber.Ctx) bool` | Skips the middleware when the function returns `true`. | `nil`            |\n| XSSProtection             | `string`                | Value for the `X-XSS-Protection` header.               | \"0\"              |\n| ContentTypeNosniff        | `string`                | Value for the `X-Content-Type-Options` header.         | \"nosniff\"        |\n| XFrameOptions             | `string`                | Value for the `X-Frame-Options` header.                | \"SAMEORIGIN\"     |\n| HSTSMaxAge                | `int`                   | `max-age` value for `Strict-Transport-Security`.       | 0                |\n| HSTSExcludeSubdomains     | `bool`                  | Disables HSTS on subdomains when `true`.               | false            |\n| ContentSecurityPolicy     | `string`                | Value for the `Content-Security-Policy` header.        | \"\"               |\n| CSPReportOnly             | `bool`                  | Enables report-only mode for CSP.                      | false            |\n| HSTSPreloadEnabled        | `bool`                  | Adds the `preload` directive to HSTS.                  | false            |\n| ReferrerPolicy            | `string`                | Value for the `Referrer-Policy` header.                | \"no-referrer\" |\n| PermissionPolicy          | `string`                | Value for the `Permissions-Policy` header.             | \"\"               |\n| CrossOriginEmbedderPolicy | `string`                | Value for the `Cross-Origin-Embedder-Policy` header.   | \"require-corp\"   |\n| CrossOriginOpenerPolicy   | `string`                | Value for the `Cross-Origin-Opener-Policy` header.     | \"same-origin\"    |\n| CrossOriginResourcePolicy | `string`                | Value for the `Cross-Origin-Resource-Policy` header.   | \"same-origin\"    |\n| OriginAgentCluster        | `string`                | Value for the `Origin-Agent-Cluster` header.           | \"?1\"             |\n| XDNSPrefetchControl       | `string`                | Value for the `X-DNS-Prefetch-Control` header.         | \"off\"            |\n| XDownloadOptions          | `string`                | Value for the `X-Download-Options` header.             | \"noopen\"         |\n| XPermittedCrossDomain     | `string`                | Value for the `X-Permitted-Cross-Domain-Policies` header. | \"none\"        |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    XSSProtection:             \"0\",\n    ContentTypeNosniff:        \"nosniff\",\n    XFrameOptions:             \"SAMEORIGIN\",\n    ReferrerPolicy:            \"no-referrer\",\n    CrossOriginEmbedderPolicy: \"require-corp\",\n    CrossOriginOpenerPolicy:   \"same-origin\",\n    CrossOriginResourcePolicy: \"same-origin\",\n    OriginAgentCluster:        \"?1\",\n    XDNSPrefetchControl:       \"off\",\n    XDownloadOptions:          \"noopen\",\n    XPermittedCrossDomain:     \"none\",\n}\n```\n"
  },
  {
    "path": "docs/middleware/idempotency.md",
    "content": "---\nid: idempotency\n---\n\n# Idempotency\n\nThe Idempotency middleware helps build fault-tolerant APIs. Duplicate requests—such as retries after network issues—won't trigger the same action twice on the server.\n\nRefer to [IETF RFC 7231 §4.2.2](https://tools.ietf.org/html/rfc7231#section-4.2.2) for definitions of safe and idempotent HTTP methods.\n\n## HTTP Method Categories\n\n* **Safe Methods** (do not modify server state): `GET`, `HEAD`, `OPTIONS`, `TRACE`\n* **Idempotent Methods** (identical requests have the same effect as a single one): all safe methods **plus** `PUT` and `DELETE`\n\n> According to the RFC, safe methods never change server state, while idempotent methods may change state but remain safe to repeat.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\nfunc IsFromCache(c fiber.Ctx) bool\nfunc WasPutToCache(c fiber.Ctx) bool\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/idempotency\"\n)\n```\n\nOnce your Fiber app is initialized, configure the middleware:\n\n### Default Config (Skip **Safe** Methods)\n\nBy default, the `Next` function skips middleware for safe methods only:\n\n```go\napp.Use(idempotency.New())\n```\n\n### Skip **Idempotent** Methods Instead\n\nSkip all idempotent methods (including `PUT` and `DELETE`) by overriding `Next`:\n\n```go\napp.Use(idempotency.New(idempotency.Config{\n    Next: func(c fiber.Ctx) bool {\n        // Skip middleware for idempotent methods (safe + PUT, DELETE)\n        return fiber.IsMethodIdempotent(c.Method())\n    },\n}))\n```\n\n### Custom Config\n\n```go\napp.Use(idempotency.New(idempotency.Config{\n    Lifetime: 42 * time.Minute,\n    // ...\n}))\n```\n\n## Config\n\nIdempotency keys are hidden in logs and error messages by default. Set `DisableValueRedaction` to `true` only when you need to expose them for debugging.\n\n| Property            | Type                   | Description                                                                                                                             | Default                                                            |\n|:--------------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------|\n| Next                | `func(fiber.Ctx) bool` | Function to skip this middleware when it returns `true`; use `IsMethodSafe` or `IsMethodIdempotent`. | `func(c fiber.Ctx) bool { return fiber.IsMethodSafe(c.Method()) }` |\n| Lifetime            | `time.Duration`        | Maximum lifetime of an idempotency key.                                                                                                 | `30 * time.Minute`                                                 |\n| KeyHeader           | `string`               | Header name containing the idempotency key.                                                                                             | `\"X-Idempotency-Key\"`                                              |\n| KeyHeaderValidate   | `func(string) error`   | Function to validate idempotency header syntax (e.g., UUID).                                                                            | UUID length check (`36` characters)                                |\n| KeepResponseHeaders | `[]string`             | List of headers to preserve from original response.                                                                                     | `nil` (keep all headers)                                           |\n| DisableValueRedaction | `bool`                | Disables idempotency key redaction in logs and error messages. | `false`                                              |\n| Lock                | `Locker`               | Locks an idempotency key to prevent race conditions.                                                                                    | In-memory locker                                                   |\n| Storage             | `fiber.Storage`        | Stores response data by idempotency key.                                                                                                | In-memory storage                                                  |\n\n## Default Config Values\n\n```go\nvar ConfigDefault = Config{\n    Next: func(c fiber.Ctx) bool {\n        // Skip middleware for safe methods per RFC 7231 §4.2.2\n        return fiber.IsMethodSafe(c.Method())\n    },\n\n    Lifetime: 30 * time.Minute,\n\n    KeyHeader: \"X-Idempotency-Key\",\n    KeyHeaderValidate: func(k string) error {\n        if l, wl := len(k), 36; l != wl { // UUID length is 36 chars\n            return fmt.Errorf(\"%w: invalid length: %d != %d\", ErrInvalidIdempotencyKey, l, wl)\n        }\n\n        return nil\n    },\n\n    KeepResponseHeaders: nil,\n\n    Lock: nil, // Set in configDefault so we don't allocate data here.\n\n    Storage: nil, // Set in configDefault so we don't allocate data here.\n    DisableValueRedaction: false,\n}\n```\n"
  },
  {
    "path": "docs/middleware/keyauth.md",
    "content": "---\nid: keyauth\n---\n\n# KeyAuth\n\nThe KeyAuth middleware implements API key authentication.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\nfunc TokenFromContext(ctx any) string\n```\n\n`TokenFromContext` accepts a `fiber.CustomCtx`, `fiber.Ctx`, a `*fasthttp.RequestCtx`, or a `context.Context`.\n\n## Examples\n\n### Basic example\n\nThis example registers KeyAuth with an API key stored in a cookie.\n\n```go\npackage main\n\nimport (\n    \"crypto/sha256\"\n    \"crypto/subtle\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/keyauth\"\n)\n\nvar (\n    apiKey = \"correct horse battery staple\"\n)\n\nfunc validateAPIKey(c fiber.Ctx, key string) (bool, error) {\n    hashedAPIKey := sha256.Sum256([]byte(apiKey))\n    hashedKey := sha256.Sum256([]byte(key))\n\n    if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {\n        return true, nil\n    }\n    return false, keyauth.ErrMissingOrMalformedAPIKey\n}\n\nfunc main() {\n    app := fiber.New()\n\n    // Register middleware before the routes that need it\n    app.Use(keyauth.New(keyauth.Config{\n        Extractor:  keyauth.FromCookie(\"access_token\"),\n        Validator:  validateAPIKey,\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Successfully authenticated!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n**Test:**\n\n```bash\n# No API key specified -> 401 Missing or invalid API Key\ncurl http://localhost:3000\n#> Missing or invalid API Key\n\n# Correct API key -> 200 OK\ncurl --cookie \"access_token=correct horse battery staple\" http://localhost:3000\n#> Successfully authenticated!\n\n# Incorrect API key -> 401 Missing or invalid API Key\ncurl --cookie \"access_token=Clearly A Wrong Key\" http://localhost:3000\n#> Missing or invalid API Key\n```\n\nFor a more detailed example, see the [`fiber-envoy-extauthz`](https://github.com/gofiber/recipes/tree/master/fiber-envoy-extauthz) recipe in the `gofiber/recipes` repository.\n\n### Authenticate only certain endpoints\n\nUse the `Next` function to run KeyAuth only on selected routes.\n\n```go\npackage main\n\nimport (\n    \"crypto/sha256\"\n    \"crypto/subtle\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/keyauth\"\n    \"regexp\"\n    \"strings\"\n)\n\nvar (\n    apiKey        = \"correct horse battery staple\"\n    protectedURLs = []*regexp.Regexp{\n        regexp.MustCompile(\"^/authenticated$\"),\n        regexp.MustCompile(\"^/auth2$\"),\n    }\n)\n\nfunc validateAPIKey(c fiber.Ctx, key string) (bool, error) {\n    hashedAPIKey := sha256.Sum256([]byte(apiKey))\n    hashedKey := sha256.Sum256([]byte(key))\n\n    if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {\n        return true, nil\n    }\n    return false, keyauth.ErrMissingOrMalformedAPIKey\n}\n\nfunc authFilter(c fiber.Ctx) bool {\n    originalURL := strings.ToLower(c.OriginalURL())\n\n    for _, pattern := range protectedURLs {\n        if pattern.MatchString(originalURL) {\n            // Run middleware for protected routes\n            return false\n        }\n    }\n    // Skip middleware for non-protected routes\n    return true\n}\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(keyauth.New(keyauth.Config{\n        Next:      authFilter,\n        Extractor: keyauth.FromCookie(\"access_token\"),\n        Validator: validateAPIKey,\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Welcome\")\n    })\n    app.Get(\"/authenticated\", func(c fiber.Ctx) error {\n        return c.SendString(\"Successfully authenticated!\")\n    })\n    app.Get(\"/auth2\", func(c fiber.Ctx) error {\n        return c.SendString(\"Successfully authenticated 2!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n**Test:**\n\n```bash\n# / doesn't require authentication\ncurl http://localhost:3000\n#> Welcome\n\n# /authenticated requires authentication\ncurl --cookie \"access_token=correct horse battery staple\" http://localhost:3000/authenticated\n#> Successfully authenticated!\n\n# /auth2 requires authentication too\ncurl --cookie \"access_token=correct horse battery staple\" http://localhost:3000/auth2\n#> Successfully authenticated 2!\n```\n\n### Apply middleware in the handler\n\nYou can apply the middleware to specific routes or groups instead of globally. This example uses the default extractor (`FromAuthHeader`).\n\n```go\npackage main\n\nimport (\n    \"crypto/sha256\"\n    \"crypto/subtle\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/keyauth\"\n)\n\nconst (\n  apiKey = \"my-super-secret-key\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    authMiddleware := keyauth.New(keyauth.Config{\n        Validator:  func(c fiber.Ctx, key string) (bool, error) {\n            hashedAPIKey := sha256.Sum256([]byte(apiKey))\n            hashedKey := sha256.Sum256([]byte(key))\n\n            if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {\n                return true, nil\n            }\n            return false, keyauth.ErrMissingOrMalformedAPIKey\n        },\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Welcome\")\n    })\n\n    app.Get(\"/allowed\",  authMiddleware, func(c fiber.Ctx) error {\n        return c.SendString(\"Successfully authenticated!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n**Test:**\n\n```bash\n# / doesn't require authentication\ncurl http://localhost:3000\n#> Welcome\n\n# /allowed requires authentication\ncurl --header \"Authorization: Bearer my-super-secret-key\"  http://localhost:3000/allowed\n#> Successfully authenticated!\n```\n\n## Key Extractors\n\nKeyAuth uses an `Extractor` from the shared [extractors](../guide/extractors) package to retrieve the API key from the request. You can specify one or more extractors in the configuration. For a full list of extractors, chaining, and advanced usage, see the [Extractors Guide](../guide/extractors).\n\n### Typical Usage\n\nSpecify the extractor in the config. For example, to extract from a cookie:\n\n```go\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromCookie(\"access_token\"),\n    Validator: validateAPIKey,\n}))\n```\n\nTo use the default (Authorization header with Bearer scheme):\n\n```go\napp.Use(keyauth.New(keyauth.Config{\n    Validator: validateAPIKey, // Extractor defaults to FromAuthHeader(\"Bearer\")\n}))\n```\n\nTo try multiple sources (header, then query):\n\n```go\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.Chain(\n        extractors.FromHeader(\"X-API-Key\"),\n        extractors.FromQuery(\"api_key\"),\n    ),\n    Validator: validateAPIKey,\n}))\n```\n\nFor custom logic, use `extractors.FromCustom`:\n\n```go\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: extractors.FromCustom(func(c fiber.Ctx) (string, error) {\n        return c.Get(\"X-My-API-Key\"), nil\n    }),\n    Validator: validateAPIKey,\n}))\n```\n\nRefer to the [Extractors Guide](../guide/extractors) for details, security notes, and advanced configuration.\n\n## Config\n\n| Property        | Type                                     | Description                                                                                            | Default                       |\n|:----------------|:-----------------------------------------|:-------------------------------------------------------------------------------------------------------|:------------------------------|\n| Next            | `func(fiber.Ctx) bool`                   | Next defines a function to skip this middleware when it returns true.                                    | `nil`                         |\n| SuccessHandler  | `fiber.Handler`                          | SuccessHandler defines a function which is executed for a valid key.                                   | `c.Next()`                         |\n| ErrorHandler    | `fiber.ErrorHandler`                     | ErrorHandler defines a function which is executed for an invalid key. By default a 401 response with a `WWW-Authenticate` challenge is sent. | Default error handler  |\n| Validator       | `func(fiber.Ctx, string) (bool, error)`  | **Required.** Validator is a function to validate the key.                                                           | `nil` (panic) |\n| Extractor       | `extractors.Extractor`                 | Extractor defines how to retrieve the key from the request. Use helper functions from the shared extractors package, e.g. `extractors.FromAuthHeader(\"Bearer\")` or `extractors.FromCookie(\"access_token\")`. | `extractors.FromAuthHeader(\"Bearer\")` |\n| Realm           | `string`                                 | Realm specifies the protected area name used in the `WWW-Authenticate` header. | `\"Restricted\"` |\n| Challenge       | `string`                                 | Value of the `WWW-Authenticate` header when no `Authorization` scheme is present. | `ApiKey realm=\"Restricted\"` |\n| Error           | `string`                                 | Error code appended as the `error` parameter in Bearer challenges. Must be `invalid_request`, `invalid_token`, or `insufficient_scope`. | `\"\"` |\n| ErrorDescription| `string`                                 | Human-readable text for the `error_description` parameter in Bearer challenges. Requires `Error`. | `\"\"` |\n| ErrorURI        | `string`                                 | URI identifying a human-readable web page with information about the `error` in Bearer challenges. Requires `Error` and must be an absolute URI. | `\"\"` |\n| Scope           | `string`                                 | Space-delimited list of scopes for the `scope` parameter in Bearer challenges. Each token must conform to the RFC 6750 `scope-token` syntax and requires `Error` set to `insufficient_scope`. | `\"\"` |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    SuccessHandler: func(c fiber.Ctx) error {\n        return c.Next()\n    },\n    ErrorHandler: func(c fiber.Ctx, _ error) error {\n        return c.Status(fiber.StatusUnauthorized).SendString(ErrMissingOrMalformedAPIKey.Error())\n    },\n    Realm:     \"Restricted\",\n    Extractor: extractors.FromAuthHeader(\"Bearer\"),\n}\n```\n"
  },
  {
    "path": "docs/middleware/limiter.md",
    "content": "---\nid: limiter\n---\n\n# Limiter\n\nThe Limiter middleware for [Fiber](https://github.com/gofiber/fiber) throttles repeated requests to public APIs or endpoints such as password resets. It's also useful for API clients, web crawlers, or other tasks that need rate limiting.\n\nLimiter redacts request keys in error paths by default so storage identifiers and rate-limit keys don't leak into logs. Set `DisableValueRedaction` to `true` when you explicitly need the raw key for troubleshooting.\n\n:::note\nThis middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases.\n:::\n\n:::note\nThis module does not share state with other processes/servers by default.\n:::\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n\ntype Handler interface {\n    New(config *Config) fiber.Handler\n}\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/limiter\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(limiter.New())\n\n// Or extend your config for customization\napp.Use(limiter.New(limiter.Config{\n    Next: func(c fiber.Ctx) bool {\n        return c.IP() == \"127.0.0.1\"\n    },\n    Max:          20,\n    MaxFunc: func(c fiber.Ctx) int {\n      return 20\n    },\n    Expiration:     30 * time.Second,\n    ExpirationFunc: func(c fiber.Ctx) time.Duration {\n      // Use longer expiration for sensitive endpoints\n      if c.Path() == \"/login\" {\n        return 60 * time.Second\n      }\n      return 30 * time.Second\n    },\n    KeyGenerator:          func(c fiber.Ctx) string {\n        return c.Get(\"x-forwarded-for\")\n    },\n    LimitReached: func(c fiber.Ctx) error {\n        return c.SendFile(\"./toofast.html\")\n    },\n    Storage: myCustomStorage{},\n}))\n```\n\n## Sliding window\n\nInstead of using the standard fixed window algorithm, you can enable the [sliding window](https://en.wikipedia.org/wiki/Sliding_window_protocol) algorithm.\n\nAn example configuration is:\n\n```go\napp.Use(limiter.New(limiter.Config{\n    Max:            20,\n    Expiration:     30 * time.Second,\n    LimiterMiddleware: limiter.SlidingWindow{},\n}))\n```\n\nEach new window also considers the previous one (if any). The rate is calculated as:\n\n```text\nweightOfPreviousWindow = previousWindowRequests * (elapsedInCurrentWindow / Expiration)\nrate = weightOfPreviousWindow + currentWindowRequests\n```\n\n## Dynamic limit\n\nYou can also calculate the limit dynamically using the `MaxFunc` parameter. It receives the request context and allows you to compute a different limit for each request.\n\nExample:\n\n```go\napp.Use(limiter.New(limiter.Config{\n    MaxFunc:  func(c fiber.Ctx) int {\n      return getUserLimit(ctx.Param(\"id\"))\n    },\n    Expiration:     30 * time.Second,\n}))\n```\n\n## Dynamic expiration\n\nYou can also calculate the expiration dynamically using the `ExpirationFunc` parameter. It receives the request context and allows you to set a different expiration window for each request.\n\nExample:\n\n```go\napp.Use(limiter.New(limiter.Config{\n    Max:     20,\n    ExpirationFunc: func(c fiber.Ctx) time.Duration {\n      return getExpirationForRoute(c.Path())\n    },\n}))\n```\n\n## Config\n\n| Property               | Type                      | Description                                                                                 | Default                                  |\n|:-----------------------|:--------------------------|:--------------------------------------------------------------------------------------------|:-----------------------------------------|\n| Next                   | `func(fiber.Ctx) bool`   | Next defines a function to skip this middleware when it returns true.                         | `nil`                                    |\n| Max                    | `int`                     | Maximum number of recent connections within `Expiration` seconds before sending a 429 response. | 5                                        |\n| MaxFunc                | `func(fiber.Ctx) int`     | Function that calculates the maximum number of recent connections within `Expiration` seconds before sending a 429 response. | A function that returns `cfg.Max`    |\n| KeyGenerator           | `func(fiber.Ctx) string` | Function to generate custom keys; uses `c.IP()` by default.                 | A function using `c.IP()` as the default   |\n| Expiration             | `time.Duration`           | Duration to keep request records in memory.                   | 1 * time.Minute                          |\n| ExpirationFunc         | `func(fiber.Ctx) time.Duration` | Function that calculates the expiration duration dynamically. | A function that returns `cfg.Expiration` |\n| LimitReached           | `fiber.Handler`           | Called when a request exceeds the limit.                                       | A function sending a 429 response          |\n| SkipFailedRequests     | `bool`                    | When set to `true`, requests with status code ≥ 400 aren't counted.                         | false                                    |\n| SkipSuccessfulRequests | `bool`                    | When set to `true`, requests with status code < 400 aren't counted.                          | false                                    |\n| DisableHeaders         | `bool`                    | When set to `true`, the middleware omits rate limit headers (`X-RateLimit-*` and `Retry-After`). | false                                    |\n| DisableValueRedaction  | `bool`                    | Disables redaction of limiter keys in error messages and logs.                                 | false                                    |\n| Storage                | `fiber.Storage`           | Persists middleware state.                                         | An in-memory store for this process only |\n| LimiterMiddleware      | `limiter.Handler`         | Selects the algorithm implementation. Implementations now receive a pointer to the active config when their `New` method is invoked. | A new Fixed Window Rate Limiter          |\n\n:::note\nA custom store can be used if it implements the `Storage` interface - more details and an example can be found in `store.go`.\n:::\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Max:        5,\n    MaxFunc: func(c fiber.Ctx) int {\n      return 5\n    },\n    Expiration: 1 * time.Minute,\n    // ExpirationFunc defaults to nil and is set dynamically to return cfg.Expiration\n    KeyGenerator: func(c fiber.Ctx) string {\n        return c.IP()\n    },\n    LimitReached: func(c fiber.Ctx) error {\n        return c.SendStatus(fiber.StatusTooManyRequests)\n    },\n    SkipFailedRequests: false,\n    SkipSuccessfulRequests: false,\n    DisableHeaders:        false,\n    DisableValueRedaction: false,\n    LimiterMiddleware: FixedWindow{},\n}\n```\n\n### Custom Storage/Database\n\nYou can use any storage from our [storage](https://github.com/gofiber/storage/) package.\n\n```go\nstorage := sqlite3.New() // From github.com/gofiber/storage/sqlite3/v2\n\napp.Use(limiter.New(limiter.Config{\n    Storage: storage,\n}))\n```\n"
  },
  {
    "path": "docs/middleware/logger.md",
    "content": "---\nid: logger\n---\n\n# Logger\n\nLogger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP requests and responses.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n)\n```\n\n:::tip\nRegistration order matters: only routes added after the logger are logged, so register it early.\n:::\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(logger.New())\n\n// Or extend your config for customization\n// Log remote IP and port\napp.Use(logger.New(logger.Config{\n    Format: \"[${ip}]:${port} ${status} - ${method} ${path}\\n\",\n}))\n\n// Logging Request ID\napp.Use(requestid.New()) // Ensure requestid middleware is used before the logger\napp.Use(logger.New(logger.Config{\n    CustomTags: map[string]logger.LogFunc{\n        \"requestid\": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {\n            return output.WriteString(requestid.FromContext(c))\n        },\n    },\n    // For more options, see the Config section\n    // Use the custom tag ${requestid} as defined above.\n    Format: \"${pid} ${requestid} ${status} - ${method} ${path}\\n\",\n}))\n\n// Changing TimeZone & TimeFormat\napp.Use(logger.New(logger.Config{\n    Format:     \"${pid} ${status} - ${method} ${path}\\n\",\n    TimeFormat: \"02-Jan-2006\",\n    TimeZone:   \"America/New_York\",\n}))\n\n// Custom File Writer\naccessLog, err := os.OpenFile(\"./access.log\", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)\nif err != nil {\n    log.Fatalf(\"error opening access.log file: %v\", err)\n}\ndefer accessLog.Close()\napp.Use(logger.New(logger.Config{\n    Stream: accessLog,\n}))\n\n// Add Custom Tags\napp.Use(logger.New(logger.Config{\n    CustomTags: map[string]logger.LogFunc{\n        \"custom_tag\": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {\n            return output.WriteString(\"it is a custom tag\")\n        },\n    },\n}))\n\n// Callback after log is written\napp.Use(logger.New(logger.Config{\n    TimeFormat: time.RFC3339Nano,\n    TimeZone:   \"Asia/Shanghai\",\n    Done: func(c fiber.Ctx, logString []byte) {\n        if c.Response().StatusCode() != fiber.StatusOK {\n            reporter.SendToSlack(logString)\n        }\n    },\n}))\n\n// Disable colors when outputting to default format\napp.Use(logger.New(logger.Config{\n    DisableColors: true,\n}))\n\n// Force the use of colors\napp.Use(logger.New(logger.Config{\n    ForceColors: true,\n}))\n\n// Use predefined formats\napp.Use(logger.New(logger.Config{\n    Format: logger.CommonFormat,\n}))\n\napp.Use(logger.New(logger.Config{\n    Format: logger.CombinedFormat,\n}))\n\napp.Use(logger.New(logger.Config{\n    Format: logger.JSONFormat,\n}))\n\napp.Use(logger.New(logger.Config{\n    Format: logger.ECSFormat,\n}))\n```\n\n### Use Logger Middleware with Other Loggers\n\nTo combine the logger middleware with loggers like Zerolog, Zap, or Logrus, use the `LoggerToWriter` helper to adapt them to an `io.Writer`.\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/contrib/fiberzap/v2\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/log\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n)\n\nfunc main() {\n    // Create a new Fiber instance\n    app := fiber.New()\n\n    // Create a new zap logger which is compatible with Fiber AllLogger interface\n    zap := fiberzap.NewLogger(fiberzap.LoggerConfig{\n        ExtraKeys: []string{\"request_id\"},\n    })\n\n    // Use the logger middleware with the zap logger\n    app.Use(logger.New(logger.Config{\n        Stream: logger.LoggerToWriter(zap, log.LevelDebug),\n    }))\n\n    // Define a route\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    // Start server on http://localhost:3000\n    app.Listen(\":3000\")\n}\n```\n\n:::tip\nWriting to `os.File` is goroutine-safe, but custom streams may require locking to serialize writes.\n:::\n\n## Config\n\n| Property      | Type                                              | Description                                                                                                                                   | Default                                                               |\n| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |\n| Next          | `func(fiber.Ctx) bool`                            | Next defines a function to skip this middleware when it returns true.                                                                           | `nil`                                                                 |\n| Skip          | `func(fiber.Ctx) bool`                            | Skip is a function to determine if logging is skipped or written to Stream.                                                                   | `nil`                                                                 |\n| Done          | `func(fiber.Ctx, []byte)`                         | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter.              | `nil`                                                                 |\n| CustomTags    | `map[string]LogFunc`                              | tagFunctions defines the custom tag action.                                                                                                   | `map[string]LogFunc`                                                  |\n| `Format`   | `string`  | Defines the logging tags. See more in [Predefined Formats](#predefined-formats), or create your own using [Tags](#constants). | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\\n` (same as `DefaultFormat`) |\n| TimeFormat    | `string`                                          | TimeFormat defines the time format for log timestamps.                                                                                        | `15:04:05`                                                            |\n| TimeZone      | `string`                                          | TimeZone can be specified, such as \"UTC\" and \"America/New_York\" and \"Asia/Chongqing\", etc                                                     | `\"Local\"`                                                             |\n| TimeInterval  | `time.Duration`                                   | TimeInterval is the delay before the timestamp is updated.                                                                                    | `500 * time.Millisecond`                                              |\n| Stream        | `io.Writer`                                       | Stream is a writer where logs are written.                                                                                                    | `os.Stdout`                                                           |\n| LoggerFunc    | `func(c fiber.Ctx, data *Data, cfg *Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance`                         |\n| DisableColors | `bool`                                            | DisableColors defines if the logs output should be colorized.                                                                                 | `false`                                                               |\n| ForceColors   | `bool`                                            | ForceColors defines if the logs output should be colorized even when the output is not a terminal.                                             | `false`                                                               |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:              nil,\n    Skip:              nil,\n    Done:              nil,\n    Format:            DefaultFormat,\n    TimeFormat:        \"15:04:05\",\n    TimeZone:          \"Local\",\n    TimeInterval:      500 * time.Millisecond,\n    Stream:            os.Stdout,\n    BeforeHandlerFunc: beforeHandlerFunc,\n    LoggerFunc:        defaultLoggerInstance,\n    enableColors:      true,\n}\n```\n\n## Predefined Formats\n\nLogger provides predefined formats that you can use by name or directly by specifying the format string.\n\n| **Format Constant** | **Format String** | **Description** |\n|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|\n| `DefaultFormat` | `\"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\\n\"` | Fiber's default logger format. |\n| `CommonFormat` | `\"${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\\n\"` | Common Log Format (CLF) used in web server logs. |\n| `CombinedFormat` | `\"${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\\n\"` | CLF format plus the `referer` and `user agent` fields. |\n| `JSONFormat` | `\"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\\n\"` | JSON format for structured logging. |\n| `ECSFormat` | `\"{\\\"@timestamp\\\":\\\"${time}\\\",\\\"ecs\\\":{\\\"version\\\":\\\"1.6.0\\\"},\\\"client\\\":{\\\"ip\\\":\\\"${ip}\\\"},\\\"http\\\":{\\\"request\\\":{\\\"method\\\":\\\"${method}\\\",\\\"url\\\":\\\"${url}\\\",\\\"protocol\\\":\\\"${protocol}\\\"},\\\"response\\\":{\\\"status_code\\\":${status},\\\"body\\\":{\\\"bytes\\\":${bytesSent}}}},\\\"log\\\":{\\\"level\\\":\\\"INFO\\\",\\\"logger\\\":\\\"fiber\\\"},\\\"message\\\":\\\"${method} ${url} responded with ${status}\\\"}\\n\"` | Elastic Common Schema (ECS) format for structured logging. |\n\n## Constants\n\n```go\n// Logger variables\nconst (\n    TagPid               = \"pid\"\n    TagTime              = \"time\"\n    TagReferer           = \"referer\"\n    TagProtocol          = \"protocol\"\n    TagPort              = \"port\"\n    TagIP                = \"ip\"\n    TagIPs               = \"ips\"\n    TagHost              = \"host\"\n    TagMethod            = \"method\"\n    TagPath              = \"path\"\n    TagURL               = \"url\"\n    TagUA                = \"ua\"\n    TagLatency           = \"latency\"\n    TagStatus            = \"status\"         // response status\n    TagResBody           = \"resBody\"        // response body\n    TagReqHeaders        = \"reqHeaders\"\n    TagQueryStringParams = \"queryParams\"    // request query parameters\n    TagBody              = \"body\"           // request body\n    TagBytesSent         = \"bytesSent\"\n    TagBytesReceived     = \"bytesReceived\"\n    TagRoute             = \"route\"\n    TagError             = \"error\"\n    TagReqHeader         = \"reqHeader:\"     // request header\n    TagRespHeader        = \"respHeader:\"    // response header\n    TagQuery             = \"query:\"         // request query\n    TagForm              = \"form:\"          // request form\n    TagCookie            = \"cookie:\"        // request cookie\n    TagLocals            = \"locals:\"\n    // colors\n    TagBlack             = \"black\"\n    TagRed               = \"red\"\n    TagGreen             = \"green\"\n    TagYellow            = \"yellow\"\n    TagBlue              = \"blue\"\n    TagMagenta           = \"magenta\"\n    TagCyan              = \"cyan\"\n    TagWhite             = \"white\"\n    TagReset             = \"reset\"\n)\n```\n"
  },
  {
    "path": "docs/middleware/paginate.md",
    "content": "---\nid: paginate\n---\n\n# Paginate\n\nPagination middleware for [Fiber](https://github.com/gofiber/fiber) that extracts pagination parameters from query strings and stores them in the request context. Supports page-based, offset-based, and cursor-based pagination with multi-field sorting.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\nfunc FromContext(ctx any) (*PageInfo, bool)\n```\n\n`FromContext` accepts `fiber.CustomCtx`, `fiber.Ctx`, `*fasthttp.RequestCtx`, or `context.Context`.\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/paginate\"\n)\n```\n\nOnce your Fiber app is initialized, choose one of the following approaches:\n\n### Basic Usage\n\n```go\napp.Use(paginate.New())\n\napp.Get(\"/users\", func(c fiber.Ctx) error {\n    pageInfo, ok := paginate.FromContext(c)\n    if !ok {\n        return fiber.ErrBadRequest\n    }\n\n    // Use pageInfo.Page, pageInfo.Limit, pageInfo.Start()\n    // GET /users?page=2&limit=20 → Page: 2, Limit: 20, Start(): 20\n    return c.JSON(pageInfo)\n})\n```\n\n### Sorting\n\n```go\napp.Use(paginate.New(paginate.Config{\n    SortKey:      \"sort\",\n    DefaultSort:  \"id\",\n    AllowedSorts: []string{\"id\", \"name\", \"created_at\"},\n}))\n\n// GET /users?sort=name,-created_at\n// → Sort: [{Field: \"name\", Order: \"asc\"}, {Field: \"created_at\", Order: \"desc\"}]\n```\n\n### Cursor Pagination\n\n```go\napp.Use(paginate.New())\n\napp.Get(\"/feed\", func(c fiber.Ctx) error {\n    pageInfo, ok := paginate.FromContext(c)\n    if !ok {\n        return fiber.ErrBadRequest\n    }\n\n    if pageInfo.Cursor != \"\" {\n        // Decode the cursor to get keyset values\n        values := pageInfo.CursorValues()\n        // Use values[\"id\"], values[\"created_at\"], etc. for WHERE clause\n    }\n\n    // results is a slice of items from your database query\n    // After fetching results, set the next cursor for the client\n    if len(results) > 0 {\n        lastItem := results[len(results)-1]\n        if err := pageInfo.SetNextCursor(map[string]any{\n            \"id\":         lastItem.ID,\n            \"created_at\": lastItem.CreatedAt,\n        }); err != nil {\n            return err\n        }\n    }\n\n    return c.JSON(fiber.Map{\n        \"data\":        results,\n        \"has_more\":    pageInfo.HasMore,\n        \"next_cursor\": pageInfo.NextCursor,\n    })\n})\n\n// First request:  GET /feed?limit=20\n// Next request:   GET /feed?cursor=<token>&limit=20\n```\n\n### Custom Configuration\n\n```go\napp.Use(paginate.New(paginate.Config{\n    PageKey:      \"p\",\n    LimitKey:     \"size\",\n    DefaultPage:  1,\n    DefaultLimit: 25,\n    SortKey:      \"order_by\",\n    DefaultSort:  \"created_at\",\n    AllowedSorts: []string{\"created_at\", \"name\", \"email\"},\n    CursorKey:    \"after\",\n    CursorParam:  \"starting_after\",\n}))\n```\n\n## Config\n\n| Property     | Type                     | Description                                                        | Default    |\n|:-------------|:-------------------------|:-------------------------------------------------------------------|:-----------|\n| Next         | `func(fiber.Ctx) bool`   | Next defines a function to skip this middleware when returned true. | `nil`      |\n| PageKey      | `string`                 | Query string key for page number.                                  | `\"page\"`   |\n| DefaultPage  | `int`                    | Default page number.                                               | `1`        |\n| LimitKey     | `string`                 | Query string key for limit.                                        | `\"limit\"`  |\n| DefaultLimit | `int`                    | Default items per page.                                            | `10`       |\n| SortKey      | `string`                 | Query string key for sort.                                         | `\"\"`       |\n| DefaultSort  | `string`                 | Default sort field.                                                | `\"id\"`     |\n| AllowedSorts | `[]string`               | Whitelist of allowed sort fields. If nil, all fields are allowed.  | `nil`      |\n| OffsetKey    | `string`                 | Query string key for offset.                                       | `\"offset\"` |\n| CursorKey    | `string`                 | Query string key for cursor-based pagination.                      | `\"cursor\"` |\n| CursorParam  | `string`                 | Optional alias for the cursor query key.                           | `\"\"`       |\n| MaxLimit     | `int`                    | Maximum items per page.                                            | `100`      |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:         nil,\n    PageKey:      \"page\",\n    DefaultPage:  1,\n    LimitKey:     \"limit\",\n    DefaultLimit: 10,\n    MaxLimit:     100,\n    DefaultSort:  \"id\",\n    OffsetKey:    \"offset\",\n    CursorKey:    \"cursor\",\n}\n```\n\n## PageInfo\n\nThe `PageInfo` struct is stored in the request context and provides:\n\n| Method                                          | Description                                                    |\n|:------------------------------------------------|:---------------------------------------------------------------|\n| `Start() int`                                   | Returns calculated start index (from page/limit or offset)     |\n| `SortBy(field, order)`                          | Adds a sort field (chainable)                                  |\n| `NextPageURL(baseURL)`                          | Generates next page URL with default keys                      |\n| `NextPageURLWithKeys(baseURL, pageKey, limitKey)` | Generates next page URL with custom query keys               |\n| `PreviousPageURL(baseURL)`                      | Generates previous page URL (empty on page 1)                  |\n| `PreviousPageURLWithKeys(baseURL, pageKey, limitKey)` | Generates previous page URL with custom query keys       |\n| `NextCursorURL(baseURL)`                        | Generates next cursor URL (empty if no more)                   |\n| `NextCursorURLWithKeys(baseURL, cursorKey, limitKey)` | Generates next cursor URL with custom query keys         |\n| `CursorValues()`                                | Decodes cursor token into key-value map                        |\n| `SetNextCursor(values)`                         | Encodes values into cursor token, sets HasMore; returns error  |\n\n## Safety\n\n- Limit is capped at `MaxLimit` (default: 100, configurable) to prevent excessive memory usage\n- Page values below 1 reset to 1\n- Negative offsets reset to 0\n- Sort fields are validated against `AllowedSorts`\n- Cursor tokens exceeding 2048 characters are rejected with `400 Bad Request`\n- `SetNextCursor` returns an error if the encoded token would exceed 2048 characters, preventing the server from issuing cursors it would later reject\n- Invalid cursor tokens return `400 Bad Request` via Fiber's error handler\n- If `DefaultSort` is not included in `AllowedSorts`, it falls back to the first allowed sort field\n- URL helpers preserve existing query parameters when building pagination links\n"
  },
  {
    "path": "docs/middleware/pprof.md",
    "content": "---\nid: pprof\n---\n\n# Pprof\n\nPprof middleware exposes runtime profiling data for analysis with the Go `pprof` tool. Importing it registers handlers under `/debug/pprof/`.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/pprof\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware as shown:\n\n```go\n// Initialize default config\napp.Use(pprof.New())\n\n// Or customize the config\n\n// For multi-ingress systems, add a URL prefix:\napp.Use(pprof.New(pprof.Config{Prefix: \"/endpoint-prefix\"}))\n\n// The resulting URL is \"/endpoint-prefix/debug/pprof/\"\n```\n\n## Config\n\n| Property | Type                    | Description                                                                                                                                                                          | Default |\n|:---------|:------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------:|\n| Next     | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true.                                                                                                                  | `nil`   |\n| Prefix   | `string`                | Prefix adds a segment before `/debug/pprof`; it must start with a slash and omit the trailing slash. Example: `/federated-fiber` | `\"\"`   |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next: nil,\n}\n```\n"
  },
  {
    "path": "docs/middleware/proxy.md",
    "content": "---\nid: proxy\n---\n\n# Proxy\n\nThe Proxy middleware forwards requests to one or more upstream servers.\n\n## Signatures\n\n```go\n// Balancer creates a load balancer among multiple upstream servers.\nfunc Balancer(config ...Config) fiber.Handler\n// Forward performs the given http request and fills the given http response.\nfunc Forward(addr string, clients ...*fasthttp.Client) fiber.Handler\n// Do performs the given http request and fills the given http response.\nfunc Do(c fiber.Ctx, addr string, clients ...*fasthttp.Client) error\n// DoRedirects performs the given http request and fills the given http response while following up to maxRedirectsCount redirects.\nfunc DoRedirects(c fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error\n// DoDeadline performs the given request and waits for response until the given deadline.\nfunc DoDeadline(c fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error\n// DoTimeout performs the given request and waits for response during the given timeout duration.\nfunc DoTimeout(c fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error\n// DomainForward performs the given http request based on the provided domain and fills the given http response.\nfunc DomainForward(hostname string, addr string, clients ...*fasthttp.Client) fiber.Handler\n// BalancerForward performs the given http request based round robin balancer and fills the given http response.\nfunc BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/proxy\"\n)\n```\n\nOnce your Fiber app is initialized, you can use the middleware as shown:\n\n```go\n// Use proxy.WithClient to set a global custom client.\nproxy.WithClient(&fasthttp.Client{\n    NoDefaultUserAgentHeader: true,\n    DisablePathNormalizing:   true,\n    // Allow self-signed certificates when proxying to HTTPS targets.\n    TLSConfig: &tls.Config{\n        InsecureSkipVerify: true,\n    },\n})\n\n// Forward requests for a specific domain with proxy.DomainForward.\napp.Get(\"/payments\", proxy.DomainForward(\"docs.gofiber.io\", \"http://localhost:8000\"))\n\n// Forward to a URL using a custom client\napp.Get(\"/gif\", proxy.Forward(\"https://i.imgur.com/IWaBepg.gif\", &fasthttp.Client{\n    NoDefaultUserAgentHeader: true,\n    DisablePathNormalizing:   true,\n}))\n\n// Make a proxied request within a handler\napp.Get(\"/:id\", func(c fiber.Ctx) error {\n    url := \"https://i.imgur.com/\" + c.Params(\"id\") + \".gif\"\n    if err := proxy.Do(c, url); err != nil {\n        return err\n    }\n    // Remove Server header from response\n    c.Response().Header.Del(fiber.HeaderServer)\n    return nil\n})\n\n// Proxy requests while following redirects\napp.Get(\"/proxy\", func(c fiber.Ctx) error {\n    if err := proxy.DoRedirects(c, \"http://google.com\", 3); err != nil {\n        return err\n    }\n    // Remove Server header from response\n    c.Response().Header.Del(fiber.HeaderServer)\n    return nil\n})\n\n// Proxy requests and wait up to five seconds before timing out\napp.Get(\"/proxy\", func(c fiber.Ctx) error {\n    if err := proxy.DoTimeout(c, \"http://localhost:3000\", time.Second * 5); err != nil {\n        return err\n    }\n    // Remove Server header from response\n    c.Response().Header.Del(fiber.HeaderServer)\n    return nil\n})\n\n// Proxy requests with a deadline one minute from now\napp.Get(\"/proxy\", func(c fiber.Ctx) error {\n    if err := proxy.DoDeadline(c, \"http://localhost\", time.Now().Add(time.Minute)); err != nil {\n        return err\n    }\n    // Remove Server header from response\n    c.Response().Header.Del(fiber.HeaderServer)\n    return nil\n})\n\n// Minimal round-robin balancer\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: []string{\n        \"http://localhost:3001\",\n        \"http://localhost:3002\",\n        \"http://localhost:3003\",\n    },\n}))\n\n// Keep the Connection header when proxying\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: []string{\n        \"http://localhost:3001\",\n    },\n    KeepConnectionHeader: true,\n}))\n\n// Or extend your balancer for customization\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: []string{\n        \"http://localhost:3001\",\n        \"http://localhost:3002\",\n        \"http://localhost:3003\",\n    },\n    ModifyRequest: func(c fiber.Ctx) error {\n        c.Request().Header.Add(\"X-Real-IP\", c.IP())\n        return nil\n    },\n    ModifyResponse: func(c fiber.Ctx) error {\n        c.Response().Header.Del(fiber.HeaderServer)\n        return nil\n    },\n}))\n\n// Or this way if the balancer is using https and the destination server is only using http.\napp.Use(proxy.BalancerForward([]string{\n    \"http://localhost:3001\",\n    \"http://localhost:3002\",\n    \"http://localhost:3003\",\n}))\n\n\n// Make round robin balancer with IPv6 support.\napp.Use(proxy.Balancer(proxy.Config{\n    Servers: []string{\n        \"http://[::1]:3001\",\n        \"http://127.0.0.1:3002\",\n        \"http://localhost:3003\",\n    },\n    // Enable TCP4 and TCP6 network stacks.\n    DialDualStack: true,\n}))\n```\n\n## Config\n\n| Property        | Type                                           | Description                                                                                                                                                                                                                        | Default         |\n|:----------------|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------|\n| Next            | `func(fiber.Ctx) bool`                        | Next defines a function to skip this middleware when it returns true.                                                                                                                                                                | `nil`           |\n| Servers         | `[]string`                                     | Servers defines a list of `<scheme>://<host>` 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)      |\n| ModifyRequest   | `fiber.Handler`                                | ModifyRequest allows you to alter the request.                                                                                                                                                                                     | `nil`           |\n| ModifyResponse  | `fiber.Handler`                                | ModifyResponse allows you to alter the response.                                                                                                                                                                                   | `nil`           |\n| Timeout         | `time.Duration`                                | Timeout is the request timeout used when calling the proxy client.                                                                                                                                                                 | 1 second        |\n| 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) |\n| WriteBufferSize | `int`                                          | Per-connection buffer size for responses' writing.                                                                                                                                                                                 | (Not specified) |\n| KeepConnectionHeader | `bool` | Keeps the `Connection` header when set to `true`. By default the header is removed to comply with RFC 7230 §6.1 and avoid proxy loops. | `false` |\n| TLSConfig       | `*tls.Config` | TLS config for the HTTP client. | `nil`           |\n| DialDualStack   | `bool`                                         | Client will attempt to connect to both IPv4 and IPv6 host addresses if set to true.                                                                                                                                                | `false`         |\n| Client          | `*fasthttp.LBClient`                           | Client is a custom client when client config is complex.                                                                                                                                                                           | `nil`           |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Next:           nil,\n    ModifyRequest:  nil,\n    ModifyResponse: nil,\n    Timeout:        fasthttp.DefaultLBClientTimeout,\n    KeepConnectionHeader: false,\n}\n```\n"
  },
  {
    "path": "docs/middleware/recover.md",
    "content": "---\nid: recover\n---\n\n# Recover\n\nThe Recover middleware for [Fiber](https://github.com/gofiber/fiber) intercepts panics and forwards them to the central [ErrorHandler](../guide/error-handling).\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    recoverer \"github.com/gofiber/fiber/v3/middleware/recover\"\n)\n```\n\nOnce your Fiber app is initialized, use the middleware like this:\n\n```go\n// Initialize default config\napp.Use(recoverer.New())\n\n// Panics in subsequent handlers are caught by the middleware\napp.Get(\"/\", func(c fiber.Ctx) error {\n    panic(\"I'm an error\")\n})\n```\n\n## Config\n\n| Property          | Type                         | Description                                           | Default                    |\n|:------------------|:-----------------------------|:------------------------------------------------------|:---------------------------|\n| Next              | `func(fiber.Ctx) bool`       | Skip when the function returns `true`.                | `nil`                      |\n| PanicHandler      | `func(fiber.Ctx, any) error` | Customize the error returned from a recovered panic.  | `DefaultPanicHandler`      |\n| EnableStackTrace  | `bool`                       | Capture and include a stack trace in error responses. | `false`                    |\n| StackTraceHandler | `func(fiber.Ctx, any)`       | Handle the captured stack trace when enabled.         | `defaultStackTraceHandler` |\n\n## Default Config\n\n```go\nvar ConfigDefault = recoverer.Config{\n    Next:              nil,\n    PanicHandler:      DefaultPanicHandler,\n    StackTraceHandler: defaultStackTraceHandler,\n    EnableStackTrace:  false,\n}\n\n// Set up a PanicHandler to hide internals.\napp.Use(recoverer.New(recoverer.Config{PanicHandler: func(c fiber.Ctx, r any) error {\n    return fiber.ErrInternalServerError\n}}))\n\n// In more elaborate scenarios you can also create a custom error which can be processed differently in the fiber.ErrorHandler.\n// See the tests for an example of such an ErrorHandler.\n// You could also just wrap the default handler's error, e.g. fmt.Errorf(\"[RECOVERED]: %w\", recoverer.DefaultPanicHandler(c, r))\napp.Use(recoverer.New(recoverer.Config{PanicHandler: func(c fiber.Ctx, r any) error {\n    return &MyCustomRecoveredFromPanicError {\n        Inner: recoverer.DefaultPanicHandler(c, r),\n    }\n}}))\n```\n"
  },
  {
    "path": "docs/middleware/redirect.md",
    "content": "---\nid: redirect\n---\n\n# Redirect\n\nRedirect middleware maps old URLs to new ones using simple rules.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/redirect\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(redirect.New(redirect.Config{\n      Rules: map[string]string{\n        \"/old\":   \"/new\",\n        \"/old/*\": \"/new/$1\",\n      },\n      StatusCode: fiber.StatusMovedPermanently,\n    }))\n\n    app.Get(\"/new\", func(c fiber.Ctx) error {\n      return c.SendString(\"Hello, World!\")\n    })\n    app.Get(\"/new/*\", func(c fiber.Ctx) error {\n      return c.SendString(\"Wildcard: \" + c.Params(\"*\"))\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n## Test\n\n```bash\ncurl http://localhost:3000/old\ncurl http://localhost:3000/old/hello\n```\n\n## Config\n\n| Property   | Type                | Description                               | Default                |\n|:-----------|:--------------------|:------------------------------------------|:-----------------------|\n| Next       | `func(fiber.Ctx) bool` | Skip when function returns true.          | nil                    |\n| Rules      | `map[string]string`   | Map paths to new ones; `$1`, `$2` insert params. | Required               |\n| StatusCode | `int`                 | HTTP code for redirects.                  | 302 Temporary Redirect |\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    StatusCode: fiber.StatusFound,\n}\n```\n"
  },
  {
    "path": "docs/middleware/requestid.md",
    "content": "---\nid: requestid\n---\n\n# RequestID\n\nThe RequestID middleware generates or propagates a request identifier, adding it to the response headers and request context.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\nfunc FromContext(ctx any) string\n```\n\n`FromContext` accepts a `fiber.CustomCtx`, `fiber.Ctx`, a `*fasthttp.RequestCtx`, or a `context.Context`.\n\n## Examples\n\nImport the middleware package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/requestid\"\n)\n```\n\nOnce your Fiber app is initialized, add the middleware like this:\n\n```go\n// Initialize default config\napp.Use(requestid.New())\n\n// Or extend your config for customization\napp.Use(requestid.New(requestid.Config{\n    Header:    \"X-Custom-Header\",\n    Generator: func() string {\n        return \"static-id\"\n    },\n}))\n```\n\nIf the request already includes the configured header, that value is reused instead of generating a new one. The middleware\nrejects IDs containing characters outside the visible ASCII range (for example, control characters or obs-text bytes) and\nwill regenerate the value using up to three attempts from the configured generator (or SecureToken when no generator is set). When a\ncustom generator fails to produce a valid ID, the middleware falls back to SecureToken to keep headers RFC-compliant\nacross transports.\n\nRetrieve the request ID\n\n```go\nfunc handler(c fiber.Ctx) error {\n    id := requestid.FromContext(c)\n    log.Printf(\"Request ID: %s\", id)\n    return c.SendString(\"Hello, World!\")\n}\n```\n\n## Config\n\n| Property  | Type                 | Description                              | Default        |\n|:----------|:---------------------|:-----------------------------------------|:---------------|\n| Next      | `func(fiber.Ctx) bool` | Skip when the function returns `true`.    | `nil`          |\n| Header    | `string`             | Header key used to store the request ID. | \"X-Request-ID\" |\n| Generator | `func() string`      | Function that generates the identifier.  | utils.SecureToken |\n\n## Default Config\n\nThe default config uses a cryptographically secure token generator for better security and privacy.\n\n```go\nvar ConfigDefault = Config{\n    Next:       nil,\n    Header:     fiber.HeaderXRequestID,\n    Generator:  utils.SecureToken,\n}\n```\n"
  },
  {
    "path": "docs/middleware/responsetime.md",
    "content": "---\nid: responsetime\n---\n\n# ResponseTime\n\nResponse time middleware for [Fiber](https://github.com/gofiber/fiber) that measures the time spent handling a request and exposes it via a response header.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/responsetime\"\n)\n```\n\n### Default config\n\n```go\napp.Use(responsetime.New())\n```\n\n### Custom header\n\n```go\napp.Use(responsetime.New(responsetime.Config{\n    Header: \"X-Elapsed\",\n}))\n```\n\n### Skip logic\n\n```go\napp.Use(responsetime.New(responsetime.Config{\n    Next: func(c fiber.Ctx) bool {\n        return c.Path() == \"/healthz\"\n    },\n}))\n```\n\n## Config\n\n| Property | Type | Description | Default |\n| :------- | :--- | :---------- | :------ |\n| Next | `func(c fiber.Ctx) bool` | Defines a function to skip this middleware when it returns `true`. | `nil` |\n| Header | `string` | Header key used to store the measured response time. If left empty, the default header is used. | `\"X-Response-Time\"` |\n"
  },
  {
    "path": "docs/middleware/rewrite.md",
    "content": "---\nid: rewrite\n---\n\n# Rewrite\n\nThe Rewrite middleware remaps the request path using custom rules, helping with backward compatibility and cleaner URLs.\n\n## Signatures\n\n```go\nfunc New(config ...Config) fiber.Handler\n```\n\n## Config\n\n| Property | Type                  | Description                                           | Default    |\n|:---------|:----------------------|:------------------------------------------------------|:-----------|\n| Next     | `func(fiber.Ctx) bool` | Skip when function returns `true`.                    | `nil`      |\n| Rules    | `map[string]string`   | Map paths to new values; use `$1`, `$2` for wildcard captures.| (Required) |\n\n:::note\nRules are stored in a map, so iteration order is undefined. Avoid overlapping patterns if precedence matters.\n:::\n\n### Examples\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/rewrite\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(rewrite.New(rewrite.Config{\n      Rules: map[string]string{\n        \"/old\":   \"/new\",\n        \"/old/*\": \"/new/$1\",\n      },\n    }))\n\n    app.Get(\"/new\", func(c fiber.Ctx) error {\n      return c.SendString(\"Hello, World!\")\n    })\n    app.Get(\"/new/*\", func(c fiber.Ctx) error {\n      return c.SendString(\"Wildcard: \" + c.Params(\"*\"))\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n## Test\n\n```bash\ncurl http://localhost:3000/old\ncurl http://localhost:3000/old/hello\n```\n"
  },
  {
    "path": "docs/middleware/session.md",
    "content": "---\nid: session\n---\n\n# Session\n\nThe Session middleware adds session management to Fiber apps through the [Storage](https://github.com/gofiber/storage) package, which offers a unified interface for multiple databases. By default, sessions live in memory, but you can plug in any storage backend.\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Usage Patterns](#usage-patterns)\n- [Session Security](#session-security)\n- [Session ID Extractors](#session-id-extractors)\n- [Configuration](#configuration)\n- [Migration Guide](#migration-guide)\n- [API Reference](#api-reference)\n- [Examples](#examples)\n\n## Quick Start\n\n```go\nimport (\n    \"fmt\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/session\"\n)\n\n// Basic usage\napp.Use(session.New())\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n\n    // Get and update visits count\n    var visits int\n    if v := sess.Get(\"visits\"); v != nil {\n        // Use type assertion with an ok check to prevent a panic\n        if vInt, ok := v.(int); ok {\n            visits = vInt\n        }\n    }\n    visits++\n    sess.Set(\"visits\", visits)\n    return c.SendString(fmt.Sprintf(\"Visits: %d\", visits))\n})\n```\n\n### Production Configuration\n\n```go\nimport (\n    \"time\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n    \"github.com/gofiber/storage/redis/v3\"\n)\n\nstorage := redis.New(redis.Config{\n    Host: \"localhost\",\n    Port: 6379,\n})\n\napp.Use(session.New(session.Config{\n    Storage:           storage,\n    CookieSecure:      true,              // HTTPS only\n    CookieHTTPOnly:    true,              // Prevent XSS\n    CookieSameSite:    \"Lax\",             // CSRF protection\n    IdleTimeout:       30 * time.Minute,  // Session timeout\n    AbsoluteTimeout:   24 * time.Hour,    // Maximum session life\n    Extractor:         extractors.FromCookie(\"__Host-session_id\"),\n}))\n\nNotes:\n\n- AbsoluteTimeout must be greater than or equal to IdleTimeout; otherwise, the middleware panics during configuration.\n- If CookieSameSite is set to \"None\", the middleware automatically forces CookieSecure=true when setting the cookie.\n```\n\n## Usage Patterns\n\n### Middleware Pattern (Recommended)\n\nThis pattern automatically manages the session lifecycle and is recommended for most applications.\n\n```go\n// Setup middleware\napp.Use(session.New())\n\n// Use in handlers\napp.Post(\"/login\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n\n    // Session is automatically saved when handler returns\n    sess.Set(\"user_id\", 123)\n    sess.Set(\"authenticated\", true)\n\n    return c.Redirect(\"/dashboard\")\n})\n```\n\n**Benefits:**\n\n- Automatic session saving\n- Automatic resource cleanup\n- No manual lifecycle management\n- Thread-safe operations\n\n### Store Pattern (Advanced)\n\nUse the store pattern for background tasks or when you need direct access to sessions.\n\n```go\nimport (\n    \"context\"\n    \"log\"\n    \"time\"\n)\n\nstore := session.NewStore()\n\n// In background tasks\nfunc backgroundTask(sessionID string) {\n    sess, err := store.GetByID(context.Background(), sessionID)\n    if err != nil {\n        return\n    }\n    defer sess.Release() // Important: Manual cleanup required\n\n    // Modify session\n    sess.Set(\"last_task\", time.Now())\n\n    // Manual save required\n    if err := sess.Save(); err != nil {\n        log.Printf(\"Failed to save session: %v\", err)\n    }\n}\n```\n\n**Requirements:**\n\n- Must call `sess.Release()` when done\n- Must call `sess.Save()` to persist changes\n- Handle errors manually\n\n## Session Security\n\n### Authentication Flow\n\nUnderstanding session lifecycle during authentication is crucial for security.\n\n#### Basic Login/Logout\n\n```go\napp.Post(\"/login\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n\n    email := c.FormValue(\"email\")\n    password := c.FormValue(\"password\")\n\n    // Simple credential validation (use proper authentication in production)\n    if email == \"admin@example.com\" && password == \"secret\" {\n        // Important: Regenerate the session ID to prevent fixation\n        // This changes the session ID while preserving existing data\n        if err := sess.Regenerate(); err != nil {\n            return c.Status(500).SendString(\"Session error\")\n        }\n\n        // Add authentication data to existing session\n        sess.Set(\"user_id\", 1)\n        sess.Set(\"authenticated\", true)\n\n        return c.Redirect(\"/dashboard\")\n    }\n\n    return c.Status(401).SendString(\"Invalid credentials\")\n})\n\napp.Post(\"/logout\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n\n    // Complete session reset (clears all data + new session ID)\n    if err := sess.Reset(); err != nil {\n        return c.Status(500).SendString(\"Session error\")\n    }\n\n    return c.Redirect(\"/\")\n})\n```\n\n#### Cart Preservation During Login\n\n```go\napp.Post(\"/login\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n\n    // Validate credentials (implement your own validation)\n    email := c.FormValue(\"email\")\n    password := c.FormValue(\"password\")\n    if !isValidUser(email, password) {\n        return c.Status(401).JSON(fiber.Map{\"error\": \"Invalid credentials\"})\n    }\n\n    // Important: Regenerate the session ID to prevent fixation\n    // This changes the session ID while preserving existing data\n    if err := sess.Regenerate(); err != nil {\n        return c.Status(500).JSON(fiber.Map{\"error\": \"Session error\"})\n    }\n\n    // Add authentication data to existing session\n    sess.Set(\"user_id\", getUserID(email))\n    sess.Set(\"authenticated\", true)\n    sess.Set(\"login_time\", time.Now())\n\n    return c.JSON(fiber.Map{\"status\": \"logged in\"})\n})\n```\n\n### Security Methods Comparison\n\n| Method | Session ID | Session Data | Use Case |\n|--------|------------|--------------|----------|\n| `Regenerate()` | ✅ Changes | ✅ Preserved | Login, privilege escalation |\n| `Reset()` | ✅ Changes | ❌ Cleared | Logout, security breach |\n| `Destroy()` | ⚪ Unchanged | ❌ Cleared | Clear data only |\n\n### Common Security Mistakes\n\n❌ **Session Fixation Vulnerability:**\n\n```go\n// DANGEROUS: Keeping same session ID after login\napp.Post(\"/login\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n    // Validate user...\n    sess.Set(\"user_id\", userID) // Attacker can hijack this session!\n    return c.Redirect(\"/dashboard\")\n})\n```\n\n✅ **Secure Implementation:**\n\n```go\n// SECURE: Always regenerate session ID after authentication\napp.Post(\"/login\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n    // Validate user...\n    if err := sess.Regenerate(); err != nil { // Prevents session fixation\n        return err\n    }\n    sess.Set(\"user_id\", userID)\n    return c.Redirect(\"/dashboard\")\n})\n```\n\n### Authentication Middleware\n\nThis is a basic example of an authentication middleware that checks if a user is logged in before accessing protected routes.\n\n```go\n// Authentication check middleware\nfunc RequireAuth(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n    if sess == nil {\n        return c.Redirect(\"/login\")\n    }\n\n    // Check if user is authenticated\n    if sess.Get(\"authenticated\") != true {\n        return c.Redirect(\"/login\")\n    }\n\n    return c.Next()\n}\n\n// Usage\napp.Use(\"/dashboard\", RequireAuth)\napp.Use(\"/admin\", RequireAuth)\n```\n\n### Automatic Session Expiration\n\nSessions automatically expire based on your configuration:\n\n```go\napp.Use(session.New(session.Config{\n    IdleTimeout:     30 * time.Minute, // Auto-expire after 30 min of inactivity\n    AbsoluteTimeout: 24 * time.Hour,   // Force expire after 24 hours regardless of activity\n}))\n```\n\n**How it works:**\n\n- `IdleTimeout`: Storage automatically removes sessions after inactivity period\n  - Any route that uses the middleware will reset the idle timer\n  - Calling `sess.Save()` will also reset the idle timer\n- `AbsoluteTimeout`: Sessions are forcibly expired after maximum duration\n- No manual cleanup required - the storage layer handles this\n\n## Session ID Extractors\n\nThis middleware uses the shared extractors module for session ID extraction. See the [Extractors Guide](../guide/extractors) for more details.\n\n### Built-in Extractors\n\n```go\n// Cookie-based (recommended for web apps)\nextractors.FromCookie(\"session_id\")\n\n// Header-based (recommended for APIs)\nextractors.FromHeader(\"X-Session-ID\")\n\n// Authorization header (read-only)\nextractors.FromAuthHeader(\"Bearer\")\n\n// Form data\nextractors.FromForm(\"session_id\")\n\n// URL query parameter\nextractors.FromQuery(\"session_id\")\n\n// URL path parameter\nextractors.FromParam(\"id\")\n```\n\n**Session Response Behavior:**\n\n- Cookie extractors: set cookie in the response\n- Header extractors (non-Authorization): set header in the response\n- Authorization header, Query, Form, Param, Custom: read-only (no response values are set)\n\n### Multiple Sources with Fallback\n\n```go\napp.Use(session.New(session.Config{\n    Extractor: extractors.Chain(\n        extractors.FromCookie(\"session_id\"),    // Try cookie first\n        extractors.FromHeader(\"X-Session-ID\"),  // Then header\n        extractors.FromQuery(\"session_id\"),     // Finally query\n    ),\n}))\n```\n\n**Response Behavior with Chained Extractors:**\n\nOnly cookie and non-Authorization header extractors contribute to response setting. Others are read-only.\n\n- Cookie + Header (non-Auth) extractors: both cookie and header are set\n- Only Cookie extractors: only cookie is set\n- Only Header (non-Auth) extractors: only header is set\n- Any mix that includes Authorization/Query/Form/Param/Custom: those sources are read-only\n\n```go\n// This will set both cookie and header in response\nextractors.Chain(\n    extractors.FromCookie(\"session_id\"),\n    extractors.FromHeader(\"X-Session-ID\")\n)\n\n// This will set only cookie in response\nextractors.Chain(\n    extractors.FromCookie(\"session_id\"),\n    extractors.FromQuery(\"session_id\")   // Ignored for response\n)\n\n// This will set nothing in response (read-only mode)\nextractors.Chain(\n    extractors.FromQuery(\"session_id\"),\n    extractors.FromForm(\"session_id\")\n)\n```\n\n### Custom Extractors (Session-specific)\n\nPrefer the helper constructors from the extractors module. See the Extractors Guide for the full API; below are session-specific examples and notes.\n\n```go\n// Authorization Bearer tokens (read-only for sessions)\n// The session middleware will NOT set Authorization back in the response.\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromAuthHeader(\"Bearer\"),\n}))\n```\n\n```go\n// Custom read-only header via FromCustom (read-only for sessions)\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCustom(\"X-Custom-Session\", func(c fiber.Ctx) (string, error) {\n        v := c.Get(\"X-Custom-Session\")\n        if v == \"\" { return \"\", extractors.ErrNotFound }\n        return v, nil\n    }),\n}))\n```\n\n## Configuration\n\n### Storage Options\n\n```go\nimport (\n    \"github.com/gofiber/storage/redis/v3\"\n    \"github.com/gofiber/storage/postgres/v3\"\n)\n\n// Redis (recommended for production)\nredisStorage := redis.New(redis.Config{\n    Host:     \"localhost\",\n    Port:     6379,\n    Password: \"\",\n    Database: 0,\n})\n\n// PostgreSQL\npgStorage := postgres.New(postgres.Config{\n    Host:     \"localhost\",\n    Port:     5432,\n    Database: \"sessions\",\n    Username: \"user\",\n    Password: \"pass\",\n})\n\napp.Use(session.New(session.Config{\n    Storage: redisStorage,\n}))\n```\n\n### Production Security Settings\n\n```go\nimport (\n    \"log\"\n    \"time\"\n    \"github.com/gofiber/utils/v2\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n)\n\napp.Use(session.New(session.Config{\n    // Storage\n    Storage: redisStorage,\n\n    // Security\n    CookieSecure:      true,    // HTTPS only (required in production)\n    CookieHTTPOnly:    true,    // No JavaScript access (prevents XSS)\n    CookieSameSite:    \"Lax\",   // CSRF protection\n\n    // Session Management\n    IdleTimeout:       30 * time.Minute,  // Inactivity timeout\n    AbsoluteTimeout:   24 * time.Hour,    // Maximum session duration\n\n    // Cookie Settings\n    CookiePath:        \"/\",\n    CookieDomain:      \"example.com\",\n    CookieSessionOnly: false,   // Persist across browser restarts\n\n    // Session ID\n    Extractor:         extractors.FromCookie(\"__Host-session_id\"),\n    KeyGenerator:      utils.SecureToken,\n\n    // Error Handling\n    ErrorHandler: func(c fiber.Ctx, err error) {\n        log.Printf(\"Session error: %v\", err)\n    },\n}))\n```\n\n### Custom Types\n\nSession data supports basic Go types by default:\n\n- `string`, `int`, `int8`, `int16`, `int32`, `int64`\n- `uint`, `uint8`, `uint16`, `uint32`, `uint64`\n- `bool`, `float32`, `float64`\n- `[]byte`, `complex64`, `complex128`\n- `interface{}`\n\nFor custom types (structs, maps, slices), you must register them for encoding/decoding:\n\n```go\nimport \"fmt\"\n\ntype User struct {\n    ID   int    `json:\"id\"`\n    Name string `json:\"name\"`\n    Role string `json:\"role\"`\n}\n\n// Method 1: Using NewWithStore\nfunc main() {\n    app := fiber.New()\n\n    sessionMiddleware, store := session.NewWithStore()\n    store.RegisterType(User{}) // Register custom type\n\n    app.Use(sessionMiddleware)\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        // Use custom type\n        sess.Set(\"user\", User{ID: 123, Name: \"John\", Role: \"admin\"})\n\n        user, ok := sess.Get(\"user\").(User)\n        if ok {\n            return c.JSON(fiber.Map{\"user\": user.Name, \"role\": user.Role})\n        }\n        return c.SendString(\"No user found\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```go\n// Method 2: Using separate store\nstore := session.NewStore()\nstore.RegisterType(User{})\n\napp.Use(session.New(session.Config{\n    Store: store,\n}))\n\n// Usage in handlers\nsess.Set(\"user\", User{ID: 123, Name: \"John\", Role: \"admin\"})\nuser, ok := sess.Get(\"user\").(User)\nif ok {\n    fmt.Printf(\"User: %s (Role: %s)\", user.Name, user.Role)\n}\n```\n\n**Important Notes:**\n\n- Custom types must be registered before using them in sessions\n- Registration must happen during application startup\n- All instances of the application must register the same types\n- Types are encoded using Go's `gob` package\n\n## Migration Guide\n\n### v2 to v3 Breaking Changes\n\n1. **Function Signature**: `session.New()` now returns middleware handler, not store\n2. **Session ID Extraction**: `KeyLookup` replaced with `Extractor` functions\n3. **Lifecycle Management**: Manual `Release()` required for store pattern\n4. **Timeout Handling**: `Expiration` split into `IdleTimeout` and `AbsoluteTimeout`\n\n### Migration Examples\n\n**v2 Code:**\n\n```go\nstore := session.New(session.Config{\n    KeyLookup: \"cookie:session_id\",\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    sess, err := store.Get(c)\n    if err != nil {\n        return err\n    }\n    // Session automatically saved and released\n    sess.Set(\"key\", \"value\")\n    return nil\n})\n```\n\n**v3 Middleware Pattern (Recommended):**\n\n```go\napp.Use(session.New(session.Config{\n    Extractor: extractors.FromCookie(\"session_id\"),\n}))\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    sess := session.FromContext(c)\n    // Session automatically saved and released\n    sess.Set(\"key\", \"value\")\n    return nil\n})\n```\n\n**v3 Store Pattern (Advanced):**\n\n```go\nstore := session.NewStore(session.Config{\n    Extractor: extractors.FromCookie(\"session_id\"),\n})\n\napp.Get(\"/\", func(c fiber.Ctx) error {\n    sess, err := store.Get(c)\n    if err != nil {\n        return err\n    }\n    defer sess.Release() // Manual cleanup required\n\n    sess.Set(\"key\", \"value\")\n    return sess.Save() // Manual save required\n})\n```\n\n### KeyLookup to Extractor Migration\n\n| v2 KeyLookup                    | v3 Extractor                                                                       |\n|---------------------------------|------------------------------------------------------------------------------------|\n| `\"cookie:session_id\"`           | `extractors.FromCookie(\"session_id\")`                                             |\n| `\"header:X-Session-ID\"`         | `extractors.FromHeader(\"X-Session-ID\")`                                           |\n| `\"query:session_id\"`            | `extractors.FromQuery(\"session_id\")`                                              |\n| `\"form:session_id\"`             | `extractors.FromForm(\"session_id\")`                                               |\n| `\"cookie:sid,header:X-Sid\"`     | `extractors.Chain(extractors.FromCookie(\"sid\"), extractors.FromHeader(\"X-Sid\"))` |\n\n## API Reference\n\n### Middleware Methods (Recommended)\n\n```go\nsess := session.FromContext(c)\n\n// Data operations\nsess.Get(key any) any\nsess.Set(key, value any)\nsess.Delete(key any)\nsess.Keys() []any\n\n// Session management\nsess.ID() string\nsess.Fresh() bool\nsess.Regenerate() error  // Change ID, keep data\nsess.Reset() error       // Change ID, clear data\nsess.Destroy() error     // Keep ID, clear data\n\n// Store access\nsess.Store() *session.Store\n```\n\n`FromContext` accepts a `fiber.CustomCtx`, `fiber.Ctx`, a `*fasthttp.RequestCtx`, or a `context.Context`.\n\n### Store Methods\n\n```go\nstore := session.NewStore()\n\n// Store operations\nstore.Get(c fiber.Ctx) (*session.Session, error)\nstore.GetByID(ctx context.Context, sessionID string) (*session.Session, error)\nstore.Reset(ctx context.Context) error\nstore.Delete(ctx context.Context, sessionID string) error\n\n// Type registration\nstore.RegisterType(interface{})\n```\n\n### Session Methods (Store Pattern)\n\n```go\nsess, err := store.Get(c)\ndefer sess.Release() // Required!\n\n// Same methods as middleware, plus:\nsess.Save() error              // Manual save required\nsess.SetIdleTimeout(duration)  // Per-session timeout\nsess.Release()                 // Manual cleanup required\n```\n\n### Extractor Functions\n\n```go\n// Built-in extractors (import \"github.com/gofiber/fiber/v3/extractors\")\nextractors.FromCookie(key string) extractors.Extractor\nextractors.FromHeader(key string) extractors.Extractor\nextractors.FromQuery(key string) extractors.Extractor\nextractors.FromForm(key string) extractors.Extractor\nextractors.FromParam(key string) extractors.Extractor\n\n// Chaining\nextractors.Chain(extractors ...extractors.Extractor) extractors.Extractor\n```\n\n### Config Properties\n\n| Property            | Type                        | Description                 | Default                                    |\n|---------------------|-----------------------------|-----------------------------|--------------------------------------------|\n| `Store`             | `*session.Store`            | Pre-built session store (use when you need to share/register types) | `nil` (auto-created)                       |\n| `Storage`           | `fiber.Storage`             | Session storage backend (used when creating a store if `Store` is nil) | `memory.New()`                             |\n| `Extractor`         | `extractors.Extractor`      | Session ID extraction       | `extractors.FromCookie(\"session_id\")`     |\n| `KeyGenerator`      | `func() string`             | Session ID generator        | `utils.SecureToken`                             |\n| `IdleTimeout`       | `time.Duration`             | Inactivity timeout          | `30 * time.Minute`                         |\n| `AbsoluteTimeout`   | `time.Duration`             | Maximum session duration    | `0` (unlimited)                            |\n| `CookieSecure`      | `bool`                      | HTTPS only                  | `false`                                    |\n| `CookieHTTPOnly`    | `bool`                      | No JavaScript access        | `false`                                    |\n| `CookieSameSite`    | `string`                    | SameSite attribute          | `\"Lax\"`                                    |\n| `CookiePath`        | `string`                    | Cookie path                 | `\"\"`                                       |\n| `CookieDomain`      | `string`                    | Cookie domain               | `\"\"`                                       |\n| `CookieSessionOnly` | `bool`                      | Session cookie              | `false`                                    |\n| `Next`              | `func(fiber.Ctx) bool`      | Skip middleware when returns true | `nil`                                  |\n| `ErrorHandler`      | `func(fiber.Ctx, error)`    | Error callback              | `DefaultErrorHandler`                      |\n\n## Examples\n\n### E-commerce with Cart Persistence\n\n```go\nimport (\n    \"time\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/session\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n    \"github.com/gofiber/storage/redis/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Session middleware\n    app.Use(session.New(session.Config{\n        Storage:           redis.New(),\n        CookieSecure:      true,\n        CookieHTTPOnly:    true,\n        CookieSameSite:    \"Lax\",\n        IdleTimeout:       30 * time.Minute,\n        AbsoluteTimeout:   24 * time.Hour,\n        Extractor:         extractors.FromCookie(\"__Host-cart_session\"),\n    }))\n\n    // Add to cart (anonymous user)\n    app.Post(\"/cart/add\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        cart, _ := sess.Get(\"cart\").([]string)\n        cart = append(cart, c.FormValue(\"item_id\"))\n        sess.Set(\"cart\", cart)\n\n        return c.JSON(fiber.Map{\"items\": len(cart)})\n    })\n\n    // Login (preserve session data)\n    app.Post(\"/login\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        // Simple validation (implement proper authentication)\n        email := c.FormValue(\"email\")\n        password := c.FormValue(\"password\")\n        if email != \"user@example.com\" || password != \"password\" {\n            return c.Status(401).JSON(fiber.Map{\"error\": \"Invalid credentials\"})\n        }\n\n        // Regenerate session ID for security\n        // This changes the session ID while preserving existing data\n        if err := sess.Regenerate(); err != nil {\n            return c.Status(500).JSON(fiber.Map{\"error\": \"Session error\"})\n        }\n\n        sess.Set(\"user_id\", 1)\n        sess.Set(\"authenticated\", true)\n\n        return c.JSON(fiber.Map{\"status\": \"logged in\"})\n    })\n\n    // Logout (clear everything)\n    app.Post(\"/logout\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        // Reset clears all data and generates new session ID\n        if err := sess.Reset(); err != nil {\n            return c.Status(500).JSON(fiber.Map{\"error\": \"Session error\"})\n        }\n\n        return c.JSON(fiber.Map{\"status\": \"logged out\"})\n    })\n\n    app.Listen(\":3000\")\n}\n\n// Helper functions (implement these properly in production)\nfunc isValidUser(email, password string) bool {\n    return email == \"user@example.com\" && password == \"password\"\n}\n\nfunc getUserID(email string) int {\n    return 1 // Return actual user ID from database\n}\n```\n\n### API with Header-based Sessions\n\n```go\nimport (\n    \"time\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/session\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n    \"github.com/gofiber/storage/redis/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // API session middleware with header extraction\n    app.Use(session.New(session.Config{\n        Storage:     redis.New(),\n        Extractor:   extractors.FromHeader(\"X-Session-Token\"),\n        IdleTimeout: time.Hour,\n    }))\n\n    // API endpoint\n    app.Post(\"/api/data\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        // Track API usage\n        count, _ := sess.Get(\"api_calls\").(int)\n        count++\n        sess.Set(\"api_calls\", count)\n        sess.Set(\"last_call\", time.Now())\n\n        return c.JSON(fiber.Map{\n            \"data\":  \"some data\",\n            \"calls\": count,\n        })\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n### Multi-source Session ID Support\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/session\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Support multiple sources with priority\n    app.Use(session.New(session.Config{\n        Extractor: extractors.Chain(\n            extractors.FromCookie(\"session_id\"),    // 1st: Cookie (web)\n            extractors.FromHeader(\"X-Session-ID\"),  // 2nd: Header (API)\n            extractors.FromQuery(\"session_id\"),     // 3rd: Query (fallback)\n        ),\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        sess := session.FromContext(c)\n\n        // Works with any of the above methods\n        return c.JSON(fiber.Map{\n            \"session_id\": sess.ID(),\n            \"source\":     \"multi-source\",\n        })\n    })\n\n    app.Listen(\":3000\")\n}\n```\n"
  },
  {
    "path": "docs/middleware/skip.md",
    "content": "---\nid: skip\n---\n\n# Skip\n\nThe Skip middleware wraps a handler and bypasses it when the predicate returns `true` for the current request.\n\n## Signatures\n\n```go\nfunc New(handler fiber.Handler, exclude func(c fiber.Ctx) bool) fiber.Handler\n```\n\n## Examples\n\nImport the package:\n\n```go\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/skip\"\n)\n```\n\n`skip.New` accepts the handler to wrap and a predicate function. The predicate\nruns for every request, and returning `true` skips the wrapped handler and\nexecutes the next middleware in the chain.\n\nAfter you initialize your Fiber app, use `skip.New` like this:\n\n```go\nfunc main() {\n    app := fiber.New()\n\n    app.Use(skip.New(BasicHandler, func(ctx fiber.Ctx) bool {\n        return ctx.Method() == fiber.MethodGet\n    }))\n\n    app.Get(\"/\", func(ctx fiber.Ctx) error {\n        return ctx.SendString(\"It was a GET request!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n\nfunc BasicHandler(ctx fiber.Ctx) error {\n    return ctx.SendString(\"It was not a GET request!\")\n}\n```\n\n:::tip\n`app.Use` processes requests on any route and method. In the example above, the handler is skipped only for `GET`.\n:::\n"
  },
  {
    "path": "docs/middleware/static.md",
    "content": "---\nid: static\n---\n\n# Static\n\nThe Static middleware serves assets such as **images**, **CSS**, and **JavaScript**.\n\n:::info\nBy default, it serves `index.html` when a directory is requested. Customize this behavior in the [Config](#config) options.\n:::\n\n## Signatures\n\n```go\nfunc New(root string, cfg ...Config) fiber.Handler\n```\n\n## Examples\n\nImport the package:\n\n```go\nimport(\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/static\"\n)\n```\n\n### Serving files from a directory\n\n```go\napp.Get(\"/*\", static.New(\"./public\"))\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/hello.html\ncurl http://localhost:3000/css/style.css\n```\n\n</details>\n\n### Serving files from a directory with `Use`\n\n```go\napp.Use(\"/\", static.New(\"./public\"))\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/hello.html\ncurl http://localhost:3000/css/style.css\n```\n\n</details>\n\n### Serving a single file\n\n```go\napp.Use(\"/static\", static.New(\"./public/hello.html\"))\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/static # will show hello.html\ncurl http://localhost:3000/static/john/doe # will show hello.html\n```\n\n</details>\n\n### Serving files using os.DirFS\n\n```go\napp.Get(\"/files*\", static.New(\"\", static.Config{\n    FS:     os.DirFS(\"files\"),\n    Browse: true,\n}))\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/files/css/style.css\ncurl http://localhost:3000/files/index.html\n```\n\n</details>\n\n### Serving files using embed.FS\n\n```go\n//go:embed path/to/files\nvar myfiles embed.FS\n\napp.Get(\"/files*\", static.New(\"\", static.Config{\n    FS:     myfiles,\n    Browse: true,\n}))\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/files/css/style.css\ncurl http://localhost:3000/files/index.html\n```\n\n</details>\n\n### SPA (Single Page Application)\n\n```go\napp.Use(\"/web\", static.New(\"\", static.Config{\n    FS: os.DirFS(\"dist\"),\n}))\n\napp.Get(\"/web*\", func(c fiber.Ctx) error {\n    return c.SendFile(\"dist/index.html\")\n})\n```\n\n<details>\n<summary>Test</summary>\n\n```sh\ncurl http://localhost:3000/web/css/style.css\ncurl http://localhost:3000/web/index.html\ncurl http://localhost:3000/web\n```\n\n</details>\n\n:::caution\nTo define static routes using `Get`, append the wildcard (`*`) operator at the end of the route.\n:::\n\n## Config\n\n| Property   | Type                    | Description                                                                                                                | Default                |\n|:-----------|:------------------------|:---------------------------------------------------------------------------------------------------------------------------|:-----------------------|\n| Next       | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true.                                                                              | `nil`                  |\n| FS       | `fs.FS` | FS is the file system to serve the static files from.<br /><br />You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.                                                 | `nil`                  |\n| Compress       | `bool` | When set to true, the server tries minimizing CPU usage by caching compressed files. The middleware will compress the response using `gzip`, `brotli`, or `zstd` compression depending on the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. <br /><br />This works differently than the github.com/gofiber/compression middleware.                                                                              | `false`                  |\n| ByteRange       | `bool` | When set to true, enables byte range requests.                                                                             | `false`                  |\n| Browse       | `bool` | When set to true, enables directory browsing.                                                                             | `false`                  |\n| Download       | `bool` | When set to true, enables direct download.                                                                             | `false`                  |\n| IndexNames       | `[]string` | The names of the index files for serving a directory.                                                                             | `[]string{\"index.html\"}`                  |\n| CacheDuration       | `time.Duration` | Expiration duration for inactive file handlers.<br /><br />Use a negative time.Duration to disable it.                                                                             | `10 * time.Second`                  |\n| MaxAge       | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds.                                                                             | `0`                  |\n| ModifyResponse       | `fiber.Handler` | ModifyResponse defines a function that allows you to alter the response.                                                                             | `nil`                  |\n| NotFoundHandler       | `fiber.Handler` | NotFoundHandler defines a function to handle when the path is not found.                                                                             | `nil`                  |\n\nWhen **Download** is enabled, the response includes a `Content-Disposition` header with the requested filename. Non-ASCII names use the `filename*` parameter as defined by [RFC 6266](https://www.rfc-editor.org/rfc/rfc6266) and [RFC 8187](https://www.rfc-editor.org/rfc/rfc8187).\n\n:::info\nYou can set `CacheDuration` config property to `-1` to disable caching.\n:::\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    IndexNames:    []string{\"index.html\"},\n    CacheDuration: 10 * time.Second,\n}\n```\n"
  },
  {
    "path": "docs/middleware/timeout.md",
    "content": "---\nid: timeout\n---\n\n# Timeout\n\nThe timeout middleware enforces a deadline on handler execution. It wraps handlers with\n`context.WithTimeout`, exposes the derived context through `c.Context()`, and\nreturns `408 Request Timeout` when the deadline is exceeded.\n\n## How It Works\n\nWhen a timeout occurs, the middleware **returns immediately** without waiting for the\nhandler to finish. This is achieved through Fiber's **Abandon mechanism**:\n\n1. The handler runs in a goroutine with a timeout context\n2. On timeout, the middleware marks the context as \"abandoned\" and returns `408` immediately\n3. The handler goroutine can continue safely (e.g., for cleanup) without blocking the response\n4. A background cleanup goroutine waits for the handler to finish and performs context cleanup\n\nHandlers can detect the timeout by listening on `c.Context().Done()` and return early.\nThis is the recommended pattern for cooperative cancellation.\n\nIf a handler panics, the middleware catches it and returns `500 Internal Server Error`.\n\n## Known limitations\n\n- Timed-out requests abandon their `fiber.Ctx` to avoid data races with the core\n  request handler (including the `ErrorHandler`). These contexts are **not**\n  returned to the pool, so each timed-out request leaks a context. Calling\n  `ForceRelease` is only safe if you can guarantee that no goroutine (including\n  Fiber internals) will touch the context anymore; the timeout middleware\n  intentionally does not call it.\n\n:::caution\n`timeout.New` wraps your final handler and can't be added with `app.Use` or\nused in a middleware chain. Register it per route and avoid calling\n`c.Next()` inside the wrapped handler—doing so will panic.\n:::\n\n## Signatures\n\n```go\nfunc New(handler fiber.Handler, config ...timeout.Config) fiber.Handler\n```\n\n## Examples\n\n### Basic example\n\nThe following program times out any request that takes longer than two seconds.\nThe handler simulates work with `sleepWithContext`, which stops when the\ncontext is canceled:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/timeout\"\n)\n\nfunc sleepWithContext(ctx context.Context, d time.Duration) error {\n    select {\n    case <-time.After(d):\n        return nil\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}\n\nfunc main() {\n    app := fiber.New()\n\n    handler := func(c fiber.Ctx) error {\n        delay, _ := time.ParseDuration(c.Params(\"delay\") + \"ms\")\n        if err := sleepWithContext(c.Context(), delay); err != nil {\n            return fmt.Errorf(\"%w: execution error\", err)\n        }\n        return c.SendString(\"finished\")\n    }\n\n    app.Get(\"/sleep/:delay\", timeout.New(handler, timeout.Config{\n        Timeout: 2 * time.Second,\n    }))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nUse these requests to see the middleware in action:\n\n```bash\ncurl -i http://localhost:3000/sleep/1000   # finishes within the timeout\ncurl -i http://localhost:3000/sleep/3000   # returns 408 Request Timeout\n```\n\n## Config\n\n| Property    | Type               | Description                                                          | Default |\n|:------------|:-------------------|:---------------------------------------------------------------------|:-------|\n| Next        | `func(fiber.Ctx) bool` | Function to skip this middleware when it returns `true`.            | `nil`  |\n| Timeout     | `time.Duration`    | Timeout duration for requests. `0` or a negative value disables the timeout. | `0`    |\n| OnTimeout   | `fiber.Handler`    | Handler executed when a timeout occurs. Defaults to returning `fiber.ErrRequestTimeout`. | `nil`  |\n| Errors      | `[]error`          | Custom errors treated as timeout errors.                            | `nil`  |\n\n### Use with a custom error\n\n```go\nvar ErrFooTimeOut = errors.New(\"foo context canceled\")\n\nfunc main() {\n    app := fiber.New()\n    h := func(c fiber.Ctx) error {\n        sleepTime, _ := time.ParseDuration(c.Params(\"sleepTime\") + \"ms\")\n        if err := sleepWithContextWithCustomError(c.Context(), sleepTime); err != nil {\n            return fmt.Errorf(\"%w: execution error\", err)\n        }\n        return nil\n    }\n\n    app.Get(\"/foo/:sleepTime\", timeout.New(h, timeout.Config{Timeout: 2 * time.Second, Errors: []error{ErrFooTimeOut}}))\n    log.Fatal(app.Listen(\":3000\"))\n}\n\nfunc sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error {\n    timer := time.NewTimer(d)\n    select {\n    case <-ctx.Done():\n        if !timer.Stop() {\n            <-timer.C\n        }\n        return ErrFooTimeOut\n    case <-timer.C:\n    }\n    return nil\n}\n```\n\n### Sample usage with a database call\n\n```go\nfunc main() {\n    app := fiber.New()\n    db, _ := gorm.Open(postgres.Open(\"postgres://localhost/foodb\"), &gorm.Config{})\n\n    handler := func(ctx fiber.Ctx) error {\n        tran := db.WithContext(ctx.Context()).Begin()\n\n        if tran = tran.Exec(\"SELECT pg_sleep(50)\"); tran.Error != nil {\n            return tran.Error\n        }\n\n        if tran = tran.Commit(); tran.Error != nil {\n            return tran.Error\n        }\n\n        return nil\n    }\n\n    app.Get(\"/foo\", timeout.New(handler, timeout.Config{Timeout: 10 * time.Second}))\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n"
  },
  {
    "path": "docs/partials/routing/handler.md",
    "content": "---\nid: route-handlers\ntitle: Route Handlers\n---\n\nimport Reference from '@site/src/components/reference';\n\nRegisters a route bound to a specific [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods).\n\n```go title=\"Signatures\"\n// HTTP methods\nfunc (app *App) Get(path string, handler any, handlers ...any) Router\nfunc (app *App) Head(path string, handler any, handlers ...any) Router\nfunc (app *App) Post(path string, handler any, handlers ...any) Router\nfunc (app *App) Put(path string, handler any, handlers ...any) Router\nfunc (app *App) Delete(path string, handler any, handlers ...any) Router\nfunc (app *App) Connect(path string, handler any, handlers ...any) Router\nfunc (app *App) Options(path string, handler any, handlers ...any) Router\nfunc (app *App) Trace(path string, handler any, handlers ...any) Router\nfunc (app *App) Patch(path string, handler any, handlers ...any) Router\n\n// Add allows you to specify multiple methods at once\n// The provided handlers are executed in order, starting with `handler` and then the variadic `handlers`.\nfunc (app *App) Add(methods []string, path string, handler any, handlers ...any) Router\n\n// All will register the route on all HTTP methods\n// Almost the same as app.Use but not bound to prefixes\nfunc (app *App) All(path string, handler any, handlers ...any) Router\n```\n\nFiber's adapter converts a variety of handler shapes to native\n`func(fiber.Ctx) error` callbacks. It currently recognizes seventeen cases (the\nnumbers below match the comments in `toFiberHandler` inside `adapter.go`). This\nlets you mix Fiber-style handlers with Express-style callbacks and even reuse\n`net/http` or `fasthttp` functions.\n\n### Fiber-native handlers (cases 1–2)\n\n- **Case 1.** `fiber.Handler` — the canonical `func(fiber.Ctx) error` form.\n- **Case 2.** `func(fiber.Ctx)` — Fiber runs the function and treats it as if it\n  returned `nil`.\n\n### Express-style request handlers (cases 3–12)\n\n- **Case 3.** `func(fiber.Req, fiber.Res) error`\n- **Case 4.** `func(fiber.Req, fiber.Res)`\n- **Case 5.** `func(fiber.Req, fiber.Res, func() error) error`\n- **Case 6.** `func(fiber.Req, fiber.Res, func() error)`\n- **Case 7.** `func(fiber.Req, fiber.Res, func()) error`\n- **Case 8.** `func(fiber.Req, fiber.Res, func())`\n- **Case 9.** `func(fiber.Req, fiber.Res, func(error))`\n- **Case 10.** `func(fiber.Req, fiber.Res, func(error)) error`\n- **Case 11.** `func(fiber.Req, fiber.Res, func(error) error)`\n- **Case 12.** `func(fiber.Req, fiber.Res, func(error) error) error`\n\nThe adapter injects a `next` callback when your signature accepts one. Fiber\npropagates downstream errors from `c.Next()` back through the wrapper, so\nreturning those errors remains optional. If you never call the injected `next`\nfunction, the handler chain stops, matching Express semantics.\n\nWhen you accept `next` callbacks that take an `error`, calling `next(nil)`\ncontinues the chain and passing a non-nil error short-circuits with that error.\nIf the handler itself returns an error, Fiber prioritizes that value over any\nrecorded `next` error.\n\n### net/http handlers (cases 13–15)\n\n- **Case 13.** `http.HandlerFunc`\n- **Case 14.** `http.Handler`\n- **Case 15.** `func(http.ResponseWriter, *http.Request)`\n\n:::caution Compatibility overhead\nFiber adapts these handlers through `fasthttpadaptor`. They do not receive\n`fiber.Ctx`, cannot call `c.Next()`, and therefore always terminate the handler\nchain. The compatibility layer also adds more overhead than running a native\nFiber handler, so prefer the other forms when possible.\n:::\n\n### fasthttp handlers (cases 16–17)\n\n- **Case 16.** `fasthttp.RequestHandler`\n- **Case 17.** `func(*fasthttp.RequestCtx) error`\n\nfasthttp handlers run with full access to the underlying `fasthttp.RequestCtx`.\nThey are expected to manage the response directly. Fiber will propagate any\nerror returned by the `func(*fasthttp.RequestCtx) error` variant but otherwise\ndoes not inspect the context state.\n\n```go title=\"Examples\"\n// Simple GET handler (Fiber accepts both func(fiber.Ctx) and func(fiber.Ctx) error)\napp.Get(\"/api/list\", func(c fiber.Ctx) error {\n    return c.SendString(\"I'm a GET request!\")\n})\n\n// Reuse an existing net/http handler without manual adaptation\nhttpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n    w.WriteHeader(http.StatusNoContent)\n})\n\napp.Get(\"/foo\", httpHandler)\n\n// Align with Express-style handlers using fiber.Req and fiber.Res helpers (works\n// for middleware and routes alike)\napp.Use(func(req fiber.Req, res fiber.Res, next func() error) error {\n    if req.IP() == \"192.168.1.254\" {\n        return res.SendStatus(fiber.StatusForbidden)\n    }\n    return next()\n})\n\napp.Get(\"/express\", func(req fiber.Req, res fiber.Res) error {\n    return res.SendString(\"Hello from Express-style handlers!\")\n})\n\n// Mount a fasthttp.RequestHandler directly\napp.Get(\"/bar\", func(ctx *fasthttp.RequestCtx) {\n    ctx.SetStatusCode(fiber.StatusAccepted)\n})\n\n// Simple POST handler\napp.Post(\"/api/register\", func(c fiber.Ctx) error {\n    return c.SendString(\"I'm a POST request!\")\n})\n```\n\n## Use\n\nCan be used for middleware packages and prefix catchers. Prefixes now require either an exact match or a slash boundary, so `/john` matches `/john` and `/john/doe` but not `/johnnnnn`. Parameter tokens like `:name`, `:name?`, `*`, and `+` are still expanded before the boundary check runs.\n\n```go title=\"Signature\"\nfunc (app *App) Use(args ...any) Router\n\n// Fiber inspects args to support these common usage patterns:\n// - app.Use(handler, handlers ...any)\n// - app.Use(path string, handler, handlers ...any)\n// - app.Use(paths []string, handler, handlers ...any)\n// - app.Use(path string, subApp *App)\n```\n\nEach handler argument can independently be a Fiber handler (with or without an\n`error` return), an Express-style callback, a `net/http` handler, or any other\nsupported shape including fasthttp callbacks that return errors.\n\n```go title=\"Examples\"\n// Match any request\napp.Use(func(c fiber.Ctx) error {\n    return c.Next()\n})\n\n// Match request starting with /api\napp.Use(\"/api\", func(c fiber.Ctx) error {\n    return c.Next()\n})\n\n// Match requests starting with /api or /home (multiple-prefix support)\napp.Use([]string{\"/api\", \"/home\"}, func(c fiber.Ctx) error {\n    return c.Next()\n})\n\n// Attach multiple handlers\napp.Use(\"/api\", func(c fiber.Ctx) error {\n    c.Set(\"X-Custom-Header\", random.String(32))\n    return c.Next()\n}, func(c fiber.Ctx) error {\n    return c.Next()\n})\n\n// Mount a sub-app\napp.Use(\"/api\", api)\n```\n"
  },
  {
    "path": "docs/whats_new.md",
    "content": "---\nid: whats_new\ntitle: 🆕 What's New in v3\nsidebar_position: 2\ntoc_max_heading_level: 4\n---\n\n## 🎉 Welcome\n\nWe are excited to announce the release of Fiber v3! 🚀\n\nIn 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`.\n\n### 🛠️ Migration tool\n\nFiber v3 introduces a CLI-powered migration helper. Install the CLI and let\nit update your project automatically:\n\n```bash\ngo install github.com/gofiber/cli/fiber@latest\nfiber migrate --to v3\n```\n\nSee the [migration guide](#-migration-guide) for more details and options.\n\nHere's a quick overview of the changes in Fiber `v3`:\n\n- [🚀 App](#-app)\n- [🎣 Hooks](#-hooks)\n- [🚀 Listen](#-listen)\n- [🗺️ Router](#-router)\n- [🧠 Context](#-context)\n- [📎 Binding](#-binding)\n- [🔬 Extractors Package](#-extractors-package)\n- [🔄️ Redirect](#-redirect)\n- [🌎 Client package](#-client-package)\n- [🧰 Generic functions](#-generic-functions)\n- [🛠️ Utils](#utils)\n- [🧩 Services](#-services)\n- [📃 Log](#-log)\n- [📦 Storage Interface](#-storage-interface)\n- [🧬 Middlewares](#-middlewares)\n  - [Important Change for Accessing Middleware Data](#important-change-for-accessing-middleware-data)\n  - [Adaptor](#adaptor)\n  - [BasicAuth](#basicauth)\n  - [Cache](#cache)\n  - [CORS](#cors)\n  - [CSRF](#csrf)\n  - [Compression](#compression)\n  - [EncryptCookie](#encryptcookie)\n  - [Favicon](#favicon)\n  - [Filesystem](#filesystem)\n  - [Healthcheck](#healthcheck)\n  - [KeyAuth](#keyauth)\n  - [Logger](#logger)\n  - [Monitor](#monitor)\n  - [Proxy](#proxy)\n  - [Recover](#recover)\n  - [Session](#session)\n- [🔌 Addons](#-addons)\n- [📋 Migration guide](#-migration-guide)\n\n## Dropping support for old Go versions\n\nFiber `v3` requires Go `1.25` or later. Update your toolchain to `1.25+` before upgrading so the module `go` directive and standard library features align with the new minimum version.\n\n## 🚀 App\n\nWe have made several changes to the Fiber app, including:\n\n- **Listen**: The `Listen` method has been unified with the configuration, allowing for more streamlined setup.\n- **Static**: The `Static` method has been removed and its functionality has been moved to the [static middleware](./middleware/static.md).\n- **app.Config properties**: Several properties have been moved to the listen configuration:\n  - `DisableStartupMessage`\n  - `EnablePrefork` (previously `Prefork`)\n  - `EnablePrintRoutes`\n  - `ListenerNetwork` (previously `Network`)\n- **Trusted Proxy Configuration**: The `EnabledTrustedProxyCheck` has been moved to `app.Config.TrustProxy`, and `TrustedProxies` has been moved to `TrustProxyConfig.Proxies`. Additionally, `ProxyHeader` must be set to read client IPs from proxy headers (e.g., `X-Forwarded-For`).\n- **XMLDecoder Config Property**: The `XMLDecoder` property has been added to allow usage of 3rd-party XML libraries in XML binder.\n\n### New Methods\n\n- **RegisterCustomBinder**: Allows for the registration of custom binders.\n- **RegisterCustomConstraint**: Allows for the registration of custom constraints.\n- **NewWithCustomCtx**: Initialize an app with a custom context in one step.\n- **State**: Provides a global state for the application, which can be used to store and retrieve data across the application. Check out the [State](./api/state) method for further details.\n- **NewErrorf**: Allows variadic parameters when creating formatted errors.\n- **GetBytes / GetString**: Helpers that detach values only when `Immutable` is enabled and the data still references request or response buffers. Access via `c.App().GetString` and `c.App().GetBytes`.\n- **ReloadViews**: Lets you re-run the configured view engine's `Load()` logic at runtime, including guard rails for missing or nil view engines so development hot-reload hooks can refresh templates safely.\n\n#### Custom Route Constraints\n\nCustom route constraints enable you to define your own validation rules for route parameters.\nUse `RegisterCustomConstraint` to add a constraint type that implements the `CustomConstraint` interface.\n\n<details>\n<summary>Example</summary>\n\n```go\ntype UlidConstraint struct {\n    fiber.CustomConstraint\n}\n\nfunc (*UlidConstraint) Name() string {\n    return \"ulid\"\n}\n\nfunc (*UlidConstraint) Execute(param string, args ...string) bool {\n    _, err := ulid.Parse(param)\n    return err == nil\n}\n\napp.RegisterCustomConstraint(&UlidConstraint{})\n\napp.Get(\"/login/:id<ulid>\", func(c fiber.Ctx) error {\n    return c.SendString(\"User \" + c.Params(\"id\"))\n})\n```\n\n</details>\n\n### Removed Methods\n\n- **Mount**: Use `app.Use()` instead.\n- **ListenTLS**: Use `app.Listen()` with `tls.Config`.\n- **ListenTLSWithCertificate**: Use `app.Listen()` with `tls.Config`.\n- **ListenMutualTLS**: Use `app.Listen()` with `tls.Config`.\n- **ListenMutualTLSWithCertificate**: Use `app.Listen()` with `tls.Config`.\n\n### Method Changes\n\n- **Test**: The `Test` method has replaced the timeout parameter with a configuration parameter. `0` or lower represents no timeout.\n- **Listen**: Now has a configuration parameter.\n- **Listener**: Now has a configuration parameter.\n\n### Custom Ctx Interface in Fiber v3\n\nFiber v3 introduces a customizable `Ctx` interface, allowing developers to extend and modify the context to fit their needs. This feature provides greater flexibility and control over request handling.\n\n#### Idea Behind Custom Ctx Classes\n\nThe idea behind custom `Ctx` classes is to give developers the ability to extend the default context with additional methods and properties tailored to the specific requirements of their application. This allows for better request handling and easier implementation of specific logic.\n\n#### NewWithCustomCtx\n\n`NewWithCustomCtx` creates the application and sets the custom context factory at initialization time.\n\n```go title=\"Signature\"\nfunc NewWithCustomCtx(fn func(app *App) CustomCtx, config ...Config) *App\n```\n\n<details>\n<summary>Example</summary>\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"github.com/gofiber/fiber/v3\"\n)\n\ntype CustomCtx struct {\n    fiber.DefaultCtx\n}\n\nfunc (c *CustomCtx) CustomMethod() string {\n    return \"custom value\"\n}\n\nfunc main() {\n    app := fiber.NewWithCustomCtx(func(app *fiber.App) fiber.CustomCtx {\n        return &CustomCtx{\n            DefaultCtx: *fiber.NewDefaultCtx(app),\n        }\n    })\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        customCtx := c.(*CustomCtx)\n        return c.SendString(customCtx.CustomMethod())\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\nThis example creates a `CustomCtx` with an extra `CustomMethod` and initializes the app with `NewWithCustomCtx`.\n\n</details>\n\n### Configurable TLS Minimum Version\n\nWe have added support for configuring the TLS minimum version. This field allows you to set the TLS minimum version for TLSAutoCert and the server listener.\n\n```go\napp.Listen(\":444\", fiber.ListenConfig{TLSMinVersion: tls.VersionTLS12})\n```\n\n#### TLS AutoCert support (ACME / Let's Encrypt)\n\nWe have added native support for automatic certificates management from Let's Encrypt and any other ACME-based providers.\n\n```go\n// Certificate manager\ncertManager := &autocert.Manager{\n    Prompt: autocert.AcceptTOS,\n    // Replace with your domain name\n    HostPolicy: autocert.HostWhitelist(\"example.com\"),\n    // Folder to store the certificates\n    Cache: autocert.DirCache(\"./certs\"),\n}\n\napp.Listen(\":444\", fiber.ListenConfig{\n    AutoCertManager:    certManager,\n})\n```\n\n### MIME Constants\n\n`MIMEApplicationJavaScript` and `MIMEApplicationJavaScriptCharsetUTF8` are deprecated. Use `MIMETextJavaScript` and `MIMETextJavaScriptCharsetUTF8` instead.\n\n## 🎣 Hooks\n\nWe have made several changes to the Fiber hooks, including:\n\n- Added new shutdown hooks to provide better control over the shutdown process:\n  - `OnPreShutdown` - Executes before the server starts shutting down\n  - `OnPostShutdown` - Executes after the server has shut down, receives any shutdown error\n  - `OnPreStartupMessage` - Executes before the startup message is printed, allowing customization of the banner and info entries\n  - `OnPostStartupMessage` - Executes after the startup message is printed, allowing post-startup logic\n- Deprecated `OnShutdown` in favor of the new pre/post shutdown hooks\n- Improved shutdown hook execution order and reliability\n- Added mutex protection for hook registration and execution\n\nImportant: When using shutdown hooks, ensure app.Listen() is called in a separate goroutine:\n\n```go\n// Correct usage\ngo app.Listen(\":3000\")\n// ... register shutdown hooks\napp.Shutdown()\n\n// Incorrect usage - hooks won't work\napp.Listen(\":3000\") // This blocks\napp.Shutdown()      // Never reached\n```\n\n## 🚀 Listen\n\nWe have made several changes to the Fiber listen, including:\n\n- Removed `OnShutdownError` and `OnShutdownSuccess` from `ListenConfig` in favor of using the `OnPostShutdown` hook, which receives the shutdown error\n\n```go\napp := fiber.New()\n\n// Before - using ListenConfig callbacks\napp.Listen(\":3000\", fiber.ListenConfig{\n    OnShutdownError: func(err error) {\n        log.Printf(\"Shutdown error: %v\", err)\n    },\n    OnShutdownSuccess: func() {\n        log.Println(\"Shutdown successful\")\n    },\n})\n\n// After - using OnPostShutdown hook\napp.Hooks().OnPostShutdown(func(err error) error {\n    if err != nil {\n        log.Printf(\"Shutdown error: %v\", err)\n    } else {\n        log.Println(\"Shutdown successful\")\n    }\n    return nil\n})\ngo app.Listen(\":3000\")\n```\n\nThis change simplifies the shutdown handling by consolidating the shutdown callbacks into a single hook that receives the error status.\n\n- Added support for Unix domain sockets via `ListenerNetwork` and `UnixSocketFileMode`\n\n```go\n// v2 - Requires manual deletion of old file and permissions change\napp := fiber.New(fiber.Config{\n    Network: \"unix\",\n})\n\nos.Remove(\"app.sock\")\napp.Hooks().OnListen(func(fiber.ListenData) error {\n    return os.Chmod(\"app.sock\", 0770)\n})\napp.Listen(\"app.sock\")\n\n// v3 - Fiber does it for you\napp := fiber.New()\napp.Listen(\"app.sock\", fiber.ListenConfig{\n    ListenerNetwork:    fiber.NetworkUnix,\n    UnixSocketFileMode: 0770,\n})\n```\n\n- Added `TLSConfig` to `ListenConfig` so external providers can supply certificates via `GetCertificate`. Prefer `TLSConfig` when configuring TLS; when set, it is cloned and takes precedence over other TLS fields.\n\n```go\napp := fiber.New()\napp.Listen(\":443\", fiber.ListenConfig{\n    TLSConfig: &tls.Config{\n        GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {\n            return myProvider.Certificate(info.ServerName)\n        },\n    },\n})\n```\n\n- Expanded `ListenData` with versioning, handler, process, and PID metadata, plus dedicated startup message hooks for customization. Check out the [Hooks](./api/hooks#startup-message-customization) documentation for further details.\n\n```go title=\"Customize the startup message\"\npackage main\n\nimport (\n    \"fmt\"\n    \"os\"\n\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Hooks().OnPreStartupMessage(func(sm *fiber.PreStartupMessageData) error {\n        sm.BannerHeader = \"FOOBER \" + sm.Version + \"\\n-------\"\n\n        // Optional: you can also remove old entries\n        // sm.ResetEntries()\n\n        sm.AddInfo(\"git-hash\", \"Git hash\", os.Getenv(\"GIT_HASH\"))\n        sm.AddInfo(\"prefork\", \"Prefork\", fmt.Sprintf(\"%v\", sm.Prefork), 15)\n        return nil\n    })\n\n    app.Hooks().OnPostStartupMessage(func(sm *fiber.PostStartupMessageData) error {\n        if !sm.Disabled && !sm.IsChild && !sm.Prevented {\n            fmt.Println(\"startup completed\")\n        }\n        return nil\n    })\n\n    app.Listen(\":5000\")\n}\n```\n\n## 🗺 Router\n\nWe have slightly adapted our router interface\n\n### Handler compatibility\n\nFiber now ships with a routing adapter (see `adapter.go`) that understands native Fiber handlers alongside `net/http` and `fasthttp` handlers. Route registration helpers accept a required `handler` argument plus optional additional `handlers`, all typed as `any`, and the adapter transparently converts supported handler styles so you can keep using the ecosystem functions you're familiar with.\n\nTo align even closer with Express, you can also register handlers that accept the new `fiber.Req` and `fiber.Res` helper interfaces. The adapter understands both two-argument (`func(fiber.Req, fiber.Res)`) and three-argument (`func(fiber.Req, fiber.Res, func() error)`) callbacks, regardless of whether they return an `error`. It also accepts Express-style `next` callbacks that take an `error` (`func(error)` or `func(error) error`). When you include the optional `next` callback, Fiber wires it to `c.Next()` for you so middleware continues to behave as expected. Calling `next(nil)` continues the chain, while passing a non-nil error short-circuits and returns that error. If your handler returns an `error`, the value returned from the injected `next()` bubbles straight back to the caller. When your handler omits an `error` return, Fiber records the result of `next()` and returns it after your function exits so downstream failures still propagate.\n\n| Case | Handler signature | Notes |\n| ---- | ----------------- | ----- |\n| 1 | `fiber.Handler` | Native Fiber handler. |\n| 2 | `func(fiber.Ctx)` | Fiber handler without an error return. |\n| 3 | `func(fiber.Req, fiber.Res) error` | Express-style request handler with error return. |\n| 4 | `func(fiber.Req, fiber.Res)` | Express-style request handler without error return. |\n| 5 | `func(fiber.Req, fiber.Res, func() error) error` | Express-style middleware with an error-returning `next` callback and handler error return. |\n| 6 | `func(fiber.Req, fiber.Res, func() error)` | Express-style middleware with an error-returning `next` callback. |\n| 7 | `func(fiber.Req, fiber.Res, func()) error` | Express-style middleware with a no-argument `next` callback and handler error return. |\n| 8 | `func(fiber.Req, fiber.Res, func())` | Express-style middleware with a no-argument `next` callback. |\n| 9 | `func(fiber.Req, fiber.Res, func(error))` | Express-style middleware with an error-accepting `next` callback. |\n| 10 | `func(fiber.Req, fiber.Res, func(error)) error` | Express-style middleware with an error-accepting `next` callback and handler error return. |\n| 11 | `func(fiber.Req, fiber.Res, func(error) error)` | Express-style middleware with an error-accepting `next` callback that returns an error. |\n| 12 | `func(fiber.Req, fiber.Res, func(error) error) error` | Express-style middleware with an error-accepting `next` callback that returns an error and handler error return. |\n| 13 | `http.HandlerFunc` | Standard-library handler function adapted through `fasthttpadaptor`. |\n| 14 | `http.Handler` | Standard-library handler implementation; pointer receivers must be non-nil. |\n| 15 | `func(http.ResponseWriter, *http.Request)` | Standard-library function handlers via `fasthttpadaptor`. |\n| 16 | `fasthttp.RequestHandler` | Direct fasthttp handler without error return. |\n| 17 | `func(*fasthttp.RequestCtx) error` | fasthttp handler that returns an error to Fiber. |\n\n### Route chaining\n\n`RouteChain` is a new helper inspired by [`Express`](https://expressjs.com/en/api.html#app.route) that makes it easy to declare a stack of handlers on the same path, while the existing `Route` helper stays available for prefix encapsulation.\n\n```go\nRouteChain(path string) Register\n```\n\n<details>\n<summary>Example</summary>\n\n```go\napp.RouteChain(\"/api\").RouteChain(\"/user/:id?\")\n    .Get(func(c fiber.Ctx) error {\n        // Get user\n        return c.JSON(fiber.Map{\"message\": \"Get user\", \"id\": c.Params(\"id\")})\n    })\n    .Post(func(c fiber.Ctx) error {\n        // Create user\n        return c.JSON(fiber.Map{\"message\": \"User created\"})\n    })\n    .Put(func(c fiber.Ctx) error {\n        // Update user\n        return c.JSON(fiber.Map{\"message\": \"User updated\", \"id\": c.Params(\"id\")})\n    })\n    .Delete(func(c fiber.Ctx) error {\n        // Delete user\n        return c.JSON(fiber.Map{\"message\": \"User deleted\", \"id\": c.Params(\"id\")})\n    })\n```\n\n</details>\n\nYou can find more information about `app.RouteChain` and `app.Route` in the API documentation ([RouteChain](./api/app#routechain), [Route](./api/app#route)).\n\n### Automatic HEAD routes for GET\n\nFiber now auto-registers a `HEAD` route whenever you add a `GET` route. The generated handler chain matches the `GET` chain so status codes and headers stay in sync while the response body remains empty, ensuring `HEAD` clients observe the same metadata as a `GET` consumer.\n\n```go title=\"GET now enables HEAD automatically\"\napp := fiber.New()\n\napp.Get(\"/health\", func(c fiber.Ctx) error {\n    c.Set(\"X-Service\", \"api\")\n    return c.SendString(\"OK\")\n})\n\n// HEAD /health reuses the GET middleware chain and returns headers only.\n```\n\nYou can still register explicit `HEAD` handlers for any `GET` route, and they continue to win when you add them:\n\n```go title=\"Override the generated HEAD handler\"\napp.Head(\"/health\", func(c fiber.Ctx) error {\n    return c.SendStatus(fiber.StatusNoContent)\n})\n```\n\nPrefer to manage `HEAD` routes yourself? Disable the feature through `fiber.Config.DisableHeadAutoRegister`:\n\n```go title=\"Disable automatic HEAD registration\"\nhandler := func(c fiber.Ctx) error {\n    c.Set(\"X-Service\", \"api\")\n    return c.SendString(\"OK\")\n}\n\napp := fiber.New(fiber.Config{DisableHeadAutoRegister: true})\napp.Get(\"/health\", handler) // HEAD /health now returns 405 unless you add it manually.\n```\n\nAuto-generated `HEAD` routes appear in tooling such as `app.Stack()` and cover the same routing scenarios as their `GET` counterparts, including groups, mounted apps, dynamic parameters, and static file handlers.\n\n### Middleware registration\n\nWe have aligned our method for middlewares closer to [`Express`](https://expressjs.com/en/api.html#app.use) and now also support the [`Use`](./api/app#use) of multiple prefixes.\n\nPrefix matching is now stricter: partial matches must end at a slash boundary (or be an exact match). This keeps `/api` middleware from running on `/apiv1` while still allowing `/api/:version` style patterns that leverage route parameters, optional segments, or wildcards.\n\nRegistering a subapp is now also possible via the [`Use`](./api/app#use) method instead of the old `app.Mount` method.\n\n<details>\n<summary>Example</summary>\n\n```go\n// register multiple prefixes\napp.Use([]string{\"/v1\", \"/v2\"}, func(c fiber.Ctx) error {\n    // Middleware for /v1 and /v2\n    return c.Next()\n})\n\n// define subapp\napi := fiber.New()\napi.Get(\"/user\", func(c fiber.Ctx) error {\n    return c.SendString(\"User\")\n})\n// register subapp\napp.Use(\"/api\", api)\n```\n\n</details>\n\nTo enable the routing changes above we had to slightly adjust the signature of the `Add` method.\n\n```diff\n-    Add(method, path string, handlers ...Handler) Router\n+    Add(methods []string, path string, handler any, handlers ...any) Router\n```\n\n### Test Config\n\nThe `app.Test()` method now allows users to customize their test configurations:\n\n<details>\n<summary>Example</summary>\n\n```go\n// Create a test app with a handler to test\napp := fiber.New()\napp.Get(\"/\", func(c fiber.Ctx) {\n    return c.SendString(\"hello world\")\n})\n\n// Define the HTTP request and custom TestConfig to test the handler\nreq := httptest.NewRequest(MethodGet, \"/\", nil)\ntestConfig := fiber.TestConfig{\n    Timeout:       0,\n    FailOnTimeout: false,\n}\n\n// Test the handler using the request and testConfig\nresp, err := app.Test(req, testConfig)\n```\n\n</details>\n\nTo provide configurable testing capabilities, we had to change\nthe signature of the `Test` method.\n\n```diff\n-    Test(req *http.Request, timeout ...time.Duration) (*http.Response, error)\n+    Test(req *http.Request, config ...fiber.TestConfig) (*http.Response, error)\n```\n\nThe `TestConfig` struct provides the following configuration options:\n\n- `Timeout`: The duration to wait before timing out the test. Use 0 for no timeout.\n- `FailOnTimeout`: Controls the behavior when a timeout occurs:\n  - When true, the test will return an `os.ErrDeadlineExceeded` if the test exceeds the `Timeout` duration.\n  - When false, the test will return the partial response received before timing out.\n\nIf a custom `TestConfig` isn't provided, then the following will be used:\n\n```go\ntestConfig := fiber.TestConfig{\n    Timeout:       time.Second,\n    FailOnTimeout: true,\n}\n```\n\n**Note:** Using this default is **NOT** the same as providing an empty `TestConfig` as an argument to `app.Test()`.\n\nAn empty `TestConfig` is the equivalent of:\n\n```go\ntestConfig := fiber.TestConfig{\n    Timeout:       0,\n    FailOnTimeout: false,\n}\n```\n\n## 🧠 Context\n\n### New Features\n\n- Cookie now allows Partitioned cookies for [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) support. CHIPS (Cookies Having Independent Partitioned State) is a feature that improves privacy by allowing cookies to be partitioned by top-level site, mitigating cross-site tracking.\n- Cookie automatic security enforcement: When setting a cookie with `SameSite=None`, Fiber automatically sets `Secure=true` as required by RFC 6265bis and modern browsers (Chrome, Firefox, Safari). This ensures compliance with the \"None\" SameSite policy. See [Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#none) and [Chrome docs](https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure) for details.\n- `Ctx` now implements the [context.Context](https://pkg.go.dev/context#Context) interface, replacing the former `UserContext` helpers.\n\n### New Methods\n\n- **AutoFormat**: Similar to Express.js, automatically formats the response based on the request's `Accept` header.\n- **Deadline**: For implementing `context.Context`.\n- **Done**: For implementing `context.Context`.\n- **Err**: For implementing `context.Context`.\n- **Host**: Similar to Express.js, returns the host name of the request.\n- **Port**: Similar to Express.js, returns the port number of the request.\n- **IsProxyTrusted**: Checks the trustworthiness of the remote IP.\n- **Reset**: Resets context fields for server handlers.\n- **Schema**: Similar to Express.js, returns the schema (HTTP or HTTPS) of the request.\n- **SendEarlyHints**: Sends `HTTP 103 Early Hints` status code with `Link` headers so browsers can preload resources while the final response is being prepared.\n- **SendStream**: Similar to Express.js, sends a stream as the response.\n- **SendStreamWriter**: Sends a stream using a writer function.\n- **SendString**: Similar to Express.js, sends a string as the response.\n- **String**: Similar to Express.js, converts a value to a string.\n- **Value**: For implementing `context.Context`. Returns request-scoped value from Locals.\n- **Context()**: Returns a `context.Context` that can be used outside the handler.\n- **SetContext**: Sets the base `context.Context` returned by `Context()` for propagating deadlines or values.\n- **ViewBind**: Binds data to a view, replacing the old `Bind` method.\n- **CBOR**: Introducing [CBOR](https://cbor.io/) binary encoding format for both request & response body. CBOR is a binary data serialization format which is both compact and efficient, making it ideal for use in web applications.\n- **MsgPack**: Introducing [MsgPack](https://msgpack.org/) binary encoding format for both request & response body. MsgPack is a binary serialization format that is more efficient than JSON, making it ideal for high-performance applications.\n- **Drop**: Terminates the client connection silently without sending any HTTP headers or response body. This can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating DDoS attacks or protecting sensitive endpoints from unauthorized access.\n- **End**: Similar to Express.js, immediately flushes the current response and closes the underlying connection.\n- **AcceptsLanguagesExtended**: Matches language ranges using RFC 4647 Extended Filtering with wildcard subtags.\n- **FullURL**: Returns the full request URL (scheme + host + original URL).\n- **RequestID**: Returns the request identifier from the response or request headers.\n- **UserAgent**: Returns the `User-Agent` request header.\n- **Referer**: Returns the `Referer` request header.\n- **AcceptLanguage**: Returns the `Accept-Language` request header.\n- **AcceptEncoding**: Returns the `Accept-Encoding` request header.\n- **HasHeader**: Reports whether the request includes a header with the given key.\n- **MediaType**: Returns the MIME type from the `Content-Type` header without parameters.\n- **Charset**: Returns the `charset` parameter from the `Content-Type` header.\n- **IsJSON**: Reports whether the `Content-Type` header is JSON.\n- **IsForm**: Reports whether the `Content-Type` header is form-encoded.\n- **IsMultipart**: Reports whether the `Content-Type` header is multipart form data.\n- **AcceptsJSON**: Reports whether the `Accept` header allows JSON.\n- **AcceptsHTML**: Reports whether the `Accept` header allows HTML.\n- **AcceptsXML**: Reports whether the `Accept` header allows XML.\n- **AcceptsEventStream**: Reports whether the `Accept` header allows `text/event-stream`.\n- **Matched**: Detects when the current request path matched a registered route.\n- **IsMiddleware**: Indicates if the current handler was registered as middleware.\n- **HasBody**: Quickly checks whether the request includes a body.\n- **OverrideParam**: Overwrites the value of an existing route parameter, or does nothing if the parameter does not exist\n- **IsWebSocket**: Reports if the request attempts a WebSocket upgrade.\n- **IsPreflight**: Identifies CORS preflight requests before handlers run.\n\n### Removed Methods\n\n- **AllParams**: Use `c.Bind().URI()` instead.\n- **ParamsInt**: Use `Params` with generic types.\n- **QueryBool**: Use `Query` with generic types.\n- **QueryFloat**: Use `Query` with generic types.\n- **QueryInt**: Use `Query` with generic types.\n- **BodyParser**: Use `c.Bind().Body()` instead.\n- **CookieParser**: Use `c.Bind().Cookie()` instead.\n- **ParamsParser**: Use `c.Bind().URI()` instead.\n- **RedirectToRoute**: Use `c.Redirect().Route()` instead.\n- **RedirectBack**: Use `c.Redirect().Back()` instead.\n- **ReqHeaderParser**: Use `c.Bind().Header()` instead.\n- **UserContext**: Removed. `Ctx` itself now satisfies `context.Context`; pass `c` directly where a `context.Context` is required.\n- **SetUserContext**: Removed. Use `SetContext` and `Context()` or `context.WithValue` on `c` to store additional request-scoped values.\n\n### Changed Methods\n\n- **Bind**: Now used for binding instead of view binding. Use `c.ViewBind()` for view binding.\n- **Format**: Parameter changed from `body interface{}` to `handlers ...ResFmt`.\n- **Redirect**: Use `c.Redirect().To()` instead.\n- **SendFile**: Now supports different configurations using a config parameter.\n- **Attachment and Download**: Non-ASCII filenames now use `filename*` as\n  specified by [RFC 6266](https://www.rfc-editor.org/rfc/rfc6266) and\n  [RFC 8187](https://www.rfc-editor.org/rfc/rfc8187).\n- **Context()**: Renamed to `RequestCtx()` to access the underlying `fasthttp.RequestCtx`.\n\n### SendEarlyHints\n\n`SendEarlyHints` sends an informational [`103 Early Hints`](https://developer.chrome.com/docs/web-platform/early-hints) response with `Link` headers based on the provided `hints` argument. This allows a browser to start preloading assets while the server is still preparing the final response.\n\n```go\nhints := []string{\"<https://cdn.com/app.js>; rel=preload; as=script\"}\napp.Get(\"/early\", func(c fiber.Ctx) error {\n    if err := c.SendEarlyHints(hints); err != nil {\n        return err\n    }\n    return c.SendString(\"done\")\n})\n```\n\nOlder HTTP/1.1 clients may ignore these interim responses or handle them inconsistently.\n\n### SendStreamWriter\n\nIn v3, we introduced support for buffered streaming with the addition of the `SendStreamWriter` method:\n\n```go\nfunc (c Ctx) SendStreamWriter(streamWriter func(w *bufio.Writer)) error\n```\n\nWith this new method, you can implement:\n\n- Server-Side Events (SSE)\n- Large file downloads\n- Live data streaming\n\n```go\napp.Get(\"/sse\", func(c fiber.Ctx) error {\n    c.Set(\"Content-Type\", \"text/event-stream\")\n    c.Set(\"Cache-Control\", \"no-cache\")\n    c.Set(\"Connection\", \"keep-alive\")\n    c.Set(\"Transfer-Encoding\", \"chunked\")\n\n    return c.SendStreamWriter(func(w *bufio.Writer) {\n        for {\n            fmt.Fprintf(w, \"event: my-event\\n\")\n            fmt.Fprintf(w, \"data: Hello SSE\\n\\n\")\n\n            if err := w.Flush(); err != nil {\n                log.Print(\"Client disconnected!\")\n                return\n            }\n        }\n    })\n})\n```\n\nYou can find more details about this feature in [/docs/api/ctx.md](./api/ctx.md).\n\n### Drop\n\nIn v3, we introduced support to silently terminate requests through `Drop`.\n\n```go\nfunc (c Ctx) Drop() error\n```\n\nWith this method, you can:\n\n- Block certain requests without notifying the client to mitigate DDoS attacks\n- Protect sensitive endpoints from unauthorized access without leaking errors.\n\n:::caution\nWhile this feature adds the ability to drop connections, it is still **highly recommended** to use additional\nmeasures (such as **firewalls**, **proxies**, etc.) to further protect your server endpoints by blocking\nmalicious connections before the server establishes a connection.\n:::\n\n```go\napp.Get(\"/\", func(c fiber.Ctx) error {\n    if c.IP() == \"192.168.1.1\" {\n        return c.Drop()\n    }\n\n    return c.SendString(\"Hello World!\")\n})\n```\n\nYou can find more details about this feature in [/docs/api/ctx.md](./api/ctx.md).\n\n### End\n\nIn v3, we introduced a new method to match the Express.js API's `res.end()` method.\n\n```go\nfunc (c Ctx) End() error\n```\n\nWith this method, you can:\n\n- Stop middleware from controlling the connection after a handler further up the method chain\n  by immediately flushing the current response and closing the connection.\n- Use `return c.End()` as an alternative to `return nil`\n\n```go\napp.Use(func (c fiber.Ctx) error {\n    err := c.Next()\n    if err != nil {\n        log.Println(\"Got error: %v\", err)\n        return c.SendString(err.Error()) // Will be unsuccessful since the response ended below\n    }\n    return nil\n})\n\napp.Get(\"/hello\", func (c fiber.Ctx) error {\n    query := c.Query(\"name\", \"\")\n    if query == \"\" {\n        _ = c.SendString(\"You don't have a name?\")\n        _ = c.End() // Closes the underlying connection; errors intentionally ignored\n        return errors.New(\"No name provided\")\n    }\n    return c.SendString(\"Hello, \" + query + \"!\")\n})\n```\n\n---\n\n## 📎 Binding\n\nFiber v3 introduces a new binding mechanism that simplifies the process of binding request data to structs. The new binding system supports binding from various sources such as URL parameters, query parameters, headers, and request bodies. This unified approach makes it easier to handle different types of request data in a consistent manner.\n\n### New Features\n\n- Unified binding from URL parameters, query parameters, headers, and request bodies.\n- Support for custom binders and constraints.\n- Improved error handling and validation.\n- Support multipart file binding for `*multipart.FileHeader`, `*[]*multipart.FileHeader`, and `[]*multipart.FileHeader` field types.\n- Support for unified binding (`Bind().All()`) with defined precedence order: (URI -> Body -> Query -> Headers -> Cookies). [Learn more](./api/bind.md#all).\n- Support MsgPack binding for request body.\n\n<details>\n<summary>Example</summary>\n\n```go\ntype User struct {\n    ID    int    `uri:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\napp.Post(\"/user/:id\", func(c fiber.Ctx) error {\n    var user User\n    if err := c.Bind().Body(&user); err != nil {\n        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n    }\n    return c.JSON(user)\n})\n```\n\nIn this example, the `Bind` method is used to bind the request body to the `User` struct. The `Body` method of the `Bind` class performs the actual binding.\n\n</details>\n\n## 🔬 Extractors Package\n\nFiber v3 introduces a new shared `extractors` package that consolidates value extraction utilities previously duplicated across middleware packages. This package provides a unified API for extracting values from headers, cookies, query parameters, form data, and URL parameters with built-in chain/fallback logic and security considerations.\n\n### Key Features\n\n- **Unified API**: Single package for extracting values from headers, cookies, query parameters, form data, and URL parameters\n- **Chain Logic**: Built-in fallback mechanism to try multiple extraction sources in order\n- **Source Awareness**: Source inspection capabilities for security-sensitive operations\n- **Type Safety**: Strongly typed extraction with proper error handling\n- **Performance**: Optimized extraction functions with minimal overhead\n\n### Available Extractors\n\n- `FromAuthHeader(authScheme string)`: Extract from Authorization header with scheme support\n- `FromCookie(key string)`: Extract from HTTP cookies\n- `FromParam(param string)`: Extract from URL path parameters\n- `FromForm(param string)`: Extract from form data\n- `FromHeader(header string)`: Extract from custom HTTP headers\n- `FromQuery(param string)`: Extract from URL query parameters\n- `FromCustom(key string, extractor func(c fiber.Ctx) (string, error))`: Define custom extraction logic with metadata\n- `Chain(extractors ...Extractor)`: Chain multiple extractors with fallback logic\n\n### Usage Example\n\n```go\nimport \"github.com/gofiber/fiber/v3/extractors\"\n\n// Extract API key from multiple sources with fallback\napiKeyExtractor := extractors.Chain(\n    extractors.FromHeader(\"X-API-Key\"),\n    extractors.FromQuery(\"api_key\"),\n    extractors.FromCookie(\"api_key\"),\n)\n\napp.Use(func(c fiber.Ctx) error {\n    apiKey, err := apiKeyExtractor.Extract(c)\n    if err != nil {\n        return c.Status(401).SendString(\"API key required\")\n    }\n    // Use apiKey for authentication\n    return c.Next()\n})\n```\n\n### Migration from Middleware-Specific Extractors\n\nMiddleware packages in Fiber v3 now use the shared extractors package instead of maintaining their own extraction logic. This provides:\n\n- **Code Deduplication**: Eliminates ~500+ lines of duplicated extraction code\n- **Consistency**: Standardized extraction behavior across all middleware\n- **Maintainability**: Single source of truth for extraction logic\n- **Security**: Unified security considerations and warnings\n\n## 🔄 Redirect\n\nFiber v3 enhances the redirect functionality by introducing new methods and improving existing ones. The new redirect methods provide more flexibility and control over the redirection process.\n\n### New Methods\n\n- `Redirect().To()`: Redirects to a specific URL.\n- `Redirect().Route()`: Redirects to a named route.\n- `Redirect().Back()`: Redirects to the previous URL.\n\n<details>\n<summary>Example</summary>\n\n```go\napp.Get(\"/old\", func(c fiber.Ctx) error {\n    return c.Redirect().To(\"/new\")\n})\n\napp.Get(\"/new\", func(c fiber.Ctx) error {\n    return c.SendString(\"Welcome to the new route!\")\n})\n```\n\n</details>\n\n### Changed behavior\n\n:::info\n\nThe default redirect status code has been updated from `302 Found` to `303 See Other` to ensure more consistent behavior across different browsers.\n\n:::\n\n## 🌎 Client package\n\nThe Gofiber client has been completely rebuilt. It includes numerous new features such as Cookiejar, request/response hooks, and more.\nYou can take a look to [client docs](./client/rest.md) to see what's new with the client.\n\n### Configuration improvements\n\nThe v3 client centralizes common configuration on the client instance and lets you override it per request with `client.Config`.\nYou can define base URLs, defaults (headers, cookies, path parameters, timeouts), and toggle path normalization once, while still\nusing axios-style helpers for each call.\n\n```go\ncc := client.New().\n    SetBaseURL(\"https://api.service.local\").\n    AddHeader(\"Authorization\", \"Bearer <token>\").\n    SetTimeout(5 * time.Second).\n    SetPathParam(\"tenant\", \"acme\")\n\nresp, err := cc.Get(\"/users/:tenant/:id\", client.Config{\n    PathParam:              map[string]string{\"id\": \"42\"},\n    Param:                  map[string]string{\"include\": \"profile\"},\n    DisablePathNormalizing: true,\n})\nif err != nil {\n    panic(err)\n}\ndefer resp.Close()\nfmt.Println(resp.StatusCode(), resp.String())\n```\n\n### Fasthttp transport integration\n\n- `client.NewWithHostClient` and `client.NewWithLBClient` allow you to plug existing `fasthttp` clients directly into Fiber while keeping retries, redirects, and hook logic consistent.\n- Dialer, TLS, and proxy helpers now update every host client inside a load balancer, so complex pools inherit the same configuration.\n- The Fiber client exposes `Do`, `DoTimeout`, `DoDeadline`, and `CloseIdleConnections`, matching the surface area of the wrapped fasthttp transports.\n\n## 🧰 Generic functions\n\nFiber v3 introduces new generic functions that provide additional utility and flexibility for developers. These functions are designed to simplify common tasks and improve code readability.\n\n### New Generic Functions\n\n- **StoreInContext**: Stores request-scoped values in both `c.Locals()` and the request `context.Context`, so the same value can be read through middleware `FromContext` helpers and direct locals access.\n- **Convert**: Converts a value with a specified converter function and default value.\n- **Locals**: Retrieves or sets local values within a request context.\n- **Params**: Retrieves route parameters and can handle various types of route parameters.\n- **Query**: Retrieves the value of a query parameter from the request URI and can handle various types of query parameters.\n- **GetReqHeader**: Returns the HTTP request header specified by the field and can handle various types of header values.\n\n`fiber.Config.PassLocalsToContext` is now available to control whether `StoreInContext` also synchronizes values with request `context.Context` for Fiber-backed contexts. The default is `false` for backward compatibility. `ValueFromContext` continues reading Fiber-backed values from `c.Locals()`.\n\n### Example\n\n<details>\n<summary>Convert</summary>\n\n```go\npackage main\n\nimport (\n    \"strconv\"\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/convert\", func(c fiber.Ctx) error {\n        value, err := fiber.Convert[int](c.Query(\"value\"), strconv.Atoi, 0)\n        if err != nil {\n            return c.Status(fiber.StatusBadRequest).SendString(err.Error())\n        }\n        return c.JSON(value)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```sh\ncurl \"http://localhost:3000/convert?value=123\"\n# Output: 123\n\ncurl \"http://localhost:3000/convert?value=abc\"\n# Output: \"failed to convert: strconv.Atoi: parsing \\\"abc\\\": invalid syntax\"\n```\n\n</details>\n\n<details>\n<summary>Locals</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(\"/user/:id\", func(c fiber.Ctx) error {\n        // ask database for user\n        // ...\n        // set local values from database\n        fiber.Locals[string](c, \"user\", \"john\")\n        fiber.Locals[int](c, \"age\", 25)\n        // ...\n\n        return c.Next()\n    })\n\n    app.Get(\"/user/*\", func(c fiber.Ctx) error {\n        // get local values\n        name := fiber.Locals[string](c, \"user\")\n        age := fiber.Locals[int](c, \"age\")\n        // ...\n        return c.JSON(fiber.Map{\"name\": name, \"age\": age})\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```sh\ncurl \"http://localhost:3000/user/5\"\n# Output: {\"name\":\"john\",\"age\":25}\n```\n\n</details>\n\n<details>\n<summary>Params</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/params/:id\", func(c fiber.Ctx) error {\n        id := fiber.Params[int](c, \"id\", 0)\n        return c.JSON(id)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```sh\ncurl \"http://localhost:3000/params/123\"\n# Output: 123\n\ncurl \"http://localhost:3000/params/abc\"\n# Output: 0\n```\n\n</details>\n\n<details>\n<summary>Query</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/query\", func(c fiber.Ctx) error {\n        age := fiber.Query[int](c, \"age\", 0)\n        return c.JSON(age)\n    })\n\n    app.Listen(\":3000\")\n}\n\n```\n\n```sh\ncurl \"http://localhost:3000/query?age=25\"\n# Output: 25\n\ncurl \"http://localhost:3000/query?age=abc\"\n# Output: 0\n```\n\n</details>\n\n<details>\n<summary>GetReqHeader</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/header\", func(c fiber.Ctx) error {\n        userAgent := fiber.GetReqHeader[string](c, \"User-Agent\", \"Unknown\")\n        return c.JSON(userAgent)\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n```sh\ncurl -H \"User-Agent: CustomAgent\" \"http://localhost:3000/header\"\n# Output: \"CustomAgent\"\n\ncurl \"http://localhost:3000/header\"\n# Output: \"Unknown\"\n```\n\n</details>\n\n## 🛠️ Utils {#utils}\n\nFiber v3 removes the built-in `utils` directory and now imports utility helpers from the separate [`github.com/gofiber/utils/v2`](https://github.com/gofiber/utils) module. See the [migration guide](#utils-migration) for detailed replacement steps and examples.\n\nThe `github.com/gofiber/utils` module also introduces new helpers like `ParseInt`, `ParseUint`, `Walk`, `ReadFile`, and `Timestamp`.\n\n## 🧩 Services\n\nFiber v3 introduces a new feature called Services. This feature allows developers to quickly start services that the application depends on, removing the need to manually provision things like database servers, caches, or message brokers, to name a few.\n\n### Example\n\n<details>\n<summary>Adding a service</summary>\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"github.com/gofiber/fiber/v3\"\n)\n\ntype myService struct {\n    img string\n    // ...\n}\n\n// Start initializes and starts the service. It implements the [fiber.Service] interface.\nfunc (s *myService) Start(ctx context.Context) error {\n    // start the service\n    return nil\n}\n\n// String returns a string representation of the service.\n// It is used to print a human-readable name of the service in the startup message.\n// It implements the [fiber.Service] interface.\nfunc (s *myService) String() string {\n    return s.img\n}\n\n// State returns the current state of the service.\n// It implements the [fiber.Service] interface.\nfunc (s *myService) State(ctx context.Context) (string, error) {\n    return \"running\", nil\n}\n\n// Terminate stops and removes the service. It implements the [fiber.Service] interface.\nfunc (s *myService) Terminate(ctx context.Context) error {\n    // stop the service\n    return nil\n}\n\nfunc main() {\n    cfg := &fiber.Config{}\n\n    cfg.Services = append(cfg.Services, &myService{img: \"postgres:latest\"})\n    cfg.Services = append(cfg.Services, &myService{img: \"redis:latest\"})\n\n    app := fiber.New(*cfg)\n\n    // ...\n}\n```\n\n</details>\n\n<details>\n<summary>Output</summary>\n\n```sh\n$ go run . -v\n\n    _______ __\n   / ____(_) /_  ___  _____\n  / /_  / / __ \\/ _ \\/ ___/\n / __/ / / /_/ /  __/ /\n/_/   /_/_.___/\\___/_/          v3.0.0\n--------------------------------------------------\nINFO Server started on:         http://127.0.0.1:3000 (bound on host 0.0.0.0 and port 3000)\nINFO Services:     2\nINFO   🧩 [ RUNNING ] postgres:latest\nINFO   🧩 [ RUNNING ] redis:latest\nINFO Total handlers count:      2\nINFO Prefork:                   Disabled\nINFO PID:                       12279\nINFO Total process count:       1\n```\n\n</details>\n\n## 📃 Log\n\n`fiber.AllLogger[T]` interface now has a new generic type parameter `T` and a method called `Logger`. This method can be used to get the underlying logger instance from the Fiber logger middleware. This is useful when you want to configure the logger middleware with a custom logger and still want to access the underlying logger instance with the appropriate type.\n\nYou can find more details about this feature in [/docs/api/log.md](./api/log.md#logger).\n\n`logger.Config` now supports a new field called `ForceColors`. This field allows you to force the logger to always use colors, even if the output is not a terminal. This is useful when you want to ensure that the logs are always colored, regardless of the output destination.\n\n```go\npackage main\n\nimport \"github.com/gofiber/fiber/v3/middleware/logger\"\n\napp.Use(logger.New(logger.Config{\n    ForceColors: true,\n}))\n```\n\n## 📦 Storage Interface\n\nThe storage interface has been updated to include new subset of methods with `WithContext` suffix. These methods allow you to pass a context to the storage operations, enabling better control over timeouts and cancellation if needed. This is particularly useful when storage implementations used outside of the Fiber core, such as in background jobs or long-running tasks.\n\n**New Methods Signatures:**\n\n```go\n// GetWithContext gets the value for the given key with a context.\n// `nil, nil` is returned when the key does not exist\nGetWithContext(ctx context.Context, key string) ([]byte, error)\n\n// SetWithContext stores the given value for the given key\n// with an expiration value, 0 means no expiration.\n// Empty key or value will be ignored without an error.\nSetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error\n\n// DeleteWithContext deletes the value for the given key with a context.\n// It returns no error if the storage does not contain the key,\nDeleteWithContext(ctx context.Context, key string) error\n\n// ResetWithContext resets the storage and deletes all keys with a context.\nResetWithContext(ctx context.Context) error\n```\n\n## 🧬 Middlewares\n\n### Important Change for Accessing Middleware Data\n\nIn Fiber v3, many middlewares that previously set values in `c.Locals()` using string keys (e.g., `c.Locals(\"requestid\")`) have been updated. To align with Go's context best practices and prevent key collisions, these middlewares now store their specific data in the request's context using unexported keys of custom types.\n\nThis means that directly accessing these values via `c.Locals(\"some_string_key\")` will no longer work for such middleware-provided data.\n\n**How to Access Middleware Data in v3:**\n\nEach affected middleware now provides dedicated exported functions to retrieve its specific data from the context. You should use these functions instead of relying on string-based lookups in `c.Locals()`.\n\nExamples include:\n\n- `requestid.FromContext(c)`\n- `csrf.TokenFromContext(c)`\n- `csrf.HandlerFromContext(c)`\n- `session.FromContext(c)`\n- `basicauth.UsernameFromContext(c)`\n- `keyauth.TokenFromContext(c)`\n\nWhen used with the Logger middleware, the recommended approach is to use the `CustomTags` feature of the logger, which allows you to call these specific `FromContext` functions. See the [Logger](#logger) section for more details.\n\n### Adaptor\n\nThe 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.\n\nIncoming body sizes now respect the Fiber app's configured `BodyLimit` (falling back to the default when unset) when running Fiber from `net/http` through the adaptor, returning `413 Request Entity Too Large` for oversized payloads.\n\n| Payload Size | Metric         | V2           | V3          | Percent Change |\n| ------------ | -------------- | ------------ | ----------- | -------------- |\n| 100KB        | Execution Time | 1056 ns/op   | 588.6 ns/op | -44.25%        |\n|              | Memory Usage   | 2644 B/op    | 254 B/op    | -90.39%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 500KB        | Execution Time | 1061 ns/op   | 562.9 ns/op | -46.94%        |\n|              | Memory Usage   | 2644 B/op    | 248 B/op    | -90.62%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 1MB          | Execution Time | 1080 ns/op   | 629.7 ns/op | -41.68%        |\n|              | Memory Usage   | 2646 B/op    | 267 B/op    | -89.91%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 5MB          | Execution Time | 1093 ns/op   | 540.3 ns/op | -50.58%        |\n|              | Memory Usage   | 2654 B/op    | 254 B/op    | -90.43%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 10MB         | Execution Time | 1044 ns/op   | 533.1 ns/op | -48.94%        |\n|              | Memory Usage   | 2665 B/op    | 258 B/op    | -90.32%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 25MB         | Execution Time | 1069 ns/op   | 540.7 ns/op | -49.42%        |\n|              | Memory Usage   | 2706 B/op    | 289 B/op    | -89.32%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n| 50MB         | Execution Time | 1137 ns/op   | 554.6 ns/op | -51.21%        |\n|              | Memory Usage   | 2734 B/op    | 298 B/op    | -89.10%        |\n|              | Allocations    | 16 allocs/op | 5 allocs/op | -68.75%        |\n\n### BasicAuth\n\nThe BasicAuth middleware now validates the `Authorization` header more rigorously and sets security-focused response headers. Passwords must be provided in **hashed** form (e.g. SHA-256 or bcrypt) rather than plaintext. The default challenge includes the `charset=\"UTF-8\"` parameter and disables caching. Responses also set a `Vary: Authorization` header to prevent caching based on credentials. Passwords are no longer stored in the request context. A `Charset` option controls the value used in the challenge header.\nA new `HeaderLimit` option restricts the maximum length of the `Authorization` header (default: `8192` bytes).\nThe `Authorizer` function now receives the current `fiber.Ctx` as a third argument, allowing credential checks to incorporate request context.\n\n### Cache\n\nWe 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 custom conditions for invalidating cache entries.\n\nThe middleware now emits `Cache-Control` headers by default via the new `DisableCacheControl` flag, increases the default `Expiration` from `1 minute` to `5 minutes`, and applies a new `MaxBytes` limit of `1 MB` (previously unlimited).\n\nAdditionally, the caching middleware has been optimized to avoid caching non-cacheable status codes, as defined by the [HTTP standards](https://datatracker.ietf.org/doc/html/rfc7231#section-6.1). This improvement enhances cache accuracy and reduces unnecessary cache storage usage.\nCached responses now include an RFC-compliant Age header, providing a standardized indication of how long a response has been stored in cache since it was originally generated. This enhancement improves HTTP compliance and facilitates better client-side caching strategies.\n\nCache keys are now redacted in logs and error messages by default, and a `DisableValueRedaction` boolean (default `false`) lets you opt out when you need the raw value for troubleshooting.\n\n:::note\nThe deprecated `Store` and `Key` options have been removed in v3. Use `Storage` and `KeyGenerator` instead.\n:::\n\n### ResponseTime\n\nA new response time middleware measures how long each request takes to process and adds the duration to the response headers.\nBy default it writes the elapsed time to `X-Response-Time`, and you can change the header name. A `Next` hook lets you skip\nendpoints such as health checks.\n\n### CORS\n\nWe've made some changes to the CORS middleware to improve its functionality and flexibility. Here's what's new:\n\n#### New Struct Fields\n\n- `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)](https://wicg.io/). 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.\n\n#### Updated Struct Fields\n\nWe'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:\n\n- `Config.AllowOrigins`: Now accepts a slice of strings, each representing an allowed origin.\n- `Config.AllowMethods`: Now accepts a slice of strings, each representing an allowed method.\n- `Config.AllowHeaders`: Now accepts a slice of strings, each representing an allowed header.\n- `Config.ExposeHeaders`: Now accepts a slice of strings, each representing an exposed header.\n\nAdditionally, panic messages and logs redact misconfigured origins by default, and a `DisableValueRedaction` flag (default `false`) lets you reveal them when necessary.\n\n### Compression\n\n- Added support for `zstd` compression alongside `gzip`, `deflate`, and `brotli`.\n- Strong `ETag` values are now recomputed for compressed payloads so validators remain accurate.\n- Compression is bypassed for responses that already specify `Content-Encoding`, for range requests or `206` statuses, and when either side sends `Cache-Control: no-transform`.\n- `HEAD` requests still negotiate compression so `Content-Encoding`, `Content-Length`, `ETag`, and `Vary` match a corresponding `GET`, but the body is omitted.\n- `Vary: Accept-Encoding` is merged into responses even when compression is skipped, preventing caches from mixing encoded and unencoded variants.\n\n### CSRF\n\nThe `Expiration` field in the CSRF middleware configuration has been renamed to `IdleTimeout` to better describe its functionality. Additionally, the default value has been reduced from 1 hour to 30 minutes.\n\nCSRF now redacts tokens and storage keys by default and exposes a `DisableValueRedaction` toggle (default `false`) if you must surface those values in diagnostics.\n\nThe CSRF middleware now validates the [`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site) header for unsafe HTTP methods. When present, requests with invalid `Sec-Fetch-Site` values (not one of \"same-origin\", \"none\", \"same-site\", or \"cross-site\") are rejected with `ErrFetchSiteInvalid`. Valid or absent headers proceed to standard origin and token validation checks, providing an early gate to catch malformed requests while maintaining compatibility with legitimate cross-site traffic.\n\n### Idempotency\n\nIdempotency middleware now redacts keys by default and offers a `DisableValueRedaction` configuration flag (default `false`) to expose them when debugging.\n\n### EncryptCookie\n\n- Added support for specifying key length when using `encryptcookie.GenerateKey(length)`. Keys must be base64-encoded and may be 16, 24, or 32 bytes when decoded, supporting AES-128, AES-192, and AES-256 (default).\n- Custom encryptor and decryptor callbacks now receive the cookie name. The default AES-GCM helpers bind it as additional authenticated data (AAD) so ciphertext cannot be replayed under a different cookie.\n- **Breaking change:** Custom encryptor/decryptor hooks now accept the cookie name as their first argument. Update overrides like:\n\n  ```go\n  // Before\n  Encryptor func(value, key string) (string, error)\n  Decryptor func(value, key string) (string, error)\n\n  // After\n  Encryptor func(name, value, key string) (string, error)\n  Decryptor func(name, value, key string) (string, error)\n  ```\n\n### Favicon\n\nThe favicon middleware now caps cached favicon assets with a configurable `MaxBytes` limit (default `1 MiB`) and uses a limited reader to guard against oversized files when loading from disk.\n\n### EnvVar\n\nThe `ExcludeVars` field has been removed from the EnvVar middleware configuration. When upgrading, remove any references to this field and explicitly list the variables you wish to expose using `ExportVars`.\n\n### Filesystem\n\nThe filesystem middleware was removed to reduce confusion with the static middleware.\nThe static middleware now covers the functionality of both. Review the [static middleware](./middleware/static.md) docs or the [migration guide](#-migration-guide) for the updated usage.\n\n### Healthcheck\n\nThe healthcheck middleware has been simplified into a single generic probe handler. No endpoints are registered automatically. Register the middleware on each route you need—using helpers like `healthcheck.LivenessEndpoint`, `healthcheck.ReadinessEndpoint`, or `healthcheck.StartupEndpoint`—and optionally supply a `Probe` function to determine the service's health. This approach lets you expose any number of health check routes.\n\nRefer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes.\n\n### KeyAuth\n\nThe keyauth middleware was updated to introduce a configurable `Realm` field for the `WWW-Authenticate` header.\nThe old string-based `KeyLookup` configuration has been replaced with an `Extractor` field. Use helper functions like `keyauth.FromHeader`, `keyauth.FromAuthHeader`, or `keyauth.FromCookie` to define where the key should be retrieved from. Multiple sources can be combined with `keyauth.Chain`. See the migration guide below.\nNew `Challenge`, `Error`, `ErrorDescription`, `ErrorURI`, and `Scope` fields allow customizing the `WWW-Authenticate` header, returning Bearer error details, and specifying required scopes. `ErrorURI` values are validated as absolute, a default `ApiKey` challenge is emitted when using non-Authorization extractors, Bearer `error` values are validated, credentials must conform to RFC 7235 `token68` syntax, and `scope` values are checked against RFC 6750's `scope-token` format. The header is also emitted only after the status code is finalized.\n\n### Logger\n\nNew helper function called `LoggerToWriter` has been added to the logger middleware. This function allows you to use 3rd party loggers such as `logrus` or `zap` with the Fiber logger middleware without an extra adapter. For example, you can use `zap` with Fiber logger middleware like this:\n\nLogger configuration now uses `Stream` instead of `Output` for the destination writer, so update your logger middleware configuration when migrating to v3.\n\nCustom logger integrations should update any `LoggerFunc` implementations to the new signature that receives a pointer to the middleware config: `func(c fiber.Ctx, data *logger.Data, cfg *logger.Config) error`.\n\n<details>\n<summary>Example</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/contrib/fiberzap/v2\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/log\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n)\n\nfunc main() {\n    // Create a new Fiber instance\n    app := fiber.New()\n\n    // Create a new zap logger which is compatible with Fiber AllLogger interface\n    zap := fiberzap.NewLogger(fiberzap.LoggerConfig{\n        ExtraKeys: []string{\"request_id\"},\n    })\n\n    // Use the logger middleware with zerolog logger\n    app.Use(logger.New(logger.Config{\n        Stream: logger.LoggerToWriter(zap, log.LevelDebug),\n    }))\n\n    // Define a route\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    // Start server on http://localhost:3000\n    app.Listen(\":3000\")\n}\n```\n\n</details>\n\n:::note\nThe deprecated `TagHeader` constant was removed. Use `TagReqHeader` when you need to log request headers.\n:::\n\n#### Logging Middleware Values (e.g., Request ID)\n\nIn Fiber v3, middleware (like `requestid`) now stores values in the request context using unexported keys of custom types. This aligns with Go's context best practices to prevent key collisions between packages.\n\nAs a result, directly accessing these values using string keys with `c.Locals(\"your_key\")` or in the logger format string with `${locals:your_key}` (e.g., `${locals:requestid}`) will no longer work for values set by such middleware.\n\n**Recommended Solution: `CustomTags`**\n\nThe cleanest and most maintainable way to include these middleware-specific values in your logs is by using the `CustomTags` option in the logger middleware configuration. This allows you to define a custom function to retrieve the value correctly from the context.\n\n<details>\n<summary>Example: Logging Request ID with CustomTags</summary>\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/middleware/logger\"\n    \"github.com/gofiber/fiber/v3/middleware/requestid\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Ensure requestid middleware is used before the logger\n    app.Use(requestid.New())\n\n    app.Use(logger.New(logger.Config{\n        CustomTags: map[string]logger.LogFunc{\n            \"requestid\": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {\n                // Retrieve the request ID using the middleware's specific function\n                return output.WriteString(requestid.FromContext(c))\n            },\n        },\n        // Use the custom tag in your format string\n        Format: \"[${time}] ${ip} - ${requestid} - ${status} ${method} ${path}\\n\",\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n</details>\n\n**Alternative: Manually Copying to `Locals`**\n\nIf you have existing logging patterns that rely on `c.Locals` or prefer to manage these values in `Locals` for other reasons, you can manually copy the value from the context to `c.Locals` in a preceding middleware:\n\n<details>\n<summary>Example: Manually setting requestid in Locals</summary>\n\n```go\napp.Use(requestid.New()) // Request ID middleware\napp.Use(func(c fiber.Ctx) error {\n    // Manually copy the request ID to Locals\n    c.Locals(\"requestid\", requestid.FromContext(c))\n    return c.Next()\n})\napp.Use(logger.New(logger.Config{\n    // Now ${locals:requestid} can be used, but CustomTags is generally preferred\n    Format: \"[${time}] ${ip} - ${locals:requestid} - ${status} ${method} ${path}\\n\",\n}))\n```\n\n</details>\n\nBoth approaches ensure your logger can access these values while respecting Go's context practices.\n\nThe `Skip` is a function to determine if logging is skipped or written to `Stream`.\n\n<details>\n<summary>Example Usage</summary>\n\n```go\napp.Use(logger.New(logger.Config{\n    Skip: func(c fiber.Ctx) bool {\n        // Skip logging HTTP 200 requests\n        return c.Response().StatusCode() == fiber.StatusOK\n    },\n}))\n```\n\n```go\napp.Use(logger.New(logger.Config{\n    Skip: func(c fiber.Ctx) bool {\n        // Only log errors, similar to an error.log\n        return c.Response().StatusCode() < 400\n    },\n}))\n```\n\n</details>\n\n#### Predefined Formats\n\nLogger provides predefined formats that you can use by name or directly by specifying the format string.\n<details>\n\n<summary>Example Usage</summary>\n\n```go\napp.Use(logger.New(logger.Config{\n    Format: logger.FormatCombined,\n}))\n```\n\nSee more in [Logger](./middleware/logger.md#predefined-formats)\n</details>\n\n### Limiter\n\nThe limiter middleware uses a new Fixed Window Rate Limiter implementation.\n\nCustom limiter algorithms should now implement the updated `limiter.Handler` interface, whose `New` method receives a pointer to the active config: `New(cfg *limiter.Config) fiber.Handler`.\n\nLimiter now redacts request keys in error paths by default. A new `DisableValueRedaction` boolean (default `false`) lets you reveal the raw limiter key if diagnostics require it.\n\n:::note\nDeprecated fields `Duration`, `Store`, and `Key` have been removed in v3. Use `Expiration`, `Storage`, and `KeyGenerator` instead.\n:::\n\n### Monitor\n\nMonitor middleware is migrated to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor) with [PR #1172](https://github.com/gofiber/contrib/pull/1172).\n\n### Proxy\n\nThe proxy middleware has been updated to improve consistency with Go naming conventions. The `TlsConfig` field in the configuration struct has been renamed to `TLSConfig`. Additionally, the `WithTlsConfig` method has been removed; you should now configure TLS directly via the `TLSConfig` property within the `Config` struct.\n\nThe new `KeepConnectionHeader` option (default `false`) drops the `Connection` header unless explicitly enabled to retain it.\n\n`proxy.Balancer` now accepts an optional variadic configuration: call `proxy.Balancer()` to use defaults or continue passing a `proxy.Config` value as before.\n\n### Recover\n\nThe Recover middleware allows customizing the error it returns. Set a `PanicHandler` in its `Config` to change the default behavior.\n\n### Session\n\nThe Session middleware has undergone key changes in v3 to improve functionality and flexibility. While v2 methods remain available for backward compatibility, we now recommend using the new middleware handler for session management.\n\n#### Key Updates\n\nThe session middleware has undergone significant improvements in v3, focusing on type safety, flexibility, and better developer experience.\n\n#### Key Changes\n\n- **Extractor Pattern**: The string-based `KeyLookup` configuration has been replaced with a more flexible and type-safe `Extractor` function pattern.\n\n- **New Middleware Handler**: The `New` function now returns a middleware handler instead of a `*Store`. To access the session store, use the `Store` method on the middleware, or opt for `NewStore` or `NewWithStore` for custom store integration.\n\n- **Manual Session Release**: Session instances are no longer automatically released after being saved. To ensure proper lifecycle management, you must manually call `sess.Release()`.\n\n- **Idle Timeout**: The `Expiration` field has been replaced with `IdleTimeout`, which handles session inactivity. If the session is idle for the specified duration, it will expire. The idle timeout is updated when the session is saved. If you are using the middleware handler, the idle timeout will be updated automatically.\n\n- **Absolute Timeout**: The `AbsoluteTimeout` field has been added. If you need to set an absolute session timeout, you can use this field to define the duration. The session will expire after the specified duration, regardless of activity.\n\n- **Default KeyGenerator**: Changed from `utils.UUIDv4` to `utils.SecureToken`, producing base64-encoded tokens instead of UUID format.\n\nFor more details on these changes and migration instructions, check the [Session Middleware Migration Guide](./middleware/session.md#migration-guide).\n\n### Timeout\n\nThe timeout middleware is now configurable. A new `Config` struct allows customizing the timeout duration, defining a handler that runs when a timeout occurs, and specifying errors to treat as timeouts. The `New` function now accepts a `Config` value instead of a duration.\n\n**Behavioral changes:**\n\n- **Immediate return on timeout**: The middleware now returns immediately when a timeout occurs, without waiting for the handler to finish. This is achieved through the new **Abandon mechanism** which marks the context as abandoned so it won't be returned to the pool while the handler is still running.\n- **Context propagation**: The timeout context is properly propagated to the handler. Handlers can detect timeouts by listening on `c.Context().Done()` and return early.\n- **Panic handling**: Panics in the handler are caught and converted to `500 Internal Server Error` responses.\n- **Race-free design**: The implementation uses fasthttp's `TimeoutErrorWithCode` combined with Fiber's Abandon mechanism to ensure complete race-freedom between the middleware, handler goroutine, and context pooling.\n\n**New Ctx methods for the Abandon mechanism:**\n\n- `Abandon()`: Marks the context as abandoned\n- `IsAbandoned()`: Returns true if the context was abandoned\n- `ForceRelease()`: Releases an abandoned context back to the pool (for advanced use)\n\n**Migration:** Replace calls like `timeout.New(handler, 2*time.Second)` with `timeout.New(handler, timeout.Config{Timeout: 2 * time.Second})`.\n\n## 🔌 Addons\n\nIn v3, Fiber introduced Addons. Addons are additional useful packages that can be used in Fiber.\n\n### Retry\n\nThe Retry addon is a new addon that implements a retry mechanism for unsuccessful network operations. It uses an exponential backoff algorithm with jitter.\nIt calls the function multiple times and tries to make it successful. If all calls are failed, then, it returns an error.\nIt adds a jitter at each retry step because adding a jitter is a way to break synchronization across the client and avoid collision.\n\n<details>\n<summary>Example</summary>\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3/addon/retry\"\n    \"github.com/gofiber/fiber/v3/client\"\n)\n\nfunc main() {\n    expBackoff := retry.NewExponentialBackoff(retry.Config{})\n\n    // Local variables that will be used inside of Retry\n    var resp *client.Response\n    var err error\n\n    // Retry a network request and return an error to signify to try again\n    err = expBackoff.Retry(func() error {\n        client := client.New()\n        resp, err = client.Get(\"https://gofiber.io\")\n        if err != nil {\n            return fmt.Errorf(\"GET gofiber.io failed: %w\", err)\n        }\n        if resp.StatusCode() != 200 {\n            return fmt.Errorf(\"GET gofiber.io did not return OK 200\")\n        }\n        return nil\n    })\n\n    // If all retries failed, panic\n    if err != nil {\n        panic(err)\n    }\n    fmt.Printf(\"GET gofiber.io succeeded with status code %d\\n\", resp.StatusCode())\n}\n```\n\n</details>\n\n## 📋 Migration guide\n\nTo streamline upgrades between Fiber versions, the Fiber CLI ships with a\n`migrate` command:\n\n```bash\ngo install github.com/gofiber/cli/fiber@latest\nfiber migrate --to v3\n```\n\n### Options\n\n- `-t, --to string` migrate to a specific version, e.g. `v3.0.0`\n- `-f, --force` force migration even if already on that version\n- `-s, --skip_go_mod` skip running `go mod tidy`, `go mod download`, and `go mod vendor`\n\n### Changes Overview\n\n- [🚀 App](#-app-1)\n- [🎣 Hooks](#-hooks-1)\n- [🚀 Listen](#-listen-1)\n- [🗺 Router](#-router-1)\n- [🧠 Context](#-context-1)\n- [📎 Binding (was Parser)](#-parser)\n- [🔄 Redirect](#-redirect-1)\n- [🧾 Log](#-log-1)\n- [🌎 Client package](#-client-package-1)\n- [🛠️ Utils](#utils-migration)\n- [🧬 Middlewares](#-middlewares-1)\n  - [Important Change for Accessing Middleware Data](#important-change-for-accessing-middleware-data)\n  - [BasicAuth](#basicauth-1)\n  - [Cache](#cache-1)\n  - [CORS](#cors-1)\n  - [CSRF](#csrf-1)\n  - [Filesystem](#filesystem-1)\n  - [EnvVar](#envvar-1)\n  - [Favicon](#favicon)\n  - [Healthcheck](#healthcheck-1)\n  - [Monitor](#monitor-1)\n  - [Proxy](#proxy-1)\n  - [Session](#session-1)\n\n### 🚀 App\n\n#### Static\n\nSince we've removed `app.Static()`, you need to move methods to static middleware like the example below:\n\n```go\n// Before\napp.Static(\"/\", \"./public\")\napp.Static(\"/prefix\", \"./public\")\napp.Static(\"/prefix\", \"./public\", Static{\n    Index: \"index.htm\",\n})\napp.Static(\"*\", \"./public/index.html\")\n```\n\n```go\n// After\napp.Get(\"/*\", static.New(\"./public\"))\napp.Get(\"/prefix*\", static.New(\"./public\"))\napp.Get(\"/prefix*\", static.New(\"./public\", static.Config{\n    IndexNames: []string{\"index.htm\", \"index.html\"},\n}))\napp.Get(\"*\", static.New(\"./public/index.html\"))\n```\n\n:::caution\nYou have to put `*` to the end of the route if you don't define static route with `app.Use`.\n:::\n\n#### Trusted Proxies\n\nWe've renamed `EnableTrustedProxyCheck` to `TrustProxy` and moved `TrustedProxies` to `TrustProxyConfig`.\n\n**Important:** To use proxy headers like `X-Forwarded-For` with `c.IP()`, you must configure **all** of `TrustProxy`, `ProxyHeader`, and a trusted proxy via `TrustProxyConfig`. If the proxy is not trusted (for example, if you set only `ProxyHeader` or only `TrustProxy` without configuring `TrustProxyConfig`), proxy headers are ignored and `c.IP()` will return the remote TCP IP instead.\n\n```go\n// Before\napp := fiber.New(fiber.Config{\n    // EnableTrustedProxyCheck enables the trusted proxy check.\n    EnableTrustedProxyCheck: true,\n    // TrustedProxies is a list of trusted proxy IP ranges/addresses.\n    TrustedProxies: []string{\"0.8.0.0\", \"127.0.0.0/8\", \"::1/128\"},\n})\n```\n\n```go\n// After\napp := fiber.New(fiber.Config{\n    // TrustProxy enables the trusted proxy check\n    TrustProxy: true,\n    // ProxyHeader specifies which header to read the real client IP from\n    ProxyHeader: fiber.HeaderXForwardedFor,\n    // TrustProxyConfig allows for configuring trusted proxies.\n    TrustProxyConfig: fiber.TrustProxyConfig{\n        // Proxies is a list of trusted proxy IP ranges/addresses.\n        Proxies: []string{\"0.8.0.0\"},\n        // Trust all loop-back IP addresses (127.0.0.0/8, ::1/128)\n        Loopback: true,\n        // Trust Unix domain socket connections\n        UnixSocket: true,\n    },\n})\n```\n\nFor detailed proxy configuration guidance, see the [reverse proxy guide](./guide/reverse-proxy.md).\n\n### 🎣 Hooks\n\n`OnShutdown` has been replaced by two hooks: `OnPreShutdown` and `OnPostShutdown`.\nUse them to run cleanup code before and after the server shuts down. When handling\nshutdown errors, register an `OnPostShutdown` hook and call `app.Listen()` in a goroutine.\n\n```go\n// Before\napp.OnShutdown(func() {\n    // Code to run before shutdown\n})\n```\n\n```go\n// After\napp.Hooks().OnPreShutdown(func() error {\n    // Code to run before shutdown\n    return nil\n})\n```\n\n### 🚀 Listen\n\nThe `Listen` helpers (`ListenTLS`, `ListenMutualTLS`, etc.) were removed. Use\n`app.Listen()` with `fiber.ListenConfig` and a `tls.Config` when TLS is required.\nOptions such as `ListenerNetwork` and `UnixSocketFileMode` are now configured via\nthis struct. Prefer `TLSConfig` when you need full control, or use `CertFile` and\n`CertKeyFile` for quick TLS setup.\n\n```go\n// Before\napp.ListenTLS(\":3000\", \"cert.pem\", \"key.pem\")\n```\n\n```go\n// After\napp.Listen(\":3000\", fiber.ListenConfig{\n    CertFile:    \"./cert.pem\",\n    CertKeyFile: \"./key.pem\",\n})\n```\n\n### 🗺 Router\n\n#### Direct `net/http` handlers\n\nRoute registration helpers now accept native `net/http` handlers. Pass an\n`http.Handler`, `http.HandlerFunc`, or compatible function directly to methods\nsuch as `app.Get`, `Group`, or `RouteChain` and Fiber will adapt it at\nregistration time. Manual wrapping through the adaptor middleware is no longer\nrequired for these common cases.\n\n:::note Compatibility considerations\nAdapted handlers stick to `net/http` semantics. They do not interact with `fiber.Ctx`\nand are slower than native Fiber handlers because of the extra conversion layer. Use\nthem to ease migrations, but prefer Fiber handlers in performance-critical paths.\n:::\n\n```go\nhttpHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n    if _, err := w.Write([]byte(\"served by net/http\")); err != nil {\n        panic(err)\n    }\n})\n\napp.Get(\"/\", httpHandler)\n```\n\n#### Middleware Registration\n\nThe signatures for [`Add`](#middleware-registration) and [`Route`](#route-chaining) have been changed.\n\nTo migrate [`Add`](#middleware-registration) you must change the `methods` in a slice.\n\n```go\n// Before\napp.Add(fiber.MethodPost, \"/api\", myHandler)\n```\n\n```go\n// After\napp.Add([]string{fiber.MethodPost}, \"/api\", myHandler)\n```\n\n#### Mounting\n\nIn this release, the `Mount` method has been removed. Instead, you can use the `Use` method to achieve similar functionality.\n\n```go\n// Before\napp.Mount(\"/api\", apiApp)\n```\n\n```go\n// After\napp.Use(\"/api\", apiApp)\n```\n\n#### Route Chaining\n\nRefer to the [route chaining](#route-chaining) section for details on the new `RouteChain` helper. The `Route` function now matches its v2 behavior for prefix encapsulation.\n\n```go\n// Before\napp.Route(\"/api\", func(apiGrp Router) {\n    apiGrp.Route(\"/user/:id?\", func(userGrp Router) {\n        userGrp.Get(\"/\", func(c fiber.Ctx) error {\n            // Get user\n            return c.JSON(fiber.Map{\"message\": \"Get user\", \"id\": c.Params(\"id\")})\n        })\n        userGrp.Post(\"/\", func(c fiber.Ctx) error {\n            // Create user\n            return c.JSON(fiber.Map{\"message\": \"User created\"})\n        })\n    })\n})\n```\n\n```go\n// After\napp.RouteChain(\"/api\").RouteChain(\"/user/:id?\")\n    .Get(func(c fiber.Ctx) error {\n        // Get user\n        return c.JSON(fiber.Map{\"message\": \"Get user\", \"id\": c.Params(\"id\")})\n    })\n    .Post(func(c fiber.Ctx) error {\n        // Create user\n        return c.JSON(fiber.Map{\"message\": \"User created\"})\n    });\n```\n\n### 🗺 RebuildTree\n\nWe introduced a new method that enables rebuilding the route tree stack at runtime. This allows you to add routes dynamically while your application is running and update the route tree to make the new routes available for use.\n\nFor more details, refer to the [app documentation](./api/app.md#rebuildtree):\n\n#### Example Usage\n\n```go\napp.Get(\"/define\", func(c fiber.Ctx) error {  // Define a new route dynamically\n    app.Get(\"/dynamically-defined\", func(c fiber.Ctx) error {  // Adding a dynamically defined route\n        return c.SendStatus(http.StatusOK)\n    })\n\n    app.RebuildTree()  // Rebuild the route tree to register the new route\n\n    return c.SendStatus(http.StatusOK)\n})\n```\n\nIn this example, a new route is defined, and `RebuildTree()` is called to ensure the new route is registered and available.\n\nNote: Use this method with caution. It is **not** thread-safe and can be very performance-intensive. Therefore, it should be used sparingly and primarily in development mode. It should not be invoke concurrently.\n\n#### RemoveRoute\n\n- **RemoveRoute**: Removes route by path\n\n- **RemoveRouteByName**: Removes route by name\n\n- **RemoveRouteFunc**: Removes route by a function having `*Route` parameter\n\nFor more details, refer to the [app documentation](./api/app.md#removeroute):\n\n### 🧠 Context\n\nFiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility.\n\n- **ParamsInt**: Use `Params` with generic types.\n- **QueryBool**: Use `Query` with generic types.\n- **QueryFloat**: Use `Query` with generic types.\n- **QueryInt**: Use `Query` with generic types.\n- **Bind**: Now used for binding instead of view binding. Use `c.ViewBind()` for view binding.\n\nIn Fiber v3, the `Ctx` parameter in handlers is now an interface, which means the `*` symbol is no longer used. Here is an example demonstrating this change:\n\n<details>\n<summary>Example</summary>\n\n**Before**:\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v2\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Route Handler with *fiber.Ctx\n    app.Get(\"/\", func(c *fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n**After**:\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Route Handler without *fiber.Ctx\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    app.Listen(\":3000\")\n}\n```\n\n**Explanation**:\n\nIn this example, the `Ctx` parameter in the handler is used as an interface (`fiber.Ctx`) instead of a pointer (`*fiber.Ctx`). This change allows for more flexibility and customization in Fiber v3.\n\n</details>\n\n#### 📎 Parser\n\nThe `Parser` section in Fiber v3 has undergone significant changes to improve functionality and flexibility.\n\n##### Migration Instructions\n\n1. **BodyParser**: Use `c.Bind().Body()` instead of `c.BodyParser()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Post(\"/user\", func(c *fiber.Ctx) error {\n        var user User\n        if err := c.BodyParser(&user); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(user)\n    })\n    ```\n\n    ```go\n    // After\n    app.Post(\"/user\", func(c fiber.Ctx) error {\n        var user User\n        if err := c.Bind().Body(&user); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(user)\n    })\n    ```\n\n    </details>\n\n2. **ParamsParser**: Use `c.Bind().URI()` instead of `c.ParamsParser()`. Note that the struct tag has changed from `params` to `uri`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    type Params struct {\n        ID int `params:\"id\"`\n    }\n\n    app.Get(\"/user/:id\", func(c *fiber.Ctx) error {\n        var params Params\n        if err := c.ParamsParser(&params); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(params)\n    })\n    ```\n\n    ```go\n    // After\n    type Params struct {\n        ID int `uri:\"id\"`\n    }\n\n    app.Get(\"/user/:id\", func(c fiber.Ctx) error {\n        var params Params\n        if err := c.Bind().URI(&params); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(params)\n    })\n    ```\n\n    </details>\n\n3. **QueryParser**: Use `c.Bind().Query()` instead of `c.QueryParser()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Get(\"/search\", func(c *fiber.Ctx) error {\n        var query Query\n        if err := c.QueryParser(&query); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(query)\n    })\n    ```\n\n    ```go\n    // After\n    app.Get(\"/search\", func(c fiber.Ctx) error {\n        var query Query\n        if err := c.Bind().Query(&query); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(query)\n    })\n    ```\n\n    </details>\n\n4. **CookieParser**: Use `c.Bind().Cookie()` instead of `c.CookieParser()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Get(\"/cookie\", func(c *fiber.Ctx) error {\n        var cookie Cookie\n        if err := c.CookieParser(&cookie); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(cookie)\n    })\n    ```\n\n    ```go\n    // After\n    app.Get(\"/cookie\", func(c fiber.Ctx) error {\n        var cookie Cookie\n        if err := c.Bind().Cookie(&cookie); err != nil {\n            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{\"error\": err.Error()})\n        }\n        return c.JSON(cookie)\n    })\n    ```\n\n    </details>\n\n#### 🔄 Redirect\n\nFiber v3 enhances the redirect functionality by introducing new methods and improving existing ones. The new redirect methods provide more flexibility and control over the redirection process.\n\n##### Migration Instructions\n\n1. **RedirectToRoute**: Use `c.Redirect().Route()` instead of `c.RedirectToRoute()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Get(\"/old\", func(c *fiber.Ctx) error {\n        return c.RedirectToRoute(\"newRoute\")\n    })\n    ```\n\n    ```go\n    // After\n    app.Get(\"/old\", func(c fiber.Ctx) error {\n        return c.Redirect().Route(\"newRoute\")\n    })\n    ```\n\n    </details>\n\n2. **RedirectBack**: Use `c.Redirect().Back()` instead of `c.RedirectBack()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Get(\"/back\", func(c *fiber.Ctx) error {\n        return c.RedirectBack()\n    })\n    ```\n\n    ```go\n    // After\n    app.Get(\"/back\", func(c fiber.Ctx) error {\n        return c.Redirect().Back()\n    })\n    ```\n\n    </details>\n\n3. **Redirect**: Use `c.Redirect().To()` instead of `c.Redirect()`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    app.Get(\"/old\", func(c *fiber.Ctx) error {\n        return c.Redirect(\"/new\")\n    })\n    ```\n\n    ```go\n    // After\n    app.Get(\"/old\", func(c fiber.Ctx) error {\n        return c.Redirect().To(\"/new\")\n    })\n    ```\n\n    </details>\n\n#### 🧾 Log\n\nThe `ConfigurableLogger` and `AllLogger` interfaces now use generics. You can specify the underlying logger type when implementing these interfaces. While `any` can be used for maximum flexibility in some contexts, when retrieving the concrete logger via `log.DefaultLogger`, you must specify the exact underlying logger type, for example `log.DefaultLogger[*MyLogger]().Logger()`.\n\n### 🌎 Client package\n\nFiber v3 introduces a completely rebuilt client package with numerous new features such as Cookiejar, request/response hooks, and more. Here is a guide to help you migrate from Fiber v2 to Fiber v3.\n\n#### New Features\n\n- **Cookiejar**: Manage cookies automatically.\n- **Request/Response Hooks**: Customize request and response handling.\n- **Improved Error Handling**: Better error management and reporting.\n\n#### Migration Instructions\n\n**Import Path**:\n\nUpdate the import path to the new client package.\n\n<details>\n<summary>Before</summary>\n\n```go\nimport \"github.com/gofiber/fiber/v2/client\"\n```\n\n</details>\n\n<details>\n<summary>After</summary>\n\n```go\nimport \"github.com/gofiber/fiber/v3/client\"\n```\n\n</details>\n\n**Common migrations**:\n\n1. **Shared defaults instead of per-call mutation**: Move headers and timeouts into the reusable client and override with `client.Config` when needed.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    status, body, errs := fiber.Get(\"https://api.example.com/users\").\n        Set(\"Authorization\", \"Bearer \"+token).\n        Timeout(5 * time.Second).\n        String()\n    if len(errs) > 0 {\n        return fmt.Errorf(\"request failed: %v\", errs)\n    }\n    fmt.Println(status, body)\n    ```\n\n    ```go\n    // After\n    cli := client.New().\n        AddHeader(\"Authorization\", \"Bearer \"+token).\n        SetTimeout(5 * time.Second)\n\n    resp, err := cli.Get(\"https://api.example.com/users\")\n    if err != nil {\n        return err\n    }\n    defer resp.Close()\n    fmt.Println(resp.StatusCode(), resp.String())\n    ```\n\n    </details>\n\n2. **Body handling**: Replace `Agent.JSON(...).Struct(&dst)` with request bodies through `client.Config` (or `Request.SetJSON`) and decode the response via `Response.JSON`.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    var created user\n    status, _, errs := fiber.Post(\"https://api.example.com/users\").\n        JSON(payload).\n        Struct(&created)\n    if len(errs) > 0 {\n        return fmt.Errorf(\"request failed: %v\", errs)\n    }\n    fmt.Println(status, created)\n    ```\n\n    ```go\n    // After\n    cli := client.New()\n\n    resp, err := cli.Post(\"https://api.example.com/users\", client.Config{\n        Body: payload,\n    })\n    if err != nil {\n        return err\n    }\n    defer resp.Close()\n\n    var created user\n    if err := resp.JSON(&created); err != nil {\n        return fmt.Errorf(\"decode failed: %w\", err)\n    }\n    fmt.Println(resp.StatusCode(), created)\n    ```\n\n    </details>\n\n3. **Path and query parameters**: Use the new path/query helpers instead of manually formatting URLs.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    code, body, errs := fiber.Get(fmt.Sprintf(\"https://api.example.com/users/%s\", id)).\n        QueryString(\"active=true\").\n        String()\n    if len(errs) > 0 {\n        return fmt.Errorf(\"request failed: %v\", errs)\n    }\n    fmt.Println(code, body)\n    ```\n\n    ```go\n    // After\n    cli := client.New().SetBaseURL(\"https://api.example.com\")\n    resp, err := cli.Get(\"/users/:id\", client.Config{\n        PathParam: map[string]string{\"id\": id},\n        Param:     map[string]string{\"active\": \"true\"},\n    })\n    if err != nil {\n        return err\n    }\n    defer resp.Close()\n    fmt.Println(resp.StatusCode(), resp.String())\n    ```\n\n    </details>\n\n4. **Agent helpers**: `Agent.Bytes`, `AcquireAgent`, and `Agent.Parse` have been removed. Reuse a `client.Client` instance (or pool requests/responses directly) and access response data through the new typed helpers.\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    agent := fiber.AcquireAgent()\n    status, body, errs := agent.Get(\"https://api.example.com/users\").Bytes()\n    fiber.ReleaseAgent(agent)\n    if len(errs) > 0 {\n        return fmt.Errorf(\"request failed: %v\", errs)\n    }\n\n    var users []user\n    if err := fiber.Parse(body, &users); err != nil {\n        return fmt.Errorf(\"parse failed: %w\", err)\n    }\n    fmt.Println(status, len(users))\n    ```\n\n    ```go\n    // After\n    cli := client.New()\n    resp, err := cli.Get(\"https://api.example.com/users\")\n    if err != nil {\n        return err\n    }\n    defer resp.Close()\n\n    var users []user\n    if err := resp.JSON(&users); err != nil {\n        return fmt.Errorf(\"decode failed: %w\", err)\n    }\n    fmt.Println(resp.StatusCode(), len(users))\n    ```\n\n    :::tip\n    If you need pooling, use `client.AcquireRequest`, `client.AcquireResponse`, and their corresponding release functions around a long-lived `client.Client` instead of the removed agent pool.\n    :::\n\n    </details>\n\n5. **Fiber-level shortcuts**: The `fiber.Get`, `fiber.Post`, and similar top-level helpers are no longer exposed from the main module. Use the client package equivalents (`client.Get`, `client.Post`, etc.) which call the shared default client (or pass your own client instance for custom defaults).\n\n    <details>\n    <summary>Example</summary>\n\n    ```go\n    // Before\n    status, body, errs := fiber.Get(\"https://api.example.com/health\").String()\n    if len(errs) > 0 {\n        return fmt.Errorf(\"request failed: %v\", errs)\n    }\n    fmt.Println(status, body)\n    ```\n\n    ```go\n    // After\n    resp, err := client.Get(\"https://api.example.com/health\")\n    if err != nil {\n        return err\n    }\n    defer resp.Close()\n\n    fmt.Println(resp.StatusCode(), resp.String())\n    ```\n\n    :::note\n    The `client.Get`/`client.Post` helpers use `client.C()` (the default shared client). For custom defaults, construct a client with `client.New()` and invoke its methods instead.\n    :::\n\n    </details>\n\n#### Complete API Migration Reference\n\n<details>\n<summary>Click to expand full v2 → v3 API mapping tables</summary>\n\n##### Core Concepts\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Import | `github.com/gofiber/fiber/v2` | `github.com/gofiber/fiber/v3/client` |\n| Client Concept | `*fiber.Agent` | `*client.Client` + `*client.Request` |\n| Response Concept | `(code int, body []byte, errs []error)` | `(*client.Response, error)` |\n\n##### Client/Agent Creation\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Create Agent/Client | `fiber.AcquireAgent()` | `client.New()` |\n| Get from pool | `fiber.AcquireAgent()` | `client.AcquireRequest()` |\n| Release | `fiber.ReleaseAgent(a)` | `client.ReleaseRequest(req)` |\n| With fasthttp.Client | - | `client.NewWithClient(c)` |\n| With HostClient | - | `client.NewWithHostClient(hc)` |\n| With LBClient | - | `client.NewWithLBClient(lb)` |\n| Get Request object | `a.Request()` | `c.R()` |\n| Default client | - | `client.C()` |\n| Replace default | - | `client.Replace(c)` |\n\n##### HTTP Methods\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| GET | `fiber.Get(url)` | `c.Get(url, cfg...)` | `req.Get(url)` |\n| POST | `fiber.Post(url)` | `c.Post(url, cfg...)` | `req.Post(url)` |\n| PUT | `fiber.Put(url)` | `c.Put(url, cfg...)` | `req.Put(url)` |\n| PATCH | `fiber.Patch(url)` | `c.Patch(url, cfg...)` | `req.Patch(url)` |\n| DELETE | `fiber.Delete(url)` | `c.Delete(url, cfg...)` | `req.Delete(url)` |\n| HEAD | `fiber.Head(url)` | `c.Head(url, cfg...)` | `req.Head(url)` |\n| OPTIONS | - | `c.Options(url, cfg...)` | `req.Options(url)` |\n| Custom | - | `c.Custom(url, method, cfg...)` | `req.Custom(url, method)` |\n\n##### URL & Method\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Set URL | `req.SetRequestURI(url)` | `req.SetURL(url)` |\n| Get URL | `req.URI().String()` | `req.URL()` |\n| Set Method | `req.Header.SetMethod(method)` | `req.SetMethod(method)` |\n| Set Base URL | - | `c.SetBaseURL(url)` |\n\n##### Request Execution & Response\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Parse Request | `a.Parse()` | Not needed |\n| Execute (bytes) | `a.Bytes()` → `(code, body, errs)` | `req.Send()` → `(*Response, error)` |\n| Execute (string) | `a.String()` | `resp.String()` |\n| Execute (struct) | `a.Struct(&v)` | `resp.JSON(&v)` / `resp.XML(&v)` |\n| Status Code | Return value `code` | `resp.StatusCode()` |\n| Status Text | - | `resp.Status()` |\n| Body (bytes) | Return value `body` | `resp.Body()` |\n| Response Header | `resp.Header.Peek(key)` | `resp.Header(key)` |\n| All Headers | `resp.Header.VisitAll(fn)` | `resp.Headers()` |\n| Cookies | - | `resp.Cookies()` |\n| Save to file | - | `resp.Save(path)` |\n| Close | - | `resp.Close()` |\n\n##### Headers\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| Set Header | `a.Set(k, v)` | `c.SetHeader(k, v)` | `req.SetHeader(k, v)` |\n| Add Header | `a.Add(k, v)` | `c.AddHeader(k, v)` | `req.AddHeader(k, v)` |\n| Multiple Headers | - | `c.SetHeaders(map)` | `req.SetHeaders(map)` |\n| Bytes variants | `a.SetBytesK/V/KV()` | - | - |\n\n##### User-Agent, Referer, Content-Type, Host\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| User-Agent | `a.UserAgent(ua)` | `c.SetUserAgent(ua)` | `req.SetUserAgent(ua)` |\n| Referer | `a.Referer(ref)` | `c.SetReferer(ref)` | `req.SetReferer(ref)` |\n| Content-Type | `a.ContentType(ct)` | - | `req.SetHeader(\"Content-Type\", ct)` |\n| Host | `a.Host(host)` | - | `req.SetHeader(\"Host\", host)` |\n| Connection Close | `a.ConnectionClose()` | - | `req.SetHeader(\"Connection\", \"close\")` |\n\n##### Cookies\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| Set Cookie | `a.Cookie(k, v)` | `c.SetCookie(k, v)` | `req.SetCookie(k, v)` |\n| Multiple | `a.Cookies(k1, v1, ...)` | `c.SetCookies(map)` | `req.SetCookies(map)` |\n| With Struct | - | `c.SetCookiesWithStruct(v)` | `req.SetCookiesWithStruct(v)` |\n| Cookie Jar | - | `c.SetCookieJar(jar)` | - |\n\n##### Query Parameters\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| Query String | `a.QueryString(qs)` | - | - |\n| Add Param | - | `c.AddParam(k, v)` | `req.AddParam(k, v)` |\n| Set Param | - | `c.SetParam(k, v)` | `req.SetParam(k, v)` |\n| With Struct | - | `c.SetParamsWithStruct(v)` | `req.SetParamsWithStruct(v)` |\n\n##### Path Parameters (NEW)\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| Set Path Param | - | `c.SetPathParam(k, v)` | `req.SetPathParam(k, v)` |\n| Multiple | - | `c.SetPathParams(map)` | `req.SetPathParams(map)` |\n| With Struct | - | `c.SetPathParamsWithStruct(v)` | `req.SetPathParamsWithStruct(v)` |\n\n##### Request Body\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Body (bytes) | `a.Body(body)` | `req.SetRawBody(body)` |\n| Body (string) | `a.BodyString(body)` | `req.SetRawBody([]byte(body))` |\n| Body Stream | `a.BodyStream(r, size)` | - |\n| JSON | `a.JSON(v)` | `req.SetJSON(v)` |\n| XML | `a.XML(v)` | `req.SetXML(v)` |\n| CBOR (NEW) | - | `req.SetCBOR(v)` |\n\n##### Form Data\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Create Args | `fiber.AcquireArgs()` | Direct on Request |\n| Send Form | `a.Form(args)` | `req.SetFormData(k, v)` |\n| Add Form Data | `args.Set(k, v)` | `req.AddFormData(k, v)` |\n| With Map | - | `req.SetFormDataWithMap(map)` |\n| With Struct | - | `req.SetFormDataWithStruct(v)` |\n\n##### File Upload\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Multipart Form | `a.MultipartForm(args)` | Automatic |\n| Boundary | `a.Boundary(b)` | `req.SetBoundary(b)` |\n| Send File | `a.SendFile(f, field...)` | `req.AddFile(path)` |\n| Multiple Files | `a.SendFiles(...)` | `req.AddFiles(files...)` |\n| With Reader | - | `req.AddFileWithReader(name, r)` |\n| FileData | `a.FileData(files...)` | `req.AddFiles(files...)` |\n\n##### Timeout & TLS\n\n| Description | v2 | v3 (Client) | v3 (Request) |\n|-------------|----|----|--------------|\n| Timeout | `a.Timeout(d)` | `c.SetTimeout(d)` | `req.SetTimeout(d)` |\n| Max Redirects | `a.MaxRedirectsCount(n)` | Via Config | `req.SetMaxRedirects(n)` |\n| TLS Config | `a.TLSConfig(cfg)` | `c.SetTLSConfig(cfg)` | - |\n| Skip Verify | `a.InsecureSkipVerify()` | Via `tls.Config` | - |\n| Certificates | - | `c.SetCertificates(...)` | - |\n| Root Cert | - | `c.SetRootCertificate(path)` | - |\n\n##### JSON/XML Encoder\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| JSON Encoder | `a.JSONEncoder(fn)` | `c.SetJSONMarshal(fn)` |\n| JSON Decoder | `a.JSONDecoder(fn)` | `c.SetJSONUnmarshal(fn)` |\n| XML Encoder | - | `c.SetXMLMarshal(fn)` |\n| XML Decoder | - | `c.SetXMLUnmarshal(fn)` |\n| CBOR (NEW) | - | `c.SetCBORMarshal/Unmarshal(fn)` |\n\n##### Authentication\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Basic Auth | `a.BasicAuth(user, pass)` | Via Header (Base64) |\n\n##### Debug & Retry\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Debug | `a.Debug(w...)` | `c.Debug()` |\n| Disable Debug | - | `c.DisableDebug()` |\n| Logger | - | `c.SetLogger(logger)` |\n| Retry | `a.RetryIf(fn)` | `c.SetRetryConfig(cfg)` |\n\n##### Reuse & Reset\n\n| Description | v2 | v3 |\n|-------------|----|----|\n| Reuse Agent | `a.Reuse()` | Use pool |\n| Reset Client | - | `c.Reset()` |\n| Dest Buffer | `a.Dest(dest)` | - |\n\n##### NEW in v3\n\n| Feature | v3 API |\n|---------|--------|\n| Request Hooks | `c.AddRequestHook(fn)` |\n| Response Hooks | `c.AddResponseHook(fn)` |\n| Proxy | `c.SetProxyURL(url)` |\n| Context | `req.SetContext(ctx)` |\n| Dial Function | `c.SetDial(fn)` |\n| Raw Request | `req.RawRequest` |\n| Raw Response | `resp.RawResponse` |\n\n##### Key Differences\n\n1. **Architecture**: v2 `Agent` → v3 separate `Client`, `Request`, `Response`\n2. **Error Handling**: v2 `[]error` → v3 single `error`\n3. **Response**: v2 tuple `(code, body, errs)` → v3 `*Response` object\n4. **No Parse()**: v3 auto-initializes requests\n5. **Hooks**: v3 adds request/response middleware\n6. **Path Params**: v3 native `:param` support\n7. **Cookie Jar**: v3 built-in session management\n8. **CBOR**: v3 adds CBOR encoding\n9. **Context**: v3 native cancellation support\n10. **Iterators**: v3 uses `iter.Seq2` for collections\n11. **Bytes variants removed**: v2 `*Bytes*` methods gone\n\n</details>\n\n### 🛠️ Utils {#utils-migration}\n\nFiber v3 removes the in-repo `utils` package in favor of the external [`github.com/gofiber/utils/v2`](https://github.com/gofiber/utils) module.\n\n1. Replace imports:\n\n```go\n- import \"github.com/gofiber/fiber/v2/utils\"\n+ import \"github.com/gofiber/utils/v2\"\n```\n\n1. Review function changes:\n\n| v2 function | v3 replacement |\n| --- | --- |\n| `AssertEqual` | removed; use testing libraries like [`github.com/stretchr/testify/assert`](https://pkg.go.dev/github.com/stretchr/testify/assert) |\n| `ToLowerBytes` | `utils.ToLowerBytes` |\n| `ToUpperBytes` | `utils.ToUpperBytes` |\n| `TrimRightBytes` | `utils.TrimRight` |\n| `TrimLeftBytes` | `utils.TrimLeft` |\n| `TrimBytes` | `utils.Trim` |\n| `EqualFoldBytes` | `utils.EqualFold` |\n| `UUID` | `utils.UUID` |\n| `UUIDv4` | `utils.UUIDv4` |\n| `FunctionName` | `utils.FunctionName` |\n| `GetArgument` | `utils.GetArgument` |\n| `IncrementIPRange` | `utils.IncrementIPRange` |\n| `ConvertToBytes` | `utils.ConvertToBytes` |\n| `CopyString` | `utils.CopyString` |\n| `CopyBytes` | `utils.CopyBytes` |\n| `ByteSize` | `utils.ByteSize` |\n| `ToString` | `utils.ToString` |\n| `UnsafeString` | `utils.UnsafeString` |\n| `UnsafeBytes` | `utils.UnsafeBytes` |\n| `GetString` | removed; use `utils.ToString` or the standard library |\n| `GetBytes` | removed; use `utils.CopyBytes` or `[]byte(s)` |\n| `ImmutableString` | removed; strings are already immutable |\n| `GetMIME` | `utils.GetMIME` |\n| `ParseVendorSpecificContentType` | `utils.ParseVendorSpecificContentType` |\n| `StatusMessage` | `utils.StatusMessage` |\n| `IsIPv4` | `utils.IsIPv4` |\n| `IsIPv6` | `utils.IsIPv6` |\n| `ToLower` | `utils.ToLower` |\n| `ToUpper` | `utils.ToUpper` |\n| `TrimLeft` | `strings.TrimLeft` |\n| `Trim` | `strings.Trim` |\n| `TrimRight` | `strings.TrimRight` |\n| `EqualFold` | `strings.EqualFold` |\n| `StartTimeStampUpdater` | `utils.StartTimeStampUpdater` (new `utils.Timestamp` provides the current value) |\n\n1. Update your code. For example:\n\n```go\n// v2\nimport oldutils \"github.com/gofiber/fiber/v2/utils\"\n\nfunc demo() {\n    b := oldutils.TrimBytes([]byte(\" fiber \"))\n    id := oldutils.UUIDv4()\n    s := oldutils.GetString([]byte(\"foo\"))\n}\n\n// v3\nimport (\n    \"github.com/gofiber/utils/v2\"\n    \"strings\"\n)\n\nfunc demo() {\n    s := utils.TrimSpace(\" fiber \")\n    id := utils.UUIDv4()\n    str := utils.ToString([]byte(\"foo\"))\n    t := strings.TrimRight(\"bar  \", \" \")\n}\n```\n\nThe `github.com/gofiber/utils/v2` module also introduces new helpers like `ParseInt`, `ParseUint`, `Walk`, `ReadFile`, and `Timestamp`.\n\n### 🧬 Middlewares\n\n#### Important Change for Accessing Middleware Data\n\n**Change:** In Fiber v2, some middlewares set data in `c.Locals()` using string keys (e.g., `c.Locals(\"requestid\")`). In Fiber v3, to align with Go's context best practices and prevent key collisions, these middlewares now store their specific data in the request's context using unexported keys of custom types.\n\n**Impact:** Directly accessing these middleware-provided values via `c.Locals(\"some_string_key\")` will no longer work.\n\n**Migration Action:**\nThe `ContextKey` configuration option has been removed from all middlewares. Values are no longer stored under user-defined keys. You must update your code to use the dedicated exported functions provided by each affected middleware to retrieve its data from the context.\n\n**Examples of new helper functions to use:**\n\n- `requestid.FromContext(c)`\n- `csrf.TokenFromContext(c)`\n- `csrf.HandlerFromContext(c)`\n- `session.FromContext(c)`\n- `basicauth.UsernameFromContext(c)`\n- `keyauth.TokenFromContext(c)`\n\n**For logging these values:**\nThe recommended approach is to use the `CustomTags` feature of the Logger middleware, which allows you to call these specific `FromContext` functions. Refer to the [Logger section in \"What's New\"](#logger) for detailed examples.\n\n:::note\nIf you were manually setting and retrieving your own application-specific values in `c.Locals()` using string keys, that functionality remains unchanged. This change specifically pertains to how Fiber's built-in (and some contrib) middlewares expose their data.\n:::\n\n#### BasicAuth\n\nThe `Authorizer` callback now receives the current request context. Update custom\nfunctions from:\n\n```go\nAuthorizer: func(user, pass string) bool {\n    // v2 style\n    return user == \"admin\" && pass == \"secret\"\n}\n```\n\nto:\n\n```go\nAuthorizer: func(user, pass string, _ fiber.Ctx) bool {\n    // v3 style with access to the Fiber context\n    return user == \"admin\" && pass == \"secret\"\n}\n```\n\nPasswords configured for BasicAuth must now be pre-hashed. If no prefix is supplied the middleware expects a SHA-256 digest encoded in hex. Common prefixes like `{SHA256}` and `{SHA512}` and bcrypt strings are also supported. Plaintext passwords are no longer accepted. Unauthorized responses also include a `Vary: Authorization` header for correct caching behavior.\n\nYou can also set the optional `HeaderLimit` and `Charset`\noptions to further control authentication behavior.\n\n#### KeyAuth\n\nThe keyauth middleware was updated to introduce a configurable `Realm` field for the `WWW-Authenticate` header.\nThe old string-based `KeyLookup` configuration has been replaced with an `Extractor` field, and the `AuthScheme` field has been removed. The auth scheme is now inferred from the extractor used (e.g., `keyauth.FromAuthHeader`). Use helper functions like `keyauth.FromHeader`, `keyauth.FromAuthHeader`, or `keyauth.FromCookie` to define where the key should be retrieved from. Multiple sources can be combined with `keyauth.Chain`.\nNew `Challenge`, `Error`, `ErrorDescription`, `ErrorURI`, and `Scope` options let you customize challenge responses, include Bearer error parameters, and specify required scopes. `ErrorURI` values are validated as absolute, credentials containing whitespace are rejected, and when multiple authorization extractors are chained, all schemes are advertised in the `WWW-Authenticate` header. The middleware defers emitting `WWW-Authenticate` until a 401 status is final, and `FromAuthHeader` now trims surrounding whitespace.\n\n```go\n// Before\napp.Use(keyauth.New(keyauth.Config{\n    KeyLookup: \"header:Authorization\",\n    AuthScheme: \"Bearer\",\n    Validator: validateAPIKey,\n}))\n\n// After\napp.Use(keyauth.New(keyauth.Config{\n    Extractor: keyauth.FromAuthHeader(fiber.HeaderAuthorization, \"Bearer\"),\n    Validator: validateAPIKey,\n}))\n```\n\nCombine multiple sources with `keyauth.Chain()` when needed.\n\n#### Cache\n\nThe deprecated `Store` and `Key` fields were removed. Use `Storage` and\n`KeyGenerator` instead to configure caching backends and cache keys.\n\nDefaults also changed: the middleware now emits `Cache-Control` headers, the default `Expiration` increased to `5 minutes` (from `1 minute`), and a new `MaxBytes` limit of `1 MB` (previously unlimited) now caps cached payloads.\n\nTo restore v2 behavior:\n\n- Set `DisableCacheControl` to `true` to suppress automatic `Cache-Control` headers.\n- Configure `Expiration` to `1*time.Minute`.\n- Set `MaxBytes` to `0` (or a higher value) when caching large responses.\n\n#### CORS\n\nThe CORS middleware has been updated to use slices instead of strings for the `AllowOrigins`, `AllowMethods`, `AllowHeaders`, and `ExposeHeaders` fields. Here's how you can update your code:\n\n```go\n// Before\napp.Use(cors.New(cors.Config{\n    AllowOrigins: \"https://example.com,https://example2.com\",\n    AllowMethods: strings.Join([]string{fiber.MethodGet, fiber.MethodPost}, \",\"),\n    AllowHeaders: \"Content-Type\",\n    ExposeHeaders: \"Content-Length\",\n}))\n\n// After\napp.Use(cors.New(cors.Config{\n    AllowOrigins: []string{\"https://example.com\", \"https://example2.com\"},\n    AllowMethods: []string{fiber.MethodGet, fiber.MethodPost},\n    AllowHeaders: []string{\"Content-Type\"},\n    ExposeHeaders: []string{\"Content-Length\"},\n}))\n```\n\n#### CSRF\n\n- **Field Renaming**: The `Expiration` field in the CSRF middleware configuration has been renamed to `IdleTimeout` to better describe its functionality. Additionally, the default value has been reduced from 1 hour to 30 minutes. Update your code as follows:\n\n```go\n// Before\napp.Use(csrf.New(csrf.Config{\n    Expiration: 10 * time.Minute,\n}))\n\n// After\napp.Use(csrf.New(csrf.Config{\n    IdleTimeout: 10 * time.Minute,\n}))\n```\n\n- **Session Key Removal**: The `SessionKey` field has been removed from the CSRF middleware configuration. The session key is now an unexported constant within the middleware to avoid potential key collisions in the session store.\n\n- **KeyLookup Field Removal**: The `KeyLookup` field has been removed from the CSRF middleware configuration. This field was deprecated and is no longer needed as the middleware now uses a more secure approach for token management.\n- **DisableValueRedaction Toggle**: CSRF redacts tokens and storage keys by default; set `DisableValueRedaction` to `true` when diagnostics require the raw values.\n\n- **Default KeyGenerator**: Changed from `utils.UUIDv4` to `utils.SecureToken`, producing base64-encoded tokens instead of UUID format.\n\n```go\n// Before\napp.Use(csrf.New(csrf.Config{\n    KeyLookup: \"header:X-Csrf-Token\",\n    // other config...\n}))\n\n// After - use Extractor instead\napp.Use(csrf.New(csrf.Config{\n    Extractor: csrf.FromHeader(\"X-Csrf-Token\"),\n    // other config...\n}))\n```\n\n- **FromCookie Extractor Removal**: The `csrf.FromCookie` extractor has been intentionally removed for security reasons. Using cookie-based extraction defeats the purpose of CSRF protection by making the extracted token always match the cookie value.\n\n```go\n// Before - This was a security vulnerability\napp.Use(csrf.New(csrf.Config{\n    Extractor: csrf.FromCookie(\"csrf_token\"), // ❌ Insecure!\n}))\n\n// After - Use secure extractors instead\napp.Use(csrf.New(csrf.Config{\n    Extractor: csrf.FromHeader(\"X-Csrf-Token\"), // ✅ Secure\n    // or\n    Extractor: csrf.FromForm(\"_csrf\"),          // ✅ Secure\n    // or\n    Extractor: csrf.FromQuery(\"csrf_token\"),    // ✅ Acceptable\n}))\n```\n\n**Security Note**: The removal of `FromCookie` prevents a common misconfiguration that would completely bypass CSRF protection. The middleware uses the Double Submit Cookie pattern, which requires the token to be submitted through a different channel than the cookie to provide meaningful protection.\n\n#### Idempotency\n\n- **DisableValueRedaction Toggle**: The idempotency middleware now hides keys in logs and error paths by default, with a `DisableValueRedaction` boolean (default `false`) to reveal them when needed.\n\n#### Timeout\n\nThe timeout middleware now accepts a configuration struct instead of a duration.\nUpdate your code as follows:\n\n```go\n// Before\napp.Use(timeout.New(handler, 2*time.Second))\n\n// After\napp.Use(timeout.New(handler, timeout.Config{Timeout: 2 * time.Second}))\n```\n\n**Important behavioral changes:**\n\n- The middleware now returns immediately on timeout without waiting for the handler (using the new Abandon mechanism).\n- Handlers can detect timeouts by listening on `c.Context().Done()` and return early.\n- Panics in the handler are caught and converted to `500 Internal Server Error`.\n\n#### Filesystem\n\nYou need to move filesystem middleware to static middleware due to it has been removed from the core.\n\n```go\n// Before\napp.Use(filesystem.New(filesystem.Config{\n    Root: http.Dir(\"./assets\"),\n}))\n\napp.Use(filesystem.New(filesystem.Config{\n    Root:         http.Dir(\"./assets\"),\n    Browse:       true,\n    Index:        \"index.html\",\n    MaxAge:       3600,\n}))\n```\n\n```go\n// After\napp.Use(static.New(\"\", static.Config{\n    FS: os.DirFS(\"./assets\"),\n}))\n\napp.Use(static.New(\"\", static.Config{\n    FS:           os.DirFS(\"./assets\"),\n    Browse:       true,\n    IndexNames:   []string{\"index.html\"},\n    MaxAge:       3600,\n}))\n```\n\n#### EnvVar\n\nThe `ExcludeVars` option has been removed. Remove any references to it and use\n`ExportVars` to explicitly list environment variables that should be exposed.\n\n#### Healthcheck\n\nPreviously, the Healthcheck middleware was configured with a combined setup for liveness and readiness probes:\n\n```go\n//before\napp.Use(healthcheck.New(healthcheck.Config{\n    LivenessProbe: func(c fiber.Ctx) bool {\n        return true\n    },\n    LivenessEndpoint: \"/live\",\n    ReadinessProbe: func(c fiber.Ctx) bool {\n        return serviceA.Ready() && serviceB.Ready() && ...\n    },\n    ReadinessEndpoint: \"/ready\",\n}))\n```\n\nWith the new version, each health check endpoint is configured separately, allowing for more flexibility:\n\n```go\n// after\n\n// Default liveness endpoint configuration\napp.Get(healthcheck.LivenessEndpoint, healthcheck.New(healthcheck.Config{\n    Probe: func(c fiber.Ctx) bool {\n        return true\n    },\n}))\n\n// Default readiness endpoint configuration\napp.Get(healthcheck.ReadinessEndpoint, healthcheck.New())\n\n// New default startup endpoint configuration\n// Default endpoint is /startupz\napp.Get(healthcheck.StartupEndpoint, healthcheck.New(healthcheck.Config{\n    Probe: func(c fiber.Ctx) bool {\n        return serviceA.Ready() && serviceB.Ready() && ...\n    },\n}))\n\n// Custom liveness endpoint configuration\napp.Get(\"/live\", healthcheck.New())\n```\n\n#### Monitor\n\nSince v3 the Monitor middleware has been moved to the [Contrib package](https://github.com/gofiber/contrib/tree/main/monitor)\n\n```go\n// Before\nimport \"github.com/gofiber/fiber/v2/middleware/monitor\"\n\napp.Use(\"/metrics\", monitor.New())\n```\n\nYou only need to change the import path to the contrib package.\n\n```go\n// After\nimport \"github.com/gofiber/contrib/monitor\"\n\napp.Use(\"/metrics\", monitor.New())\n```\n\n#### Proxy\n\nIn previous versions, TLS settings for the proxy middleware were set using the `WithTlsConfig` method. This method has been removed in favor of a more idiomatic configuration via the `TLSConfig` field in the `Config` struct.\n\n#### Before (v2 usage)\n\n```go\nproxy.WithTlsConfig(&tls.Config{\n    InsecureSkipVerify: true,\n})\n\n// Forward to url\napp.Get(\"/gif\", proxy.Forward(\"https://i.imgur.com/IWaBepg.gif\"))\n```\n\n#### After (v3 usage)\n\n```go\nproxy.WithClient(&fasthttp.Client{\n    TLSConfig: &tls.Config{InsecureSkipVerify: true},\n})\n\n// Forward to url\napp.Get(\"/gif\", proxy.Forward(\"https://i.imgur.com/IWaBepg.gif\"))\n```\n\n`proxy.Balancer` also adopts the common middleware signature pattern and now accepts an optional variadic config: call `proxy.Balancer()` to use the defaults or continue passing a single `proxy.Config` value as in v2.\n\n#### Session\n\n`session.New()` now returns a middleware handler. When using the store pattern,\ncreate a store with `session.NewStore()` or call `Store()` on the middleware.\nSessions obtained from a store must be released manually via `sess.Release()`.\nAdditionally, replace the deprecated `KeyLookup` option with extractor\nfunctions such as `session.FromCookie()` or `session.FromHeader()`. Multiple\nextractors can be combined with `session.Chain()`.\n\n```go\n// Before\napp.Use(session.New(session.Config{\n    KeyLookup: \"cookie:session_id\",\n    Store:     session.NewStore(),\n}))\n```\n\n```go\n// After\napp.Use(session.New(session.Config{\n    Extractor: session.FromCookie(\"session_id\"),\n    Store:     session.NewStore(),\n}))\n```\n\nSee the [Session Middleware Migration Guide](./middleware/session.md#migration-guide)\nfor complete details.\n"
  },
  {
    "path": "error.go",
    "content": "package fiber\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/gofiber/schema\"\n)\n\n// Wrap and return this for unreachable code if panicking is undesirable (i.e., in a handler).\n// Unexported because users will hopefully never need to see it.\nvar errUnreachable = errors.New(\"fiber: unreachable code, please create an issue at github.com/gofiber/fiber\")\n\n// General errors\nvar (\n\tErrGracefulTimeout = errors.New(\"shutdown: graceful timeout has been reached, exiting\")\n\t// ErrNotRunning indicates that a Shutdown method was called when the server was not running.\n\tErrNotRunning = errors.New(\"shutdown: server is not running\")\n\t// ErrHandlerExited is returned by App.Test if a handler panics or calls runtime.Goexit().\n\tErrHandlerExited = errors.New(\"runtime.Goexit() called in handler or server panic\")\n\t// ErrNoViewEngineConfigured indicates that a helper requiring a view engine was invoked without one configured.\n\tErrNoViewEngineConfigured = errors.New(\"fiber: no view engine configured\")\n\t// ErrAutoCertWithCertFile indicates AutoCertManager cannot be used with CertFile/CertKeyFile.\n\tErrAutoCertWithCertFile = errors.New(\"tls: AutoCertManager cannot be combined with CertFile/CertKeyFile\")\n)\n\n// Fiber redirection errors\nvar (\n\tErrRedirectBackNoFallback = NewError(StatusInternalServerError, \"Referer not found, you have to enter fallback URL for redirection.\")\n)\n\n// Range errors\nvar (\n\tErrRangeMalformed     = errors.New(\"range: malformed range header string\")\n\tErrRangeTooLarge      = NewError(StatusRequestedRangeNotSatisfiable, \"range: too many ranges\")\n\tErrRangeUnsatisfiable = errors.New(\"range: unsatisfiable range\")\n)\n\n// Binder errors\nvar ErrCustomBinderNotFound = errors.New(\"binder: custom binder not found, please be sure to enter the right name\")\n\n// Format errors\nvar (\n\t// ErrNoHandlers is returned when c.Format is called with no arguments.\n\tErrNoHandlers = errors.New(\"format: at least one handler is required, but none were set\")\n)\n\n// gofiber/schema errors\ntype (\n\t// ConversionError Conversion error exposes the internal schema.ConversionError for public use.\n\tConversionError = schema.ConversionError\n\t// UnknownKeyError error exposes the internal schema.UnknownKeyError for public use.\n\tUnknownKeyError = schema.UnknownKeyError\n\t// EmptyFieldError error exposes the internal schema.EmptyFieldError for public use.\n\tEmptyFieldError = schema.EmptyFieldError\n\t// MultiError error exposes the internal schema.MultiError for public use.\n\tMultiError = schema.MultiError\n)\n\n// encoding/json errors\ntype (\n\t// InvalidUnmarshalError describes an invalid argument passed to Unmarshal.\n\t// (The argument to Unmarshal must be a non-nil pointer.)\n\tInvalidUnmarshalError = json.InvalidUnmarshalError\n\n\t// MarshalerError represents an error from calling a MarshalJSON or MarshalText method.\n\tMarshalerError = json.MarshalerError\n\n\t// SyntaxError is a description of a JSON syntax error.\n\tSyntaxError = json.SyntaxError\n\n\t// UnmarshalTypeError describes a JSON value that was\n\t// not appropriate for a value of a specific Go type.\n\tUnmarshalTypeError = json.UnmarshalTypeError\n\n\t// UnsupportedTypeError is returned by Marshal when attempting\n\t// to encode an unsupported value type.\n\tUnsupportedTypeError = json.UnsupportedTypeError\n\n\t// UnsupportedValueError exposes json.UnsupportedValueError to describe unsupported values encountered during encoding.\n\tUnsupportedValueError = json.UnsupportedValueError\n)\n"
  },
  {
    "path": "error_test.go",
    "content": "package fiber\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/gofiber/schema\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_ConversionError(t *testing.T) {\n\tt.Parallel()\n\tok := errors.As(ConversionError{}, &schema.ConversionError{})\n\trequire.True(t, ok)\n}\n\nfunc Test_UnknownKeyError(t *testing.T) {\n\tt.Parallel()\n\tok := errors.As(UnknownKeyError{}, &schema.UnknownKeyError{})\n\trequire.True(t, ok)\n}\n\nfunc Test_EmptyFieldError(t *testing.T) {\n\tt.Parallel()\n\tok := errors.As(EmptyFieldError{}, &schema.EmptyFieldError{})\n\trequire.True(t, ok)\n}\n\nfunc Test_MultiError(t *testing.T) {\n\tt.Parallel()\n\tok := errors.As(MultiError{}, &schema.MultiError{})\n\trequire.True(t, ok)\n}\n\nfunc Test_InvalidUnmarshalError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.InvalidUnmarshalError\n\tok := errors.As(&InvalidUnmarshalError{}, &e)\n\trequire.True(t, ok)\n}\n\nfunc Test_MarshalerError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.MarshalerError\n\tok := errors.As(&MarshalerError{}, &e)\n\trequire.True(t, ok)\n}\n\nfunc Test_SyntaxError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.SyntaxError\n\tok := errors.As(&SyntaxError{}, &e)\n\trequire.True(t, ok)\n}\n\nfunc Test_UnmarshalTypeError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.UnmarshalTypeError\n\tok := errors.As(&UnmarshalTypeError{}, &e)\n\trequire.True(t, ok)\n}\n\nfunc Test_UnsupportedTypeError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.UnsupportedTypeError\n\tok := errors.As(&UnsupportedTypeError{}, &e)\n\trequire.True(t, ok)\n}\n\nfunc Test_UnsupportedValeError(t *testing.T) {\n\tt.Parallel()\n\tvar e *json.UnsupportedValueError\n\tok := errors.As(&UnsupportedValueError{}, &e)\n\trequire.True(t, ok)\n}\n"
  },
  {
    "path": "errors_internal.go",
    "content": "package fiber\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\terrBindPoolTypeAssertion  = errors.New(\"failed to type-assert to *Bind\")\n\terrCustomCtxTypeAssertion = errors.New(\"failed to type-assert to CustomCtx\")\n\terrInvalidEscapeSequence  = errors.New(\"invalid escape sequence\")\n\terrRedirectTypeAssertion  = errors.New(\"failed to type-assert to *Redirect\")\n)\n"
  },
  {
    "path": "extractors/README.md",
    "content": "# Extractors Package\n\nPackage providing shared value extraction utilities for Fiber middleware packages.\n\n## Audience\n\n**This README is targeted at middleware developers and contributors.** If you are a Fiber framework user looking to use extractors in your application, please refer to the [Extractors Guide](https://docs.gofiber.io/guide/extractors) instead.\n\n## Architecture\n\n### Core Types\n\n- `Extractor`: Core extraction function with metadata\n- `Source`: Enumeration of extraction sources (Header, AuthHeader, Query, Form, Param, Cookie, Custom)\n- `ErrNotFound`: Standardized error for missing values\n\n### Extractor Structure\n\n```go\ntype Extractor struct {\n  Extract    func(fiber.Ctx) (string, error)\n  Key        string      // The parameter/header name used for extraction\n  AuthScheme string      // The auth scheme used, e.g., \"Bearer\"\n  Chain      []Extractor // For chained extractors, stores all extractors in the chain\n  Source     Source      // The type of source being extracted from\n}\n```\n\n### Available Functions\n\n- `FromAuthHeader(authScheme string)`: Extract from Authorization header with optional scheme\n- `FromCookie(key string)`: Extract from HTTP cookies\n- `FromParam(param string)`: Extract from URL path parameters\n- `FromForm(param string)`: Extract from form data\n- `FromHeader(header string)`: Extract from custom HTTP headers\n- `FromQuery(param string)`: Extract from URL query parameters\n- `FromCustom(key string, fn func(fiber.Ctx) (string, error))`: Define custom extraction logic with metadata\n- `Chain(extractors ...Extractor)`: Chain multiple extractors with fallback\n\n### Source Inspection\n\nThe `Source` field provides **security-aware extraction** by explicitly identifying the origin of extracted values. This enables middleware to enforce security policies based on data source:\n\n```go\nswitch extractor.Source {\ncase SourceAuthHeader:\n    // Authorization header - commonly used for authentication tokens\ncase SourceHeader:\n    // Custom HTTP headers - application-specific data\ncase SourceCookie:\n    // HTTP cookies - client-side stored data\ncase SourceQuery:\n    // URL query parameters - visible in URLs and logs (security consideration)\ncase SourceForm:\n    // Form data - POST body data\ncase SourceParam:\n    // URL path parameters - route-based data\ncase SourceCustom:\n    // Custom extraction logic\n}\n```\n\n### Chain Behavior\n\nThe `Chain` function implements fallback logic:\n\n- Returns first successful extraction (non-empty value, no error)\n- If all extractors fail, returns the last error encountered or `ErrNotFound`\n- **Skips extractors with `nil` Extract functions** (graceful error handling)\n- Preserves metadata from first extractor for introspection\n- Stores defensive copy for runtime inspection via the `Chain` field\n\n## Security Considerations\n\n### Source Awareness and Custom Extractors\n\nAs described in the [Source Inspection](#source-inspection) section, the `Source` field enables middleware to enforce security policies based on data source:\n\n- **CSRF Protection**: The double-submit-cookie pattern requires tokens to be submitted in both a cookie AND a form field/header. Source awareness allows CSRF middleware to verify that tokens come from both expected sources, and not for example only from cookies\n- **Authentication**: Security middleware can enforce source-specific policies (e.g., auth tokens from headers, not query parameters)\n- **Audit Trails**: Source information enables security analysis and compliance reporting\n\nHowever, when using `FromCustom`, middleware cannot determine the source of the extracted value, which can limit the ability of a middleware to provide warnings about potential security risks. Documentation and examples should clearly warn about these risks when using custom extractors.\n"
  },
  {
    "path": "extractors/extractors.go",
    "content": "package extractors\n\n// Package extractors provides shared value extraction utilities for Fiber middleware.\n// This package helps reduce code duplication across middleware packages\n// while ensuring consistent behavior, security practices, and RFC compliance.\n// It can extract string values from various HTTP request sources including\n// headers, cookies, query parameters, form data, and URL parameters.\n//\n// Example usage:\n//\n//\timport \"github.com/gofiber/fiber/v3/extractors\"\n//\n//\t// Extract from Authorization header\n//\tauthExtractor := extractors.FromAuthHeader(\"Bearer\")\n//\n//\t// Chain multiple sources with fallback\n//\ttokenExtractor := extractors.Chain(\n//\t    extractors.FromHeader(\"X-API-Key\"),\n//\t    extractors.FromCookie(\"api_key\"),\n//\t    extractors.FromQuery(\"token\"),\n//\t)\n//\n// Security considerations:\n//   - Query parameters and form data can leak sensitive information\n//   - Use HTTPS to protect extracted values in transit\n//   - Consider source-specific security policies for your use case\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Source represents the type of source from which an API key is extracted.\n// This is informational metadata that helps developers understand the extractor behavior.\ntype Source int\n\nconst (\n\t// SourceHeader indicates the value is extracted from an HTTP header.\n\tSourceHeader Source = iota\n\n\t// SourceAuthHeader indicates the value is extracted from the Authorization header.\n\tSourceAuthHeader\n\n\t// SourceForm indicates the value is extracted from form data.\n\tSourceForm\n\n\t// SourceQuery indicates the value is extracted from URL query parameters.\n\tSourceQuery\n\n\t// SourceParam indicates the value is extracted from URL path parameters.\n\tSourceParam\n\n\t// SourceCookie indicates the value is extracted from cookies.\n\tSourceCookie\n\n\t// SourceCustom indicates the value is extracted using a custom extractor function.\n\tSourceCustom\n)\n\n// ErrNotFound is returned when the requested value is missing or empty.\nvar ErrNotFound = errors.New(\"value not found\")\n\n// Extractor defines a value extraction method with metadata.\ntype Extractor struct {\n\tExtract    func(fiber.Ctx) (string, error)\n\tKey        string      // The parameter/header name used for extraction\n\tAuthScheme string      // The auth scheme used, e.g., \"Bearer\"\n\tChain      []Extractor // For chained extractors, stores all extractors in the chain\n\tSource     Source      // The type of source being extracted from\n}\n\n// FromAuthHeader extracts a value from the Authorization header with an optional prefix.\n// This function implements RFC 9110 compliant Authorization header parsing with strict token68 validation.\n//\n// RFC Compliance:\n//   - Follows RFC 9110 Section 11.6.2 for Authorization header format\n//   - Enforces 1*SP (one or more spaces) between auth-scheme and credentials\n//   - Implements RFC 7235 token68 character validation for extracted tokens\n//   - Case-insensitive auth scheme matching per HTTP standards\n//\n// Token68 Validation:\n//   - Only allows characters: A-Z, a-z, 0-9, -, ., _, ~, +, /, =\n//   - Rejects tokens containing spaces, tabs, or other whitespace\n//   - Validates proper padding: = only at end, no characters after padding starts\n//   - Prevents tokens starting with = (invalid padding)\n//\n// Security Features:\n//   - Strict validation prevents header injection attacks\n//   - Rejects malformed tokens that could bypass authentication\n//   - Consistent error handling for missing or invalid credentials\n//\n// Parameters:\n//   - authScheme: The auth scheme to strip from the header value (e.g., \"Bearer\", \"Basic\").\n//     If empty, the entire header value is returned without validation.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve and parse the Authorization header.\n//\tReturns ErrNotFound if the header is missing, malformed, or doesn't match the expected scheme.\n//\n// Examples:\n//\n//\t// Extract Bearer token with validation\n//\textractor := FromAuthHeader(\"Bearer\")\n//\t// Input: \"Bearer abc123\" -> Output: \"abc123\"\n//\t// Input: \"Bearer abc def\" -> Output: ErrNotFound (space in token)\n//\t// Input: \"Basic dXNlcjpwYXNz\" -> Output: ErrNotFound (wrong scheme)\n//\n//\t// Extract raw header value (no validation)\n//\textractor := FromAuthHeader(\"\")\n//\t// Input: \"CustomAuth token123\" -> Output: \"CustomAuth token123\"\nfunc FromAuthHeader(authScheme string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tauthHeader := c.Get(fiber.HeaderAuthorization)\n\t\t\tif authHeader == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\n\t\t\t// Check if the header starts with the specified auth scheme\n\t\t\tif authScheme != \"\" {\n\t\t\t\tschemeLen := len(authScheme)\n\t\t\t\tif len(authHeader) <= schemeLen || !utils.EqualFold(authHeader[:schemeLen], authScheme) {\n\t\t\t\t\treturn \"\", ErrNotFound\n\t\t\t\t}\n\t\t\t\trest := authHeader[schemeLen:]\n\t\t\t\tif rest == \"\" || rest[0] != ' ' {\n\t\t\t\t\treturn \"\", ErrNotFound\n\t\t\t\t}\n\n\t\t\t\t// Extract token after the required space\n\t\t\t\ttoken := rest[1:]\n\t\t\t\tif token == \"\" {\n\t\t\t\t\treturn \"\", ErrNotFound\n\t\t\t\t}\n\n\t\t\t\tif !isValidToken68(token) {\n\t\t\t\t\treturn \"\", ErrNotFound\n\t\t\t\t}\n\n\t\t\t\treturn token, nil\n\t\t\t}\n\n\t\t\treturn authHeader, nil\n\t\t},\n\t\tKey:        fiber.HeaderAuthorization,\n\t\tSource:     SourceAuthHeader,\n\t\tAuthScheme: authScheme,\n\t}\n}\n\n// FromCookie creates an Extractor that retrieves a value from a specified cookie in the request.\n//\n// The function:\n//   - Retrieves the cookie value using the specified name\n//   - Returns ErrNotFound if the cookie is missing\n//\n// Parameters:\n//   - key: The name of the cookie from which to extract the value.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve the value from the specified cookie.\n//\tReturns ErrNotFound if the cookie is not present.\n//\n// Security Note:\n//\n//\tCookies are generally more secure than query parameters for sensitive data\n//\tas they are not logged in access logs or visible in browser history.\n//\tHowever, ensure cookies are properly secured with appropriate flags.\n//\n// Example:\n//\n//\textractor := FromCookie(\"session_id\")\n//\t// Cookie: \"session_id=abc123\" -> Output: \"abc123\"\n//\t// Missing cookie -> Output: ErrNotFound\nfunc FromCookie(key string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.Cookies(key)\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t\tKey:    key,\n\t\tSource: SourceCookie,\n\t}\n}\n\n// FromParam creates an Extractor that retrieves a value from a specified URL parameter in the request.\n// URL parameters are extracted from the route path (e.g., /users/:id).\n//\n// SECURITY WARNING: Extracting values from URL parameters can leak sensitive information through:\n//   - Server access logs and error logs\n//   - Browser referrer headers when following links\n//   - Proxy and intermediary server logs\n//   - Browser history and bookmarks\n//   - Network monitoring tools\n//\n// For sensitive data, prefer FromAuthHeader, FromCookie, or FromHeader instead.\n//\n// Parameters:\n//   - param: The name of the URL parameter from which to extract the value.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve the value from the specified URL parameter.\n//\tReturns ErrNotFound if the parameter is not present.\n//\n// Example:\n//\n//\t// Route: GET /users/:userId/posts/:postId\n//\tuserExtractor := FromParam(\"userId\")\n//\tpostExtractor := FromParam(\"postId\")\n//\t// URL: /users/123/posts/456 -> userId: \"123\", postId: \"456\"\nfunc FromParam(param string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.Params(param)\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\tunescapedValue, err := url.PathUnescape(value)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn unescapedValue, nil\n\t\t},\n\t\tKey:    param,\n\t\tSource: SourceParam,\n\t}\n}\n\n// FromForm creates an Extractor that retrieves a value from a specified form field in the request.\n// Form data is typically submitted via POST requests with content-type application/x-www-form-urlencoded.\n//\n// SECURITY WARNING: Extracting values from form data can leak sensitive information through:\n//   - Server access logs and error logs\n//   - Browser referrer headers (especially if form is submitted via GET)\n//   - Proxy and intermediary server logs\n//   - Browser history (if form uses GET method)\n//\n// For sensitive data, prefer FromAuthHeader or FromCookie instead.\n// If using form data, ensure the form uses POST method and HTTPS.\n//\n// Parameters:\n//   - param: The name of the form field from which to extract the value.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve the value from the specified form field.\n//\tReturns ErrNotFound if the field is not present.\n//\n// Example:\n//\n//\textractor := FromForm(\"username\")\n//\t// Form data: \"username=john_doe&password=secret\" -> Output: \"john_doe\"\n//\t// Missing field -> Output: ErrNotFound\nfunc FromForm(param string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.FormValue(param)\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t\tKey:    param,\n\t\tSource: SourceForm,\n\t}\n}\n\n// FromHeader creates an Extractor that retrieves a value from a specified HTTP header in the request.\n// HTTP headers are commonly used for API keys, tokens, and other metadata.\n//\n// The function:\n//   - Retrieves the header value using the specified name\n//   - Returns ErrNotFound if the header is missing\n//\n// Parameters:\n//   - header: The name of the HTTP header from which to extract the value.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve the value from the specified HTTP header.\n//\tReturns ErrNotFound if the header is not present.\n//\n// Security Note:\n//\n//\tHeaders are generally secure for sensitive data as they are not logged\n//\tin access logs by default. However, be aware that some proxies may log headers.\n//\n// Example:\n//\n//\textractor := FromHeader(\"X-API-Key\")\n//\t// Header: \"X-API-Key: abc123\" -> Output: \"abc123\"\n//\t// Missing header -> Output: ErrNotFound\nfunc FromHeader(header string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.Get(header)\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t\tKey:    header,\n\t\tSource: SourceHeader,\n\t}\n}\n\n// FromQuery creates an Extractor that retrieves a value from a specified query parameter in the request.\n// Query parameters are extracted from the URL query string (e.g., ?key=value&foo=bar).\n//\n// SECURITY WARNING: Extracting values from URL query parameters can leak sensitive information through:\n//   - Server access logs and error logs\n//   - Browser referrer headers when following links\n//   - Proxy and intermediary server logs\n//   - Browser history and bookmarks\n//   - Network monitoring tools and packet sniffers\n//   - Web browser developer tools\n//\n// For sensitive data, prefer FromAuthHeader, FromCookie, or FromHeader instead.\n// If query parameters must be used, ensure HTTPS is enforced.\n//\n// Parameters:\n//   - param: The name of the query parameter from which to extract the value.\n//\n// Returns:\n//\n//\tAn Extractor that attempts to retrieve the value from the specified query parameter.\n//\tReturns ErrNotFound if the parameter is not present.\n//\n// Example:\n//\n//\textractor := FromQuery(\"token\")\n//\t// URL: /api/data?token=abc123&format=json -> Output: \"abc123\"\n//\t// URL: /api/data?format=json -> Output: ErrNotFound\nfunc FromQuery(param string) Extractor {\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.Query(param)\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t\tKey:    param,\n\t\tSource: SourceQuery,\n\t}\n}\n\n// FromCustom creates an Extractor using a provided function.\n// This allows for custom extraction logic beyond the built-in extractors.\n//\n// The function:\n//   - Accepts a custom extraction function with signature func(fiber.Ctx) (string, error)\n//   - Handles nil functions gracefully by returning ErrNotFound\n//   - Preserves the custom function for execution\n//\n// Parameters:\n//   - key: A descriptive identifier for the custom extractor.\n//     Used for debugging, logging, and Chain metadata. Should be meaningful for introspection.\n//     Examples: \"X-Custom-Header\", \"Database-Lookup\", \"Cache-Key\"\n//   - fn: The custom function to extract the value from the fiber.Ctx.\n//     If nil, the extractor will return ErrNotFound when executed.\n//     The function should return (value, nil) on success or (\"\", error) on failure.\n//\n// Returns:\n//\n//\tAn Extractor that uses the provided function for extraction.\n//\tIf fn is nil, the returned extractor will always return ErrNotFound.\n//\n// Examples:\n//\n//\t// Custom header with transformation\n//\textractor := FromCustom(\"X-API-Key\", func(c fiber.Ctx) (string, error) {\n//\t    value := c.Get(\"X-API-Key\")\n//\t    if value == \"\" {\n//\t        return \"\", ErrNotFound\n//\t    }\n//\t    return strings.ToUpper(value), nil\n//\t})\n//\n//\t// Database lookup (pseudo-code)\n//\tuserExtractor := FromCustom(\"user-from-db\", func(c fiber.Ctx) (string, error) {\n//\t    userID := c.Params(\"userId\")\n//\t    user, err := db.GetUser(userID)\n//\t    if err != nil {\n//\t        return \"\", err\n//\t    }\n//\t    return user.Name, nil\n//\t})\n//\n//\t// Conditional extraction\n//\tsmartExtractor := FromCustom(\"smart-auth\", func(c fiber.Ctx) (string, error) {\n//\t    if c.Get(\"X-Service-Auth\") != \"\" {\n//\t        return c.Get(\"X-Service-Auth\"), nil\n//\t    }\n//\t    return c.Cookies(\"session\"), nil\n//\t})\nfunc FromCustom(key string, fn func(fiber.Ctx) (string, error)) Extractor {\n\tif fn == nil {\n\t\tfn = func(fiber.Ctx) (string, error) { return \"\", ErrNotFound }\n\t}\n\treturn Extractor{\n\t\tExtract: fn,\n\t\tKey:     key,\n\t\tSource:  SourceCustom,\n\t}\n}\n\n// Chain creates an Extractor that tries multiple extractors in order until one succeeds.\n// This implements a fallback pattern where multiple extraction sources are attempted in sequence.\n//\n// The function:\n//   - Tries each extractor in the order provided\n//   - Returns the first successful extraction (non-empty value with no error)\n//   - Skips extractors with nil Extract functions\n//   - Returns the last error encountered if all extractors fail\n//   - Returns ErrNotFound if no extractors are provided or all return empty values\n//\n// Parameters:\n//   - extractors: A variadic list of Extractor instances to try in sequence.\n//     The order matters - more secure/preferred sources should be listed first.\n//\n// Returns:\n//\n//\tAn Extractor that attempts each provided extractor in order.\n//\tThe returned extractor uses the Source and Key from the first extractor for metadata.\n//\n// Behavior:\n//   - Success: Returns the first non-empty value with no error\n//   - Partial failure: Continues to next extractor if current returns error or empty value\n//   - Total failure: Returns last error encountered, or ErrNotFound if no errors\n//   - Empty chain: Always returns ErrNotFound\n//\n// Examples:\n//\n//\t// Try header first, then cookie, then query param\n//\textractor := Chain(\n//\t    FromHeader(\"Authorization\"),\n//\t    FromCookie(\"auth_token\"),\n//\t    FromQuery(\"token\"),\n//\t)\n//\n//\t// API key from multiple possible sources\n//\tapiKeyExtractor := Chain(\n//\t    FromHeader(\"X-API-Key\"),\n//\t    FromQuery(\"api_key\"),\n//\t    FromForm(\"apiKey\"),\n//\t)\n//\n// Security Note:\n//\n//\tOrder extractors by security preference. Most secure sources (headers, cookies)\n//\tshould be attempted before less secure ones (query params, form data).\nfunc Chain(extractors ...Extractor) Extractor {\n\tif len(extractors) == 0 {\n\t\treturn Extractor{\n\t\t\tExtract: func(fiber.Ctx) (string, error) {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t},\n\t\t\tSource: SourceCustom,\n\t\t\tKey:    \"\",\n\t\t\tChain:  []Extractor{},\n\t\t}\n\t}\n\n\t// Use the source and key from the first extractor as the primary\n\tprimarySource := extractors[0].Source\n\tprimaryKey := extractors[0].Key\n\n\treturn Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tvar lastErr error // last error encountered (including ErrNotFound)\n\n\t\t\tfor _, extractor := range extractors {\n\t\t\t\tif extractor.Extract == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tv, err := extractor.Extract(c)\n\t\t\t\tif err == nil && v != \"\" {\n\t\t\t\t\treturn v, nil\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tlastErr = err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif lastErr != nil {\n\t\t\t\treturn \"\", lastErr\n\t\t\t}\n\t\t\treturn \"\", ErrNotFound\n\t\t},\n\t\tSource: primarySource,\n\t\tKey:    primaryKey,\n\t\tChain:  append([]Extractor(nil), extractors...), // Defensive copy for introspection\n\t}\n}\n\n// isValidToken68 checks if a string is a valid token68 per RFC 7235/9110.\nfunc isValidToken68(token string) bool {\n\tif token == \"\" {\n\t\treturn false\n\t}\n\tpaddingStarted := false\n\tfor i := 0; i < len(token); i++ {\n\t\tc := token[i]\n\t\tswitch {\n\t\tcase (c >= 'A' && c <= 'Z') ||\n\t\t\t(c >= 'a' && c <= 'z') ||\n\t\t\t(c >= '0' && c <= '9') ||\n\t\t\tc == '-' || c == '.' || c == '_' || c == '~' || c == '+' || c == '/':\n\t\t\tif paddingStarted {\n\t\t\t\treturn false // No characters allowed after padding starts\n\t\t\t}\n\t\tcase c == '=':\n\t\t\tif i == 0 {\n\t\t\t\treturn false // Cannot start with padding\n\t\t\t}\n\t\t\tpaddingStarted = true\n\t\tdefault:\n\t\t\treturn false // Invalid character\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "extractors/extractors_test.go",
    "content": "package extractors\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// go test -run Test_Extractors_Missing\nfunc Test_Extractors_Missing(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\t// Add a route to test the missing param\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\ttoken, err := FromParam(\"token\").Extract(c)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t\treturn nil\n\t})\n\t_, err := app.Test(newRequest(fiber.MethodGet, \"/test\"))\n\trequire.NoError(t, err)\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t// Missing form\n\ttoken, err := FromForm(\"token\").Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.ErrorIs(t, err, ErrNotFound)\n\n\t// Missing query\n\ttoken, err = FromQuery(\"token\").Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.ErrorIs(t, err, ErrNotFound)\n\n\t// Missing header\n\ttoken, err = FromHeader(\"X-Token\").Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.ErrorIs(t, err, ErrNotFound)\n\n\t// Missing Auth header\n\ttoken, err = FromAuthHeader(\"Bearer\").Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.ErrorIs(t, err, ErrNotFound)\n\n\t// Missing cookie\n\ttoken, err = FromCookie(\"token\").Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.ErrorIs(t, err, ErrNotFound)\n}\n\n// newRequest creates a new *http.Request for Fiber's app.Test\nfunc newRequest(method, target string) *http.Request {\n\treq, err := http.NewRequestWithContext(context.Background(), method, target, http.NoBody)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn req\n}\n\n// go test -run Test_Extractors\nfunc Test_Extractors(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"FromParam\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/test/:token\", func(c fiber.Ctx) error {\n\t\t\ttoken, err := FromParam(\"token\").Extract(c)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"token_from_param\", token)\n\t\t\treturn nil\n\t\t})\n\t\t_, err := app.Test(newRequest(fiber.MethodGet, \"/test/token_from_param\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"FromForm\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.SetContentType(fiber.MIMEApplicationForm)\n\t\tctx.Request().Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request().SetBodyString(\"token=token_from_form\")\n\t\ttoken, err := FromForm(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_form\", token)\n\t})\n\n\tt.Run(\"FromQuery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().SetRequestURI(\"/?token=token_from_query\")\n\t\ttoken, err := FromQuery(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_query\", token)\n\t})\n\n\tt.Run(\"FromHeader\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(\"X-Token\", \"token_from_header\")\n\t\ttoken, err := FromHeader(\"X-Token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_header\", token)\n\t})\n\n\tt.Run(\"FromAuthHeader\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"Bearer token_from_auth_header\")\n\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_auth_header\", token)\n\t})\n\n\tt.Run(\"FromCookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.SetCookie(\"token\", \"token_from_cookie\")\n\t\ttoken, err := FromCookie(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_cookie\", token)\n\t})\n}\n\n// go test -run Test_Extractor_Chain\nfunc Test_Extractor_Chain(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no_extractors\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\ttoken, err := Chain().Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n\n\tt.Run(\"first_extractor_succeeds\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(\"X-Token\", \"token_from_header\")\n\t\tctx.Request().SetRequestURI(\"/?token=token_from_query\")\n\t\ttoken, err := Chain(FromHeader(\"X-Token\"), FromQuery(\"token\")).Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_header\", token)\n\t})\n\n\tt.Run(\"second_extractor_succeeds\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().SetRequestURI(\"/?token=token_from_query\")\n\t\ttoken, err := Chain(FromHeader(\"X-Token\"), FromQuery(\"token\")).Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token_from_query\", token)\n\t})\n\n\tt.Run(\"all_extractors_fail\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\ttoken, err := Chain(FromHeader(\"X-Token\"), FromQuery(\"token\")).Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n\n\tt.Run(\"empty_extractor_returns_not_found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\t// This extractor will return \"\", nil\n\t\tdummyExtractor := Extractor{\n\t\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tSource: SourceCustom,\n\t\t\tKey:    \"token\",\n\t\t}\n\t\ttoken, err := Chain(dummyExtractor).Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n}\n\n// go test -run Test_Extractor_FromAuthHeader_EdgeCases\nfunc Test_Extractor_FromAuthHeader_EdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"wrong_scheme\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"Basic dXNlcjpwYXNz\") // Basic auth instead of Bearer\n\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n\n\tt.Run(\"missing_space_after_scheme\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"Bearertoken\") // Missing space after Bearer\n\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n\n\tt.Run(\"case_insensitive_scheme_matching\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"bearer token\") // lowercase bearer\n\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token\", token)\n\t})\n}\n\n// go test -run Test_Extractor_Chain_Introspection\nfunc Test_Extractor_Chain_Introspection(t *testing.T) {\n\tt.Parallel()\n\n\t// Test chain introspection\n\textractor1 := FromHeader(\"X-Token\")\n\textractor2 := FromQuery(\"token\")\n\textractor3 := FromCookie(\"auth\")\n\n\tchainExtractor := Chain(extractor1, extractor2, extractor3)\n\n\t// Verify chain metadata\n\trequire.Equal(t, SourceHeader, chainExtractor.Source)\n\trequire.Equal(t, \"X-Token\", chainExtractor.Key)\n\trequire.Len(t, chainExtractor.Chain, 3)\n\n\t// Verify individual extractors in chain\n\trequire.Equal(t, SourceHeader, chainExtractor.Chain[0].Source)\n\trequire.Equal(t, \"X-Token\", chainExtractor.Chain[0].Key)\n\trequire.Equal(t, SourceQuery, chainExtractor.Chain[1].Source)\n\trequire.Equal(t, \"token\", chainExtractor.Chain[1].Key)\n\trequire.Equal(t, SourceCookie, chainExtractor.Chain[2].Source)\n\trequire.Equal(t, \"auth\", chainExtractor.Chain[2].Key)\n}\n\n// go test -run Test_Extractor_FromCustom\nfunc Test_Extractor_FromCustom(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"successful_extraction\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(\"X-Custom\", \"custom-value\")\n\n\t\tcustomExtractor := FromCustom(\"X-Custom\", func(c fiber.Ctx) (string, error) {\n\t\t\tvalue := c.Get(\"X-Custom\")\n\t\t\tif value == \"\" {\n\t\t\t\treturn \"\", ErrNotFound\n\t\t\t}\n\t\t\treturn strings.ToUpper(value), nil\n\t\t})\n\n\t\ttoken, err := customExtractor.Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"CUSTOM-VALUE\", token)\n\n\t\t// Verify metadata\n\t\trequire.Equal(t, SourceCustom, customExtractor.Source)\n\t\trequire.Equal(t, \"X-Custom\", customExtractor.Key)\n\t\trequire.Empty(t, customExtractor.AuthScheme)\n\t})\n\n\tt.Run(\"extraction_with_error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\terrorExtractor := FromCustom(\"test\", func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusBadRequest, \"Custom error\")\n\t\t})\n\n\t\ttoken, err := errorExtractor.Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"Custom error\")\n\t})\n\n\tt.Run(\"extraction_returning_empty_string\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\temptyExtractor := FromCustom(\"empty\", func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", nil\n\t\t})\n\n\t\ttoken, err := emptyExtractor.Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.NoError(t, err) // Should return empty string with no error\n\t})\n\n\tt.Run(\"nil_function\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\tnilExtractor := FromCustom(\"nil\", nil)\n\n\t\ttoken, err := nilExtractor.Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound) // Should return ErrNotFound for nil function\n\t})\n}\n\n// go test -run Test_Extractor_Chain_Error_Propagation\nfunc Test_Extractor_Chain_Error_Propagation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Create extractors that return different errors\n\terrorExtractor1 := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusBadRequest, \"First error\")\n\t\t},\n\t\tKey:    \"error1\",\n\t\tSource: SourceCustom,\n\t}\n\n\terrorExtractor2 := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusUnauthorized, \"Second error\")\n\t\t},\n\t\tKey:    \"error2\",\n\t\tSource: SourceCustom,\n\t}\n\n\tchainExtractor := Chain(errorExtractor1, errorExtractor2)\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\ttoken, err := chainExtractor.Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"Second error\") // Should return the last error\n\tvar fe *fiber.Error\n\trequire.ErrorAs(t, err, &fe)\n\trequire.Equal(t, fiber.StatusUnauthorized, fe.Code)\n}\n\n// go test -run Test_Extractor_Chain_With_Success\nfunc Test_Extractor_Chain_With_Success(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// First extractor fails, second succeeds\n\tfailingExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", ErrNotFound\n\t\t},\n\t\tKey:    \"fail\",\n\t\tSource: SourceCustom,\n\t}\n\n\tsuccessExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"success-token\", nil\n\t\t},\n\t\tKey:    \"success\",\n\t\tSource: SourceCustom,\n\t}\n\n\tchainExtractor := Chain(failingExtractor, successExtractor)\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\ttoken, err := chainExtractor.Extract(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"success-token\", token)\n}\n\n// go test -run Test_Extractor_FromAuthHeader_CustomScheme\nfunc Test_Extractor_FromAuthHeader_CustomScheme(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Test with custom auth scheme\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"CustomScheme my-token\")\n\n\textractor := FromAuthHeader(\"CustomScheme\")\n\ttoken, err := extractor.Extract(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"my-token\", token)\n\n\t// Verify metadata\n\trequire.Equal(t, SourceAuthHeader, extractor.Source)\n\trequire.Equal(t, fiber.HeaderAuthorization, extractor.Key)\n\trequire.Equal(t, \"CustomScheme\", extractor.AuthScheme)\n}\n\n// go test -run Test_Extractor_FromAuthHeader_WhitespaceToken\nfunc Test_Extractor_FromAuthHeader_WhitespaceToken(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Test with token containing whitespace (should be rejected per RFC 7235 token68 spec)\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"Bearer token with spaces and\\ttabs\")\n\n\textractor := FromAuthHeader(\"Bearer\")\n\ttoken, err := extractor.Extract(ctx)\n\trequire.Error(t, err)\n\trequire.ErrorIs(t, err, ErrNotFound)\n\trequire.Empty(t, token)\n\n\t// Verify metadata\n\trequire.Equal(t, SourceAuthHeader, extractor.Source)\n\trequire.Equal(t, fiber.HeaderAuthorization, extractor.Key)\n\trequire.Equal(t, \"Bearer\", extractor.AuthScheme)\n}\n\n// go test -run Test_Extractor_FromAuthHeader_RFC_Compliance\nfunc Test_Extractor_FromAuthHeader_RFC_Compliance(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\theader        string\n\t\texpectedToken string\n\t\tdescription   string\n\t\tshouldFail    bool\n\t}{\n\t\t{\n\t\t\tname:        \"tab_after_scheme\",\n\t\t\theader:      \"Bearer\\ttoken\",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"tab character after scheme should be rejected - RFC specifies 1*SP, not tabs\",\n\t\t},\n\t\t{\n\t\t\tname:          \"single_space_after_scheme\",\n\t\t\theader:        \"Bearer token\",\n\t\t\tshouldFail:    false,\n\t\t\texpectedToken: \"token\",\n\t\t\tdescription:   \"single space after scheme should be accepted - standard format\",\n\t\t},\n\t\t{\n\t\t\tname:        \"multiple_spaces_after_scheme\",\n\t\t\theader:      \"Bearer  token\",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"multiple spaces after scheme rejected for simplicity - single space is standard\",\n\t\t},\n\t\t{\n\t\t\tname:        \"mixed_whitespace_after_scheme\",\n\t\t\theader:      \"Bearer \\t \\ttoken\",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"mixed whitespace after scheme should be rejected - RFC specifies 1*SP, not tabs\",\n\t\t},\n\t\t{\n\t\t\tname:        \"no_whitespace_after_scheme\",\n\t\t\theader:      \"Bearertoken\",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"no whitespace after scheme should fail\",\n\t\t},\n\t\t{\n\t\t\tname:        \"header_too_short\",\n\t\t\theader:      \"Bearer\",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"header too short for scheme + space + token\",\n\t\t},\n\t\t{\n\t\t\tname:        \"only_whitespace_after_scheme\",\n\t\t\theader:      \"Bearer   \\t  \",\n\t\t\tshouldFail:  true,\n\t\t\tdescription: \"only whitespace after scheme should fail\",\n\t\t},\n\t\t{\n\t\t\tname:          \"case_insensitive_scheme\",\n\t\t\theader:        \"BEARER token\",\n\t\t\tshouldFail:    false,\n\t\t\texpectedToken: \"token\",\n\t\t\tdescription:   \"case-insensitive scheme matching should work\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, tc.header)\n\t\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\n\t\t\tif tc.shouldFail {\n\t\t\t\trequire.Error(t, err, \"Expected error for %s\", tc.description)\n\t\t\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t\t\t\trequire.Empty(t, token)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Expected no error for %s\", tc.description)\n\t\t\t\trequire.Equal(t, tc.expectedToken, token)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Special case for case-insensitive scheme matching with different extractor scheme\n\tt.Run(\"case_insensitive_extractor_scheme\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"BEARER token\")\n\t\ttoken, err := FromAuthHeader(\"bearer\").Extract(ctx) // lowercase extractor scheme\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token\", token)\n\t})\n}\n\n// go test -run Test_Extractor_FromAuthHeader_Token68_Validation\nfunc Test_Extractor_FromAuthHeader_Token68_Validation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Test valid token68 characters (should pass)\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"Bearer ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~+/=\")\n\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~+/=\", token)\n\n\t// Test tokens with spaces (should fail)\n\ttestCases := []struct {\n\t\tname        string\n\t\theader      string\n\t\tdescription string\n\t\tshouldFail  bool\n\t}{\n\t\t{name: \"space_in_token\", header: \"Bearer abc def\", shouldFail: true, description: \"space in token\"},\n\t\t{name: \"space_after_scheme\", header: \"Bearer  abc\", shouldFail: true, description: \"multiple spaces after scheme\"},\n\t\t{name: \"no_space_after_scheme\", header: \"Bearertoken\", shouldFail: true, description: \"no space after scheme\"},\n\t\t{name: \"only_scheme\", header: \"Bearer\", shouldFail: true, description: \"only scheme, no token\"},\n\t\t{name: \"tab_after_scheme\", header: \"Bearer\\ttoken\", shouldFail: true, description: \"tab after scheme\"},\n\t\t{name: \"tab_in_token\", header: \"Bearer abc\\tdef\", shouldFail: true, description: \"tab in token\"},\n\t\t{name: \"newline_in_token\", header: \"Bearer abc\\ndef\", shouldFail: true, description: \"newline in token\"},\n\t\t{name: \"leading_space_in_token\", header: \"Bearer  abc\", shouldFail: true, description: \"leading space in token after scheme space\"},\n\t\t{name: \"trailing_space_in_token\", header: \"Bearer abc \", shouldFail: true, description: \"trailing space in token\"},\n\t\t{name: \"comma_in_token\", header: \"Bearer abc,def\", shouldFail: true, description: \"comma in token\"},\n\t\t{name: \"semicolon_in_token\", header: \"Bearer abc;def\", shouldFail: true, description: \"semicolon in token\"},\n\t\t{name: \"quote_in_token\", header: \"Bearer abc\\\"def\", shouldFail: true, description: \"quote in token\"},\n\t\t{name: \"bracket_in_token\", header: \"Bearer abc[def\", shouldFail: true, description: \"bracket in token\"},\n\t\t{name: \"equals_at_start\", header: \"Bearer =abc\", shouldFail: true, description: \"equals at start of token\"},\n\t\t{name: \"equals_in_middle\", header: \"Bearer ab=cd\", shouldFail: true, description: \"equals in middle of token\"},\n\t\t{name: \"valid_equals_at_end\", header: \"Bearer abc=\", shouldFail: false, description: \"valid equals at end\"},\n\t\t{name: \"valid_double_equals\", header: \"Bearer abc==\", shouldFail: false, description: \"valid double equals at end\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, tc.header)\n\n\t\t\ttoken, err := FromAuthHeader(\"Bearer\").Extract(ctx)\n\n\t\t\tif tc.shouldFail {\n\t\t\t\trequire.Error(t, err, \"Expected error for %s\", tc.description)\n\t\t\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t\t\t\trequire.Empty(t, token)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Expected no error for %s\", tc.description)\n\t\t\t\trequire.NotEmpty(t, token)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -run Test_Extractor_FromAuthHeader_NoScheme\nfunc Test_Extractor_FromAuthHeader_NoScheme(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"returns_header_value\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.Set(fiber.HeaderAuthorization, \"some-token-value\")\n\n\t\textractor := FromAuthHeader(\"\") // No scheme\n\t\ttoken, err := extractor.Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"some-token-value\", token)\n\n\t\t// Verify metadata\n\t\trequire.Equal(t, SourceAuthHeader, extractor.Source)\n\t\trequire.Equal(t, fiber.HeaderAuthorization, extractor.Key)\n\t\trequire.Empty(t, extractor.AuthScheme)\n\t})\n\n\tt.Run(\"empty_header_returns_not_found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\t// No Authorization header set\n\n\t\textractor := FromAuthHeader(\"\") // No scheme\n\t\ttoken, err := extractor.Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.ErrorIs(t, err, ErrNotFound)\n\t})\n}\n\n// go test -run Test_Extractor_Chain_NilFunctions\nfunc Test_Extractor_Chain_NilFunctions(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Test chain with nil extractor functions\n\tnilExtractor := Extractor{\n\t\tExtract: nil,\n\t\tKey:     \"nil\",\n\t\tSource:  SourceCustom,\n\t}\n\n\tvalidExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"valid-token\", nil\n\t\t},\n\t\tKey:    \"valid\",\n\t\tSource: SourceCustom,\n\t}\n\n\tchainExtractor := Chain(nilExtractor, validExtractor)\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\ttoken, err := chainExtractor.Extract(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"valid-token\", token)\n}\n\n// go test -run Test_Extractor_Chain_AllErrors\nfunc Test_Extractor_Chain_AllErrors(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// Test chain where all extractors return errors\n\terrorExtractor1 := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusUnauthorized, \"First auth error\")\n\t\t},\n\t\tKey:    \"error1\",\n\t\tSource: SourceCustom,\n\t}\n\n\terrorExtractor2 := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusForbidden, \"Second auth error\")\n\t\t},\n\t\tKey:    \"error2\",\n\t\tSource: SourceCustom,\n\t}\n\n\tchainExtractor := Chain(errorExtractor1, errorExtractor2)\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\ttoken, err := chainExtractor.Extract(ctx)\n\trequire.Empty(t, token)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"Second auth error\") // Should return last error\n\n\tvar fe *fiber.Error\n\trequire.ErrorAs(t, err, &fe)\n\trequire.Equal(t, fiber.StatusForbidden, fe.Code)\n}\n\n// go test -run Test_Extractor_Chain_MixedScenarios\nfunc Test_Extractor_Chain_MixedScenarios(t *testing.T) {\n\tt.Parallel()\n\n\t// Define reusable extractors\n\tfailingExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", ErrNotFound\n\t\t},\n\t\tKey:    \"fail\",\n\t\tSource: SourceCustom,\n\t}\n\n\terrorExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusBadRequest, \"Bad request\")\n\t\t},\n\t\tKey:    \"error\",\n\t\tSource: SourceCustom,\n\t}\n\n\tsuccessExtractor := Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"success\", nil\n\t\t},\n\t\tKey:    \"success\",\n\t\tSource: SourceCustom,\n\t}\n\n\tt.Run(\"error_then_success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\tchain := Chain(errorExtractor, successExtractor)\n\t\ttoken, err := chain.Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"success\", token)\n\t})\n\n\tt.Run(\"fail_then_error_then_success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\tchain := Chain(failingExtractor, errorExtractor, successExtractor)\n\t\ttoken, err := chain.Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"success\", token)\n\t})\n\n\tt.Run(\"fail_then_error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\t\tchain := Chain(failingExtractor, errorExtractor)\n\t\ttoken, err := chain.Extract(ctx)\n\t\trequire.Empty(t, token)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"Bad request\")\n\t})\n}\n\n// go test -run Test_Extractor_SourceTypes\nfunc Test_Extractor_SourceTypes(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"individual_extractor_sources\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Test that all source types are properly set\n\t\trequire.Equal(t, SourceHeader, FromHeader(\"test\").Source)\n\t\trequire.Equal(t, SourceAuthHeader, FromAuthHeader(\"Bearer\").Source)\n\t\trequire.Equal(t, SourceAuthHeader, FromAuthHeader(\"\").Source) // Empty scheme should still be SourceAuthHeader\n\t\trequire.Equal(t, SourceForm, FromForm(\"test\").Source)\n\t\trequire.Equal(t, SourceQuery, FromQuery(\"test\").Source)\n\t\trequire.Equal(t, SourceParam, FromParam(\"test\").Source)\n\t\trequire.Equal(t, SourceCookie, FromCookie(\"test\").Source)\n\t\trequire.Equal(t, SourceCustom, FromCustom(\"test\", func(_ fiber.Ctx) (string, error) { return \"test\", nil }).Source)\n\t})\n\n\tt.Run(\"chain_source_metadata\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Test chain source (should use first extractor's source)\n\t\tchain := Chain(FromHeader(\"X-Test\"), FromQuery(\"test\"))\n\t\trequire.Equal(t, SourceHeader, chain.Source)\n\t\trequire.Equal(t, \"X-Test\", chain.Key)\n\t})\n}\n\n// go test -run Test_Extractor_URL_Encoded\nfunc Test_Extractor_URL_Encoded(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"FromQuery_with_spaces\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().SetRequestURI(\"/?token=token%20with%20spaces\")\n\t\ttoken, err := FromQuery(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token with spaces\", token) // Should be URL-decoded automatically by fasthttp\n\t})\n\n\tt.Run(\"FromForm_with_plus\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tctx.Request().Header.SetContentType(fiber.MIMEApplicationForm)\n\t\tctx.Request().Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request().SetBodyString(\"token=token%2Bwith%2Bplus\")\n\t\ttoken, err := FromForm(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"token+with+plus\", token) // URL-decoded\n\t})\n\n\tt.Run(\"FromQuery_base64_encoded\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\t\tbase64Value := \"cGFzc3dvcmQ%3D\" // URL-encoded base64 \"cGFzc3dvcmQ=\"\n\t\tctx.Request().SetRequestURI(\"/?token=\" + base64Value)\n\t\ttoken, err := FromQuery(\"token\").Extract(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"cGFzc3dvcmQ=\", token) // Should be URL-decoded\n\t})\n\n\tt.Run(\"FromParam_with_slashes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/test/:token\", func(c fiber.Ctx) error {\n\t\t\ttoken, extractErr := FromParam(\"token\").Extract(c)\n\t\t\trequire.NoError(t, extractErr)\n\t\t\trequire.Equal(t, \"token/with/slashes\", token)\n\t\t\treturn nil\n\t\t})\n\t\t_, err := app.Test(newRequest(fiber.MethodGet, \"/test/token%2Fwith%2Fslashes\"))\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_isValidToken68(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tname  string\n\t\ttoken string\n\t\twant  bool\n\t}{\n\t\t{name: \"empty string\", token: \"\", want: false},\n\t\t{name: \"single uppercase\", token: \"A\", want: true},\n\t\t{name: \"single lowercase\", token: \"a\", want: true},\n\t\t{name: \"single digit\", token: \"0\", want: true},\n\t\t{name: \"all allowed symbols except =\", token: \"-._~+/\", want: true},\n\t\t{name: \"letters and digits\", token: \"token68\", want: true},\n\t\t{name: \"equals at end\", token: \"token=\", want: true},\n\t\t{name: \"multiple equals\", token: \"token==\", want: true},\n\t\t{name: \"equals at start\", token: \"=token\", want: false},\n\t\t{name: \"equals in middle\", token: \"tok=en\", want: false},\n\t\t{name: \"equals not at end with other chars\", token: \"token=extra\", want: false},\n\t\t{name: \"space in token\", token: \"token space\", want: false},\n\t\t{name: \"tab character in token\", token: \"token\\ttab\", want: false},\n\t\t{name: \"invalid symbol\", token: \"token@\", want: false},\n\t\t{name: \"valid token68\", token: \"token68\", want: true},\n\t\t{token: \"token68=\", want: true, name: \"valid token68 with equals at end\"},\n\t\t{token: \"token68==\", want: true, name: \"multiple equals at end\"},\n\t\t{token: \"token68=extra\", want: false, name: \"equals followed by extra chars\"},\n\t\t{token: \"T0ken-._~+/=\", want: true, name: \"all allowed chars with equals at end\"},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := isValidToken68(tc.token)\n\t\t\tif got != tc.want {\n\t\t\t\tt.Errorf(\"isValidToken68(%q) = %v, want %v\", tc.token, got, tc.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gofiber/fiber/v3\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/schema v1.7.0\n\tgithub.com/gofiber/utils/v2 v2.0.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/mattn/go-colorable v0.1.14\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/shamaton/msgpack/v3 v3.1.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tinylib/msgp v1.6.3\n\tgithub.com/valyala/bytebufferpool v1.0.0\n\tgithub.com/valyala/fasthttp v1.69.0\n\tgolang.org/x/crypto v0.49.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // direct\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.35.0\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=\ngithub.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=\ngithub.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=\ngithub.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=\ngithub.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=\ngithub.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=\ngithub.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "group.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// Group represents a collection of routes that share middleware and a common\n// path prefix.\ntype Group struct {\n\tapp         *App\n\tparentGroup *Group\n\tname        string\n\n\tPrefix          string\n\tanyRouteDefined bool\n}\n\n// Name Assign name to specific route or group itself.\n//\n// If this method is used before any route added to group, it'll set group name and OnGroupNameHook will be used.\n// Otherwise, it'll set route name and OnName hook will be used.\nfunc (grp *Group) Name(name string) Router {\n\tif grp.anyRouteDefined {\n\t\tgrp.app.Name(name)\n\n\t\treturn grp\n\t}\n\n\tgrp.app.mutex.Lock()\n\tif grp.parentGroup != nil {\n\t\tgrp.name = grp.parentGroup.name + name\n\t} else {\n\t\tgrp.name = name\n\t}\n\n\tif err := grp.app.hooks.executeOnGroupNameHooks(*grp); err != nil {\n\t\tpanic(err)\n\t}\n\tgrp.app.mutex.Unlock()\n\n\treturn grp\n}\n\n// Use registers a middleware route that will match requests\n// with the provided prefix (which is optional and defaults to \"/\").\n// Also, you can pass another app instance as a sub-router along a routing path.\n// It's very useful to split up a large API as many independent routers and\n// compose them as a single service using Use. The fiber's error handler and\n// any of the fiber's sub apps are added to the application's error handlers\n// to be invoked on errors that happen within the prefix route.\n//\n//\t\tapp.Use(func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t\tapp.Use(\"/api\", func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t\tapp.Use(\"/api\", handler, func(c fiber.Ctx) error {\n//\t\t     return c.Next()\n//\t\t})\n//\t \tsubApp := fiber.New()\n//\t\tapp.Use(\"/mounted-path\", subApp)\n//\n// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...\nfunc (grp *Group) Use(args ...any) Router {\n\tvar subApp *App\n\tvar prefix string\n\tvar prefixes []string\n\tvar handlers []Handler\n\n\tfor i := range args {\n\t\tswitch arg := args[i].(type) {\n\t\tcase string:\n\t\t\tprefix = arg\n\t\tcase *App:\n\t\t\tsubApp = arg\n\t\tcase []string:\n\t\t\tprefixes = arg\n\t\tdefault:\n\t\t\thandler, ok := toFiberHandler(arg)\n\t\t\tif !ok {\n\t\t\t\tpanic(fmt.Sprintf(\"use: invalid handler %v\\n\", reflect.TypeOf(arg)))\n\t\t\t}\n\t\t\thandlers = append(handlers, handler)\n\t\t}\n\t}\n\n\tif len(prefixes) == 0 {\n\t\tprefixes = append(prefixes, prefix)\n\t}\n\n\tfor _, prefix := range prefixes {\n\t\tif subApp != nil {\n\t\t\treturn grp.mount(prefix, subApp)\n\t\t}\n\n\t\tgrp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, handlers...)\n\t}\n\n\tif !grp.anyRouteDefined {\n\t\tgrp.anyRouteDefined = true\n\t}\n\n\treturn grp\n}\n\n// Get registers a route for GET methods that requests a representation\n// of the specified resource. Requests using GET should only retrieve data.\nfunc (grp *Group) Get(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodGet}, path, handler, handlers...)\n}\n\n// Head registers a route for HEAD methods that asks for a response identical\n// to that of a GET request, but without the response body.\nfunc (grp *Group) Head(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodHead}, path, handler, handlers...)\n}\n\n// Post registers a route for POST methods that is used to submit an entity to the\n// specified resource, often causing a change in state or side effects on the server.\nfunc (grp *Group) Post(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodPost}, path, handler, handlers...)\n}\n\n// Put registers a route for PUT methods that replaces all current representations\n// of the target resource with the request payload.\nfunc (grp *Group) Put(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodPut}, path, handler, handlers...)\n}\n\n// Delete registers a route for DELETE methods that deletes the specified resource.\nfunc (grp *Group) Delete(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodDelete}, path, handler, handlers...)\n}\n\n// Connect registers a route for CONNECT methods that establishes a tunnel to the\n// server identified by the target resource.\nfunc (grp *Group) Connect(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodConnect}, path, handler, handlers...)\n}\n\n// Options registers a route for OPTIONS methods that is used to describe the\n// communication options for the target resource.\nfunc (grp *Group) Options(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodOptions}, path, handler, handlers...)\n}\n\n// Trace registers a route for TRACE methods that performs a message loop-back\n// test along the path to the target resource.\nfunc (grp *Group) Trace(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodTrace}, path, handler, handlers...)\n}\n\n// Patch registers a route for PATCH methods that is used to apply partial\n// modifications to a resource.\nfunc (grp *Group) Patch(path string, handler any, handlers ...any) Router {\n\treturn grp.Add([]string{MethodPatch}, path, handler, handlers...)\n}\n\n// Add allows you to specify multiple HTTP methods to register a route.\n// The provided handlers are executed in order, starting with `handler` and then the variadic `handlers`.\nfunc (grp *Group) Add(methods []string, path string, handler any, handlers ...any) Router {\n\tconverted := collectHandlers(\"group\", append([]any{handler}, handlers...)...)\n\tgrp.app.register(methods, getGroupPath(grp.Prefix, path), grp, converted...)\n\tif !grp.anyRouteDefined {\n\t\tgrp.anyRouteDefined = true\n\t}\n\n\treturn grp\n}\n\n// All will register the handler on all HTTP methods\nfunc (grp *Group) All(path string, handler any, handlers ...any) Router {\n\t_ = grp.Add(grp.app.config.RequestMethods, path, handler, handlers...)\n\treturn grp\n}\n\n// Group is used for Routes with common prefix to define a new sub-router with optional middleware.\n//\n//\tapi := app.Group(\"/api\")\n//\tapi.Get(\"/users\", handler)\nfunc (grp *Group) Group(prefix string, handlers ...any) Router {\n\tprefix = getGroupPath(grp.Prefix, prefix)\n\tif len(handlers) > 0 {\n\t\tconverted := collectHandlers(\"group\", handlers...)\n\t\tgrp.app.register([]string{methodUse}, prefix, grp, converted...)\n\t}\n\n\t// Create new group\n\tnewGrp := &Group{Prefix: prefix, app: grp.app, parentGroup: grp}\n\tif err := grp.app.hooks.executeOnGroupHooks(*newGrp); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn newGrp\n}\n\n// RouteChain creates a Registering instance scoped to the group's prefix,\n// allowing chained route declarations for the same path.\nfunc (grp *Group) RouteChain(path string) Register {\n\t// Create new group\n\tregister := &Registering{app: grp.app, group: grp, path: getGroupPath(grp.Prefix, path)}\n\n\treturn register\n}\n\n// Route is used to define routes with a common prefix inside the supplied\n// function. It mirrors the legacy helper and reuses the Group method to create\n// a sub-router.\nfunc (grp *Group) Route(prefix string, fn func(router Router), name ...string) Router {\n\tif fn == nil {\n\t\tpanic(\"route handler 'fn' cannot be nil\")\n\t}\n\t// Create new group\n\tgroup := grp.Group(prefix)\n\tif len(name) > 0 {\n\t\tgroup.Name(name[0])\n\t}\n\n\t// Define routes\n\tfn(group)\n\n\treturn group\n}\n"
  },
  {
    "path": "helpers.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// acceptedType is a struct that holds the parsed value of an Accept header\n// along with quality, specificity, parameters, and order.\n// Used for sorting accept headers.\ntype acceptedType struct {\n\tparams      headerParams\n\tspec        string\n\tquality     float64\n\tspecificity int\n\torder       int\n}\n\nconst noCacheValue = \"no-cache\"\n\n// Pre-allocated byte slices for accept header parsing\nvar (\n\tsemicolonQEquals = []byte(\";q=\")\n\twildcardAll      = []byte(\"*/*\")\n\twildcardSuffix   = []byte(\"/*\")\n)\n\ntype headerParams map[string][]byte\n\n// ValueFromContext retrieves a value stored under key from supported context types.\n//\n// Supported context types:\n//   - Ctx (including CustomCtx implementations)\n//   - *fasthttp.RequestCtx\n//   - context.Context\nfunc ValueFromContext[T any](ctx, key any) (T, bool) {\n\tswitch typed := ctx.(type) {\n\tcase Ctx:\n\t\tval, ok := typed.Locals(key).(T)\n\t\treturn val, ok\n\tcase *fasthttp.RequestCtx:\n\t\tval, ok := typed.UserValue(key).(T)\n\t\treturn val, ok\n\tcase context.Context:\n\t\tval, ok := typed.Value(key).(T)\n\t\treturn val, ok\n\tdefault:\n\t\tvar zero T\n\t\treturn zero, false\n\t}\n}\n\n// StoreInContext stores key/value in both Fiber locals and request context.\n//\n// This is useful when values need to be available via both c.Locals() and\n// context.Context lookups throughout middleware and handlers.\nfunc StoreInContext(c Ctx, key, value any) {\n\tc.Locals(key, value)\n\n\tif c.App().config.PassLocalsToContext {\n\t\tc.SetContext(context.WithValue(c.Context(), key, value))\n\t}\n}\n\n// getTLSConfig returns a net listener's tls config\nfunc getTLSConfig(ln net.Listener) *tls.Config {\n\tif ln == nil {\n\t\treturn nil\n\t}\n\n\ttype tlsConfigProvider interface {\n\t\tTLSConfig() *tls.Config\n\t}\n\n\ttype configProvider interface {\n\t\tConfig() *tls.Config\n\t}\n\n\tif provider, ok := ln.(tlsConfigProvider); ok {\n\t\treturn provider.TLSConfig()\n\t}\n\n\tif provider, ok := ln.(configProvider); ok {\n\t\treturn provider.Config()\n\t}\n\n\tpointer := reflect.ValueOf(ln)\n\tif !pointer.IsValid() {\n\t\treturn nil\n\t}\n\n\t// Reflection fallback for listeners that do not expose a TLS config method.\n\tval := reflect.Indirect(pointer)\n\tif !val.IsValid() {\n\t\treturn nil\n\t}\n\n\tfield := val.FieldByName(\"config\")\n\tif !field.IsValid() {\n\t\treturn nil\n\t}\n\n\tif field.Type() != reflect.TypeFor[*tls.Config]() {\n\t\treturn nil\n\t}\n\n\tif field.CanInterface() {\n\t\tif cfg, ok := field.Interface().(*tls.Config); ok {\n\t\t\treturn cfg\n\t\t}\n\t\treturn nil\n\t}\n\n\tif !field.CanAddr() {\n\t\treturn nil\n\t}\n\n\tvalue := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() //nolint:gosec // Access to unexported field is required for listeners that don't expose TLS config methods.\n\tif !value.IsValid() {\n\t\treturn nil\n\t}\n\n\tcfg, ok := value.Interface().(*tls.Config)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn cfg\n}\n\n// readContent opens a named file and read content from it\nfunc readContent(rf io.ReaderFrom, name string) (int64, error) {\n\t// Read file\n\tf, err := os.Open(filepath.Clean(name))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to open: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err = f.Close(); err != nil {\n\t\t\tlog.Errorf(\"Error closing file: %s\", err)\n\t\t}\n\t}()\n\tn, readErr := rf.ReadFrom(f)\n\tif readErr != nil {\n\t\treturn n, fmt.Errorf(\"failed to read: %w\", readErr)\n\t}\n\treturn n, nil\n}\n\n// quoteString escapes special characters using percent-encoding.\n// Non-ASCII bytes are encoded as well so the result is always ASCII.\nfunc (app *App) quoteString(raw string) string {\n\tbb := bytebufferpool.Get()\n\tquoted := app.toString(fasthttp.AppendQuotedArg(bb.B, app.toBytes(raw)))\n\tbytebufferpool.Put(bb)\n\treturn quoted\n}\n\n// quoteRawString escapes only characters that need quoting according to\n// https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4 so the result may\n// contain non-ASCII bytes.\nfunc (app *App) quoteRawString(raw string) string {\n\tconst hex = \"0123456789ABCDEF\"\n\tbb := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(bb)\n\n\tfor i := 0; i < len(raw); i++ {\n\t\tc := raw[i]\n\t\tswitch {\n\t\tcase c == '\\\\' || c == '\"':\n\t\t\t// escape backslash and quote\n\t\t\tbb.B = append(bb.B, '\\\\', c)\n\t\tcase c == '\\n':\n\t\t\tbb.B = append(bb.B, '\\\\', 'n')\n\t\tcase c == '\\r':\n\t\t\tbb.B = append(bb.B, '\\\\', 'r')\n\t\tcase c < 0x20 || c == 0x7f:\n\t\t\t// percent-encode control and DEL\n\t\t\tbb.B = append(bb.B,\n\t\t\t\t'%',\n\t\t\t\thex[c>>4],\n\t\t\t\thex[c&0x0f],\n\t\t\t)\n\t\tdefault:\n\t\t\tbb.B = append(bb.B, c)\n\t\t}\n\t}\n\n\treturn app.toString(bb.B)\n}\n\n// isASCII reports whether the provided string contains only ASCII characters.\n// See: https://www.rfc-editor.org/rfc/rfc0020\nfunc (*App) isASCII(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tif s[i] > 127 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// uniqueRouteStack drop all not unique routes from the slice\nfunc uniqueRouteStack(stack []*Route) []*Route {\n\tm := make(map[*Route]struct{}, len(stack))\n\tunique := make([]*Route, 0, len(stack))\n\tfor _, v := range stack {\n\t\tif _, ok := m[v]; !ok {\n\t\t\tm[v] = struct{}{}\n\t\t\tunique = append(unique, v)\n\t\t}\n\t}\n\n\treturn unique\n}\n\n// defaultString returns the value or a default value if it is set\nfunc defaultString(value string, defaultValue []string) string {\n\tif value == \"\" && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn value\n}\n\nfunc getGroupPath(prefix, path string) string {\n\tif path == \"\" {\n\t\treturn prefix\n\t}\n\n\tif path[0] != '/' {\n\t\tpath = \"/\" + path\n\t}\n\n\treturn utils.TrimRight(prefix, '/') + path\n}\n\n// acceptsOffer determines if an offer matches a given specification.\n// It supports a trailing '*' wildcard and performs case-insensitive exact matching.\n// Returns true if the offer matches the specification, false otherwise.\nfunc acceptsOffer(spec, offer string, _ headerParams) bool {\n\tif len(spec) >= 1 && spec[len(spec)-1] == '*' {\n\t\tprefix := spec[:len(spec)-1]\n\t\tif len(offer) < len(prefix) {\n\t\t\treturn false\n\t\t}\n\t\treturn utils.EqualFold(prefix, offer[:len(prefix)])\n\t}\n\n\treturn utils.EqualFold(spec, offer)\n}\n\n// acceptsLanguageOfferBasic determines if a language tag offer matches a range\n// according to RFC 4647 Basic Filtering.\n// A match occurs if the range exactly equals the tag or is a prefix of the tag\n// followed by a hyphen. The comparison is case-insensitive. Only a single \"*\"\n// as the entire range is allowed. Any \"*\" appearing after a hyphen renders the\n// range invalid and will not match.\nfunc acceptsLanguageOfferBasic(spec, offer string, _ headerParams) bool {\n\tif spec == \"*\" {\n\t\treturn true\n\t}\n\tif strings.IndexByte(spec, '*') >= 0 {\n\t\treturn false\n\t}\n\tif utils.EqualFold(spec, offer) {\n\t\treturn true\n\t}\n\treturn len(offer) > len(spec) &&\n\t\tutils.EqualFold(offer[:len(spec)], spec) &&\n\t\toffer[len(spec)] == '-'\n}\n\n// acceptsLanguageOfferExtended determines if a language tag offer matches a\n// range according to RFC 4647 Extended Filtering (§3.3.2).\n// - Case-insensitive comparisons\n// - '*' matches zero or more subtags (can \"slide\")\n// - Unspecified subtags are treated like '*' (so trailing/extraneous tag subtags are fine)\n// - Matching fails if sliding encounters a singleton (incl. 'x')\nfunc acceptsLanguageOfferExtended(spec, offer string, _ headerParams) bool {\n\tif spec == \"*\" {\n\t\treturn true\n\t}\n\tif spec == \"\" || offer == \"\" {\n\t\treturn false\n\t}\n\n\t// Use stack-allocated arrays to avoid heap allocations for typical language tags\n\tvar rsBuf, tsBuf [8]string\n\trs := rsBuf[:0]\n\tts := tsBuf[:0]\n\n\t// Parse spec subtags without allocation for typical cases\n\tfor s := range strings.SplitSeq(spec, \"-\") {\n\t\trs = append(rs, s)\n\t}\n\t// Parse offer subtags without allocation for typical cases\n\tfor s := range strings.SplitSeq(offer, \"-\") {\n\t\tts = append(ts, s)\n\t}\n\n\t// Step 2: first subtag must match (or be '*')\n\tif rs[0] != \"*\" && !utils.EqualFold(rs[0], ts[0]) {\n\t\treturn false\n\t}\n\n\ti, j := 1, 1 // i = range index, j = tag index\n\tfor i < len(rs) {\n\t\tif rs[i] == \"*\" { // 3.A: '*' matches zero or more subtags\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\tif j >= len(ts) { // 3.B: ran out of tag subtags\n\t\t\treturn false\n\t\t}\n\t\tif utils.EqualFold(rs[i], ts[j]) { // 3.C: exact subtag match\n\t\t\ti++\n\t\t\tj++\n\t\t\tcontinue\n\t\t}\n\t\t// 3.D: singleton barrier (one letter or digit, incl. 'x')\n\t\tif len(ts[j]) == 1 {\n\t\t\treturn false\n\t\t}\n\t\t// 3.E: slide forward in the tag and try again\n\t\tj++\n\t}\n\t// 4: matched all range subtags\n\treturn true\n}\n\n// acceptsOfferType This function determines if an offer type matches a given specification.\n// It checks if the specification is equal to */* (i.e., all types are accepted).\n// It gets the MIME type of the offer (either from the offer itself or by its file extension).\n// It checks if the offer MIME type matches the specification MIME type or if the specification is of the form <MIME_type>/* and the offer MIME type has the same MIME type.\n// It checks if the offer contains every parameter present in the specification.\n// Returns true if the offer type matches the specification, false otherwise.\nfunc acceptsOfferType(spec, offerType string, specParams headerParams) bool {\n\tvar offerMime, offerParams string\n\n\tif i := strings.IndexByte(offerType, ';'); i == -1 {\n\t\tofferMime = offerType\n\t} else {\n\t\tofferMime = offerType[:i]\n\t\tofferParams = offerType[i:]\n\t}\n\n\t// Accept: */*\n\tif spec == \"*/*\" {\n\t\treturn paramsMatch(specParams, offerParams)\n\t}\n\n\tvar mimetype string\n\tif strings.IndexByte(offerMime, '/') != -1 {\n\t\tmimetype = offerMime // MIME type\n\t} else {\n\t\tmimetype = utils.GetMIME(offerMime) // extension\n\t}\n\n\tif spec == mimetype {\n\t\t// Accept: <MIME_type>/<MIME_subtype>\n\t\treturn paramsMatch(specParams, offerParams)\n\t}\n\n\ts := strings.IndexByte(mimetype, '/')\n\tspecSlash := strings.IndexByte(spec, '/')\n\t// Accept: <MIME_type>/*\n\tif s != -1 && specSlash != -1 {\n\t\tif utils.EqualFold(spec[:specSlash], mimetype[:s]) && (spec[specSlash:] == \"/*\" || mimetype[s:] == \"/*\") {\n\t\t\treturn paramsMatch(specParams, offerParams)\n\t\t}\n\t}\n\n\treturn false\n}\n\n// paramsMatch returns whether offerParams contains all parameters present in specParams.\n// Matching is case-insensitive, and surrounding quotes are stripped.\n// To align with the behavior of res.format from Express, the order of parameters is\n// ignored, and if a parameter is specified twice in the incoming Accept, the last\n// provided value is given precedence.\n// In the case of quoted values, RFC 9110 says that we must treat any character escaped\n// by a backslash as equivalent to the character itself (e.g., \"a\\aa\" is equivalent to \"aaa\").\n// For the sake of simplicity, we forgo this and compare the value as-is. Besides, it would\n// be highly unusual for a client to escape something other than a double quote or backslash.\n// See https://www.rfc-editor.org/rfc/rfc9110#name-parameters\nfunc paramsMatch(specParamStr headerParams, offerParams string) bool {\n\tif len(specParamStr) == 0 {\n\t\treturn true\n\t}\n\n\tallSpecParamsMatch := true\n\tfor specParam, specVal := range specParamStr {\n\t\tfoundParam := false\n\t\tfasthttp.VisitHeaderParams(utils.UnsafeBytes(offerParams), func(key, value []byte) bool {\n\t\t\tif utils.EqualFold(specParam, utils.UnsafeString(key)) {\n\t\t\t\tfoundParam = true\n\t\t\t\tunescaped, err := unescapeHeaderValue(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tallSpecParamsMatch = false\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tallSpecParamsMatch = utils.EqualFold(specVal, unescaped)\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif !foundParam || !allSpecParamsMatch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn allSpecParamsMatch\n}\n\n// getSplicedStrList function takes a string and a string slice as an argument, divides the string into different\n// elements divided by ',' and stores these elements in the string slice.\n// It returns the populated string slice as an output.\n//\n// If the given slice hasn't enough space, it will allocate more and return.\nfunc getSplicedStrList(headerValue string, dst []string) []string {\n\tif headerValue == \"\" {\n\t\treturn nil\n\t}\n\n\tdst = dst[:0]\n\tsegmentStart := 0\n\tfor i := 0; i < len(headerValue); i++ {\n\t\tif headerValue[i] == ',' {\n\t\t\tdst = append(dst, utils.TrimSpace(headerValue[segmentStart:i]))\n\t\t\tsegmentStart = i + 1\n\t\t}\n\t}\n\tdst = append(dst, utils.TrimSpace(headerValue[segmentStart:]))\n\n\treturn dst\n}\n\nfunc joinHeaderValues(headers [][]byte) []byte {\n\tswitch len(headers) {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn headers[0]\n\tdefault:\n\t\treturn bytes.Join(headers, []byte{','})\n\t}\n}\n\nfunc unescapeHeaderValue(v []byte) ([]byte, error) {\n\tif bytes.IndexByte(v, '\\\\') == -1 {\n\t\treturn v, nil\n\t}\n\tres := make([]byte, 0, len(v))\n\tescaping := false\n\tfor i, c := range v {\n\t\tif escaping {\n\t\t\tres = append(res, c)\n\t\t\tescaping = false\n\t\t\tcontinue\n\t\t}\n\t\tif c == '\\\\' {\n\t\t\t// invalid escape at end of string\n\t\t\tif i == len(v)-1 {\n\t\t\t\treturn nil, errInvalidEscapeSequence\n\t\t\t}\n\t\t\tescaping = true\n\t\t\tcontinue\n\t\t}\n\t\tres = append(res, c)\n\t}\n\tif escaping {\n\t\treturn nil, errInvalidEscapeSequence\n\t}\n\treturn res, nil\n}\n\n// forEachMediaRange parses an Accept or Content-Type header, calling functor\n// on each media range.\n// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields\nfunc forEachMediaRange(header []byte, functor func([]byte)) {\n\thasDQuote := bytes.IndexByte(header, '\"') != -1\n\n\tfor len(header) > 0 {\n\t\tn := 0\n\t\theader = utils.TrimLeft(header, ' ')\n\t\tquotes := 0\n\t\tescaping := false\n\n\t\tif hasDQuote {\n\t\t\t// Complex case. We need to keep track of quotes and quoted-pairs (i.e.,  characters escaped with \\ )\n\t\tloop:\n\t\t\tfor n < len(header) {\n\t\t\t\tswitch header[n] {\n\t\t\t\tcase ',':\n\t\t\t\t\tif quotes%2 == 0 {\n\t\t\t\t\t\tbreak loop\n\t\t\t\t\t}\n\t\t\t\tcase '\"':\n\t\t\t\t\tif !escaping {\n\t\t\t\t\t\tquotes++\n\t\t\t\t\t}\n\t\t\t\tcase '\\\\':\n\t\t\t\t\tif quotes%2 == 1 {\n\t\t\t\t\t\tescaping = !escaping\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t// all other characters are ignored\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\t\t} else {\n\t\t\t// Simple case. Just look for the next comma.\n\t\t\tif n = bytes.IndexByte(header, ','); n == -1 {\n\t\t\t\tn = len(header)\n\t\t\t}\n\t\t}\n\n\t\tfunctor(header[:n])\n\n\t\tif n >= len(header) {\n\t\t\treturn\n\t\t}\n\t\theader = header[n+1:]\n\t}\n}\n\n// Pool for headerParams instances. The headerParams object *must*\n// be cleared before being returned to the pool.\nvar headerParamPool = sync.Pool{\n\tNew: func() any {\n\t\treturn make(headerParams)\n\t},\n}\n\n// getOffer return valid offer for header negotiation.\nfunc getOffer(header []byte, isAccepted func(spec, offer string, specParams headerParams) bool, offers ...string) string {\n\tif len(offers) == 0 {\n\t\treturn \"\"\n\t}\n\tif len(header) == 0 {\n\t\treturn offers[0]\n\t}\n\n\tacceptedTypes := make([]acceptedType, 0, 8)\n\torder := 0\n\n\t// Parse header and get accepted types with their quality and specificity\n\t// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields\n\tforEachMediaRange(header, func(accept []byte) {\n\t\torder++\n\t\tspec, quality := accept, 1.0\n\t\tvar params headerParams\n\n\t\tif i := bytes.IndexByte(accept, ';'); i != -1 {\n\t\t\tspec = accept[:i]\n\n\t\t\t// Optimized quality parsing\n\t\t\tqIndex := i + 3\n\t\t\tif bytes.HasPrefix(accept[i:], semicolonQEquals) && bytes.IndexByte(accept[qIndex:], ';') == -1 {\n\t\t\t\tif q, err := fasthttp.ParseUfloat(accept[qIndex:]); err == nil {\n\t\t\t\t\tquality = q\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tparams, _ = headerParamPool.Get().(headerParams) //nolint:errcheck // only contains headerParams\n\t\t\t\tfor k := range params {\n\t\t\t\t\tdelete(params, k)\n\t\t\t\t}\n\t\t\t\tfasthttp.VisitHeaderParams(accept[i:], func(key, value []byte) bool {\n\t\t\t\t\tif len(key) == 1 && key[0] == 'q' {\n\t\t\t\t\t\tif q, err := fasthttp.ParseUfloat(value); err == nil {\n\t\t\t\t\t\t\tquality = q\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tlowerKey := utils.UnsafeString(utilsbytes.UnsafeToLower(key))\n\t\t\t\t\tval, err := unescapeHeaderValue(value)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tparams[lowerKey] = val\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Skip this accept type if quality is 0.0\n\t\t\t// See: https://www.rfc-editor.org/rfc/rfc9110#quality.values\n\t\t\tif quality == 0.0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tspec = utils.TrimSpace(spec)\n\n\t\t// Determine specificity\n\t\tvar specificity int\n\n\t\t// check for wildcard this could be a mime */* or a wildcard character *\n\t\tswitch {\n\t\tcase len(spec) == 1 && spec[0] == '*':\n\t\t\tspecificity = 1\n\t\tcase bytes.Equal(spec, wildcardAll):\n\t\t\tspecificity = 1\n\t\tcase bytes.HasSuffix(spec, wildcardSuffix):\n\t\t\tspecificity = 2\n\t\tcase bytes.IndexByte(spec, '/') != -1:\n\t\t\tspecificity = 3\n\t\tdefault:\n\t\t\tspecificity = 4\n\t\t}\n\n\t\t// Add to accepted types\n\t\tacceptedTypes = append(acceptedTypes, acceptedType{\n\t\t\tspec:        utils.UnsafeString(spec),\n\t\t\tquality:     quality,\n\t\t\tspecificity: specificity,\n\t\t\torder:       order,\n\t\t\tparams:      params,\n\t\t})\n\t})\n\n\tif len(acceptedTypes) > 1 {\n\t\t// Sort accepted types by quality and specificity, preserving order of equal elements\n\t\tsortAcceptedTypes(acceptedTypes)\n\t}\n\n\t// Find the first offer that matches the accepted types\n\tfor _, acceptedType := range acceptedTypes {\n\t\tfor _, offer := range offers {\n\t\t\tif offer == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isAccepted(acceptedType.spec, offer, acceptedType.params) {\n\t\t\t\tif acceptedType.params != nil {\n\t\t\t\t\theaderParamPool.Put(acceptedType.params)\n\t\t\t\t}\n\t\t\t\treturn offer\n\t\t\t}\n\t\t}\n\t\tif acceptedType.params != nil {\n\t\t\theaderParamPool.Put(acceptedType.params)\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements\n// A type with parameters has higher priority than an equivalent one without parameters.\n// e.g., text/html;a=1;b=2 comes before text/html;a=1\n// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields\nfunc sortAcceptedTypes(at []acceptedType) {\n\tfor i := 1; i < len(at); i++ {\n\t\tlo, hi := 0, i-1\n\t\tfor lo <= hi {\n\t\t\tmid := (lo + hi) / 2\n\t\t\tif at[i].quality < at[mid].quality ||\n\t\t\t\t(at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) ||\n\t\t\t\t(at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) < len(at[mid].params)) ||\n\t\t\t\t(at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) {\n\t\t\t\tlo = mid + 1\n\t\t\t} else {\n\t\t\t\thi = mid - 1\n\t\t\t}\n\t\t}\n\t\tfor j := i; j > lo; j-- {\n\t\t\tat[j-1], at[j] = at[j], at[j-1]\n\t\t}\n\t}\n}\n\n// normalizeEtag validates an entity tag and returns the\n// value without quotes. weak is true if the tag has the \"W/\" prefix.\nfunc normalizeEtag(t string) (value string, weak, ok bool) { //nolint:nonamedreturns // gocritic unnamedResult requires naming the parsed ETag components\n\tweak = strings.HasPrefix(t, \"W/\")\n\tif weak {\n\t\tt = t[2:]\n\t}\n\n\tif len(t) < 2 || t[0] != '\"' || t[len(t)-1] != '\"' {\n\t\treturn \"\", weak, false\n\t}\n\treturn t[1 : len(t)-1], weak, true\n}\n\n// matchEtag performs a weak comparison of entity tags according to\n// RFC 9110 §8.8.3.2. The weak indicator (\"W/\") is ignored, but both tags must\n// be properly quoted. Invalid tags result in a mismatch.\nfunc matchEtag(s, etag string) bool {\n\tn1, _, ok1 := normalizeEtag(s)\n\tn2, _, ok2 := normalizeEtag(etag)\n\tif !ok1 || !ok2 {\n\t\treturn false\n\t}\n\n\treturn n1 == n2\n}\n\n// matchEtagStrong performs a strong entity-tag comparison following\n// RFC 9110 §8.8.3.1. A weak tag never matches a strong one, even if the quoted\n// values are identical.\nfunc matchEtagStrong(s, etag string) bool {\n\tn1, w1, ok1 := normalizeEtag(s)\n\tn2, w2, ok2 := normalizeEtag(etag)\n\tif !ok1 || !ok2 || w1 || w2 {\n\t\treturn false\n\t}\n\n\treturn n1 == n2\n}\n\n// isEtagStale reports whether a response with the given ETag would be considered\n// stale when presented with the raw If-None-Match header value. Comparison is\n// weak as defined by RFC 9110 §8.8.3.2.\nfunc (app *App) isEtagStale(etag string, noneMatchBytes []byte) bool {\n\tvar start, end int\n\theader := utils.TrimSpace(app.toString(noneMatchBytes))\n\n\t// Short-circuit the wildcard case: \"*\" never counts as stale.\n\tif header == \"*\" {\n\t\treturn false\n\t}\n\n\t// Adapted from:\n\t// https://github.com/jshttp/fresh/blob/master/index.js#L110\n\tfor i := range noneMatchBytes {\n\t\tswitch noneMatchBytes[i] {\n\t\tcase 0x20:\n\t\t\tif start == end {\n\t\t\t\tstart = i + 1\n\t\t\t\tend = i + 1\n\t\t\t}\n\t\tcase 0x2c:\n\t\t\tif matchEtag(app.toString(noneMatchBytes[start:end]), etag) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tstart = i + 1\n\t\t\tend = i + 1\n\t\tdefault:\n\t\t\tend = i + 1\n\t\t}\n\t}\n\n\treturn !matchEtag(app.toString(noneMatchBytes[start:end]), etag)\n}\n\nfunc parseAddr(raw string) (host, port string) { //nolint:nonamedreturns // gocritic unnamedResult requires naming host and port parts for clarity\n\tif raw == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\n\traw = utils.TrimSpace(raw)\n\n\t// Handle IPv6 addresses enclosed in brackets as defined by RFC 3986\n\tif strings.HasPrefix(raw, \"[\") {\n\t\tif end := strings.IndexByte(raw, ']'); end != -1 {\n\t\t\thost = raw[:end+1] // keep the closing ]\n\t\t\tif len(raw) > end+1 && raw[end+1] == ':' {\n\t\t\t\treturn host, raw[end+2:]\n\t\t\t}\n\t\t\treturn host, \"\"\n\t\t}\n\t}\n\n\t// Everything else with a colon\n\tif i := strings.LastIndexByte(raw, ':'); i != -1 {\n\t\thost, port = raw[:i], raw[i+1:]\n\n\t\t// If “host” still contains ':', we must have hit an un-bracketed IPv6\n\t\t// literal. In that form a port is impossible, so treat the whole thing\n\t\t// as host.\n\t\tif strings.IndexByte(host, ':') >= 0 {\n\t\t\treturn raw, \"\"\n\t\t}\n\t\treturn host, port\n\t}\n\n\t// No colon, nothing to split\n\treturn raw, \"\"\n}\n\n// isNoCache checks if the cacheControl header value contains a `no-cache` directive.\n// Per RFC 9111 §5.2.2.4, no-cache can appear as either:\n// - \"no-cache\" (applies to entire response)\n// - \"no-cache=field-name\" (applies to specific header field)\n// Both forms indicate the response should not be served from cache without revalidation.\nfunc isNoCache(cacheControl string) bool {\n\tn := len(cacheControl)\n\tif n < len(noCacheValue) {\n\t\treturn false\n\t}\n\n\tconst noCacheLen = len(noCacheValue)\n\tconst asciiCaseFold = byte(0x20)\n\tfor i := 0; i <= n-noCacheLen; i++ {\n\t\tif (cacheControl[i] | asciiCaseFold) != 'n' {\n\t\t\tcontinue\n\t\t}\n\t\tif !matchNoCacheToken(cacheControl, i) {\n\t\t\tcontinue\n\t\t}\n\t\tif i > 0 && !isNoCacheDelimiter(cacheControl[i-1]) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Handle: \"no-cache\", \"no-cache, ...\", \"no-cache=...\", \"no-cache ,\"\n\t\tif i+noCacheLen == n {\n\t\t\treturn true\n\t\t}\n\t\tif isNoCacheDelimiter(cacheControl[i+noCacheLen]) || cacheControl[i+noCacheLen] == '=' {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc isNoCacheDelimiter(c byte) bool {\n\treturn c == ' ' || c == '\\t' || c == ','\n}\n\nfunc matchNoCacheToken(s string, i int) bool {\n\t// ASCII-only case-insensitive compare for \"no-cache\".\n\tconst asciiCaseFold = byte(0x20)\n\tb := s[i:]\n\n\treturn (b[0]|asciiCaseFold) == 'n' &&\n\t\t(b[1]|asciiCaseFold) == 'o' &&\n\t\tb[2] == '-' &&\n\t\t(b[3]|asciiCaseFold) == 'c' &&\n\t\t(b[4]|asciiCaseFold) == 'a' &&\n\t\t(b[5]|asciiCaseFold) == 'c' &&\n\t\t(b[6]|asciiCaseFold) == 'h' &&\n\t\t(b[7]|asciiCaseFold) == 'e'\n}\n\nvar errTestConnClosed = errors.New(\"testConn is closed\")\n\ntype testConn struct {\n\tr        bytes.Buffer\n\tw        bytes.Buffer\n\tisClosed bool\n\tsync.Mutex\n}\n\n// Read implements net.Conn by reading from the buffered input.\nfunc (c *testConn) Read(b []byte) (int, error) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\treturn c.r.Read(b) //nolint:wrapcheck // This must not be wrapped\n}\n\n// Write implements net.Conn by appending to the buffered output.\nfunc (c *testConn) Write(b []byte) (int, error) {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tif c.isClosed {\n\t\treturn 0, errTestConnClosed\n\t}\n\treturn c.w.Write(b) //nolint:wrapcheck // This must not be wrapped\n}\n\n// Close marks the connection as closed and prevents further writes.\nfunc (c *testConn) Close() error {\n\tc.Lock()\n\tdefer c.Unlock()\n\n\tc.isClosed = true\n\treturn nil\n}\n\n// LocalAddr implements net.Conn and returns a placeholder address.\nfunc (*testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: \"\", IP: net.IPv4zero} }\n\n// RemoteAddr implements net.Conn and returns a placeholder address.\nfunc (*testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: \"\", IP: net.IPv4zero} }\n\n// SetDeadline implements net.Conn but is a no-op for the in-memory connection.\nfunc (*testConn) SetDeadline(_ time.Time) error { return nil }\n\n// SetReadDeadline implements net.Conn but is a no-op for the in-memory connection.\nfunc (*testConn) SetReadDeadline(_ time.Time) error { return nil }\n\n// SetWriteDeadline implements net.Conn but is a no-op for the in-memory connection.\nfunc (*testConn) SetWriteDeadline(_ time.Time) error { return nil }\n\nfunc toStringImmutable(b []byte) string {\n\treturn string(b)\n}\n\nfunc toBytesImmutable(s string) []byte {\n\treturn []byte(s)\n}\n\n// HTTP methods and their unique INTs\nfunc (app *App) methodInt(s string) int {\n\t// For better performance\n\tif len(app.configured.RequestMethods) == 0 {\n\t\tswitch s {\n\t\tcase MethodGet:\n\t\t\treturn methodGet\n\t\tcase MethodHead:\n\t\t\treturn methodHead\n\t\tcase MethodPost:\n\t\t\treturn methodPost\n\t\tcase MethodPut:\n\t\t\treturn methodPut\n\t\tcase MethodDelete:\n\t\t\treturn methodDelete\n\t\tcase MethodConnect:\n\t\t\treturn methodConnect\n\t\tcase MethodOptions:\n\t\t\treturn methodOptions\n\t\tcase MethodTrace:\n\t\t\treturn methodTrace\n\t\tcase MethodPatch:\n\t\t\treturn methodPatch\n\t\tdefault:\n\t\t\treturn -1\n\t\t}\n\t}\n\t// For method customization\n\treturn slices.Index(app.config.RequestMethods, s)\n}\n\nfunc (app *App) method(methodInt int) string {\n\treturn app.config.RequestMethods[methodInt]\n}\n\n// IsMethodSafe reports whether the HTTP method is considered safe.\n// See https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1\nfunc IsMethodSafe(m string) bool {\n\tswitch m {\n\tcase MethodGet,\n\t\tMethodHead,\n\t\tMethodOptions,\n\t\tMethodTrace:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// IsMethodIdempotent reports whether the HTTP method is considered idempotent.\n// See https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.2\nfunc IsMethodIdempotent(m string) bool {\n\tif IsMethodSafe(m) {\n\t\treturn true\n\t}\n\n\tswitch m {\n\tcase MethodPut, MethodDelete:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Convert a string value to a specified type, handling errors and optional default values.\nfunc Convert[T any](value string, converter func(string) (T, error), defaultValue ...T) (T, error) {\n\tconverted, err := converter(value)\n\tif err != nil {\n\t\tif len(defaultValue) > 0 {\n\t\t\treturn defaultValue[0], nil\n\t\t}\n\n\t\treturn converted, fmt.Errorf(\"failed to convert: %w\", err)\n\t}\n\n\treturn converted, nil\n}\n\nvar (\n\terrParsedEmptyString = errors.New(\"parsed result is empty string\")\n\terrParsedEmptyBytes  = errors.New(\"parsed result is empty bytes\")\n\terrParsedType        = errors.New(\"unsupported generic type\")\n)\n\nfunc genericParseType[V GenericType](str string) (V, error) {\n\tvar v V\n\tswitch any(v).(type) {\n\tcase int:\n\t\tresult, err := utils.ParseInt(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse int: %w\", err)\n\t\t}\n\t\treturn any(int(result)).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase int8:\n\t\tresult, err := utils.ParseInt8(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse int8: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase int16:\n\t\tresult, err := utils.ParseInt16(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse int16: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase int32:\n\t\tresult, err := utils.ParseInt32(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse int32: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase int64:\n\t\tresult, err := utils.ParseInt(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse int64: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase uint:\n\t\tresult, err := utils.ParseUint(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse uint: %w\", err)\n\t\t}\n\t\treturn any(uint(result)).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase uint8:\n\t\tresult, err := utils.ParseUint8(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse uint8: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase uint16:\n\t\tresult, err := utils.ParseUint16(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse uint16: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase uint32:\n\t\tresult, err := utils.ParseUint32(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse uint32: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase uint64:\n\t\tresult, err := utils.ParseUint(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse uint64: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase float32:\n\t\tresult, err := utils.ParseFloat32(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse float32: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase float64:\n\t\tresult, err := utils.ParseFloat64(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse float64: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase bool:\n\t\tresult, err := strconv.ParseBool(str)\n\t\tif err != nil {\n\t\t\treturn v, fmt.Errorf(\"failed to parse bool: %w\", err)\n\t\t}\n\t\treturn any(result).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase string:\n\t\tif str == \"\" {\n\t\t\treturn v, errParsedEmptyString\n\t\t}\n\t\treturn any(str).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tcase []byte:\n\t\tif str == \"\" {\n\t\t\treturn v, errParsedEmptyBytes\n\t\t}\n\t\treturn any([]byte(str)).(V), nil //nolint:errcheck,forcetypeassert // not needed\n\tdefault:\n\t\treturn v, errParsedType\n\t}\n}\n\n// GenericType enumerates the values that can be parsed from strings by the\n// generic helper functions.\ntype GenericType interface {\n\tGenericTypeInteger | GenericTypeFloat | bool | string | []byte\n}\n\n// GenericTypeInteger is the union of all supported integer types.\ntype GenericTypeInteger interface {\n\tGenericTypeIntegerSigned | GenericTypeIntegerUnsigned\n}\n\n// GenericTypeIntegerSigned is the union of supported signed integer types.\ntype GenericTypeIntegerSigned interface {\n\tint | int8 | int16 | int32 | int64\n}\n\n// GenericTypeIntegerUnsigned is the union of supported unsigned integer types.\ntype GenericTypeIntegerUnsigned interface {\n\tuint | uint8 | uint16 | uint32 | uint64\n}\n\n// GenericTypeFloat is the union of supported floating-point types.\ntype GenericTypeFloat interface {\n\tfloat32 | float64\n}\n"
  },
  {
    "path": "helpers_fuzz_test.go",
    "content": "//go:build go1.18\n\npackage fiber\n\nimport (\n\t\"testing\"\n)\n\n// go test -v -run=^$ -fuzz=FuzzUtilsGetOffer\nfunc FuzzUtilsGetOffer(f *testing.F) {\n\tinputs := []string{\n\t\t`application/json; v=1; foo=bar; q=0.938; extra=param, text/plain;param=\"big fox\"; q=0.43`,\n\t\t`text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8`,\n\t\t`*/*`,\n\t\t`text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`,\n\t}\n\tfor _, input := range inputs {\n\t\tf.Add(input)\n\t}\n\tf.Fuzz(func(_ *testing.T, spec string) {\n\t\tgetOffer([]byte(spec), acceptsOfferType, `application/json;version=1;v=1;foo=bar`, `text/plain;param=\"big fox\"`)\n\t})\n}\n"
  },
  {
    "path": "helpers_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📝 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_Utils_GetOffer(t *testing.T) {\n\tt.Parallel()\n\trequire.Empty(t, getOffer([]byte(\"hello\"), acceptsOffer))\n\trequire.Equal(t, \"1\", getOffer([]byte(\"\"), acceptsOffer, \"1\"))\n\trequire.Empty(t, getOffer([]byte(\"2\"), acceptsOffer, \"1\"))\n\n\trequire.Empty(t, getOffer([]byte(\"\"), acceptsOfferType))\n\trequire.Empty(t, getOffer([]byte(\"text/html\"), acceptsOfferType))\n\trequire.Empty(t, getOffer([]byte(\"text/html\"), acceptsOfferType, \"application/json\"))\n\trequire.Empty(t, getOffer([]byte(\"text/html;q=0\"), acceptsOfferType, \"text/html\"))\n\trequire.Empty(t, getOffer([]byte(\"application/json, */*; q=0\"), acceptsOfferType, \"image/png\"))\n\trequire.Equal(t, \"application/xml\", getOffer([]byte(\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"), acceptsOfferType, \"application/xml\", \"application/json\"))\n\trequire.Equal(t, \"text/html\", getOffer([]byte(\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"), acceptsOfferType, \"text/html\"))\n\trequire.Equal(t, \"application/pdf\", getOffer([]byte(\"text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000\"), acceptsOfferType, \"application/pdf\", \"application/json\"))\n\trequire.Equal(t, \"application/pdf\", getOffer([]byte(\"text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000\"), acceptsOfferType, \"application/pdf\", \"application/json\"))\n\trequire.Equal(t, \"text/plain;a=1\", getOffer([]byte(\"text/plain;a=1\"), acceptsOfferType, \"text/plain;a=1\"))\n\trequire.Empty(t, getOffer([]byte(\"text/plain;a=1;b=2\"), acceptsOfferType, \"text/plain;b=2\"))\n\n\t// Spaces, quotes, out of order params, and case insensitivity\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain  \"), acceptsOfferType, \"text/plain\"))\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain;q=0.4  \"), acceptsOfferType, \"text/plain\"))\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain;q=0.4  ;\"), acceptsOfferType, \"text/plain\"))\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain;q=0.4  ; p=foo\"), acceptsOfferType, \"text/plain\"))\n\trequire.Equal(t, \"text/plain;b=2;a=1\", getOffer([]byte(\"text/plain ;a=1;b=2\"), acceptsOfferType, \"text/plain;b=2;a=1\"))\n\trequire.Equal(t, \"text/plain;a=1\", getOffer([]byte(\"text/plain;   a=1   \"), acceptsOfferType, \"text/plain;a=1\"))\n\trequire.Equal(t, `text/plain;a=\"1;b=2\\\",text/plain\"`, getOffer([]byte(`text/plain;a=\"1;b=2\\\",text/plain\";q=0.9`), acceptsOfferType, `text/plain;a=1;b=2`, `text/plain;a=\"1;b=2\\\",text/plain\"`))\n\trequire.Equal(t, \"text/plain;A=CAPS\", getOffer([]byte(`text/plain;a=\"caPs\"`), acceptsOfferType, \"text/plain;A=CAPS\"))\n\n\t// Priority\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain\"), acceptsOfferType, \"text/plain\", \"text/plain;a=1\"))\n\trequire.Equal(t, \"text/plain;a=1\", getOffer([]byte(\"text/plain\"), acceptsOfferType, \"text/plain;a=1\", \"\", \"text/plain\"))\n\trequire.Equal(t, \"text/plain;a=1\", getOffer([]byte(\"text/plain,text/plain;a=1\"), acceptsOfferType, \"text/plain\", \"text/plain;a=1\"))\n\trequire.Equal(t, \"text/plain\", getOffer([]byte(\"text/plain;q=0.899,text/plain;a=1;q=0.898\"), acceptsOfferType, \"text/plain\", \"text/plain;a=1\"))\n\trequire.Equal(t, \"text/plain;a=1;b=2\", getOffer([]byte(\"text/plain,text/plain;a=1,text/plain;a=1;b=2\"), acceptsOfferType, \"text/plain\", \"text/plain;a=1\", \"text/plain;a=1;b=2\"))\n\n\t// Takes the last value specified\n\trequire.Equal(t, \"text/plain;a=1;b=2\", getOffer([]byte(\"text/plain;a=1;b=1;B=2\"), acceptsOfferType, \"text/plain;a=1;b=1\", \"text/plain;a=1;b=2\"))\n\n\trequire.Empty(t, getOffer([]byte(\"utf-8, iso-8859-1;q=0.5\"), acceptsOffer))\n\trequire.Empty(t, getOffer([]byte(\"utf-8, iso-8859-1;q=0.5\"), acceptsOffer, \"ascii\"))\n\trequire.Equal(t, \"utf-8\", getOffer([]byte(\"utf-8, iso-8859-1;q=0.5\"), acceptsOffer, \"utf-8\"))\n\trequire.Equal(t, \"iso-8859-1\", getOffer([]byte(\"utf-8;q=0, iso-8859-1;q=0.5\"), acceptsOffer, \"utf-8\", \"iso-8859-1\"))\n\n\t// Accept-Charset wildcard coverage\n\trequire.Equal(t, \"utf-8\", getOffer([]byte(\"utf-*\"), acceptsOffer, \"utf-8\"))\n\trequire.Equal(t, \"UTF-16\", getOffer([]byte(\"utf-*\"), acceptsOffer, \"UTF-16\", \"iso-8859-1\"))\n\trequire.Empty(t, getOffer([]byte(\"utf-*\"), acceptsOffer, \"iso-8859-1\"))\n\trequire.Empty(t, getOffer([]byte(\"utf-*\"), acceptsOffer, \"utf\"))\n\trequire.Empty(t, getOffer([]byte(\"utf-*\"), acceptsOffer, \"x-utf-8\"))\n\n\t// Complex wildcard negotiation\n\trequire.Equal(t, \"utf-16le\", getOffer([]byte(\"utf-8;q=0.4, utf-*;q=0.8, iso-8859-1;q=0.6\"), acceptsOffer, \"iso-8859-1\", \"utf-16le\"))\n\trequire.Equal(t, \"iso-8859-1\", getOffer([]byte(\"utf-*;q=0.9, iso-8859-1;q=1\"), acceptsOffer, \"x-utf-16\", \"iso-8859-1\"))\n\trequire.Empty(t, getOffer([]byte(\"utf-*;q=0.5, iso-8859-1;q=0.4\"), acceptsOffer, \"ascii\", \"us-ascii\"))\n\n\trequire.Equal(t, \"deflate\", getOffer([]byte(\"gzip, deflate\"), acceptsOffer, \"deflate\"))\n\trequire.Empty(t, getOffer([]byte(\"gzip, deflate;q=0\"), acceptsOffer, \"deflate\"))\n\n\t// Accept-Language Basic Filtering\n\trequire.True(t, acceptsLanguageOfferBasic(\"en\", \"en-US\", nil))\n\trequire.False(t, acceptsLanguageOfferBasic(\"en-US\", \"en\", nil))\n\trequire.True(t, acceptsLanguageOfferBasic(\"EN\", \"en-us\", nil))\n\trequire.False(t, acceptsLanguageOfferBasic(\"en\", \"en_US\", nil))\n\trequire.Equal(t, \"en-US\", getOffer([]byte(\"fr-CA;q=0.8, en-US\"), acceptsLanguageOfferBasic, \"en-US\", \"fr-CA\"))\n\trequire.Empty(t, getOffer([]byte(\"xx\"), acceptsLanguageOfferBasic, \"en\"))\n\trequire.False(t, acceptsLanguageOfferBasic(\"en-*\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferBasic(\"*\", \"en-US\", nil))\n\n\t// Accept-Language Extended Filtering\n\trequire.True(t, acceptsLanguageOfferExtended(\"en\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"en\", \"en-Latn-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"en-*\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"*-US\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"en-US-*\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"en-*\", \"en-US-CA\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"en-US\", \"en-GB\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"fr\", \"en-US\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"\", \"en-US\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"en\", \"\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"*\", \"en-US\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"en-*\", \"en\", nil))\n\trequire.Equal(t, \"en-US\", getOffer([]byte(\"fr-CA;q=0.8, en-*\"), acceptsLanguageOfferExtended, \"en-US\", \"fr-CA\"))\n\n\t// Sliding and singleton barriers\n\trequire.True(t, acceptsLanguageOfferExtended(\"de-*-DE\", \"de-DE\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"de-*-DE\", \"de-DE-x-goethe\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"de-*-DE\", \"de-Latn-DE-1996\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"de-*-DE\", \"de\", nil))\n\trequire.False(t, acceptsLanguageOfferExtended(\"de-*-DE\", \"de-x-DE\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"*-CH\", \"de-CH\", nil))\n\trequire.True(t, acceptsLanguageOfferExtended(\"*-CH\", \"de-Latn-CH\", nil))\n}\n\nfunc Test_ReadContentReturnsBytes(t *testing.T) {\n\tt.Parallel()\n\n\tcontent := []byte(\"fiber read content test\")\n\ttempFile, err := os.CreateTemp(\"\", \"fiber-read-content-*.txt\")\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, os.Remove(tempFile.Name()))\n\t})\n\n\t_, err = tempFile.Write(content)\n\trequire.NoError(t, err)\n\trequire.NoError(t, tempFile.Close())\n\n\tvar buffer bytes.Buffer\n\tn, err := readContent(&buffer, tempFile.Name())\n\trequire.NoError(t, err)\n\trequire.Equal(t, int64(len(content)), n)\n\trequire.Equal(t, content, buffer.Bytes())\n}\n\ntype wrappedListener struct {\n\tnet.Listener\n}\n\ntype tlsConfigMethodListener struct {\n\tnet.Listener\n\tcfg *tls.Config\n}\n\nfunc (ln *tlsConfigMethodListener) TLSConfig() *tls.Config {\n\treturn ln.cfg\n}\n\ntype configMethodListener struct {\n\tnet.Listener\n\tcfg *tls.Config\n}\n\nfunc (ln *configMethodListener) Config() *tls.Config {\n\treturn ln.cfg\n}\n\nfunc Test_GetTLSConfig(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"tls listener\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := newLocalListener(t)\n\t\tcfg := &tls.Config{MinVersion: tls.VersionTLS12}\n\t\ttlsListener := tls.NewListener(base, cfg)\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, tlsListener.Close())\n\t\t})\n\n\t\trequire.Same(t, cfg, getTLSConfig(tlsListener), \"*tls.Listener should expose its TLS config\")\n\t})\n\n\tt.Run(\"wrapped tls listener\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := newLocalListener(t)\n\t\tcfg := &tls.Config{MinVersion: tls.VersionTLS13}\n\t\ttlsListener := tls.NewListener(base, cfg)\n\t\twrapped := &wrappedListener{Listener: tlsListener}\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, wrapped.Close())\n\t\t})\n\n\t\trequire.Nil(t, getTLSConfig(wrapped), \"wrapping without Config()-like methods should return nil\")\n\t})\n\n\tt.Run(\"listener with tls config method\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := newLocalListener(t)\n\t\tcfg := &tls.Config{MinVersion: tls.VersionTLS13}\n\t\tlistener := &tlsConfigMethodListener{Listener: base, cfg: cfg}\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, listener.Close())\n\t\t})\n\n\t\trequire.Same(t, cfg, getTLSConfig(listener), \"TLSConfig() should be preferred for TLS discovery\")\n\t})\n\n\tt.Run(\"listener with config method\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := newLocalListener(t)\n\t\tcfg := &tls.Config{MinVersion: tls.VersionTLS12}\n\t\tlistener := &configMethodListener{Listener: base, cfg: cfg}\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, listener.Close())\n\t\t})\n\n\t\trequire.Same(t, cfg, getTLSConfig(listener), \"Config() should be preferred for TLS discovery\")\n\t})\n\n\tt.Run(\"non tls listener\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := newLocalListener(t)\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, base.Close())\n\t\t})\n\n\t\trequire.Nil(t, getTLSConfig(base), \"plain listeners should not report TLS config\")\n\t})\n}\n\nfunc newLocalListener(t *testing.T) net.Listener {\n\tt.Helper()\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\treturn ln\n}\n\n// go test -v -run=^$ -bench=Benchmark_Utils_GetOffer -benchmem -count=4\nfunc Benchmark_Utils_GetOffer(b *testing.B) {\n\ttestCases := []struct {\n\t\tdescription string\n\t\taccept      string\n\t\toffers      []string\n\t}{\n\t\t{\n\t\t\tdescription: \"simple\",\n\t\t\taccept:      \"application/json\",\n\t\t\toffers:      []string{\"application/json\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"6 offers\",\n\t\t\taccept:      \"text/plain\",\n\t\t\toffers:      []string{\"junk/a\", \"junk/b\", \"junk/c\", \"junk/d\", \"junk/e\", \"text/plain\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"1 parameter\",\n\t\t\taccept:      \"application/json; version=1\",\n\t\t\toffers:      []string{\"application/json;version=1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"2 parameters\",\n\t\t\taccept:      \"application/json; version=1; foo=bar\",\n\t\t\toffers:      []string{\"application/json;version=1;foo=bar\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"3 parameters\",\n\t\t\taccept:      \"application/json; version=1; foo=bar; charset=utf-8\",\n\t\t\toffers:      []string{\"application/json;version=1;foo=bar;charset=utf-8\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"10 parameters\",\n\t\t\taccept:      \"text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10\",\n\t\t\toffers:      []string{\"text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"6 offers w/params\",\n\t\t\taccept:      \"text/plain; format=flowed\",\n\t\t\toffers: []string{\n\t\t\t\t\"junk/a;a=b\",\n\t\t\t\t\"junk/b;b=c\",\n\t\t\t\t\"junk/c;c=d\",\n\t\t\t\t\"text/plain; format=justified\",\n\t\t\t\t\"text/plain; format=flat\",\n\t\t\t\t\"text/plain; format=flowed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdescription: \"mime extension\",\n\t\t\taccept:      \"utf-8, iso-8859-1;q=0.5\",\n\t\t\toffers:      []string{\"utf-8\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"mime extension\",\n\t\t\taccept:      \"utf-8, iso-8859-1;q=0.5\",\n\t\t\toffers:      []string{\"iso-8859-1\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"mime extension\",\n\t\t\taccept:      \"utf-8, iso-8859-1;q=0.5\",\n\t\t\toffers:      []string{\"iso-8859-1\", \"utf-8\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"mime extension\",\n\t\t\taccept:      \"gzip, deflate\",\n\t\t\toffers:      []string{\"deflate\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"web browser\",\n\t\t\taccept:      \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n\t\t\toffers:      []string{\"text/html\", \"application/xml\", \"application/xml+xhtml\"},\n\t\t},\n\t}\n\n\tb.ReportAllocs()\n\tfor _, tc := range testCases {\n\t\taccept := []byte(tc.accept)\n\t\tb.Run(tc.description, func(b *testing.B) {\n\t\t\tfor b.Loop() {\n\t\t\t\tgetOffer(accept, acceptsOfferType, tc.offers...)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Utils_ParamsMatch(t *testing.T) {\n\ttestCases := []struct {\n\t\tdescription string\n\t\taccept      headerParams\n\t\toffer       string\n\t\tmatch       bool\n\t}{\n\t\t{\n\t\t\tdescription: \"empty accept and offer\",\n\t\t\taccept:      nil,\n\t\t\toffer:       \"\",\n\t\t\tmatch:       true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"accept is empty, offer has params\",\n\t\t\taccept:      make(headerParams),\n\t\t\toffer:       \";foo=bar\",\n\t\t\tmatch:       true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"offer is empty, accept has params\",\n\t\t\taccept:      headerParams{\"foo\": []byte(\"bar\")},\n\t\t\toffer:       \"\",\n\t\t\tmatch:       false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"accept has extra parameters\",\n\t\t\taccept:      headerParams{\"foo\": []byte(\"bar\"), \"a\": []byte(\"1\")},\n\t\t\toffer:       \";foo=bar\",\n\t\t\tmatch:       false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"matches regardless of order\",\n\t\t\taccept:      headerParams{\"b\": []byte(\"2\"), \"a\": []byte(\"1\")},\n\t\t\toffer:       \";b=2;a=1\",\n\t\t\tmatch:       true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"case-insensitive\",\n\t\t\taccept:      headerParams{\"ParaM\": []byte(\"FoO\")},\n\t\t\toffer:       \";pAram=foO\",\n\t\t\tmatch:       true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\trequire.Equal(t, tc.match, paramsMatch(tc.accept, tc.offer), tc.description)\n\t}\n}\n\nfunc Benchmark_Utils_ParamsMatch(b *testing.B) {\n\tvar match bool\n\n\tspecParams := headerParams{\n\t\t\"appLe\": []byte(\"orange\"),\n\t\t\"param\": []byte(\"foo\"),\n\t}\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tmatch = paramsMatch(specParams, `;param=foo; apple=orange`)\n\t}\n\trequire.True(b, match)\n}\n\nfunc Test_Utils_AcceptsOfferType(t *testing.T) {\n\tt.Parallel()\n\ttestCases := []struct {\n\t\tdescription string\n\t\tspec        string\n\t\tspecParams  headerParams\n\t\tofferType   string\n\t\taccepts     bool\n\t}{\n\t\t{\n\t\t\tdescription: \"no params, matching\",\n\t\t\tspec:        \"application/json\",\n\t\t\tofferType:   \"application/json\",\n\t\t\taccepts:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"no params, mismatch\",\n\t\t\tspec:        \"application/json\",\n\t\t\tofferType:   \"application/xml\",\n\t\t\taccepts:     false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"mismatch with subtype prefix\",\n\t\t\tspec:        \"application/json\",\n\t\t\tofferType:   \"application/json+xml\",\n\t\t\taccepts:     false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"params match\",\n\t\t\tspec:        \"application/json\",\n\t\t\tspecParams:  headerParams{\"format\": []byte(\"foo\"), \"version\": []byte(\"1\")},\n\t\t\tofferType:   \"application/json;version=1;format=foo;q=0.1\",\n\t\t\taccepts:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"spec has extra params\",\n\t\t\tspec:        \"text/html\",\n\t\t\tspecParams:  headerParams{\"charset\": []byte(\"utf-8\")},\n\t\t\tofferType:   \"text/html\",\n\t\t\taccepts:     false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"offer has extra params\",\n\t\t\tspec:        \"text/html\",\n\t\t\tofferType:   \"text/html;charset=utf-8\",\n\t\t\taccepts:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignores optional whitespace\",\n\t\t\tspec:        \"application/json\",\n\t\t\tspecParams:  headerParams{\"format\": []byte(\"foo\"), \"version\": []byte(\"1\")},\n\t\t\tofferType:   \"application/json;  version=1 ;    format=foo   \",\n\t\t\taccepts:     true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"ignores optional whitespace\",\n\t\t\tspec:        \"application/json\",\n\t\t\tspecParams:  headerParams{\"format\": []byte(\"foo bar\"), \"version\": []byte(\"1\")},\n\t\t\tofferType:   `application/json;version=\"1\";format=\"foo bar\"`,\n\t\t\taccepts:     true,\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\taccepts := acceptsOfferType(tc.spec, tc.offerType, tc.specParams)\n\t\trequire.Equal(t, tc.accepts, accepts, tc.description)\n\t}\n}\n\nfunc Test_Utils_GetSplicedStrList(t *testing.T) {\n\tt.Parallel()\n\ttestCases := []struct {\n\t\tdescription  string\n\t\theaderValue  string\n\t\texpectedList []string\n\t}{\n\t\t{\n\t\t\tdescription:  \"normal case\",\n\t\t\theaderValue:  \"gzip, deflate,br\",\n\t\t\texpectedList: []string{\"gzip\", \"deflate\", \"br\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"no matter the value\",\n\t\t\theaderValue:  \"   gzip,deflate, br, zip\",\n\t\t\texpectedList: []string{\"gzip\", \"deflate\", \"br\", \"zip\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"comma with trailing spaces around values\",\n\t\t\theaderValue:  \"gzip , br\",\n\t\t\texpectedList: []string{\"gzip\", \"br\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"comma with tabbed whitespace\",\n\t\t\theaderValue:  \"gzip\\t,br\",\n\t\t\texpectedList: []string{\"gzip\", \"br\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"headerValue is empty\",\n\t\t\theaderValue:  \"\",\n\t\t\texpectedList: nil,\n\t\t},\n\t\t{\n\t\t\tdescription:  \"has a comma without element\",\n\t\t\theaderValue:  \"gzip,\",\n\t\t\texpectedList: []string{\"gzip\", \"\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"has a space between words\",\n\t\t\theaderValue:  \"  foo bar, hello  world\",\n\t\t\texpectedList: []string{\"foo bar\", \"hello  world\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"single comma\",\n\t\t\theaderValue:  \",\",\n\t\t\texpectedList: []string{\"\", \"\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"multiple comma\",\n\t\t\theaderValue:  \",,\",\n\t\t\texpectedList: []string{\"\", \"\", \"\"},\n\t\t},\n\t\t{\n\t\t\tdescription:  \"comma with space\",\n\t\t\theaderValue:  \",  ,\",\n\t\t\texpectedList: []string{\"\", \"\", \"\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\ttc := tc // create a new 'tc' variable for the goroutine\n\t\t\tt.Parallel()\n\t\t\tdst := make([]string, 10)\n\t\t\tresult := getSplicedStrList(tc.headerValue, dst)\n\t\t\trequire.Equal(t, tc.expectedList, result)\n\t\t})\n\t}\n}\n\nfunc Benchmark_Utils_GetSplicedStrList(b *testing.B) {\n\tdestination := make([]string, 5)\n\tresult := destination\n\tconst input = `deflate, gzip,br,brotli,zstd`\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tresult = getSplicedStrList(input, destination)\n\t}\n\trequire.Equal(b, []string{\"deflate\", \"gzip\", \"br\", \"brotli\", \"zstd\"}, result)\n}\n\nfunc Test_Utils_SortAcceptedTypes(t *testing.T) {\n\tt.Parallel()\n\tacceptedTypes := []acceptedType{\n\t\t{spec: \"text/html\", quality: 1, specificity: 3, order: 0},\n\t\t{spec: \"text/*\", quality: 0.5, specificity: 2, order: 1},\n\t\t{spec: \"*/*\", quality: 0.1, specificity: 1, order: 2},\n\t\t{spec: \"application/xml\", quality: 1, specificity: 3, order: 4},\n\t\t{spec: \"application/pdf\", quality: 1, specificity: 3, order: 5},\n\t\t{spec: \"image/png\", quality: 1, specificity: 3, order: 6},\n\t\t{spec: \"image/jpeg\", quality: 1, specificity: 3, order: 7},\n\t\t{spec: \"image/*\", quality: 1, specificity: 2, order: 8},\n\t\t{spec: \"image/gif\", quality: 1, specificity: 3, order: 9},\n\t\t{spec: \"text/plain\", quality: 1, specificity: 3, order: 10},\n\t\t{spec: \"application/json\", quality: 0.999, specificity: 3, params: headerParams{\"a\": []byte(\"1\")}, order: 11},\n\t\t{spec: \"application/json\", quality: 0.999, specificity: 3, order: 3},\n\t}\n\tsortAcceptedTypes(acceptedTypes)\n\trequire.Equal(t, []acceptedType{\n\t\t{spec: \"text/html\", quality: 1, specificity: 3, order: 0},\n\t\t{spec: \"application/xml\", quality: 1, specificity: 3, order: 4},\n\t\t{spec: \"application/pdf\", quality: 1, specificity: 3, order: 5},\n\t\t{spec: \"image/png\", quality: 1, specificity: 3, order: 6},\n\t\t{spec: \"image/jpeg\", quality: 1, specificity: 3, order: 7},\n\t\t{spec: \"image/gif\", quality: 1, specificity: 3, order: 9},\n\t\t{spec: \"text/plain\", quality: 1, specificity: 3, order: 10},\n\t\t{spec: \"image/*\", quality: 1, specificity: 2, order: 8},\n\t\t{spec: \"application/json\", quality: 0.999, specificity: 3, params: headerParams{\"a\": []byte(\"1\")}, order: 11},\n\t\t{spec: \"application/json\", quality: 0.999, specificity: 3, order: 3},\n\t\t{spec: \"text/*\", quality: 0.5, specificity: 2, order: 1},\n\t\t{spec: \"*/*\", quality: 0.1, specificity: 1, order: 2},\n\t}, acceptedTypes)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4\nfunc Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {\n\tacceptedTypes := make([]acceptedType, 3)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tacceptedTypes[0] = acceptedType{spec: \"text/html\", quality: 1, specificity: 1, order: 0}\n\t\tacceptedTypes[1] = acceptedType{spec: \"text/*\", quality: 0.5, specificity: 1, order: 1}\n\t\tacceptedTypes[2] = acceptedType{spec: \"*/*\", quality: 0.1, specificity: 1, order: 2}\n\t\tsortAcceptedTypes(acceptedTypes)\n\t}\n\trequire.Equal(b, \"text/html\", acceptedTypes[0].spec)\n\trequire.Equal(b, \"text/*\", acceptedTypes[1].spec)\n\trequire.Equal(b, \"*/*\", acceptedTypes[2].spec)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4\nfunc Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {\n\tacceptedTypes := make([]acceptedType, 11)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tacceptedTypes[0] = acceptedType{spec: \"text/html\", quality: 1, specificity: 3, order: 0}\n\t\tacceptedTypes[1] = acceptedType{spec: \"text/*\", quality: 0.5, specificity: 2, order: 1}\n\t\tacceptedTypes[2] = acceptedType{spec: \"*/*\", quality: 0.1, specificity: 1, order: 2}\n\t\tacceptedTypes[3] = acceptedType{spec: \"application/json\", quality: 0.999, specificity: 3, order: 3}\n\t\tacceptedTypes[4] = acceptedType{spec: \"application/xml\", quality: 1, specificity: 3, order: 4}\n\t\tacceptedTypes[5] = acceptedType{spec: \"application/pdf\", quality: 1, specificity: 3, order: 5}\n\t\tacceptedTypes[6] = acceptedType{spec: \"image/png\", quality: 1, specificity: 3, order: 6}\n\t\tacceptedTypes[7] = acceptedType{spec: \"image/jpeg\", quality: 1, specificity: 3, order: 7}\n\t\tacceptedTypes[8] = acceptedType{spec: \"image/*\", quality: 1, specificity: 2, order: 8}\n\t\tacceptedTypes[9] = acceptedType{spec: \"image/gif\", quality: 1, specificity: 3, order: 9}\n\t\tacceptedTypes[10] = acceptedType{spec: \"text/plain\", quality: 1, specificity: 3, order: 10}\n\t\tsortAcceptedTypes(acceptedTypes)\n\t}\n\trequire.Equal(b, []acceptedType{\n\t\t{spec: \"text/html\", quality: 1, specificity: 3, order: 0},\n\t\t{spec: \"application/xml\", quality: 1, specificity: 3, order: 4},\n\t\t{spec: \"application/pdf\", quality: 1, specificity: 3, order: 5},\n\t\t{spec: \"image/png\", quality: 1, specificity: 3, order: 6},\n\t\t{spec: \"image/jpeg\", quality: 1, specificity: 3, order: 7},\n\t\t{spec: \"image/gif\", quality: 1, specificity: 3, order: 9},\n\t\t{spec: \"text/plain\", quality: 1, specificity: 3, order: 10},\n\t\t{spec: \"image/*\", quality: 1, specificity: 2, order: 8},\n\t\t{spec: \"application/json\", quality: 0.999, specificity: 3, order: 3},\n\t\t{spec: \"text/*\", quality: 0.5, specificity: 2, order: 1},\n\t\t{spec: \"*/*\", quality: 0.1, specificity: 1, order: 2},\n\t}, acceptedTypes)\n}\n\nfunc Test_Utils_UniqueRouteStack(t *testing.T) {\n\tt.Parallel()\n\troute1 := &Route{}\n\troute2 := &Route{}\n\troute3 := &Route{}\n\trequire.Equal(\n\t\tt,\n\t\t[]*Route{\n\t\t\troute1,\n\t\t\troute2,\n\t\t\troute3,\n\t\t},\n\t\tuniqueRouteStack([]*Route{\n\t\t\troute1,\n\t\t\troute1,\n\t\t\troute1,\n\t\t\troute2,\n\t\t\troute2,\n\t\t\troute2,\n\t\t\troute3,\n\t\t\troute3,\n\t\t\troute3,\n\t\t\troute1,\n\t\t\troute2,\n\t\t\troute3,\n\t\t}))\n}\n\nfunc Test_Utils_getGroupPath(t *testing.T) {\n\tt.Parallel()\n\tres := getGroupPath(\"/v1\", \"/\")\n\trequire.Equal(t, \"/v1/\", res)\n\n\tres = getGroupPath(\"/v1/\", \"/\")\n\trequire.Equal(t, \"/v1/\", res)\n\n\tres = getGroupPath(\"/\", \"/\")\n\trequire.Equal(t, \"/\", res)\n\n\tres = getGroupPath(\"/v1/api/\", \"/\")\n\trequire.Equal(t, \"/v1/api/\", res)\n\n\tres = getGroupPath(\"/v1/api\", \"group\")\n\trequire.Equal(t, \"/v1/api/group\", res)\n\n\tres = getGroupPath(\"/v1/api\", \"\")\n\trequire.Equal(t, \"/v1/api\", res)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3\nfunc Benchmark_Utils_getGroupPath(b *testing.B) {\n\tvar res string\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = getGroupPath(\"/v1/long/path/john/doe\", \"/why/this/name/is/so/awesome\")\n\t\t_ = getGroupPath(\"/v1\", \"/\")\n\t\t_ = getGroupPath(\"/v1\", \"/api\")\n\t\tres = getGroupPath(\"/v1\", \"/api/register/:project\")\n\t}\n\trequire.Equal(b, \"/v1/api/register/:project\", res)\n}\n\nfunc Benchmark_Utils_Unescape(b *testing.B) {\n\tunescaped := \"\"\n\tdst := make([]byte, 0)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tsource := \"/cr%C3%A9er\"\n\t\tpathBytes := utils.UnsafeBytes(source)\n\t\tpathBytes = fasthttp.AppendUnquotedArg(dst[:0], pathBytes)\n\t\tunescaped = utils.UnsafeString(pathBytes)\n\t}\n\n\trequire.Equal(b, \"/créer\", unescaped)\n}\n\nfunc Test_Utils_Parse_Address(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\taddr, host, port string\n\t}{\n\t\t{addr: \"[::1]:3000\", host: \"[::1]\", port: \"3000\"},\n\t\t{addr: \"127.0.0.1:3000\", host: \"127.0.0.1\", port: \"3000\"},\n\t\t{addr: \"[::1]\", host: \"[::1]\", port: \"\"},\n\t\t{addr: \"2001:db8::1\", host: \"2001:db8::1\", port: \"\"},\n\t\t{addr: \"/path/to/unix/socket\", host: \"/path/to/unix/socket\", port: \"\"},\n\t\t{addr: \"127.0.0.1\", host: \"127.0.0.1\", port: \"\"},\n\t\t{addr: \"localhost:8080\", host: \"localhost\", port: \"8080\"},\n\t\t{addr: \"example.com\", host: \"example.com\", port: \"\"},\n\t\t{addr: \"[fe80::1%lo0]:1234\", host: \"[fe80::1%lo0]\", port: \"1234\"},\n\t\t{addr: \"[fe80::1%lo0]\", host: \"[fe80::1%lo0]\", port: \"\"},\n\t\t{addr: \":9090\", host: \"\", port: \"9090\"},\n\t\t{addr: \" 127.0.0.1:8080 \", host: \"127.0.0.1\", port: \"8080\"},\n\t\t{addr: \"\", host: \"\", port: \"\"},\n\t}\n\n\tfor _, c := range testCases {\n\t\thost, port := parseAddr(c.addr)\n\t\trequire.Equal(t, c.host, host, \"addr host: %q\", c.addr)\n\t\trequire.Equal(t, c.port, port, \"addr port: %q\", c.addr)\n\t}\n}\n\nfunc Test_Utils_TestConn_Deadline(t *testing.T) {\n\tt.Parallel()\n\tconn := &testConn{}\n\trequire.NoError(t, conn.SetDeadline(time.Time{}))\n\trequire.NoError(t, conn.SetReadDeadline(time.Time{}))\n\trequire.NoError(t, conn.SetWriteDeadline(time.Time{}))\n}\n\nfunc Test_Utils_TestConn_ReadWrite(t *testing.T) {\n\tt.Parallel()\n\tconn := &testConn{}\n\n\t// Verify read of request\n\t_, err := conn.r.Write([]byte(\"Request\"))\n\trequire.NoError(t, err)\n\n\treq := make([]byte, 7)\n\t_, err = conn.Read(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"Request\"), req)\n\n\t// Verify write of response\n\t_, err = conn.Write([]byte(\"Response\"))\n\trequire.NoError(t, err)\n\n\tres := make([]byte, 8)\n\t_, err = conn.w.Read(res)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"Response\"), res)\n}\n\nfunc Test_Utils_TestConn_Closed_Write(t *testing.T) {\n\tt.Parallel()\n\tconn := &testConn{}\n\n\t// Verify write of response\n\t_, err := conn.Write([]byte(\"Response 1\\n\"))\n\trequire.NoError(t, err)\n\n\t// Close early, write should fail\n\tconn.Close() //nolint:errcheck // It is fine to ignore the error here\n\t_, err = conn.Write([]byte(\"Response 2\\n\"))\n\trequire.ErrorIs(t, err, errTestConnClosed)\n\n\tres := make([]byte, 11)\n\t_, err = conn.w.Read(res)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"Response 1\\n\"), res)\n}\n\nfunc Test_Utils_IsNoCache(t *testing.T) {\n\tt.Parallel()\n\ttestCases := []struct {\n\t\tstring\n\t\tbool\n\t}{\n\t\t{string: \"public\", bool: false},\n\t\t{string: \"no-cache\", bool: true},\n\t\t{string: \"public, no-cache, max-age=30\", bool: true},\n\t\t{string: \"public,no-cache\", bool: true},\n\t\t{string: \"public,no-cacheX\", bool: false},\n\t\t{string: \"no-cache, public\", bool: true},\n\t\t{string: \"Xno-cache, public\", bool: false},\n\t\t{string: \"max-age=30, no-cache,public\", bool: true},\n\t\t{string: \"NO-CACHE\", bool: true},\n\t\t{string: \"public, NO-CACHE\", bool: true},\n\t\t// RFC 9111 §5.2.2.4: no-cache with field-name argument\n\t\t{string: \"no-cache=\\\"Set-Cookie\\\"\", bool: true},\n\t\t{string: \"public, no-cache=\\\"Set-Cookie, Set-Cookie2\\\"\", bool: true},\n\t\t{string: \"no-cache=Set-Cookie\", bool: true},\n\t\t// Edge cases with spaces\n\t\t{string: \"no-cache ,public\", bool: true},\n\t\t{string: \"public, no-cache =field\", bool: true},\n\t}\n\n\tfor _, c := range testCases {\n\t\tok := isNoCache(c.string)\n\t\trequire.Equal(t, c.bool, ok, \"want %t, got isNoCache(%s)=%t\", c.bool, c.string, ok)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4\nfunc Benchmark_Utils_IsNoCache(b *testing.B) {\n\tvar ok bool\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = isNoCache(\"public\")\n\t\t_ = isNoCache(\"no-cache\")\n\t\t_ = isNoCache(\"public, no-cache, max-age=30\")\n\t\t_ = isNoCache(\"public,no-cache\")\n\t\t_ = isNoCache(\"no-cache, public\")\n\t\tok = isNoCache(\"max-age=30, no-cache,public\")\n\t}\n\trequire.True(b, ok)\n}\n\n// go test -run Test_HeaderContainsValue\nfunc Test_HeaderContainsValue(t *testing.T) {\n\tt.Parallel()\n\ttestCases := []struct {\n\t\theader   string\n\t\tvalue    string\n\t\texpected bool\n\t}{\n\t\t// Exact match\n\t\t{header: \"gzip\", value: \"gzip\", expected: true},\n\t\t{header: \"gzip\", value: \"deflate\", expected: false},\n\t\t// Prefix match (value at start with comma)\n\t\t{header: \"gzip, deflate\", value: \"gzip\", expected: true},\n\t\t{header: \"gzip,deflate\", value: \"gzip\", expected: true},\n\t\t// Suffix match (value at end)\n\t\t{header: \"deflate, gzip\", value: \"gzip\", expected: true},\n\t\t{header: \"deflate,gzip\", value: \"gzip\", expected: true}, // No space - OWS is optional per RFC 9110\n\t\t{header: \"br, gzip\", value: \"gzip\", expected: true},\n\t\t// Middle match (value in middle)\n\t\t{header: \"deflate, gzip, br\", value: \"gzip\", expected: true},\n\t\t{header: \"deflate,gzip,br\", value: \"gzip\", expected: true}, // No spaces - OWS is optional per RFC 9110\n\t\t// No match - similar but not equal\n\t\t{header: \"gzip2\", value: \"gzip\", expected: false},\n\t\t{header: \"2gzip\", value: \"gzip\", expected: false},\n\t\t{header: \"gzip2, deflate\", value: \"gzip\", expected: false},\n\t\t// Whitespace handling (OWS per RFC 9110)\n\t\t{header: \"  gzip  ,  deflate  \", value: \"gzip\", expected: true},\n\t\t{header: \"deflate,  gzip  \", value: \"gzip\", expected: true},\n\t\t// Empty cases\n\t\t{header: \"\", value: \"gzip\", expected: false},\n\t\t{header: \"gzip\", value: \"\", expected: false},\n\t\t{header: \"\", value: \"\", expected: false}, // Both empty - should return false\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := headerContainsValue(tc.header, tc.value)\n\t\trequire.Equal(t, tc.expected, result,\n\t\t\t\"headerContainsValue(%q, %q) = %v, want %v\",\n\t\t\ttc.header, tc.value, result, tc.expected)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_HeaderContainsValue -benchmem -count=4\nfunc Benchmark_HeaderContainsValue(b *testing.B) {\n\tvar ok bool\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = headerContainsValue(\"gzip\", \"gzip\")\n\t\t_ = headerContainsValue(\"gzip, deflate, br\", \"deflate\")\n\t\t_ = headerContainsValue(\"deflate, gzip\", \"gzip\")\n\t\tok = headerContainsValue(\"deflate, gzip, br\", \"gzip\")\n\t}\n\trequire.True(b, ok)\n}\n\ntype testGenericParseTypeIntCase struct {\n\tvalue int64\n\tbits  int\n}\n\n// go test -run Test_GenericParseTypeInts\nfunc Test_GenericParseTypeInts(t *testing.T) {\n\tt.Parallel()\n\tints := []testGenericParseTypeIntCase{\n\t\t{\n\t\t\tvalue: 0,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 4,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: -1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt64,\n\t\t\tbits:  64,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt64,\n\t\t\tbits:  64,\n\t\t},\n\t}\n\n\ttestGenericTypeInt[int8](t, \"test_genericParseTypeInt8s\", ints)\n\ttestGenericTypeInt[int16](t, \"test_genericParseTypeInt16s\", ints)\n\ttestGenericTypeInt[int32](t, \"test_genericParseTypeInt32s\", ints)\n\ttestGenericTypeInt[int64](t, \"test_genericParseTypeInt64s\", ints)\n\ttestGenericTypeInt[int](t, \"test_genericParseTypeInts\", ints)\n}\n\nfunc testGenericTypeInt[V GenericTypeInteger](t *testing.T, name string, cases []testGenericParseTypeIntCase) {\n\tt.Helper()\n\tt.Run(name, func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range cases {\n\t\t\tv, err := genericParseType[V](strconv.FormatInt(test.value, 10))\n\t\t\tif test.bits <= int(unsafe.Sizeof(V(0)))*8 {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, V(test.value), v)\n\t\t\t} else {\n\t\t\t\trequire.ErrorIs(t, err, strconv.ErrRange)\n\t\t\t}\n\t\t}\n\t\ttestGenericParseError[V](t)\n\t})\n}\n\ntype testGenericParseTypeUintCase struct {\n\tvalue uint64\n\tbits  int\n}\n\n// go test -run Test_GenericParseTypeUints\nfunc Test_GenericParseTypeUints(t *testing.T) {\n\tt.Parallel()\n\tuints := []testGenericParseTypeUintCase{\n\t\t{\n\t\t\tvalue: 0,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 4,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint64,\n\t\t\tbits:  64,\n\t\t},\n\t}\n\n\ttestGenericTypeUint[uint8](t, \"test_genericParseTypeUint8s\", uints)\n\ttestGenericTypeUint[uint16](t, \"test_genericParseTypeUint16s\", uints)\n\ttestGenericTypeUint[uint32](t, \"test_genericParseTypeUint32s\", uints)\n\ttestGenericTypeUint[uint64](t, \"test_genericParseTypeUint64s\", uints)\n\ttestGenericTypeUint[uint](t, \"test_genericParseTypeUints\", uints)\n}\n\nfunc testGenericTypeUint[V GenericTypeInteger](t *testing.T, name string, cases []testGenericParseTypeUintCase) {\n\tt.Helper()\n\tt.Run(name, func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range cases {\n\t\t\tv, err := genericParseType[V](strconv.FormatUint(test.value, 10))\n\t\t\tif test.bits <= int(unsafe.Sizeof(V(0)))*8 {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, V(test.value), v)\n\t\t\t} else {\n\t\t\t\trequire.ErrorIs(t, err, strconv.ErrRange)\n\t\t\t}\n\t\t}\n\t\ttestGenericParseError[V](t)\n\t})\n}\n\n// go test -run Test_GenericParseTypeFloats\nfunc Test_GenericParseTypeFloats(t *testing.T) {\n\tt.Parallel()\n\n\tfloats := []struct {\n\t\tstr   string\n\t\tvalue float64\n\t}{\n\t\t{\n\t\t\tvalue: 3.1415,\n\t\t\tstr:   \"3.1415\",\n\t\t},\n\t\t{\n\t\t\tvalue: 1.234,\n\t\t\tstr:   \"1.234\",\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tstr:   \"2\",\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tstr:   \"3\",\n\t\t},\n\t}\n\n\tt.Run(\"test_genericParseTypeFloat32s\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range floats {\n\t\t\tv, err := genericParseType[float32](test.str)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.InEpsilon(t, float32(test.value), v, epsilon)\n\t\t}\n\t\ttestGenericParseError[float32](t)\n\t})\n\n\tt.Run(\"test_genericParseTypeFloat64s\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range floats {\n\t\t\tv, err := genericParseType[float64](test.str)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.InEpsilon(t, test.value, v, epsilon)\n\t\t}\n\t\ttestGenericParseError[float64](t)\n\t})\n}\n\n// go test -run Test_GenericParseTypeBytes\nfunc Test_GenericParseTypeBytes(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tstr   string\n\t\terr   error\n\t\tvalue []byte\n\t}{\n\t\t{\n\t\t\tvalue: []byte(\"alex\"),\n\t\t\tstr:   \"alex\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(\"32.23\"),\n\t\t\tstr:   \"32.23\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(\"john\"),\n\t\t\tstr:   \"john\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(nil),\n\t\t\tstr:   \"\",\n\t\t\terr:   errParsedEmptyBytes,\n\t\t},\n\t}\n\n\tt.Run(\"test_genericParseTypeBytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range cases {\n\t\t\tv, err := genericParseType[[]byte](test.str)\n\t\t\tif test.err == nil {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.ErrorIs(t, err, test.err)\n\t\t\t}\n\t\t\trequire.Equal(t, test.value, v)\n\t\t}\n\t})\n}\n\n// go test -run Test_GenericParseTypeString\nfunc Test_GenericParseTypeString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []string{\"john\", \"doe\", \"hello\", \"fiber\"}\n\n\tfor _, test := range tests {\n\t\tt.Run(\"test_genericParseTypeString\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tv, err := genericParseType[string](test)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, test, v)\n\t\t})\n\t}\n}\n\n// go test -run Test_GenericParseTypeBoolean\nfunc Test_GenericParseTypeBoolean(t *testing.T) {\n\tt.Parallel()\n\n\tbools := []struct {\n\t\tstr   string\n\t\tvalue bool\n\t}{\n\t\t{\n\t\t\tstr:   \"True\",\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\tstr:   \"False\",\n\t\t\tvalue: false,\n\t\t},\n\t\t{\n\t\t\tstr:   \"true\",\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\tstr:   \"false\",\n\t\t\tvalue: false,\n\t\t},\n\t}\n\n\tt.Run(\"test_genericParseTypeBoolean\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfor _, test := range bools {\n\t\t\tv, err := genericParseType[bool](test.str)\n\t\t\trequire.NoError(t, err)\n\t\t\tif test.value {\n\t\t\t\trequire.True(t, v)\n\t\t\t} else {\n\t\t\t\trequire.False(t, v)\n\t\t\t}\n\t\t}\n\t\ttestGenericParseError[bool](t)\n\t})\n}\n\nfunc testGenericParseError[V GenericType](t *testing.T) {\n\tt.Helper()\n\tvar expected V\n\tv, err := genericParseType[V](\"invalid-string\")\n\trequire.Error(t, err)\n\trequire.Equal(t, expected, v)\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeInts -benchmem -count=4\nfunc Benchmark_GenericParseTypeInts(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\tints := []testGenericParseTypeIntCase{\n\t\t{\n\t\t\tvalue: 0,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 4,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: -1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxInt64,\n\t\t\tbits:  64,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MinInt64,\n\t\t\tbits:  64,\n\t\t},\n\t}\n\tfor _, test := range ints {\n\t\tbenchGenericParseTypeInt[int8](b, \"bench_genericParseTypeInt8s\", test)\n\t\tbenchGenericParseTypeInt[int16](b, \"bench_genericParseTypeInt16s\", test)\n\t\tbenchGenericParseTypeInt[int32](b, \"bench_genericParseTypeInt32s\", test)\n\t\tbenchGenericParseTypeInt[int64](b, \"bench_genericParseTypeInt64s\", test)\n\t\tbenchGenericParseTypeInt[int](b, \"bench_genericParseTypeInts\", test)\n\t}\n}\n\nfunc benchGenericParseTypeInt[V GenericTypeInteger](b *testing.B, name string, test testGenericParseTypeIntCase) {\n\tb.Helper()\n\tb.Run(name, func(t *testing.B) {\n\t\tvar v V\n\t\tvar err error\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tv, err = genericParseType[V](strconv.FormatInt(test.value, 10))\n\t\t\t}\n\t\t})\n\t\tif test.bits <= int(unsafe.Sizeof(V(0)))*8 {\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, V(test.value), v)\n\t\t} else {\n\t\t\trequire.ErrorIs(t, err, strconv.ErrRange)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeUints -benchmem -count=4\nfunc Benchmark_GenericParseTypeUints(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\tuints := []struct {\n\t\tvalue uint64\n\t\tbits  int\n\t}{\n\t\t{\n\t\t\tvalue: 0,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 1,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: 4,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint8,\n\t\t\tbits:  8,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint16,\n\t\t\tbits:  16,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint32,\n\t\t\tbits:  32,\n\t\t},\n\t\t{\n\t\t\tvalue: math.MaxUint64,\n\t\t\tbits:  64,\n\t\t},\n\t}\n\n\tfor _, test := range uints {\n\t\tbenchGenericParseTypeUInt[uint8](b, \"benchmark_genericParseTypeUint8s\", test)\n\t\tbenchGenericParseTypeUInt[uint16](b, \"benchmark_genericParseTypeUint16s\", test)\n\t\tbenchGenericParseTypeUInt[uint32](b, \"benchmark_genericParseTypeUint32s\", test)\n\t\tbenchGenericParseTypeUInt[uint64](b, \"benchmark_genericParseTypeUint64s\", test)\n\t\tbenchGenericParseTypeUInt[uint](b, \"benchmark_genericParseTypeUints\", test)\n\t}\n}\n\nfunc benchGenericParseTypeUInt[V GenericTypeInteger](b *testing.B, name string, test testGenericParseTypeUintCase) {\n\tb.Helper()\n\tb.Run(name, func(t *testing.B) {\n\t\tvar v V\n\t\tvar err error\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tv, err = genericParseType[V](strconv.FormatUint(test.value, 10))\n\t\t\t}\n\t\t})\n\t\tif test.bits <= int(unsafe.Sizeof(V(0)))*8 {\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, V(test.value), v)\n\t\t} else {\n\t\t\trequire.ErrorIs(t, err, strconv.ErrRange)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeFloats -benchmem -count=4\nfunc Benchmark_GenericParseTypeFloats(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\tfloats := []struct {\n\t\tstr   string\n\t\tvalue float64\n\t}{\n\t\t{\n\t\t\tvalue: 3.1415,\n\t\t\tstr:   \"3.1415\",\n\t\t},\n\t\t{\n\t\t\tvalue: 1.234,\n\t\t\tstr:   \"1.234\",\n\t\t},\n\t\t{\n\t\t\tvalue: 2,\n\t\t\tstr:   \"2\",\n\t\t},\n\t\t{\n\t\t\tvalue: 3,\n\t\t\tstr:   \"3\",\n\t\t},\n\t}\n\n\tfor _, test := range floats {\n\t\tb.Run(\"benchmark_genericParseTypeFloat32s\", func(t *testing.B) {\n\t\t\tvar v float32\n\t\t\tvar err error\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tv, err = genericParseType[float32](test.str)\n\t\t\t\t}\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.InEpsilon(t, float32(test.value), v, epsilon)\n\t\t})\n\t}\n\n\tfor _, test := range floats {\n\t\tb.Run(\"benchmark_genericParseTypeFloat64s\", func(t *testing.B) {\n\t\t\tvar v float64\n\t\t\tvar err error\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tv, err = genericParseType[float64](test.str)\n\t\t\t\t}\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.InEpsilon(t, test.value, v, epsilon)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeBytes -benchmem -count=4\nfunc Benchmark_GenericParseTypeBytes(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\tcases := []struct {\n\t\tstr   string\n\t\terr   error\n\t\tvalue []byte\n\t}{\n\t\t{\n\t\t\tvalue: []byte(\"alex\"),\n\t\t\tstr:   \"alex\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(\"32.23\"),\n\t\t\tstr:   \"32.23\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(\"john\"),\n\t\t\tstr:   \"john\",\n\t\t},\n\t\t{\n\t\t\tvalue: []byte(nil),\n\t\t\tstr:   \"\",\n\t\t\terr:   errParsedEmptyBytes,\n\t\t},\n\t}\n\n\tfor _, test := range cases {\n\t\tb.Run(\"benchmark_genericParseTypeBytes\", func(b *testing.B) {\n\t\t\tvar v []byte\n\t\t\tvar err error\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tv, err = genericParseType[[]byte](test.str)\n\t\t\t\t}\n\t\t\t})\n\t\t\tif test.err == nil {\n\t\t\t\trequire.NoError(b, err)\n\t\t\t} else {\n\t\t\t\trequire.ErrorIs(b, err, test.err)\n\t\t\t}\n\t\t\trequire.Equal(b, test.value, v)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeString -benchmem -count=4\nfunc Benchmark_GenericParseTypeString(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\ttests := []string{\"john\", \"doe\", \"hello\", \"fiber\"}\n\n\tfor _, test := range tests {\n\t\tb.Run(\"benchmark_genericParseTypeString\", func(b *testing.B) {\n\t\t\tvar v string\n\t\t\tvar err error\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tv, err = genericParseType[string](test)\n\t\t\t\t}\n\t\t\t})\n\t\t\trequire.NoError(b, err)\n\t\t\trequire.Equal(b, test, v)\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_GenericParseTypeBoolean -benchmem -count=4\nfunc Benchmark_GenericParseTypeBoolean(b *testing.B) {\n\tb.Skip(\"Skipped: too fast to compare reliably (results in sub-ns range are unstable)\")\n\tbools := []struct {\n\t\tstr   string\n\t\tvalue bool\n\t}{\n\t\t{\n\t\t\tstr:   \"True\",\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\tstr:   \"False\",\n\t\t\tvalue: false,\n\t\t},\n\t\t{\n\t\t\tstr:   \"true\",\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\tstr:   \"false\",\n\t\t\tvalue: false,\n\t\t},\n\t}\n\n\tfor _, test := range bools {\n\t\tb.Run(\"benchmark_genericParseTypeBoolean\", func(b *testing.B) {\n\t\t\tvar v bool\n\t\t\tvar err error\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tv, err = genericParseType[bool](test.str)\n\t\t\t\t}\n\t\t\t})\n\t\t\trequire.NoError(b, err)\n\t\t\tif test.value {\n\t\t\t\trequire.True(b, v)\n\t\t\t} else {\n\t\t\t\trequire.False(b, v)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_UnescapeHeaderValue(t *testing.T) {\n\tt.Parallel()\n\tcases := []struct {\n\t\tin  string\n\t\tout []byte\n\t\tok  bool\n\t}{\n\t\t{in: \"abc\", out: []byte(\"abc\"), ok: true},\n\t\t{in: \"a\\\\\\\"b\", out: []byte(\"a\\\"b\"), ok: true},\n\t\t{in: \"c\\\\\\\\d\", out: []byte(\"c\\\\d\"), ok: true},\n\t\t{in: \"bad\\\\\", ok: false},\n\t}\n\tfor _, tc := range cases {\n\t\tout, err := unescapeHeaderValue([]byte(tc.in))\n\t\tif tc.ok {\n\t\t\trequire.NoError(t, err, tc.in)\n\t\t\trequire.Equal(t, tc.out, out, tc.in)\n\t\t} else {\n\t\t\trequire.Error(t, err, tc.in)\n\t\t}\n\t}\n}\n\nfunc Test_JoinHeaderValues(t *testing.T) {\n\tt.Parallel()\n\trequire.Nil(t, joinHeaderValues(nil))\n\trequire.Equal(t, []byte(\"a\"), joinHeaderValues([][]byte{[]byte(\"a\")}))\n\trequire.Equal(t, []byte(\"a,b\"), joinHeaderValues([][]byte{[]byte(\"a\"), []byte(\"b\")}))\n}\n\nfunc Test_ParamsMatch_InvalidEscape(t *testing.T) {\n\tt.Parallel()\n\tmatch := paramsMatch(headerParams{\"foo\": []byte(\"bar\")}, `;foo=\"bar\\\\`)\n\trequire.False(t, match)\n}\n\nfunc Test_MatchEtag(t *testing.T) {\n\tt.Parallel()\n\n\trequire.True(t, matchEtag(`\"a\"`, `\"a\"`))\n\trequire.True(t, matchEtag(`W/\"a\"`, `\"a\"`))\n\trequire.True(t, matchEtag(`\"a\"`, `W/\"a\"`))\n\trequire.False(t, matchEtag(`\"a\"`, `\"b\"`))\n\trequire.False(t, matchEtag(`a`, `\"a\"`))\n\trequire.False(t, matchEtag(`\"a\"`, `b`))\n}\n\nfunc Test_MatchEtagStrong(t *testing.T) {\n\tt.Parallel()\n\n\trequire.True(t, matchEtagStrong(`\"a\"`, `\"a\"`))\n\trequire.False(t, matchEtagStrong(`W/\"a\"`, `\"a\"`))\n\trequire.False(t, matchEtagStrong(`\"a\"`, `W/\"a\"`))\n\trequire.False(t, matchEtagStrong(`\"a\"`, `\"b\"`))\n\trequire.False(t, matchEtagStrong(`a`, `\"a\"`))\n\trequire.False(t, matchEtagStrong(`\"a\"`, `b`))\n}\n\nfunc Test_IsEtagStale(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// Invalid/unquoted tags are considered a mismatch, so it's stale\n\trequire.True(t, app.isEtagStale(`\"a\"`, []byte(\"b\")))\n\trequire.True(t, app.isEtagStale(`\"a\"`, []byte(\"a\")))\n\n\t// Matching tags, not stale\n\trequire.False(t, app.isEtagStale(`\"a\"`, []byte(`\"a\"`)))\n\trequire.False(t, app.isEtagStale(`W/\"a\"`, []byte(`\"a\"`)))\n\n\t// List of tags, not stale\n\trequire.False(t, app.isEtagStale(`\"c\"`, []byte(`\"a\", \"b\", \"c\"`)))\n\trequire.False(t, app.isEtagStale(`W/\"c\"`, []byte(`\"a\", \"b\", \"c\"`)))\n\trequire.False(t, app.isEtagStale(`\"c\"`, []byte(`\"a\", \"b\", W/\"c\"`)))\n\trequire.False(t, app.isEtagStale(`\"c\"`, []byte(`\"c\", \"b\", \"a\"`)))\n\trequire.False(t, app.isEtagStale(`\"c\"`, []byte(`  \"a\",   \"c\"   , \"b\"  `)))\n\n\t// List of tags, stale\n\trequire.True(t, app.isEtagStale(`\"d\"`, []byte(`\"a\", \"b\", \"c\"`)))\n\trequire.True(t, app.isEtagStale(`W/\"d\"`, []byte(`\"a\", \"b\", \"c\"`)))\n\n\t// Wildcard\n\trequire.False(t, app.isEtagStale(`\"a\"`, []byte(\"*\")))\n\trequire.False(t, app.isEtagStale(`\"a\"`, []byte(\" *   \")))\n\trequire.False(t, app.isEtagStale(`W/\"a\"`, []byte(\"*\")))\n\n\t// Empty case\n\trequire.True(t, app.isEtagStale(`\"a\"`, []byte(\"\")))\n\trequire.True(t, app.isEtagStale(`\"a\"`, []byte(\"   \")))\n\n\t// Weak vs. weak\n\trequire.False(t, app.isEtagStale(`W/\"a\"`, []byte(`W/\"a\"`)))\n}\n\nfunc Test_App_quoteRawString(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  string\n\t}{\n\t\t{\"empty\", \"\", \"\"},\n\t\t{\"simple\", \"simple\", \"simple\"},\n\t\t{\"backslash\", \"A\\\\B\", \"A\\\\\\\\B\"},\n\t\t{\"quote\", `He said \"Yo\"`, `He said \\\"Yo\\\"`},\n\t\t{\"newline\", \"Hello\\n\", \"Hello\\\\n\"},\n\t\t{\"carriage\", \"Hello\\r\", \"Hello\\\\r\"},\n\t\t{\"controls\", string([]byte{0, 31, 127}), \"%00%1F%7F\"},\n\t\t{\"mixed\", \"test \\\"A\\n\\r\" + string([]byte{1}) + \"\\\\\", `test \\\"A\\n\\r%01\\\\`},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := New()\n\t\t\trequire.Equal(t, tc.out, app.quoteRawString(tc.in))\n\t\t})\n\t}\n}\n\nfunc TestStoreInContext(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{PassLocalsToContext: true})\n\traw := &fasthttp.RequestCtx{}\n\tc := app.AcquireCtx(raw)\n\tdefer app.ReleaseCtx(c)\n\n\tStoreInContext(c, \"key\", \"value\")\n\n\tlocalValue, ok := c.Locals(\"key\").(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, \"value\", localValue)\n\n\tcontextValue, ok := c.Context().Value(\"key\").(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, \"value\", contextValue)\n}\n\nfunc TestValueFromContext(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fiber.Ctx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.Locals(\"key\", \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](c, \"key\")\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n\n\tt.Run(\"fiber.CustomCtx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := NewWithCustomCtx(func(app *App) CustomCtx {\n\t\t\treturn &customCtx{DefaultCtx: *NewDefaultCtx(app)}\n\t\t})\n\t\traw := &fasthttp.RequestCtx{}\n\t\tc := app.AcquireCtx(raw)\n\t\tdefer app.ReleaseCtx(c)\n\n\t\tc.Locals(\"key\", \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](c, \"key\")\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n\n\tt.Run(\"fasthttp request ctx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\traw := &fasthttp.RequestCtx{}\n\t\traw.SetUserValue(\"key\", \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](raw, \"key\")\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n\n\tt.Run(\"context.Context\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttype testContextKey struct{}\n\n\t\tctx := context.WithValue(context.Background(), testContextKey{}, \"value\")\n\n\t\tvalue, ok := ValueFromContext[string](ctx, testContextKey{})\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"value\", value)\n\t})\n\n\tt.Run(\"unsupported ctx\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvalue, ok := ValueFromContext[string](42, \"key\")\n\t\trequire.False(t, ok)\n\t\trequire.Empty(t, value)\n\t})\n}\n"
  },
  {
    "path": "hooks.go",
    "content": "package fiber\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\ntype (\n\t// OnRouteHandler defines the hook signature invoked whenever a route is registered.\n\tOnRouteHandler = func(Route) error\n\t// OnNameHandler shares the OnRouteHandler signature for route naming callbacks.\n\tOnNameHandler = OnRouteHandler\n\t// OnGroupHandler defines the hook signature invoked whenever a group is registered.\n\tOnGroupHandler = func(Group) error\n\t// OnGroupNameHandler shares the OnGroupHandler signature for group naming callbacks.\n\tOnGroupNameHandler = OnGroupHandler\n\t// OnListenHandler runs when the application begins listening and receives the listener details.\n\tOnListenHandler = func(ListenData) error\n\t// OnPreStartupMessageHandler runs before Fiber prints the startup banner.\n\tOnPreStartupMessageHandler = func(*PreStartupMessageData) error\n\t// OnPostStartupMessageHandler runs after Fiber prints (or skips) the startup banner.\n\tOnPostStartupMessageHandler = func(*PostStartupMessageData) error\n\t// OnPreShutdownHandler runs before the application shuts down.\n\tOnPreShutdownHandler = func() error\n\t// OnPostShutdownHandler runs after shutdown and receives the shutdown result.\n\tOnPostShutdownHandler = func(error) error\n\t// OnForkHandler runs inside a forked worker process and receives the worker ID.\n\tOnForkHandler = func(int) error\n\t// OnMountHandler runs after a sub-application mounts to a parent and receives the parent app reference.\n\tOnMountHandler = func(*App) error\n)\n\n// Hooks is a struct to use it with App.\ntype Hooks struct {\n\t// Embed app\n\tapp *App\n\n\t// Hooks\n\tonRoute        []OnRouteHandler\n\tonName         []OnNameHandler\n\tonGroup        []OnGroupHandler\n\tonGroupName    []OnGroupNameHandler\n\tonListen       []OnListenHandler\n\tonPreStartup   []OnPreStartupMessageHandler\n\tonPostStartup  []OnPostStartupMessageHandler\n\tonPreShutdown  []OnPreShutdownHandler\n\tonPostShutdown []OnPostShutdownHandler\n\tonFork         []OnForkHandler\n\tonMount        []OnMountHandler\n}\n\ntype StartupMessageLevel int\n\nconst (\n\t// StartupMessageLevelInfo represents informational startup message entries.\n\tStartupMessageLevelInfo StartupMessageLevel = iota\n\t// StartupMessageLevelWarning represents warning startup message entries.\n\tStartupMessageLevelWarning\n\t// StartupMessageLevelError represents error startup message entries.\n\tStartupMessageLevelError\n)\n\nconst errString = \"ERROR\"\n\n// startupMessageEntry represents a single line of startup message information.\ntype startupMessageEntry struct {\n\tkey      string\n\ttitle    string\n\tvalue    string\n\tpriority int\n\tlevel    StartupMessageLevel\n}\n\n// ListenData contains the listener metadata provided to OnListenHandler.\ntype ListenData struct {\n\tColorScheme Colors\n\tHost        string\n\tPort        string\n\tVersion     string\n\tAppName     string\n\n\tChildPIDs []int\n\n\tHandlerCount int\n\tProcessCount int\n\tPID          int\n\n\tTLS     bool\n\tPrefork bool\n}\n\n// PreStartupMessageData contains metadata exposed to OnPreStartupMessage hooks.\ntype PreStartupMessageData struct {\n\t*ListenData\n\n\t// BannerHeader allows overriding the ASCII art banner displayed at startup.\n\tBannerHeader string\n\n\tentries []startupMessageEntry\n\n\t// PreventDefault, when set to true, suppresses the default startup message.\n\tPreventDefault bool\n}\n\n// AddInfo adds an informational entry to the startup message with \"INFO\" label.\nfunc (sm *PreStartupMessageData) AddInfo(key, title, value string, priority ...int) {\n\tpri := -1\n\tif len(priority) > 0 {\n\t\tpri = priority[0]\n\t}\n\n\tsm.addEntry(key, title, value, pri, StartupMessageLevelInfo)\n}\n\n// AddWarning adds a warning entry to the startup message with \"WARNING\" label.\nfunc (sm *PreStartupMessageData) AddWarning(key, title, value string, priority ...int) {\n\tpri := -1\n\tif len(priority) > 0 {\n\t\tpri = priority[0]\n\t}\n\n\tsm.addEntry(key, title, value, pri, StartupMessageLevelWarning)\n}\n\n// AddError adds an error entry to the startup message with \"ERROR\" label.\nfunc (sm *PreStartupMessageData) AddError(key, title, value string, priority ...int) {\n\tpri := -1\n\tif len(priority) > 0 {\n\t\tpri = priority[0]\n\t}\n\n\tsm.addEntry(key, title, value, pri, StartupMessageLevelError)\n}\n\n// EntryKeys returns all entry keys currently present in the startup message.\nfunc (sm *PreStartupMessageData) EntryKeys() []string {\n\tkeys := make([]string, 0, len(sm.entries))\n\tfor _, entry := range sm.entries {\n\t\tkeys = append(keys, entry.key)\n\t}\n\treturn keys\n}\n\n// ResetEntries removes all existing entries from the startup message.\nfunc (sm *PreStartupMessageData) ResetEntries() {\n\tsm.entries = sm.entries[:0]\n}\n\n// DeleteEntry removes a specific entry from the startup message by its key.\nfunc (sm *PreStartupMessageData) DeleteEntry(key string) {\n\tif sm.entries == nil {\n\t\treturn\n\t}\n\n\tfor i, entry := range sm.entries {\n\t\tif entry.key == key {\n\t\t\tsm.entries = append(sm.entries[:i], sm.entries[i+1:]...)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (sm *PreStartupMessageData) addEntry(key, title, value string, priority int, level StartupMessageLevel) {\n\tif sm.entries == nil {\n\t\tsm.entries = make([]startupMessageEntry, 0, 8)\n\t}\n\n\tfor i, entry := range sm.entries {\n\t\tif entry.key != key {\n\t\t\tcontinue\n\t\t}\n\n\t\tsm.entries[i].value = value\n\t\tsm.entries[i].title = title\n\t\tsm.entries[i].level = level\n\t\tsm.entries[i].priority = priority\n\t\treturn\n\t}\n\n\tsm.entries = append(sm.entries, startupMessageEntry{\n\t\tkey:      key,\n\t\ttitle:    title,\n\t\tvalue:    value,\n\t\tpriority: priority,\n\t\tlevel:    level,\n\t})\n}\n\nfunc newPreStartupMessageData(listenData *ListenData) *PreStartupMessageData {\n\treturn &PreStartupMessageData{ListenData: listenData}\n}\n\n// PostStartupMessageData contains metadata exposed to OnPostStartupMessage hooks.\ntype PostStartupMessageData struct {\n\t*ListenData\n\n\t// Disabled indicates whether the startup message was disabled via configuration.\n\tDisabled bool\n\n\t// IsChild indicates whether the current process is a child in prefork mode.\n\tIsChild bool\n\n\t// Prevented indicates whether the startup message was suppressed by a pre-startup hook using PreventDefault property.\n\tPrevented bool\n}\n\nfunc newPostStartupMessageData(listenData *ListenData, disabled, isChild, prevented bool) *PostStartupMessageData {\n\tclone := *listenData\n\tif len(listenData.ChildPIDs) > 0 {\n\t\tclone.ChildPIDs = slices.Clone(listenData.ChildPIDs)\n\t}\n\n\treturn &PostStartupMessageData{\n\t\tListenData: &clone,\n\t\tDisabled:   disabled,\n\t\tIsChild:    isChild,\n\t\tPrevented:  prevented,\n\t}\n}\n\nfunc newHooks(app *App) *Hooks {\n\treturn &Hooks{\n\t\tapp:            app,\n\t\tonRoute:        make([]OnRouteHandler, 0),\n\t\tonGroup:        make([]OnGroupHandler, 0),\n\t\tonGroupName:    make([]OnGroupNameHandler, 0),\n\t\tonName:         make([]OnNameHandler, 0),\n\t\tonListen:       make([]OnListenHandler, 0),\n\t\tonPreStartup:   make([]OnPreStartupMessageHandler, 0),\n\t\tonPostStartup:  make([]OnPostStartupMessageHandler, 0),\n\t\tonPreShutdown:  make([]OnPreShutdownHandler, 0),\n\t\tonPostShutdown: make([]OnPostShutdownHandler, 0),\n\t\tonFork:         make([]OnForkHandler, 0),\n\t\tonMount:        make([]OnMountHandler, 0),\n\t}\n}\n\n// OnRoute is a hook to execute user functions on each route registration.\n// Also you can get route properties by route parameter.\nfunc (h *Hooks) OnRoute(handler ...OnRouteHandler) {\n\th.app.mutex.Lock()\n\th.onRoute = append(h.onRoute, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnName is a hook to execute user functions on each route naming.\n// Also you can get route properties by route parameter.\n//\n// WARN: OnName only works with naming routes, not groups.\nfunc (h *Hooks) OnName(handler ...OnNameHandler) {\n\th.app.mutex.Lock()\n\th.onName = append(h.onName, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnGroup is a hook to execute user functions on each group registration.\n// Also you can get group properties by group parameter.\nfunc (h *Hooks) OnGroup(handler ...OnGroupHandler) {\n\th.app.mutex.Lock()\n\th.onGroup = append(h.onGroup, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnGroupName is a hook to execute user functions on each group naming.\n// Also you can get group properties by group parameter.\n//\n// WARN: OnGroupName only works with naming groups, not routes.\nfunc (h *Hooks) OnGroupName(handler ...OnGroupNameHandler) {\n\th.app.mutex.Lock()\n\th.onGroupName = append(h.onGroupName, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnListen is a hook to execute user functions on Listen or Listener.\nfunc (h *Hooks) OnListen(handler ...OnListenHandler) {\n\th.app.mutex.Lock()\n\th.onListen = append(h.onListen, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnPreStartupMessage is a hook to execute user functions before the startup message is printed.\nfunc (h *Hooks) OnPreStartupMessage(handler ...OnPreStartupMessageHandler) {\n\th.app.mutex.Lock()\n\th.onPreStartup = append(h.onPreStartup, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnPostStartupMessage is a hook to execute user functions after the startup message is printed (or skipped).\nfunc (h *Hooks) OnPostStartupMessage(handler ...OnPostStartupMessageHandler) {\n\th.app.mutex.Lock()\n\th.onPostStartup = append(h.onPostStartup, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnPreShutdown is a hook to execute user functions before Shutdown.\nfunc (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) {\n\th.app.mutex.Lock()\n\th.onPreShutdown = append(h.onPreShutdown, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnPostShutdown is a hook to execute user functions after Shutdown.\nfunc (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) {\n\th.app.mutex.Lock()\n\th.onPostShutdown = append(h.onPostShutdown, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnFork is a hook to execute user function after fork process.\nfunc (h *Hooks) OnFork(handler ...OnForkHandler) {\n\th.app.mutex.Lock()\n\th.onFork = append(h.onFork, handler...)\n\th.app.mutex.Unlock()\n}\n\n// OnMount is a hook to execute user function after mounting process.\n// The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter.\n// It works for app and group mounting.\nfunc (h *Hooks) OnMount(handler ...OnMountHandler) {\n\th.app.mutex.Lock()\n\th.onMount = append(h.onMount, handler...)\n\th.app.mutex.Unlock()\n}\n\nfunc (h *Hooks) executeOnRouteHooks(route *Route) error {\n\tif route == nil {\n\t\treturn nil\n\t}\n\n\tcloned := *route\n\n\t// Check mounting\n\tif h.app.mountFields.mountPath != \"\" {\n\t\tcloned.path = h.app.mountFields.mountPath + cloned.path\n\t\tcloned.Path = cloned.path\n\t}\n\n\tfor _, v := range h.onRoute {\n\t\tif err := v(cloned); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnNameHooks(route *Route) error {\n\tif route == nil {\n\t\treturn nil\n\t}\n\n\tcloned := *route\n\n\t// Check mounting\n\tif h.app.mountFields.mountPath != \"\" {\n\t\tcloned.path = h.app.mountFields.mountPath + cloned.path\n\t\tcloned.Path = cloned.path\n\t}\n\n\tfor _, v := range h.onName {\n\t\tif err := v(cloned); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnGroupHooks(group Group) error {\n\t// Check mounting\n\tif h.app.mountFields.mountPath != \"\" {\n\t\tgroup.Prefix = h.app.mountFields.mountPath + group.Prefix\n\t}\n\n\tfor _, v := range h.onGroup {\n\t\tif err := v(group); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnGroupNameHooks(group Group) error {\n\t// Check mounting\n\tif h.app.mountFields.mountPath != \"\" {\n\t\tgroup.Prefix = h.app.mountFields.mountPath + group.Prefix\n\t}\n\n\tfor _, v := range h.onGroupName {\n\t\tif err := v(group); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnListenHooks(listenData *ListenData) error {\n\tfor _, v := range h.onListen {\n\t\tif err := v(*listenData); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnPreStartupMessageHooks(data *PreStartupMessageData) error {\n\tfor _, handler := range h.onPreStartup {\n\t\tif err := handler(data); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnPostStartupMessageHooks(data *PostStartupMessageData) error {\n\tfor _, handler := range h.onPostStartup {\n\t\tif err := handler(data); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *Hooks) executeOnPreShutdownHooks() {\n\tfor _, v := range h.onPreShutdown {\n\t\tif err := v(); err != nil {\n\t\t\tlog.Errorf(\"failed to call pre shutdown hook: %v\", err)\n\t\t}\n\t}\n}\n\nfunc (h *Hooks) executeOnPostShutdownHooks(err error) {\n\tfor _, v := range h.onPostShutdown {\n\t\tif hookErr := v(err); hookErr != nil {\n\t\t\tlog.Errorf(\"failed to call post shutdown hook: %v\", hookErr)\n\t\t}\n\t}\n}\n\nfunc (h *Hooks) executeOnForkHooks(pid int) {\n\tfor _, v := range h.onFork {\n\t\tif err := v(pid); err != nil {\n\t\t\tlog.Errorf(\"failed to call fork hook: %v\", err)\n\t\t}\n\t}\n}\n\nfunc (h *Hooks) executeOnMountHooks(app *App) error {\n\tfor _, v := range h.onMount {\n\t\tif err := v(app); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "hooks_test.go",
    "content": "package fiber\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"os\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/bytebufferpool\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\nconst testMountPath = \"/api\"\n\nfunc testSimpleHandler(c Ctx) error {\n\treturn c.SendString(\"simple\")\n}\n\nfunc Test_Hook_OnRoute(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnRoute(func(r Route) error {\n\t\trequire.Empty(t, r.Name)\n\n\t\treturn nil\n\t})\n\n\tapp.Get(\"/\", testSimpleHandler).Name(\"x\")\n\n\tsubApp := New()\n\tsubApp.Get(\"/test\", testSimpleHandler)\n\n\tapp.Use(\"/sub\", subApp)\n}\n\nfunc Test_Hook_OnRoute_Mount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tsubApp := New()\n\tapp.Use(\"/sub\", subApp)\n\n\tsubApp.Hooks().OnRoute(func(r Route) error {\n\t\trequire.Equal(t, \"/sub/test\", r.Path)\n\n\t\treturn nil\n\t})\n\n\tapp.Hooks().OnRoute(func(r Route) error {\n\t\trequire.Equal(t, \"/\", r.Path)\n\n\t\treturn nil\n\t})\n\n\tapp.Get(\"/\", testSimpleHandler).Name(\"x\")\n\tsubApp.Get(\"/test\", testSimpleHandler)\n}\n\nfunc Test_Hook_OnName(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Hooks().OnName(func(r Route) error {\n\t\t_, err := buf.WriteString(r.Name)\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tapp.Get(\"/\", testSimpleHandler).Name(\"index\")\n\n\tsubApp := New()\n\tsubApp.Get(\"/test\", testSimpleHandler)\n\tsubApp.Get(\"/test2\", testSimpleHandler)\n\n\tapp.Use(\"/sub\", subApp)\n\n\trequire.Equal(t, \"index\", buf.String())\n}\n\nfunc Test_Hook_OnName_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnName(func(_ Route) error {\n\t\treturn errors.New(\"unknown error\")\n\t})\n\n\trequire.PanicsWithError(t, \"unknown error\", func() {\n\t\tapp.Get(\"/\", testSimpleHandler).Name(\"index\")\n\t})\n}\n\nfunc Test_Hook_OnGroup(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Hooks().OnGroup(func(g Group) error {\n\t\t_, err := buf.WriteString(g.Prefix)\n\t\trequire.NoError(t, err)\n\t\treturn nil\n\t})\n\n\tgrp := app.Group(\"/x\").Name(\"x.\")\n\tgrp.Group(\"/a\")\n\n\trequire.Equal(t, \"/x/x/a\", buf.String())\n}\n\nfunc Test_Hook_OnGroup_Mount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tmicro := New()\n\tmicro.Use(\"/john\", app)\n\n\tapp.Hooks().OnGroup(func(g Group) error {\n\t\trequire.Equal(t, \"/john/v1\", g.Prefix)\n\t\treturn nil\n\t})\n\n\tv1 := app.Group(\"/v1\")\n\tv1.Get(\"/doe\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n}\n\nfunc Test_Hook_OnGroupName(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tbuf2 := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf2)\n\n\tapp.Hooks().OnGroupName(func(g Group) error {\n\t\t_, err := buf.WriteString(g.name)\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tapp.Hooks().OnName(func(r Route) error {\n\t\t_, err := buf2.WriteString(r.Name)\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tgrp := app.Group(\"/x\").Name(\"x.\")\n\tgrp.Get(\"/test\", testSimpleHandler).Name(\"test\")\n\tgrp.Get(\"/test2\", testSimpleHandler)\n\n\trequire.Equal(t, \"x.\", buf.String())\n\trequire.Equal(t, \"x.test\", buf2.String())\n}\n\nfunc Test_Hook_OnGroupName_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnGroupName(func(_ Group) error {\n\t\treturn errors.New(\"unknown error\")\n\t})\n\n\trequire.PanicsWithError(t, \"unknown error\", func() {\n\t\t_ = app.Group(\"/x\").Name(\"x.\")\n\t})\n}\n\nfunc Test_Hook_OnPreShutdown(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Hooks().OnPreShutdown(func() error {\n\t\t_, err := buf.WriteString(\"pre-shutdown\")\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, app.Shutdown())\n\trequire.Equal(t, \"pre-shutdown\", buf.String())\n}\n\nfunc Test_Hook_OnPostShutdown(t *testing.T) {\n\tt.Run(\"should execute post shutdown hook with error\", func(t *testing.T) {\n\t\tapp := New()\n\t\texpectedErr := errors.New(\"test shutdown error\")\n\n\t\thookCalled := make(chan error, 1)\n\t\tdefer close(hookCalled)\n\n\t\tapp.Hooks().OnPostShutdown(func(err error) error {\n\t\t\thookCalled <- err\n\t\t\treturn nil\n\t\t})\n\n\t\tgo func() {\n\t\t\tif err := app.Listen(\":0\"); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\tapp.hooks.executeOnPostShutdownHooks(expectedErr)\n\n\t\tselect {\n\t\tcase err := <-hookCalled:\n\t\t\trequire.Equal(t, expectedErr, err)\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"hook execution timeout\")\n\t\t}\n\n\t\trequire.NoError(t, app.Shutdown())\n\t})\n\n\tt.Run(\"should execute multiple hooks in order\", func(t *testing.T) {\n\t\tapp := New()\n\n\t\texecution := make([]int, 0)\n\n\t\tapp.Hooks().OnPostShutdown(func(_ error) error {\n\t\t\texecution = append(execution, 1)\n\t\t\treturn nil\n\t\t})\n\n\t\tapp.Hooks().OnPostShutdown(func(_ error) error {\n\t\t\texecution = append(execution, 2)\n\t\t\treturn nil\n\t\t})\n\n\t\tapp.hooks.executeOnPostShutdownHooks(nil)\n\n\t\trequire.Len(t, execution, 2, \"expected 2 hooks to execute\")\n\t\trequire.Equal(t, []int{1, 2}, execution, \"hooks executed in wrong order\")\n\t})\n\n\tt.Run(\"should handle hook error\", func(_ *testing.T) {\n\t\tapp := New()\n\t\thookErr := errors.New(\"hook error\")\n\n\t\tapp.Hooks().OnPostShutdown(func(_ error) error {\n\t\t\treturn hookErr\n\t\t})\n\n\t\t// Should not panic\n\t\tapp.hooks.executeOnPostShutdownHooks(nil)\n\t})\n}\n\nfunc Test_Hook_OnListen(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Hooks().OnListen(func(_ ListenData) error {\n\t\t_, err := buf.WriteString(\"ready\")\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\trequire.NoError(t, app.Listen(\":0\"))\n\n\trequire.Equal(t, \"ready\", buf.String())\n}\n\nfunc Test_ListenDataMetadata(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{AppName: \"meta\"})\n\tapp.handlersCount = 42\n\n\tcfg := ListenConfig{EnablePrefork: true}\n\tchildPIDs := []int{11, 22}\n\tlistenData := app.prepareListenData(\":3030\", true, &cfg, childPIDs)\n\n\tapp.Hooks().OnListen(func(data ListenData) error {\n\t\trequire.Equal(t, globalIpv4Addr, data.Host)\n\t\trequire.Equal(t, \"3030\", data.Port)\n\t\trequire.True(t, data.TLS)\n\t\trequire.Equal(t, Version, data.Version)\n\t\trequire.Equal(t, \"meta\", data.AppName)\n\t\trequire.Equal(t, 42, data.HandlerCount)\n\t\trequire.Equal(t, runtime.GOMAXPROCS(0), data.ProcessCount)\n\t\trequire.Equal(t, os.Getpid(), data.PID)\n\t\trequire.True(t, data.Prefork)\n\t\trequire.Equal(t, childPIDs, data.ChildPIDs)\n\t\trequire.Equal(t, app.config.ColorScheme, data.ColorScheme)\n\n\t\treturn nil\n\t})\n\n\tapp.runOnListenHooks(listenData)\n\n\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\trequire.Equal(t, globalIpv4Addr, data.Host)\n\t\trequire.Equal(t, \"3030\", data.Port)\n\t\trequire.True(t, data.TLS)\n\t\trequire.Equal(t, Version, data.Version)\n\t\trequire.Equal(t, \"meta\", data.AppName)\n\t\trequire.Equal(t, 42, data.HandlerCount)\n\t\trequire.Equal(t, runtime.GOMAXPROCS(0), data.ProcessCount)\n\t\trequire.Equal(t, os.Getpid(), data.PID)\n\t\trequire.True(t, data.Prefork)\n\t\trequire.Equal(t, childPIDs, data.ChildPIDs)\n\t\trequire.Equal(t, app.config.ColorScheme, data.ColorScheme)\n\n\t\tdata.ResetEntries()\n\n\t\tdata.AddInfo(\"custom\", \"Custom Info\", \"value\", 3)\n\t\tdata.AddInfo(\"other\", \"Other Info\", \"value\", 2)\n\n\t\treturn nil\n\t})\n\n\tpre := newPreStartupMessageData(listenData)\n\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\n\trequire.Equal(t, \"value\", pre.entries[0].value)\n\trequire.Equal(t, \"Custom Info\", pre.entries[0].title)\n\trequire.Equal(t, 3, pre.entries[0].priority)\n\n\trequire.Equal(t, \"value\", pre.entries[1].value)\n\trequire.Equal(t, \"Other Info\", pre.entries[1].title)\n\trequire.Equal(t, 2, pre.entries[1].priority)\n\trequire.False(t, pre.PreventDefault)\n}\n\nfunc Test_ListenData_Hook_HelperFunctions(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"EntryKeys\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddInfo(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\tdata.AddInfo(\"key2\", \"Title 2\", \"Value 2\", 2)\n\n\t\t\tkeys := data.EntryKeys()\n\t\t\trequire.Len(t, keys, 2)\n\t\t\trequire.Equal(t, \"key1\", keys[0])\n\t\t\trequire.Equal(t, \"key2\", keys[1])\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"ResetEntries\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddInfo(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\tdata.AddInfo(\"key2\", \"Title 2\", \"Value 2\", 2)\n\n\t\t\trequire.Len(t, data.entries, 2)\n\n\t\t\tdata.ResetEntries()\n\t\t\trequire.Empty(t, data.entries)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"AddInfo\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddInfo(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\trequire.Len(t, data.entries, 1)\n\t\t\trequire.Equal(t, \"key1\", data.entries[0].key)\n\t\t\trequire.Equal(t, \"Title 1\", data.entries[0].title)\n\t\t\trequire.Equal(t, \"Value 1\", data.entries[0].value)\n\t\t\trequire.Equal(t, 1, data.entries[0].priority)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"AddWarning\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddWarning(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\trequire.Len(t, data.entries, 1)\n\t\t\trequire.Equal(t, \"key1\", data.entries[0].key)\n\t\t\trequire.Equal(t, \"Title 1\", data.entries[0].title)\n\t\t\trequire.Equal(t, \"Value 1\", data.entries[0].value)\n\t\t\trequire.Equal(t, 1, data.entries[0].priority)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"AddError\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddError(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\trequire.Len(t, data.entries, 1)\n\t\t\trequire.Equal(t, \"key1\", data.entries[0].key)\n\t\t\trequire.Equal(t, \"Title 1\", data.entries[0].title)\n\t\t\trequire.Equal(t, \"Value 1\", data.entries[0].value)\n\t\t\trequire.Equal(t, 1, data.entries[0].priority)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"AddInfo-UpdateExisting\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddInfo(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\tdata.AddInfo(\"key1\", \"Updated Title\", \"Updated Value\", 2)\n\n\t\t\trequire.Len(t, data.entries, 1)\n\t\t\trequire.Equal(t, \"key1\", data.entries[0].key)\n\t\t\trequire.Equal(t, \"Updated Title\", data.entries[0].title)\n\t\t\trequire.Equal(t, \"Updated Value\", data.entries[0].value)\n\t\t\trequire.Equal(t, 2, data.entries[0].priority)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n\n\tt.Run(\"DeleteEntry\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\n\t\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\t\tdata.ResetEntries()\n\n\t\t\tdata.AddInfo(\"key1\", \"Title 1\", \"Value 1\", 1)\n\t\t\tdata.AddInfo(\"key2\", \"Title 2\", \"Value 2\", 2)\n\n\t\t\trequire.Len(t, data.entries, 2)\n\n\t\t\tdata.DeleteEntry(\"key1\")\n\t\t\trequire.Len(t, data.entries, 1)\n\t\t\trequire.Equal(t, \"key2\", data.entries[0].key)\n\n\t\t\tdata.DeleteEntry(\"key-not-exist\") // should not panic\n\t\t\trequire.Len(t, data.entries, 1)\n\n\t\t\treturn nil\n\t\t})\n\n\t\tpre := newPreStartupMessageData(&ListenData{})\n\t\trequire.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre))\n\t})\n}\n\nfunc Test_Hook_OnListenPrefork(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Hooks().OnListen(func(_ ListenData) error {\n\t\t_, err := buf.WriteString(\"ready\")\n\t\trequire.NoError(t, err)\n\n\t\treturn nil\n\t})\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{DisableStartupMessage: true, EnablePrefork: true}))\n\trequire.Equal(t, \"ready\", buf.String())\n}\n\nfunc Test_Hook_OnHook(t *testing.T) {\n\tapp := New()\n\n\t// Reset test var\n\ttestPreforkMaster = true\n\ttestOnPrefork = true\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\tapp.Hooks().OnFork(func(pid int) error {\n\t\trequire.Equal(t, 1, pid)\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, app.prefork(\":0\", nil, &ListenConfig{DisableStartupMessage: true, EnablePrefork: true}))\n}\n\nfunc Test_Hook_OnMount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", testSimpleHandler).Name(\"x\")\n\n\tsubApp := New()\n\tsubApp.Get(\"/test\", testSimpleHandler)\n\n\tsubApp.Hooks().OnMount(func(parent *App) error {\n\t\trequire.Empty(t, parent.mountFields.mountPath)\n\n\t\treturn nil\n\t})\n\n\tapp.Use(\"/sub\", subApp)\n}\n\nfunc Test_executeOnRouteHooks_ErrorWithMount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.mountFields.mountPath = testMountPath\n\n\tvar received string\n\tapp.Hooks().OnRoute(func(r Route) error {\n\t\treceived = r.Path\n\t\treturn errors.New(\"hook error\")\n\t})\n\n\terr := app.hooks.executeOnRouteHooks(&Route{Path: \"/foo\", path: \"/foo\"})\n\trequire.Equal(t, testMountPath+\"/foo\", received)\n\trequire.EqualError(t, err, \"hook error\")\n}\n\nfunc Test_executeOnNameHooks_ErrorWithMount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.mountFields.mountPath = testMountPath\n\n\tvar received string\n\tapp.Hooks().OnName(func(r Route) error {\n\t\treceived = r.Path\n\t\treturn errors.New(\"name error\")\n\t})\n\n\terr := app.hooks.executeOnNameHooks(&Route{Path: \"/bar\", path: \"/bar\"})\n\trequire.Equal(t, testMountPath+\"/bar\", received)\n\trequire.EqualError(t, err, \"name error\")\n}\n\nfunc Test_executeOnGroupHooks_ErrorWithMount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.mountFields.mountPath = testMountPath\n\n\tvar prefix string\n\tapp.Hooks().OnGroup(func(g Group) error {\n\t\tprefix = g.Prefix\n\t\treturn errors.New(\"group error\")\n\t})\n\n\terr := app.hooks.executeOnGroupHooks(Group{Prefix: \"/grp\"})\n\trequire.Equal(t, testMountPath+\"/grp\", prefix)\n\trequire.EqualError(t, err, \"group error\")\n}\n\nfunc Test_executeOnGroupNameHooks_ErrorWithMount(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.mountFields.mountPath = testMountPath\n\n\tvar prefix string\n\tapp.Hooks().OnGroupName(func(g Group) error {\n\t\tprefix = g.Prefix\n\t\treturn errors.New(\"group name error\")\n\t})\n\n\terr := app.hooks.executeOnGroupNameHooks(Group{Prefix: \"/grp\"})\n\trequire.Equal(t, testMountPath+\"/grp\", prefix)\n\trequire.EqualError(t, err, \"group name error\")\n}\n\nfunc Test_executeOnListenHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnListen(func(_ ListenData) error {\n\t\treturn errors.New(\"listen error\")\n\t})\n\n\terr := app.hooks.executeOnListenHooks(&ListenData{Host: \"127.0.0.1\", Port: \"0\"})\n\trequire.EqualError(t, err, \"listen error\")\n}\n\nfunc Test_executeOnPreStartupMessageHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnPreStartupMessage(func(_ *PreStartupMessageData) error {\n\t\treturn errors.New(\"pre startup message error\")\n\t})\n\n\terr := app.hooks.executeOnPreStartupMessageHooks(newPreStartupMessageData(&ListenData{}))\n\trequire.EqualError(t, err, \"pre startup message error\")\n}\n\nfunc Test_executeOnPostStartupMessageHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnPostStartupMessage(func(_ *PostStartupMessageData) error {\n\t\treturn errors.New(\"post startup message error\")\n\t})\n\n\terr := app.hooks.executeOnPostStartupMessageHooks(newPostStartupMessageData(&ListenData{}, false, false, false))\n\trequire.EqualError(t, err, \"post startup message error\")\n}\n\nfunc Test_executeOnPreShutdownHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnPreShutdown(func() error {\n\t\treturn errors.New(\"pre error\")\n\t})\n\n\tvar buf bytes.Buffer\n\tlog.SetOutput(&buf)\n\tapp.hooks.executeOnPreShutdownHooks()\n\trequire.NotZero(t, buf.Len())\n}\n\nfunc Test_executeOnForkHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Hooks().OnFork(func(pid int) error {\n\t\trequire.Equal(t, 1, pid)\n\t\treturn errors.New(\"fork error\")\n\t})\n\n\tvar buf bytes.Buffer\n\tlog.SetOutput(&buf)\n\tapp.hooks.executeOnForkHooks(1)\n\trequire.NotZero(t, buf.Len())\n}\n\nfunc Test_executeOnMountHooks_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tparent := New()\n\n\tapp.Hooks().OnMount(func(a *App) error {\n\t\trequire.Equal(t, parent, a)\n\t\treturn errors.New(\"mount error\")\n\t})\n\n\terr := app.hooks.executeOnMountHooks(parent)\n\trequire.EqualError(t, err, \"mount error\")\n}\n"
  },
  {
    "path": "internal/memory/memory.go",
    "content": "// Package memory provides a high-performance in-memory storage that can store\n// any type without encoding overhead. Unlike the standard storage interface,\n// this storage works directly with Go types for maximum speed.\n//\n// # Safety Considerations\n//\n// This storage automatically performs defensive copying for:\n//   - String keys: Copied to prevent corruption from pooled buffers\n//   - []byte values: Copied on both Set and Get to prevent external mutation\n//\n// For other types (structs, ints, etc.), Go's value semantics provide natural\n// protection. However, if storing pointers or slices of non-byte types,\n// callers are responsible for not mutating the underlying data.\n//\n// This storage is primarily used internally by middleware for performance-\n// critical operations where the stored data types are known and controlled.\npackage memory\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Storage stores arbitrary values in memory for use in tests and benchmarks.\ntype Storage struct {\n\tdata map[string]item // data\n\tmu   sync.RWMutex\n}\n\ntype item struct {\n\tv any // val\n\t// max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000\n\te uint32 // exp\n}\n\n// New constructs an in-memory Storage initialized with a background GC loop.\nfunc New() *Storage {\n\tstore := &Storage{\n\t\tdata: make(map[string]item),\n\t}\n\tutils.StartTimeStampUpdater()\n\tgo store.gc(1 * time.Second)\n\treturn store\n}\n\n// Get retrieves the value stored under key, returning nil when the entry does\n// not exist or has expired.\n//\n// For []byte values, this returns a defensive copy to prevent callers from\n// mutating the stored data. Other types are returned as-is.\nfunc (s *Storage) Get(key string) any {\n\ts.mu.RLock()\n\tv, ok := s.data[key]\n\ts.mu.RUnlock()\n\tif !ok || v.e != 0 && v.e <= utils.Timestamp() {\n\t\treturn nil\n\t}\n\n\t// Defensive copy for byte slices to prevent external mutation\n\tif b, ok := v.v.([]byte); ok {\n\t\treturn utils.CopyBytes(b)\n\t}\n\n\treturn v.v\n}\n\n// Set stores val under key and applies the optional ttl before expiring the\n// entry. A non-positive ttl keeps the item forever.\n//\n// String keys are defensively copied to prevent corruption from pooled buffers.\n// []byte values are also copied to prevent external mutation of stored data.\n// Other types are stored as-is (structs are copied by value automatically).\nfunc (s *Storage) Set(key string, val any, ttl time.Duration) {\n\tvar exp uint32\n\tif ttl > 0 {\n\t\texp = uint32(ttl.Seconds()) + utils.Timestamp()\n\t}\n\n\t// Defensive copies to prevent unsafe reuse from sync.Pool\n\tkeyCopy := utils.CopyString(key)\n\n\t// Copy byte slices to prevent external mutation\n\tif b, ok := val.([]byte); ok {\n\t\tval = utils.CopyBytes(b)\n\t}\n\n\ti := item{e: exp, v: val}\n\ts.mu.Lock()\n\ts.data[keyCopy] = i\n\ts.mu.Unlock()\n}\n\n// Delete removes key and its associated value from the storage.\nfunc (s *Storage) Delete(key string) {\n\ts.mu.Lock()\n\tdelete(s.data, key)\n\ts.mu.Unlock()\n}\n\n// Reset clears the storage by dropping every stored key.\nfunc (s *Storage) Reset() {\n\tnd := make(map[string]item)\n\ts.mu.Lock()\n\ts.data = nd\n\ts.mu.Unlock()\n}\n\nfunc (s *Storage) gc(sleep time.Duration) {\n\tticker := time.NewTicker(sleep)\n\tdefer ticker.Stop()\n\tvar expired []string\n\n\tfor range ticker.C {\n\t\tts := utils.Timestamp()\n\t\texpired = expired[:0]\n\t\ts.mu.RLock()\n\t\tfor key, v := range s.data {\n\t\t\tif v.e != 0 && v.e <= ts {\n\t\t\t\texpired = append(expired, key)\n\t\t\t}\n\t\t}\n\t\ts.mu.RUnlock()\n\n\t\tif len(expired) == 0 {\n\t\t\t// avoid locking if nothing to delete\n\t\t\tcontinue\n\t\t}\n\n\t\ts.mu.Lock()\n\t\t// Double-checked locking.\n\t\t// We might have replaced the item in the meantime.\n\t\tfor i := range expired {\n\t\t\tv := s.data[expired[i]]\n\t\t\tif v.e != 0 && v.e <= ts {\n\t\t\t\tdelete(s.data, expired[i])\n\t\t\t}\n\t\t}\n\t\ts.mu.Unlock()\n\t}\n}\n"
  },
  {
    "path": "internal/memory/memory_test.go",
    "content": "package memory\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_Memory -v -race\nfunc Test_Memory(t *testing.T) {\n\tt.Parallel()\n\tstore := New()\n\tvar (\n\t\tkey     = \"john-internal\"\n\t\tval any = []byte(\"doe\")\n\t\texp     = 1 * time.Second\n\t)\n\n\t// Set key with value\n\tstore.Set(key, val, 0)\n\tresult := store.Get(key)\n\trequire.Equal(t, val, result)\n\n\t// Get non-existing key\n\tresult = store.Get(\"empty\")\n\trequire.Nil(t, result)\n\n\t// Set key with value and ttl\n\tstore.Set(key, val, exp)\n\ttime.Sleep(1100 * time.Millisecond)\n\tresult = store.Get(key)\n\trequire.Nil(t, result)\n\n\t// Set key with value and no expiration\n\tstore.Set(key, val, 0)\n\tresult = store.Get(key)\n\trequire.Equal(t, val, result)\n\n\t// Delete key\n\tstore.Delete(key)\n\tresult = store.Get(key)\n\trequire.Nil(t, result)\n\n\t// Reset all keys\n\tstore.Set(\"john-reset\", val, 0)\n\tstore.Set(\"doe-reset\", val, 0)\n\tstore.Reset()\n\n\t// Check if all keys are deleted\n\tresult = store.Get(\"john-reset\")\n\trequire.Nil(t, result)\n\tresult = store.Get(\"doe-reset\")\n\trequire.Nil(t, result)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4\nfunc Benchmark_Memory(b *testing.B) {\n\tkeyLength := 1000\n\tkeys := make([]string, keyLength)\n\tfor i := range keyLength {\n\t\tkeys[i] = utils.UUIDv4()\n\t}\n\tvalue := []byte(\"joe\")\n\n\tttl := 2 * time.Second\n\tb.Run(\"fiber_memory\", func(b *testing.B) {\n\t\td := New()\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tfor _, key := range keys {\n\t\t\t\td.Set(key, value, ttl)\n\t\t\t}\n\t\t\tfor _, key := range keys {\n\t\t\t\t_ = d.Get(key)\n\t\t\t}\n\t\t\tfor _, key := range keys {\n\t\t\t\td.Delete(key)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/storage/memory/config.go",
    "content": "package memory\n\nimport (\n\t\"time\"\n)\n\n// Config defines the config for storage.\ntype Config struct {\n\t// Time before deleting expired keys\n\t//\n\t// Default is 10 * time.Second\n\tGCInterval time.Duration\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tGCInterval: 10 * time.Second,\n}\n\n// configDefault is a helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif int(cfg.GCInterval.Seconds()) <= 0 {\n\t\tcfg.GCInterval = ConfigDefault.GCInterval\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "internal/storage/memory/memory.go",
    "content": "// Package memory Is a copy of the storage memory from the external storage packet as a purpose to test the behavior\n// in the unittests when using a storages from these packets\npackage memory\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Storage provides an in-memory implementation of the storage interface for\n// testing purposes.\ntype Storage struct {\n\tdb         map[string]Entry\n\tdone       chan struct{}\n\tgcInterval time.Duration\n\tmux        sync.RWMutex\n}\n\n// Entry represents a value stored in memory along with its expiration.\ntype Entry struct {\n\tdata []byte\n\t// max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000\n\texpiry uint32\n}\n\n// New creates a new memory storage.\nfunc New(config ...Config) *Storage {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Create storage\n\tstore := &Storage{\n\t\tdb:         make(map[string]Entry),\n\t\tgcInterval: cfg.GCInterval,\n\t\tdone:       make(chan struct{}),\n\t}\n\n\t// Start garbage collector\n\tutils.StartTimeStampUpdater()\n\tgo store.gc()\n\n\treturn store\n}\n\n// Get returns the stored value for key, ignoring missing or expired entries by\n// returning nil.\nfunc (s *Storage) Get(key string) ([]byte, error) {\n\tif key == \"\" {\n\t\treturn nil, nil\n\t}\n\ts.mux.RLock()\n\tv, ok := s.db[key]\n\ts.mux.RUnlock()\n\n\tif !ok || v.expiry != 0 && v.expiry <= utils.Timestamp() {\n\t\treturn nil, nil\n\t}\n\n\t// Return a copy to prevent callers from mutating stored data\n\treturn utils.CopyBytes(v.data), nil\n}\n\n// GetWithContext retrieves the value for the given key while honoring context\n// cancellation.\nfunc (s *Storage) GetWithContext(ctx context.Context, key string) ([]byte, error) {\n\tif err := wrapContextError(ctx, \"get\"); err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.Get(key)\n}\n\n// Set saves val under key and schedules it to expire after exp. A zero exp keeps\n// the entry indefinitely.\nfunc (s *Storage) Set(key string, val []byte, exp time.Duration) error {\n\t// Ain't Nobody Got Time For That\n\tif key == \"\" || len(val) == 0 {\n\t\treturn nil\n\t}\n\n\tvar expire uint32\n\tif exp != 0 {\n\t\texpire = uint32(exp.Seconds()) + utils.Timestamp()\n\t}\n\n\t// Copy both key and value to avoid unsafe reuse from sync.Pool\n\tkeyCopy := utils.CopyString(key)\n\tvalCopy := utils.CopyBytes(val)\n\n\te := Entry{data: valCopy, expiry: expire}\n\ts.mux.Lock()\n\ts.db[keyCopy] = e\n\ts.mux.Unlock()\n\treturn nil\n}\n\n// SetWithContext sets the value for the given key while honoring context\n// cancellation.\nfunc (s *Storage) SetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error {\n\tif err := wrapContextError(ctx, \"set\"); err != nil {\n\t\treturn err\n\t}\n\treturn s.Set(key, val, exp)\n}\n\n// Delete removes the value stored for key.\nfunc (s *Storage) Delete(key string) error {\n\t// Ain't Nobody Got Time For That\n\tif key == \"\" {\n\t\treturn nil\n\t}\n\ts.mux.Lock()\n\tdelete(s.db, key)\n\ts.mux.Unlock()\n\treturn nil\n}\n\n// DeleteWithContext removes the value for the given key while honoring\n// context cancellation.\nfunc (s *Storage) DeleteWithContext(ctx context.Context, key string) error {\n\tif err := wrapContextError(ctx, \"delete\"); err != nil {\n\t\treturn err\n\t}\n\treturn s.Delete(key)\n}\n\n// Reset clears all keys and values from the storage map.\nfunc (s *Storage) Reset() error {\n\tndb := make(map[string]Entry)\n\ts.mux.Lock()\n\ts.db = ndb\n\ts.mux.Unlock()\n\treturn nil\n}\n\n// ResetWithContext clears all stored keys while honoring context\n// cancellation.\nfunc (s *Storage) ResetWithContext(ctx context.Context) error {\n\tif err := wrapContextError(ctx, \"reset\"); err != nil {\n\t\treturn err\n\t}\n\treturn s.Reset()\n}\n\n// Close stops the background garbage collector and releases resources\n// associated with the storage instance.\nfunc (s *Storage) Close() error {\n\ts.done <- struct{}{}\n\treturn nil\n}\n\nfunc (s *Storage) gc() {\n\tticker := time.NewTicker(s.gcInterval)\n\tdefer ticker.Stop()\n\tvar expired []string\n\n\tfor {\n\t\tselect {\n\t\tcase <-s.done:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tts := utils.Timestamp()\n\t\t\texpired = expired[:0]\n\t\t\ts.mux.RLock()\n\t\t\tfor id, v := range s.db {\n\t\t\t\tif v.expiry != 0 && v.expiry < ts {\n\t\t\t\t\texpired = append(expired, id)\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.mux.RUnlock()\n\n\t\t\tif len(expired) == 0 {\n\t\t\t\t// avoid locking if nothing to delete\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ts.mux.Lock()\n\t\t\t// Double-checked locking.\n\t\t\t// We might have replaced the item in the meantime.\n\t\t\tfor i := range expired {\n\t\t\t\tv := s.db[expired[i]]\n\t\t\t\tif v.expiry != 0 && v.expiry <= ts {\n\t\t\t\t\tdelete(s.db, expired[i])\n\t\t\t\t}\n\t\t\t}\n\t\t\ts.mux.Unlock()\n\t\t}\n\t}\n}\n\n// Conn returns the underlying storage map. The map must not be modified by\n// callers.\nfunc (s *Storage) Conn() map[string]Entry {\n\ts.mux.RLock()\n\tdefer s.mux.RUnlock()\n\treturn s.db\n}\n\n// Keys returns all keys stored in the memory storage.\nfunc (s *Storage) Keys() ([][]byte, error) {\n\ts.mux.RLock()\n\tdefer s.mux.RUnlock()\n\n\tif len(s.db) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tts := utils.Timestamp()\n\tkeys := make([][]byte, 0, len(s.db))\n\tfor key, v := range s.db {\n\t\t// Filter out the expired keys\n\t\tif v.expiry == 0 || v.expiry > ts {\n\t\t\tkeys = append(keys, []byte(key))\n\t\t}\n\t}\n\n\t// Double check if no valid keys were found\n\tif len(keys) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn keys, nil\n}\n\nfunc wrapContextError(ctx context.Context, op string) error {\n\tif err := ctx.Err(); err != nil {\n\t\treturn fmt.Errorf(\"memory storage %s: %w\", op, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/storage/memory/memory_test.go",
    "content": "package memory\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_Storage_Memory_Set(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n}\n\nfunc Test_Storage_Memory_SetWithContext(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\terr := testStore.SetWithContext(ctx, key, val, 0)\n\trequire.ErrorIs(t, err, context.Canceled)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_Set_Override(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\terr = testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n}\n\nfunc Test_Storage_Memory_Get(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tresult, err := testStore.Get(key)\n\trequire.NoError(t, err)\n\trequire.Equal(t, val, result)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n}\n\nfunc Test_Storage_Memory_GetWithContext(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\tresult, err := testStore.GetWithContext(ctx, key)\n\trequire.ErrorIs(t, err, context.Canceled)\n\trequire.Nil(t, result)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n}\n\nfunc Test_Storage_Memory_Set_Expiration(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New(Config{\n\t\t\tGCInterval: 300 * time.Millisecond,\n\t\t})\n\t\tkey = \"john\"\n\t\tval = []byte(\"doe\")\n\t\texp = 1 * time.Second\n\t)\n\n\terr := testStore.Set(key, val, exp)\n\trequire.NoError(t, err)\n\n\t// interval + expire + buffer\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tresult, err := testStore.Get(key)\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_Set_Long_Expiration_with_Keys(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t\texp       = 3 * time.Second\n\t)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n\n\terr = testStore.Set(key, val, exp)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1100 * time.Millisecond)\n\n\tkeys, err = testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n\n\ttime.Sleep(4000 * time.Millisecond)\n\tresult, err := testStore.Get(key)\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tkeys, err = testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_Get_NotExist(t *testing.T) {\n\tt.Parallel()\n\ttestStore := New()\n\tresult, err := testStore.Get(\"notexist\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_Delete(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n\n\terr = testStore.Delete(key)\n\trequire.NoError(t, err)\n\n\tresult, err := testStore.Get(key)\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tkeys, err = testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_DeleteWithContext(t *testing.T) {\n\tt.Parallel()\n\tvar (\n\t\ttestStore = New()\n\t\tkey       = \"john\"\n\t\tval       = []byte(\"doe\")\n\t)\n\n\terr := testStore.Set(key, val, 0)\n\trequire.NoError(t, err)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\terr = testStore.DeleteWithContext(ctx, key)\n\trequire.ErrorIs(t, err, context.Canceled)\n\n\tresult, err := testStore.Get(key)\n\trequire.NoError(t, err)\n\trequire.Equal(t, val, result)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 1)\n}\n\nfunc Test_Storage_Memory_Reset(t *testing.T) {\n\tt.Parallel()\n\ttestStore := New()\n\tval := []byte(\"doe\")\n\n\terr := testStore.Set(\"john1\", val, 0)\n\trequire.NoError(t, err)\n\n\terr = testStore.Set(\"john2\", val, 0)\n\trequire.NoError(t, err)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 2)\n\n\terr = testStore.Reset()\n\trequire.NoError(t, err)\n\n\tresult, err := testStore.Get(\"john1\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tresult, err = testStore.Get(\"john2\")\n\trequire.NoError(t, err)\n\trequire.Empty(t, result)\n\n\tkeys, err = testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Nil(t, keys)\n}\n\nfunc Test_Storage_Memory_ResetWithContext(t *testing.T) {\n\tt.Parallel()\n\ttestStore := New()\n\tval := []byte(\"doe\")\n\n\terr := testStore.Set(\"john1\", val, 0)\n\trequire.NoError(t, err)\n\n\terr = testStore.Set(\"john2\", val, 0)\n\trequire.NoError(t, err)\n\n\tkeys, err := testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 2)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\terr = testStore.ResetWithContext(ctx)\n\trequire.ErrorIs(t, err, context.Canceled)\n\n\tresult, err := testStore.Get(\"john1\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, val, result)\n\n\tresult, err = testStore.Get(\"john2\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, val, result)\n\n\tkeys, err = testStore.Keys()\n\trequire.NoError(t, err)\n\trequire.Len(t, keys, 2)\n}\n\nfunc Test_Storage_Memory_Close(t *testing.T) {\n\tt.Parallel()\n\ttestStore := New()\n\trequire.NoError(t, testStore.Close())\n}\n\nfunc Test_Storage_Memory_Conn(t *testing.T) {\n\tt.Parallel()\n\ttestStore := New()\n\trequire.NotNil(t, testStore.Conn())\n}\n\n// Benchmarks for Set operation\nfunc Benchmark_Memory_Set(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\t_ = testStore.Set(\"john\", []byte(\"doe\"), 0) //nolint:errcheck // error not needed for benchmark\n\t}\n}\n\nfunc Benchmark_Memory_Set_Parallel(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_ = testStore.Set(\"john\", []byte(\"doe\"), 0) //nolint:errcheck // error not needed for benchmark\n\t\t}\n\t})\n}\n\nfunc Benchmark_Memory_Set_Asserted(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\t\trequire.NoError(b, err)\n\t}\n}\n\nfunc Benchmark_Memory_Set_Asserted_Parallel(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n}\n\n// Benchmarks for Get operation\nfunc Benchmark_Memory_Get(b *testing.B) {\n\ttestStore := New()\n\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\trequire.NoError(b, err)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\t_, _ = testStore.Get(\"john\") //nolint:errcheck // error not needed for benchmark\n\t}\n}\n\nfunc Benchmark_Memory_Get_Parallel(b *testing.B) {\n\ttestStore := New()\n\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\trequire.NoError(b, err)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_, _ = testStore.Get(\"john\") //nolint:errcheck // error not needed for benchmark\n\t\t}\n\t})\n}\n\nfunc Benchmark_Memory_Get_Asserted(b *testing.B) {\n\ttestStore := New()\n\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\trequire.NoError(b, err)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\t_, err := testStore.Get(\"john\")\n\t\trequire.NoError(b, err)\n\t}\n}\n\nfunc Benchmark_Memory_Get_Asserted_Parallel(b *testing.B) {\n\ttestStore := New()\n\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\trequire.NoError(b, err)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_, err := testStore.Get(\"john\")\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n}\n\n// Benchmarks for SetAndDelete operation\nfunc Benchmark_Memory_SetAndDelete(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\t_ = testStore.Set(\"john\", []byte(\"doe\"), 0) //nolint:errcheck // error not needed for benchmark\n\t\t_ = testStore.Delete(\"john\")                //nolint:errcheck // error not needed for benchmark\n\t}\n}\n\nfunc Benchmark_Memory_SetAndDelete_Parallel(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_ = testStore.Set(\"john\", []byte(\"doe\"), 0) //nolint:errcheck // error not needed for benchmark\n\t\t\t_ = testStore.Delete(\"john\")                //nolint:errcheck // error not needed for benchmark\n\t\t}\n\t})\n}\n\nfunc Benchmark_Memory_SetAndDelete_Asserted(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\t\trequire.NoError(b, err)\n\n\t\terr = testStore.Delete(\"john\")\n\t\trequire.NoError(b, err)\n\t}\n}\n\nfunc Benchmark_Memory_SetAndDelete_Asserted_Parallel(b *testing.B) {\n\ttestStore := New()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\terr := testStore.Set(\"john\", []byte(\"doe\"), 0)\n\t\t\trequire.NoError(b, err)\n\n\t\t\terr = testStore.Delete(\"john\")\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/tlstest/tls.go",
    "content": "package tlstest\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"time\"\n)\n\nvar errAppendCACert = errors.New(\"failed to append CA certificate to certificate pool\")\n\n// GetTLSConfigs generates TLS configurations for a test server and client that\n// trust each other using an in-memory certificate authority.\nfunc GetTLSConfigs() (serverTLSConf, clientTLSConf *tls.Config, err error) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming server and client TLS configurations along with the error\n\t// set up our CA certificate\n\tca := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2021),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Fiber\"},\n\t\t\tCountry:       []string{\"NL\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"Amsterdam\"},\n\t\t\tStreetAddress: []string{\"Huidenstraat\"},\n\t\t\tPostalCode:    []string{\"1011 AA\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().AddDate(10, 0, 0),\n\t\tIsCA:                  true,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// create our private and public key\n\tcaPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"generate CA key: %w\", err)\n\t}\n\n\t// create the CA\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivateKey.PublicKey, caPrivateKey)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"create CA certificate: %w\", err)\n\t}\n\n\t// pem encode\n\tvar caPEM bytes.Buffer\n\tif err = pem.Encode(&caPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: caBytes,\n\t}); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"encode CA cert: %w\", err)\n\t}\n\n\tvar caPrivKeyPEM bytes.Buffer\n\tif err = pem.Encode(&caPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(caPrivateKey),\n\t}); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"encode CA private key: %w\", err)\n\t}\n\n\t// set up our server certificate\n\tcert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2021),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Fiber\"},\n\t\t\tCountry:       []string{\"NL\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"Amsterdam\"},\n\t\t\tStreetAddress: []string{\"Huidenstraat\"},\n\t\t\tPostalCode:    []string{\"1011 AA\"},\n\t\t},\n\t\tIPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     time.Now().AddDate(10, 0, 0),\n\t\tSubjectKeyId: []byte{1, 2, 3, 4, 6},\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t}\n\n\tcertPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"generate server key: %w\", err)\n\t}\n\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivateKey.PublicKey, caPrivateKey)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"create server certificate: %w\", err)\n\t}\n\n\tvar certPEM bytes.Buffer\n\tif err = pem.Encode(&certPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certBytes,\n\t}); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"encode server cert: %w\", err)\n\t}\n\n\tvar certPrivateKeyPEM bytes.Buffer\n\tif err = pem.Encode(&certPrivateKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(certPrivateKey),\n\t}); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"encode server private key: %w\", err)\n\t}\n\n\tserverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivateKeyPEM.Bytes())\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"load server key pair: %w\", err)\n\t}\n\n\tserverTLSConf = &tls.Config{\n\t\tCertificates: []tls.Certificate{serverCert},\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\n\tcertPool := x509.NewCertPool()\n\tif ok := certPool.AppendCertsFromPEM(caPEM.Bytes()); !ok {\n\t\treturn nil, nil, errAppendCACert\n\t}\n\tclientTLSConf = &tls.Config{\n\t\tRootCAs:    certPool,\n\t\tMinVersion: tls.VersionTLS12,\n\t}\n\n\treturn serverTLSConf, clientTLSConf, nil\n}\n"
  },
  {
    "path": "listen.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"github.com/mattn/go-colorable\"\n\t\"github.com/mattn/go-isatty\"\n\t\"golang.org/x/crypto/acme/autocert\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\n// Figlet text to show Fiber ASCII art on startup message\nvar figletFiberText = `\n    _______ __\n   / ____(_) /_  ___  _____\n  / /_  / / __ \\/ _ \\/ ___/\n / __/ / / /_/ /  __/ /\n/_/   /_/_.___/\\___/_/          %s`\n\nconst (\n\tglobalIpv4Addr = \"0.0.0.0\"\n)\n\n// ListenConfig is a struct to customize startup of Fiber.\ntype ListenConfig struct {\n\t// GracefulContext is a field to shutdown Fiber by given context gracefully.\n\t//\n\t// Default: nil\n\tGracefulContext context.Context `json:\"graceful_context\"` //nolint:containedctx // It's needed to set context inside Listen.\n\n\t// TLSConfigFunc allows customizing tls.Config as you want.\n\t//\n\t// Default: nil\n\tTLSConfigFunc func(tlsConfig *tls.Config) `json:\"tls_config_func\"`\n\n\t// TLSConfig allows providing a tls.Config used as the base for TLS settings.\n\t// This enables external certificate providers via GetCertificate.\n\t//\n\t// Default: nil\n\tTLSConfig *tls.Config `json:\"tls_config\"`\n\n\t// ListenerFunc allows accessing and customizing net.Listener.\n\t//\n\t// Default: nil\n\tListenerAddrFunc func(addr net.Addr) `json:\"listener_addr_func\"`\n\n\t// BeforeServeFunc allows customizing and accessing fiber app before serving the app.\n\t//\n\t// Default: nil\n\tBeforeServeFunc func(app *App) error `json:\"before_serve_func\"`\n\n\t// AutoCertManager manages TLS certificates automatically using the ACME protocol,\n\t// Enables integration with Let's Encrypt or other ACME-compatible providers.\n\t//\n\t// Default: nil\n\tAutoCertManager *autocert.Manager `json:\"auto_cert_manager\"`\n\n\t// Known networks are \"tcp\", \"tcp4\" (IPv4-only), \"tcp6\" (IPv6-only), \"unix\" (Unix Domain Sockets)\n\t// WARNING: When prefork is set to true, only \"tcp4\" and \"tcp6\" can be chosen.\n\t//\n\t// Default: NetworkTCP4\n\tListenerNetwork string `json:\"listener_network\"`\n\n\t// CertFile is a path of certificate file.\n\t// If you want to use TLS, you have to enter this field.\n\t//\n\t// Default : \"\"\n\tCertFile string `json:\"cert_file\"`\n\n\t// KeyFile is a path of certificate's private key.\n\t// If you want to use TLS, you have to enter this field.\n\t//\n\t// Default : \"\"\n\tCertKeyFile string `json:\"cert_key_file\"`\n\n\t// CertClientFile is a path of client certificate.\n\t// If you want to use mTLS, you have to enter this field.\n\t//\n\t// Default : \"\"\n\tCertClientFile string `json:\"cert_client_file\"`\n\n\t// When the graceful shutdown begins, use this field to set the timeout\n\t// duration. If the timeout is reached, OnPostShutdown will be called with the error.\n\t// Set to 0 to disable the timeout and wait indefinitely.\n\t//\n\t// Default: 10 * time.Second\n\tShutdownTimeout time.Duration `json:\"shutdown_timeout\"`\n\n\t// FileMode to set for Unix Domain Socket (ListenerNetwork must be \"unix\")\n\t//\n\t// Default: 0770\n\tUnixSocketFileMode os.FileMode `json:\"unix_socket_file_mode\"`\n\n\t// TLSMinVersion allows to set TLS minimum version.\n\t//\n\t// Default: tls.VersionTLS12\n\t// WARNING: TLS1.0 and TLS1.1 versions are not supported.\n\tTLSMinVersion uint16 `json:\"tls_min_version\"`\n\n\t// When set to true, it will not print out the «Fiber» ASCII art and listening address.\n\t//\n\t// Default: false\n\tDisableStartupMessage bool `json:\"disable_startup_message\"`\n\n\t// When set to true, this will spawn multiple Go processes listening on the same port.\n\t//\n\t// Default: false\n\tEnablePrefork bool `json:\"enable_prefork\"`\n\n\t// If set to true, will print all routes with their method, path and handler.\n\t//\n\t// Default: false\n\tEnablePrintRoutes bool `json:\"enable_print_routes\"`\n}\n\n// listenConfigDefault is a function to set default values of ListenConfig.\nfunc listenConfigDefault(config ...ListenConfig) ListenConfig {\n\tif len(config) < 1 {\n\t\treturn ListenConfig{\n\t\t\tTLSMinVersion:      tls.VersionTLS12,\n\t\t\tListenerNetwork:    NetworkTCP4,\n\t\t\tUnixSocketFileMode: 0o770,\n\t\t\tShutdownTimeout:    10 * time.Second,\n\t\t}\n\t}\n\n\tcfg := config[0]\n\tif cfg.ListenerNetwork == \"\" {\n\t\tcfg.ListenerNetwork = NetworkTCP4\n\t}\n\n\tif cfg.UnixSocketFileMode == 0 {\n\t\tcfg.UnixSocketFileMode = 0o770\n\t}\n\n\tif cfg.TLSMinVersion == 0 {\n\t\tcfg.TLSMinVersion = tls.VersionTLS12\n\t}\n\n\tif cfg.TLSMinVersion != tls.VersionTLS12 && cfg.TLSMinVersion != tls.VersionTLS13 {\n\t\tpanic(\"unsupported TLS version, please use tls.VersionTLS12 or tls.VersionTLS13\")\n\t}\n\n\treturn cfg\n}\n\n// Listen serves HTTP requests from the given addr.\n// You should enter custom ListenConfig to customize startup. (TLS, mTLS, prefork...)\n//\n//\tapp.Listen(\":8080\")\n//\tapp.Listen(\"127.0.0.1:8080\")\n//\tapp.Listen(\":8080\", ListenConfig{EnablePrefork: true})\nfunc (app *App) Listen(addr string, config ...ListenConfig) error {\n\tcfg := listenConfigDefault(config...)\n\n\t// Configure TLS\n\tvar tlsConfig *tls.Config\n\tif cfg.TLSConfig != nil {\n\t\ttlsConfig = cfg.TLSConfig.Clone()\n\t} else {\n\t\tswitch {\n\t\tcase cfg.AutoCertManager != nil && (cfg.CertFile != \"\" || cfg.CertKeyFile != \"\"):\n\t\t\treturn ErrAutoCertWithCertFile\n\t\tcase cfg.CertFile != \"\" && cfg.CertKeyFile != \"\":\n\t\t\tcert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.CertKeyFile)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w\", cfg.CertFile, cfg.CertKeyFile, err)\n\t\t\t}\n\n\t\t\ttlsHandler := &TLSHandler{}\n\t\t\ttlsConfig = &tls.Config{ //nolint:gosec // This is a user input\n\t\t\t\tMinVersion: cfg.TLSMinVersion,\n\t\t\t\tCertificates: []tls.Certificate{\n\t\t\t\t\tcert,\n\t\t\t\t},\n\t\t\t\tGetCertificate: tlsHandler.GetClientInfo,\n\t\t\t}\n\n\t\t\tif cfg.CertClientFile != \"\" {\n\t\t\t\tclientCACert, err := os.ReadFile(filepath.Clean(cfg.CertClientFile))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to read file: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tclientCertPool := x509.NewCertPool()\n\t\t\t\tclientCertPool.AppendCertsFromPEM(clientCACert)\n\n\t\t\t\ttlsConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\t\t\t\ttlsConfig.ClientCAs = clientCertPool\n\t\t\t}\n\n\t\t\t// Attach the tlsHandler to the config\n\t\t\tapp.SetTLSHandler(tlsHandler)\n\t\tcase cfg.AutoCertManager != nil:\n\t\t\ttlsConfig = &tls.Config{ //nolint:gosec // This is a user input\n\t\t\t\tMinVersion:     cfg.TLSMinVersion,\n\t\t\t\tGetCertificate: cfg.AutoCertManager.GetCertificate,\n\t\t\t\tNextProtos:     []string{\"http/1.1\", \"acme-tls/1\"},\n\t\t\t}\n\t\tdefault:\n\t\t}\n\n\t\tif tlsConfig != nil && cfg.TLSConfigFunc != nil {\n\t\t\tcfg.TLSConfigFunc(tlsConfig)\n\t\t}\n\t}\n\n\t// Graceful shutdown\n\tif cfg.GracefulContext != nil {\n\t\tctx, cancel := context.WithCancel(cfg.GracefulContext)\n\t\tdefer cancel()\n\n\t\tgo app.gracefulShutdown(ctx, &cfg)\n\t}\n\n\t// Start prefork\n\tif cfg.EnablePrefork {\n\t\treturn app.prefork(addr, tlsConfig, &cfg)\n\t}\n\n\t// Configure Listener\n\tln, err := app.createListener(addr, tlsConfig, &cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to listen: %w\", err)\n\t}\n\n\t// prepare the server for the start\n\tapp.startupProcess()\n\n\tlistenData := app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg, nil)\n\n\t// run hooks\n\tapp.runOnListenHooks(listenData)\n\n\t// Print startup message & routes\n\tapp.printMessages(&cfg, listenData)\n\n\t// Serve\n\tif cfg.BeforeServeFunc != nil {\n\t\tif err := cfg.BeforeServeFunc(app); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn app.server.Serve(ln)\n}\n\n// Listener serves HTTP requests from the given listener.\n// You should enter custom ListenConfig to customize startup. (prefork, startup message, graceful shutdown...)\nfunc (app *App) Listener(ln net.Listener, config ...ListenConfig) error {\n\tcfg := listenConfigDefault(config...)\n\n\t// Graceful shutdown\n\tif cfg.GracefulContext != nil {\n\t\tctx, cancel := context.WithCancel(cfg.GracefulContext)\n\t\tdefer cancel()\n\n\t\tgo app.gracefulShutdown(ctx, &cfg)\n\t}\n\n\t// prepare the server for the start\n\tapp.startupProcess()\n\n\tlistenData := app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg, nil)\n\n\t// run hooks\n\tapp.runOnListenHooks(listenData)\n\n\t// Print startup message & routes\n\tapp.printMessages(&cfg, listenData)\n\n\t// Serve\n\tif cfg.BeforeServeFunc != nil {\n\t\tif err := cfg.BeforeServeFunc(app); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Prefork is not supported for custom listeners\n\tif cfg.EnablePrefork {\n\t\tlog.Warn(\"Prefork isn't supported for custom listeners.\")\n\t}\n\n\treturn app.server.Serve(ln)\n}\n\n// Create listener function.\nfunc (*App) createListener(addr string, tlsConfig *tls.Config, cfg *ListenConfig) (net.Listener, error) {\n\tif cfg == nil {\n\t\tcfg = &ListenConfig{}\n\t}\n\tvar listener net.Listener\n\tvar err error\n\n\t// Remove previously created socket, to make sure it's possible to listen\n\tif cfg.ListenerNetwork == NetworkUnix {\n\t\tif err = os.Remove(addr); err != nil && !os.IsNotExist(err) {\n\t\t\treturn nil, fmt.Errorf(\"unexpected error when trying to remove unix socket file %q: %w\", addr, err)\n\t\t}\n\t}\n\n\tif tlsConfig != nil {\n\t\tlistener, err = tls.Listen(cfg.ListenerNetwork, addr, tlsConfig)\n\t} else {\n\t\tlistener, err = net.Listen(cfg.ListenerNetwork, addr)\n\t}\n\n\t// Check for error before using the listener\n\tif err != nil {\n\t\t// Wrap the error from tls.Listen/net.Listen\n\t\treturn nil, fmt.Errorf(\"failed to listen: %w\", err)\n\t}\n\n\tif cfg.ListenerNetwork == NetworkUnix {\n\t\tif err = os.Chmod(addr, cfg.UnixSocketFileMode); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cannot chmod %#o for %q: %w\", cfg.UnixSocketFileMode, addr, err)\n\t\t}\n\t}\n\n\tif cfg.ListenerAddrFunc != nil {\n\t\tcfg.ListenerAddrFunc(listener.Addr())\n\t}\n\n\treturn listener, nil\n}\n\nfunc (app *App) printMessages(cfg *ListenConfig, listenData *ListenData) {\n\tapp.startupMessage(listenData, cfg)\n\n\tif cfg.EnablePrintRoutes {\n\t\tapp.printRoutesMessage()\n\t}\n}\n\n// prepareListenData creates a ListenData instance populated with the application metadata.\nfunc (app *App) prepareListenData(addr string, isTLS bool, cfg *ListenConfig, childPIDs []int) *ListenData { //revive:disable-line:flag-parameter // Accepting a bool param named isTLS is fine here\n\thost, port := parseAddr(addr)\n\tif host == \"\" {\n\t\tif cfg.ListenerNetwork == NetworkTCP6 {\n\t\t\thost = \"[::1]\"\n\t\t} else {\n\t\t\thost = globalIpv4Addr\n\t\t}\n\t}\n\n\tprocessCount := 1\n\tif cfg.EnablePrefork {\n\t\tprocessCount = runtime.GOMAXPROCS(0)\n\t}\n\n\tvar clonedPIDs []int\n\tif len(childPIDs) > 0 {\n\t\tclonedPIDs = slices.Clone(childPIDs)\n\t}\n\n\treturn &ListenData{\n\t\tHost:         host,\n\t\tPort:         port,\n\t\tVersion:      Version,\n\t\tAppName:      app.config.AppName,\n\t\tColorScheme:  app.config.ColorScheme,\n\t\tChildPIDs:    clonedPIDs,\n\t\tHandlerCount: int(app.handlersCount),\n\t\tProcessCount: processCount,\n\t\tPID:          os.Getpid(),\n\t\tTLS:          isTLS,\n\t\tPrefork:      cfg.EnablePrefork,\n\t}\n}\n\n// startupMessage renders the startup banner using the provided listener metadata and configuration.\nfunc (app *App) startupMessage(listenData *ListenData, cfg *ListenConfig) {\n\tpreData := newPreStartupMessageData(listenData)\n\tcolors := listenData.ColorScheme\n\n\tout := colorable.NewColorableStdout()\n\tif os.Getenv(\"TERM\") == \"dumb\" || os.Getenv(\"NO_COLOR\") == \"1\" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {\n\t\tout = colorable.NewNonColorable(os.Stdout)\n\t}\n\n\t// Add default entries\n\tscheme := schemeHTTP\n\tif listenData.TLS {\n\t\tscheme = schemeHTTPS\n\t}\n\n\tif listenData.Host == globalIpv4Addr {\n\t\tpreData.AddInfo(\"server_address\", \"Server started on\", fmt.Sprintf(\"%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)\",\n\t\t\tcolors.Blue, scheme, listenData.Port, colors.Reset, listenData.Port), 10)\n\t} else {\n\t\tpreData.AddInfo(\"server_address\", \"Server started on\", fmt.Sprintf(\"%s%s://%s:%s%s\",\n\t\t\tcolors.Blue, scheme, listenData.Host, listenData.Port, colors.Reset), 10)\n\t}\n\n\tif listenData.AppName != \"\" {\n\t\tpreData.AddInfo(\"app_name\", \"Application name\", fmt.Sprintf(\"\\t%s%s%s\", colors.Blue, listenData.AppName, colors.Reset), 9)\n\t}\n\n\tpreData.AddInfo(\"total_handlers\", \"Total handlers\", fmt.Sprintf(\"\\t%s%d%s\", colors.Blue, listenData.HandlerCount, colors.Reset), 8)\n\n\tif listenData.Prefork {\n\t\tpreData.AddInfo(\"prefork\", \"Prefork\", fmt.Sprintf(\"\\t\\t%sEnabled%s\", colors.Blue, colors.Reset), 7)\n\t} else {\n\t\tpreData.AddInfo(\"prefork\", \"Prefork\", fmt.Sprintf(\"\\t\\t%sDisabled%s\", colors.Red, colors.Reset), 6)\n\t}\n\n\tpreData.AddInfo(\"pid\", \"PID\", fmt.Sprintf(\"\\t\\t%s%d%s\", colors.Blue, listenData.PID, colors.Reset), 5)\n\n\tpreData.AddInfo(\"process_count\", \"Total process count\", fmt.Sprintf(\"%s%d%s\", colors.Blue, listenData.ProcessCount, colors.Reset), 4)\n\n\tif err := app.hooks.executeOnPreStartupMessageHooks(preData); err != nil {\n\t\tlog.Errorf(\"failed to call pre startup message hook: %v\", err)\n\t}\n\n\tdisabled := cfg.DisableStartupMessage\n\tisChild := IsChild()\n\tprevented := preData != nil && preData.PreventDefault\n\n\tdefer func() {\n\t\tpostData := newPostStartupMessageData(listenData, disabled, isChild, prevented)\n\t\tif err := app.hooks.executeOnPostStartupMessageHooks(postData); err != nil {\n\t\t\tlog.Errorf(\"failed to call post startup message hook: %v\", err)\n\t\t}\n\t}()\n\n\tif preData == nil || disabled || isChild || prevented {\n\t\treturn\n\t}\n\n\tif preData.BannerHeader != \"\" {\n\t\theader := preData.BannerHeader\n\t\tfmt.Fprint(out, header)\n\t\tif !strings.HasSuffix(header, \"\\n\") {\n\t\t\tfmt.Fprintln(out)\n\t\t}\n\t} else {\n\t\tfmt.Fprintf(out, \"%s\\n\", fmt.Sprintf(figletFiberText, colors.Red+\"v\"+listenData.Version+colors.Reset))\n\t\tfmt.Fprintln(out, strings.Repeat(\"-\", 50))\n\t}\n\n\tprintStartupEntries(out, &colors, preData.entries)\n\n\tif err := app.logServices(app.servicesStartupCtx(), out, &colors); err != nil {\n\t\tlog.Errorf(\"failed to log services: %v\", err)\n\t}\n\n\tif listenData.Prefork && len(listenData.ChildPIDs) > 0 {\n\t\tfmt.Fprintf(out, \"%sINFO%s Child PIDs: \\t\\t%s\", colors.Green, colors.Reset, colors.Blue)\n\n\t\ttotalPIDs := len(listenData.ChildPIDs)\n\t\trowTotalPidCount := 10\n\n\t\tfor i := 0; i < totalPIDs; i += rowTotalPidCount {\n\t\t\tstart := i\n\t\t\tend := min(i+rowTotalPidCount, totalPIDs)\n\n\t\t\tfor idx, pid := range listenData.ChildPIDs[start:end] {\n\t\t\t\tfmt.Fprintf(out, \"%d\", pid)\n\t\t\t\tif idx+1 != len(listenData.ChildPIDs[start:end]) {\n\t\t\t\t\tfmt.Fprint(out, \", \")\n\t\t\t\t}\n\t\t\t}\n\t\t\tfmt.Fprintf(out, \"\\n%s\", colors.Reset)\n\t\t}\n\t}\n\n\tfmt.Fprintf(out, \"\\n%s\", colors.Reset)\n}\n\nfunc printStartupEntries(out io.Writer, colors *Colors, entries []startupMessageEntry) {\n\t// Sort entries by priority (higher priority first)\n\tsort.Slice(entries, func(i, j int) bool {\n\t\treturn entries[i].priority > entries[j].priority\n\t})\n\n\tfor _, entry := range entries {\n\t\tvar label string\n\t\tvar color string\n\t\tswitch entry.level {\n\t\tcase StartupMessageLevelWarning:\n\t\t\tlabel, color = \"WARN\", colors.Yellow\n\t\tcase StartupMessageLevelError:\n\t\t\tlabel, color = errString, colors.Red\n\t\tdefault:\n\t\t\tlabel, color = \"INFO\", colors.Green\n\t\t}\n\n\t\tfmt.Fprintf(out, \"%s%s%s %s: \\t%s%s%s\\n\", color, label, colors.Reset, entry.title, colors.Blue, entry.value, colors.Reset)\n\t}\n}\n\n// printRoutesMessage print all routes with method, path, name and handlers\n// in a format of table, like this:\n// method | path | name      | handlers\n// GET    | /    | routeName | github.com/gofiber/fiber/v3.emptyHandler\n// HEAD   | /    |           | github.com/gofiber/fiber/v3.emptyHandler\nfunc (app *App) printRoutesMessage() {\n\t// ignore child processes\n\tif IsChild() {\n\t\treturn\n\t}\n\n\t// Alias colors\n\tcolors := app.config.ColorScheme\n\n\tvar routes []RouteMessage\n\tfor _, routeStack := range app.stack {\n\t\tfor _, route := range routeStack {\n\t\t\tvar newRoute RouteMessage\n\t\t\tnewRoute.name = route.Name\n\t\t\tnewRoute.method = route.Method\n\t\t\tnewRoute.path = route.Path\n\t\t\tfor _, handler := range route.Handlers {\n\t\t\t\tnewRoute.handlers += runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() + \" \"\n\t\t\t}\n\t\t\troutes = append(routes, newRoute)\n\t\t}\n\t}\n\n\tout := colorable.NewColorableStdout()\n\tif os.Getenv(\"TERM\") == \"dumb\" || os.Getenv(\"NO_COLOR\") == \"1\" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {\n\t\tout = colorable.NewNonColorable(os.Stdout)\n\t}\n\n\tw := tabwriter.NewWriter(out, 1, 1, 1, ' ', 0)\n\t// Sort routes by path\n\tsort.Slice(routes, func(i, j int) bool {\n\t\treturn routes[i].path < routes[j].path\n\t})\n\n\tfmt.Fprintf(w, \"%smethod\\t%s| %spath\\t%s| %sname\\t%s| %shandlers\\t%s\\n\", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset)\n\tfmt.Fprintf(w, \"%s------\\t%s| %s----\\t%s| %s----\\t%s| %s--------\\t%s\\n\", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset)\n\n\tfor _, route := range routes {\n\t\tfmt.Fprintf(w, \"%s%s\\t%s| %s%s\\t%s| %s%s\\t%s| %s%s%s\\n\", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset)\n\t}\n\n\t_ = w.Flush() //nolint:errcheck // It is fine to ignore the error here\n}\n\n// shutdown goroutine\nfunc (app *App) gracefulShutdown(ctx context.Context, cfg *ListenConfig) {\n\t<-ctx.Done()\n\n\tvar err error\n\n\tif cfg != nil && cfg.ShutdownTimeout != 0 {\n\t\terr = app.ShutdownWithTimeout(cfg.ShutdownTimeout) //nolint:contextcheck // TODO: Implement it\n\t} else {\n\t\terr = app.Shutdown() //nolint:contextcheck // TODO: Implement it\n\t}\n\n\tif err != nil {\n\t\tapp.hooks.executeOnPostShutdownHooks(err)\n\t\treturn\n\t}\n\n\tapp.hooks.executeOnPostShutdownHooks(nil)\n}\n"
  },
  {
    "path": "listen_test.go",
    "content": "package fiber\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\" //nolint:depguard // TODO: Required to capture output, use internal log package instead\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n\t\"golang.org/x/crypto/acme/autocert\"\n)\n\n// go test -run Test_Listen\nfunc Test_Listen(t *testing.T) {\n\tapp := New()\n\n\trequire.Error(t, app.Listen(\":99999\"))\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{DisableStartupMessage: true}))\n}\n\n// go test -run Test_Listen_Graceful_Shutdown\nfunc Test_Listen_Graceful_Shutdown(t *testing.T) {\n\tt.Run(\"Basic Graceful Shutdown\", func(t *testing.T) {\n\t\ttestGracefulShutdown(t, 0)\n\t})\n\n\tt.Run(\"Shutdown With Timeout\", func(t *testing.T) {\n\t\ttestGracefulShutdown(t, 500*time.Millisecond)\n\t})\n\n\tt.Run(\"Shutdown With Timeout Error\", func(t *testing.T) {\n\t\ttestGracefulShutdown(t, 1*time.Nanosecond)\n\t})\n}\n\nfunc testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) {\n\tt.Helper()\n\n\tvar mu sync.Mutex\n\tvar shutdown bool\n\tvar receivedErr error\n\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\ttime.Sleep(10 * time.Millisecond)\n\t\treturn c.SendString(c.Hostname())\n\t})\n\n\tln := fasthttputil.NewInmemoryListener()\n\terrs := make(chan error, 1)\n\n\tapp.hooks.OnPostShutdown(func(err error) error {\n\t\tmu.Lock()\n\t\tdefer mu.Unlock()\n\t\tshutdown = true\n\t\treceivedErr = err\n\t\treturn nil\n\t})\n\n\tgo func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\t\tdefer cancel()\n\n\t\terrs <- app.Listener(ln, ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t\tGracefulContext:       ctx,\n\t\t\tShutdownTimeout:       shutdownTimeout,\n\t\t})\n\t}()\n\n\trequire.Eventually(t, func() bool {\n\t\tconn, err := ln.Dial()\n\t\tif err == nil {\n\t\t\tif err := conn.Close(); err != nil {\n\t\t\t\tt.Logf(\"error closing connection: %v\", err)\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}, time.Second, 100*time.Millisecond, \"Server failed to become ready\")\n\n\tclient := fasthttp.HostClient{\n\t\tDial: func(_ string) (net.Conn, error) { return ln.Dial() },\n\t}\n\n\ttype testCase struct {\n\t\texpectedErr        error\n\t\texpectedBody       string\n\t\tname               string\n\t\twaitTime           time.Duration\n\t\texpectedStatusCode int\n\t\tcloseConnection    bool\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tname:               \"Server running normally\",\n\t\t\twaitTime:           500 * time.Millisecond,\n\t\t\texpectedBody:       \"example.com\",\n\t\t\texpectedStatusCode: StatusOK,\n\t\t\texpectedErr:        nil,\n\t\t\tcloseConnection:    true,\n\t\t},\n\t\t{\n\t\t\tname:               \"Server shutdown complete\",\n\t\t\twaitTime:           3 * time.Second,\n\t\t\texpectedBody:       \"\",\n\t\t\texpectedStatusCode: StatusOK,\n\t\t\texpectedErr:        fasthttputil.ErrInmemoryListenerClosed,\n\t\t\tcloseConnection:    true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttime.Sleep(tc.waitTime)\n\n\t\t\treq := fasthttp.AcquireRequest()\n\t\t\tdefer fasthttp.ReleaseRequest(req)\n\t\t\treq.SetRequestURI(\"http://example.com\")\n\n\t\t\tresp := fasthttp.AcquireResponse()\n\t\t\tdefer fasthttp.ReleaseResponse(resp)\n\n\t\t\terr := client.Do(req, resp)\n\n\t\t\tif tc.expectedErr == nil {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, tc.expectedStatusCode, resp.StatusCode())\n\t\t\t\trequire.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body()))\n\t\t\t} else {\n\t\t\t\trequire.ErrorIs(t, err, tc.expectedErr)\n\t\t\t}\n\t\t})\n\t}\n\n\tmu.Lock()\n\trequire.True(t, shutdown)\n\tif shutdownTimeout == 1*time.Nanosecond {\n\t\trequire.Error(t, receivedErr)\n\t\trequire.ErrorIs(t, receivedErr, context.DeadlineExceeded)\n\t}\n\trequire.NoError(t, <-errs)\n\tmu.Unlock()\n}\n\n// go test -run Test_Listen_Prefork\nfunc Test_Listen_Prefork(t *testing.T) {\n\ttestPreforkMaster = true\n\n\tapp := New()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{DisableStartupMessage: true, EnablePrefork: true}))\n}\n\n// go test -run Test_Listen_TLSMinVersion\nfunc Test_Listen_TLSMinVersion(t *testing.T) {\n\ttestPreforkMaster = true\n\n\tapp := New()\n\n\t// Invalid TLSMinVersion\n\trequire.Panics(t, func() {\n\t\t_ = app.Listen(\":0\", ListenConfig{TLSMinVersion: tls.VersionTLS10}) //nolint:errcheck // ignore error\n\t})\n\trequire.Panics(t, func() {\n\t\t_ = app.Listen(\":0\", ListenConfig{TLSMinVersion: tls.VersionTLS11}) //nolint:errcheck // ignore error\n\t})\n\n\t// Prefork\n\trequire.Panics(t, func() {\n\t\t_ = app.Listen(\":0\", ListenConfig{DisableStartupMessage: true, EnablePrefork: true, TLSMinVersion: tls.VersionTLS10}) //nolint:errcheck // ignore error\n\t})\n\trequire.Panics(t, func() {\n\t\t_ = app.Listen(\":0\", ListenConfig{DisableStartupMessage: true, EnablePrefork: true, TLSMinVersion: tls.VersionTLS11}) //nolint:errcheck // ignore error\n\t})\n\n\t// Valid TLSMinVersion\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{TLSMinVersion: tls.VersionTLS13}))\n\n\t// Valid TLSMinVersion with Prefork\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{DisableStartupMessage: true, EnablePrefork: true, TLSMinVersion: tls.VersionTLS13}))\n}\n\n// go test -run Test_Listen_TLS\nfunc Test_Listen_TLS(t *testing.T) {\n\tapp := New()\n\n\t// invalid port\n\trequire.Error(t, app.Listen(\":99999\", ListenConfig{\n\t\tCertFile:    \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile: \"./.github/testdata/ssl.key\",\n\t}))\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tCertFile:    \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile: \"./.github/testdata/ssl.key\",\n\t}))\n}\n\n// go test -run Test_Listen_TLS_Prefork\nfunc Test_Listen_TLS_Prefork(t *testing.T) {\n\ttestPreforkMaster = true\n\n\tapp := New()\n\n\t// invalid key file content\n\trequire.Error(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tEnablePrefork:         true,\n\t\tCertFile:              \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:           \"./.github/testdata/template.tmpl\",\n\t}))\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tEnablePrefork:         true,\n\t\tCertFile:              \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:           \"./.github/testdata/ssl.key\",\n\t}))\n}\n\n// go test -run Test_Listen_MutualTLS\nfunc Test_Listen_MutualTLS(t *testing.T) {\n\tapp := New()\n\n\t// invalid port\n\trequire.Error(t, app.Listen(\":99999\", ListenConfig{\n\t\tCertFile:       \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:    \"./.github/testdata/ssl.key\",\n\t\tCertClientFile: \"./.github/testdata/ca-chain.cert.pem\",\n\t}))\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tCertFile:       \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:    \"./.github/testdata/ssl.key\",\n\t\tCertClientFile: \"./.github/testdata/ca-chain.cert.pem\",\n\t}))\n}\n\n// go test -run Test_Listen_MutualTLS_Prefork\nfunc Test_Listen_MutualTLS_Prefork(t *testing.T) {\n\ttestPreforkMaster = true\n\n\tapp := New()\n\n\t// invalid key file content\n\trequire.Error(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tEnablePrefork:         true,\n\t\tCertFile:              \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:           \"./.github/testdata/template.html\",\n\t\tCertClientFile:        \"./.github/testdata/ca-chain.cert.pem\",\n\t}))\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tEnablePrefork:         true,\n\t\tCertFile:              \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:           \"./.github/testdata/ssl.key\",\n\t\tCertClientFile:        \"./.github/testdata/ca-chain.cert.pem\",\n\t}))\n}\n\n// go test -run Test_Listener\nfunc Test_Listener(t *testing.T) {\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\tln := fasthttputil.NewInmemoryListener()\n\trequire.NoError(t, app.Listener(ln))\n}\n\nfunc Test_App_Listener_TLS_Listener(t *testing.T) {\n\t// Create tls certificate\n\tcer, err := tls.LoadX509KeyPair(\"./.github/testdata/ssl.pem\", \"./.github/testdata/ssl.key\")\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\t//nolint:gosec // We're in a test so using old ciphers is fine\n\tconfig := &tls.Config{Certificates: []tls.Certificate{cer}}\n\n\t//nolint:gosec // We're in a test so listening on all interfaces is fine\n\tln, err := tls.Listen(NetworkTCP4, \":0\", config)\n\trequire.NoError(t, err)\n\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(time.Millisecond * 500)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listener(ln))\n}\n\n// go test -run Test_Listen_TLSConfigFunc\nfunc Test_Listen_TLSConfigFunc(t *testing.T) {\n\tvar callTLSConfig bool\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tTLSConfigFunc: func(_ *tls.Config) {\n\t\t\tcallTLSConfig = true\n\t\t},\n\t\tCertFile:    \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile: \"./.github/testdata/ssl.key\",\n\t}))\n\n\trequire.True(t, callTLSConfig)\n}\n\n// go test -run Test_Listen_TLSConfig\nfunc Test_Listen_TLSConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcert, err := tls.LoadX509KeyPair(\"./.github/testdata/ssl.pem\", \"./.github/testdata/ssl.key\")\n\trequire.NoError(t, err)\n\n\trun := func(name string, cfg ListenConfig) {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := New()\n\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(1000 * time.Millisecond)\n\t\t\t\tassert.NoError(t, app.Shutdown())\n\t\t\t}()\n\n\t\t\trequire.NoError(t, app.Listen(\":0\", cfg))\n\t\t})\n\t}\n\n\trun(\"TLSConfig with certificates\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tTLSConfig: &tls.Config{\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\tCertificates: []tls.Certificate{cert},\n\t\t},\n\t})\n\n\trun(\"TLSConfig with GetCertificate\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tTLSConfig: &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tGetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\t\t\treturn &cert, nil\n\t\t\t},\n\t\t},\n\t})\n\n\trun(\"TLSConfig ignores other TLS fields\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tTLSConfig: &tls.Config{\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\tCertificates: []tls.Certificate{cert},\n\t\t},\n\t\tCertFile:       \"./.github/testdata/does-not-exist.pem\",\n\t\tCertKeyFile:    \"./.github/testdata/does-not-exist.key\",\n\t\tCertClientFile: \"./.github/testdata/does-not-exist-ca.pem\",\n\t\tAutoCertManager: &autocert.Manager{\n\t\t\tPrompt: autocert.AcceptTOS,\n\t\t},\n\t})\n}\n\n// go test -run Test_Listen_TLSCertFiles\nfunc Test_Listen_TLSCertFiles(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tCertFile:              \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:           \"./.github/testdata/ssl.key\",\n\t\tCertClientFile:        \"./.github/testdata/ssl.pem\",\n\t}))\n}\n\n// go test -run Test_Listen_TLSConfig_WithTLSConfigFunc\nfunc Test_Listen_TLSConfig_WithTLSConfigFunc(t *testing.T) {\n\tt.Parallel()\n\n\tcert, err := tls.LoadX509KeyPair(\"./.github/testdata/ssl.pem\", \"./.github/testdata/ssl.key\")\n\trequire.NoError(t, err)\n\n\tvar calledTLSConfigFunc bool\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tTLSConfig: &tls.Config{\n\t\t\tMinVersion:   tls.VersionTLS12,\n\t\t\tCertificates: []tls.Certificate{cert},\n\t\t},\n\t\tTLSConfigFunc: func(_ *tls.Config) {\n\t\t\tcalledTLSConfigFunc = true\n\t\t},\n\t}))\n\n\trequire.False(t, calledTLSConfigFunc)\n}\n\n// go test -run Test_Listen_AutoCert_Conflicts\nfunc Test_Listen_AutoCert_Conflicts(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\terr := app.Listen(\":0\", ListenConfig{\n\t\tAutoCertManager: &autocert.Manager{},\n\t\tCertFile:        \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile:     \"./.github/testdata/ssl.key\",\n\t})\n\trequire.ErrorIs(t, err, ErrAutoCertWithCertFile)\n}\n\n// go test -run Test_Listen_ListenerAddrFunc\nfunc Test_Listen_ListenerAddrFunc(t *testing.T) {\n\tvar network string\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\tnetwork = addr.Network()\n\t\t},\n\t\tCertFile:    \"./.github/testdata/ssl.pem\",\n\t\tCertKeyFile: \"./.github/testdata/ssl.key\",\n\t}))\n\n\trequire.Equal(t, \"tcp\", network)\n}\n\n// go test -run Test_Listen_BeforeServeFunc\nfunc Test_Listen_BeforeServeFunc(t *testing.T) {\n\tvar handlers uint32\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\twantErr := errors.New(\"test\")\n\trequire.ErrorIs(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tBeforeServeFunc: func(fiber *App) error {\n\t\t\thandlers = fiber.HandlersCount()\n\n\t\t\treturn wantErr\n\t\t},\n\t}), wantErr)\n\n\trequire.Zero(t, handlers)\n}\n\n// go test -run Test_Listen_ListenerNetwork\nfunc Test_Listen_ListenerNetwork(t *testing.T) {\n\tvar network string\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tListenerNetwork:       NetworkTCP6,\n\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\tnetwork = addr.String()\n\t\t},\n\t}))\n\n\trequire.Contains(t, network, \"[::]:\")\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(\":0\", ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tListenerNetwork:       NetworkTCP4,\n\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\tnetwork = addr.String()\n\t\t},\n\t}))\n\n\trequire.Contains(t, network, \"0.0.0.0:\")\n}\n\n// go test -run Test_Listen_ListenerNetwork_Unix\nfunc Test_Listen_ListenerNetwork_Unix(t *testing.T) {\n\tapp := New()\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendString(\"all good\")\n\t})\n\n\tvar (\n\t\tf       os.FileInfo\n\t\tnetwork string\n\n\t\treqErr error\n\t\tresp   = &fasthttp.Response{}\n\t)\n\n\t// Create temporary directory for storing socket in\n\ttmp, err := os.MkdirTemp(os.TempDir(), \"fiber-test\")\n\trequire.NoError(t, err)\n\tsock := filepath.Join(tmp, \"fiber-test.sock\")\n\n\t// Make sure temporary directory is cleaned up\n\tdefer func() { assert.NoError(t, os.RemoveAll(tmp)) }()\n\n\t// Send request through socket\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\n\t\tclient := &fasthttp.HostClient{\n\t\t\tAddr: sock,\n\t\t\tDial: func(addr string) (net.Conn, error) {\n\t\t\t\treturn net.Dial(\"unix\", addr)\n\t\t\t},\n\t\t}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.SetRequestURI(\"http://host/test\")\n\n\t\treqErr = client.Do(req, resp)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\trequire.NoError(t, app.Listen(sock, ListenConfig{\n\t\tDisableStartupMessage: true,\n\t\tListenerNetwork:       NetworkUnix,\n\t\tUnixSocketFileMode:    0o666,\n\t\tListenerAddrFunc: func(addr net.Addr) {\n\t\t\tnetwork = addr.String()\n\t\t\tf, err = os.Stat(network)\n\t\t},\n\t}))\n\n\t// Verify that listening and setting permissions works correctly\n\trequire.Equal(t, sock, network)\n\trequire.NoError(t, err)\n\trequire.Equal(t, os.FileMode(0o666), f.Mode().Perm())\n\n\t// Verify that request was successful\n\trequire.NoError(t, reqErr)\n\trequire.Equal(t, 200, resp.StatusCode())\n\trequire.Equal(t, \"all good\", string(resp.Body()))\n}\n\n// go test -run Test_Listen_Master_Process_Show_Startup_Message\nfunc Test_Listen_Master_Process_Show_Startup_Message(t *testing.T) {\n\tcfg := ListenConfig{\n\t\tEnablePrefork: true,\n\t}\n\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr, ok := ln.Addr().(*net.TCPAddr)\n\trequire.True(t, ok)\n\tport := addr.Port\n\trequire.NoError(t, ln.Close())\n\n\tchildTemplate := []int{11111, 22222, 33333, 44444, 55555, 60000}\n\tchildPIDs := make([]int, 0, len(childTemplate)*10)\n\tfor range 10 {\n\t\tchildPIDs = append(childPIDs, childTemplate...)\n\t}\n\n\tapp := New()\n\tlistenData := app.prepareListenData(fmt.Sprintf(\":%d\", port), true, &cfg, childPIDs)\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\tcolors := Colors{}\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"https://127.0.0.1:%d\", port))\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"(bound on host 0.0.0.0 and port %d)\", port))\n\trequire.Contains(t, startupMessage, \"Child PIDs\")\n\trequire.Contains(t, startupMessage, \"11111, 22222, 33333, 44444, 55555, 60000\")\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"Prefork: \\t\\t\\t%sEnabled%s\", colors.Blue, colors.Reset))\n}\n\n// go test -run Test_Listen_Master_Process_Show_Startup_MessageWithAppName\nfunc Test_Listen_Master_Process_Show_Startup_MessageWithAppName(t *testing.T) {\n\tcfg := ListenConfig{\n\t\tEnablePrefork: true,\n\t}\n\n\tapp := New(Config{AppName: \"Test App v3.0.0\"})\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr, ok := ln.Addr().(*net.TCPAddr)\n\trequire.True(t, ok)\n\tport := addr.Port\n\trequire.NoError(t, ln.Close())\n\n\tchildTemplate := []int{11111, 22222, 33333, 44444, 55555, 60000}\n\tchildPIDs := make([]int, 0, len(childTemplate)*10)\n\tfor range 10 {\n\t\tchildPIDs = append(childPIDs, childTemplate...)\n\t}\n\n\tlistenData := app.prepareListenData(fmt.Sprintf(\":%d\", port), true, &cfg, childPIDs)\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\trequire.Equal(t, \"Test App v3.0.0\", app.Config().AppName)\n\trequire.Contains(t, startupMessage, app.Config().AppName)\n}\n\n// go test -run Test_Listen_Master_Process_Show_Startup_MessageWithAppNameNonAscii\nfunc Test_Listen_Master_Process_Show_Startup_MessageWithAppNameNonAscii(t *testing.T) {\n\tcfg := ListenConfig{\n\t\tEnablePrefork: true,\n\t}\n\n\tappName := \"Serveur de vérification des données\"\n\tapp := New(Config{AppName: appName})\n\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr, ok := ln.Addr().(*net.TCPAddr)\n\trequire.True(t, ok)\n\tport := addr.Port\n\trequire.NoError(t, ln.Close())\n\n\tlistenData := app.prepareListenData(fmt.Sprintf(\":%d\", port), false, &cfg, nil)\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\trequire.Contains(t, startupMessage, \"Serveur de vérification des données\")\n}\n\n// go test -run Test_Listen_Master_Process_Show_Startup_MessageWithDisabledPreforkAndCustomEndpoint\nfunc Test_Listen_Master_Process_Show_Startup_MessageWithDisabledPreforkAndCustomEndpoint(t *testing.T) {\n\tcfg := ListenConfig{\n\t\tEnablePrefork: false,\n\t}\n\n\tappName := \"Fiber Example Application\"\n\tapp := New(Config{AppName: appName})\n\tln, err := net.Listen(NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr, ok := ln.Addr().(*net.TCPAddr)\n\trequire.True(t, ok)\n\tport := addr.Port\n\trequire.NoError(t, ln.Close())\n\n\tlistenData := app.prepareListenData(fmt.Sprintf(\"server.com:%d\", port), true, &cfg, nil)\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\tcolors := Colors{}\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"%sINFO%s\", colors.Green, colors.Reset))\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"%s%s%s\", colors.Blue, appName, colors.Reset))\n\texpectedURL := fmt.Sprintf(\"https://server.com:%d\", port)\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"%s%s%s\", colors.Blue, expectedURL, colors.Reset))\n\trequire.Contains(t, startupMessage, fmt.Sprintf(\"Prefork: \\t\\t\\t%sDisabled%s\", colors.Red, colors.Reset))\n}\n\nfunc Test_StartupMessageCustomization(t *testing.T) {\n\tcfg := ListenConfig{}\n\tapp := New()\n\tlistenData := app.prepareListenData(\":8080\", false, &cfg, nil)\n\n\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\tdata.BannerHeader = \"FOOBER v98\\n-------\"\n\n\t\tdata.ResetEntries()\n\t\tdata.AddInfo(\"git_hash\", \"Git hash\", \"abc123\", 3)\n\t\tdata.AddInfo(\"version\", \"Version\", \"v98\", 2)\n\n\t\treturn nil\n\t})\n\n\tvar post PostStartupMessageData\n\tapp.Hooks().OnPostStartupMessage(func(data *PostStartupMessageData) error {\n\t\tpost = *data\n\n\t\treturn nil\n\t})\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\n\trequire.Contains(t, startupMessage, \"FOOBER v98\")\n\trequire.Contains(t, startupMessage, \"Git hash: \\tabc123\")\n\trequire.Contains(t, startupMessage, \"Version: \\tv98\")\n\trequire.NotContains(t, startupMessage, \"Server started on:\")\n\trequire.NotContains(t, startupMessage, \"Prefork:\")\n\n\trequire.False(t, post.Disabled)\n\trequire.False(t, post.IsChild)\n\trequire.False(t, post.Prevented)\n}\n\nfunc Test_StartupMessageDisabledPostHook(t *testing.T) {\n\tcfg := ListenConfig{DisableStartupMessage: true}\n\tapp := New()\n\tlistenData := app.prepareListenData(\":7070\", false, &cfg, nil)\n\n\tvar post PostStartupMessageData\n\tapp.Hooks().OnPostStartupMessage(func(data *PostStartupMessageData) error {\n\t\tpost = *data\n\n\t\treturn nil\n\t})\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\n\trequire.Empty(t, startupMessage)\n\trequire.True(t, post.Disabled)\n\trequire.False(t, post.IsChild)\n\trequire.False(t, post.Prevented)\n}\n\nfunc Test_StartupMessagePreventedByHook(t *testing.T) {\n\tcfg := ListenConfig{}\n\tapp := New()\n\tlistenData := app.prepareListenData(\":9090\", false, &cfg, nil)\n\n\tapp.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error {\n\t\tdata.PreventDefault = true\n\n\t\treturn nil\n\t})\n\n\tvar post PostStartupMessageData\n\tapp.Hooks().OnPostStartupMessage(func(data *PostStartupMessageData) error {\n\t\tpost = *data\n\n\t\treturn nil\n\t})\n\n\tstartupMessage := captureOutput(func() {\n\t\tapp.startupMessage(listenData, &cfg)\n\t})\n\n\trequire.Empty(t, startupMessage)\n\trequire.False(t, post.Disabled)\n\trequire.False(t, post.IsChild)\n\trequire.True(t, post.Prevented)\n}\n\n// go test -run Test_Listen_Print_Route\nfunc Test_Listen_Print_Route(t *testing.T) {\n\tapp := New()\n\tapp.Get(\"/\", emptyHandler).Name(\"routeName\")\n\tprintRoutesMessage := captureOutput(func() {\n\t\tapp.printRoutesMessage()\n\t})\n\trequire.Contains(t, printRoutesMessage, MethodGet)\n\trequire.Contains(t, printRoutesMessage, \"/\")\n\trequire.Contains(t, printRoutesMessage, \"emptyHandler\")\n\trequire.Contains(t, printRoutesMessage, \"routeName\")\n}\n\n// go test -run Test_Listen_Print_Route_With_Group\nfunc Test_Listen_Print_Route_With_Group(t *testing.T) {\n\tapp := New()\n\tapp.Get(\"/\", emptyHandler)\n\n\tv1 := app.Group(\"v1\")\n\tv1.Get(\"/test\", emptyHandler).Name(\"v1\")\n\tv1.Post(\"/test/fiber\", emptyHandler)\n\tv1.Put(\"/test/fiber/*\", emptyHandler)\n\n\tprintRoutesMessage := captureOutput(func() {\n\t\tapp.printRoutesMessage()\n\t})\n\n\trequire.Contains(t, printRoutesMessage, MethodGet)\n\trequire.Contains(t, printRoutesMessage, \"/\")\n\trequire.Contains(t, printRoutesMessage, \"emptyHandler\")\n\trequire.Contains(t, printRoutesMessage, \"/v1/test\")\n\trequire.Contains(t, printRoutesMessage, \"POST\")\n\trequire.Contains(t, printRoutesMessage, \"/v1/test/fiber\")\n\trequire.Contains(t, printRoutesMessage, \"PUT\")\n\trequire.Contains(t, printRoutesMessage, \"/v1/test/fiber/*\")\n}\n\nfunc captureOutput(f func()) string {\n\treader, writer, err := os.Pipe()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstdout := os.Stdout\n\tstderr := os.Stderr\n\tdefer func() {\n\t\tos.Stdout = stdout\n\t\tos.Stderr = stderr\n\t\tlog.SetOutput(os.Stderr)\n\t}()\n\tos.Stdout = writer\n\tos.Stderr = writer\n\tlog.SetOutput(writer)\n\tout := make(chan string)\n\tgo func() {\n\t\tvar buf bytes.Buffer\n\t\t_, copyErr := io.Copy(&buf, reader)\n\t\tif copyErr != nil {\n\t\t\tpanic(copyErr)\n\t\t}\n\t\tout <- buf.String() // this out channel helps in synchronization\n\t}()\n\tf()\n\terr = writer.Close()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn <-out\n}\n\nfunc emptyHandler(_ Ctx) error {\n\treturn nil\n}\n"
  },
  {
    "path": "log/default.go",
    "content": "package log\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/bytebufferpool\"\n)\n\nvar _ AllLogger[*log.Logger] = (*defaultLogger)(nil)\n\ntype defaultLogger struct {\n\tstdlog *log.Logger\n\tlevel  Level\n\tdepth  int\n}\n\n// privateLog logs a message at a given level log the default logger.\n// when the level is fatal, it will exit the program.\nfunc (l *defaultLogger) privateLog(lv Level, fmtArgs []any) {\n\tif l.level > lv {\n\t\treturn\n\t}\n\tlevel := lv.toString()\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(level)\n\tfmt.Fprint(buf, fmtArgs...)\n\n\t_ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error\n\tif lv == LevelPanic {\n\t\tpanic(buf.String())\n\t}\n\n\tbuf.Reset()\n\tbytebufferpool.Put(buf)\n\tif lv == LevelFatal {\n\t\tos.Exit(1) //nolint:revive // we want to exit the program when Fatal is called\n\t}\n}\n\n// privateLogf logs a formatted message at a given level log the default logger.\n// when the level is fatal, it will exit the program.\nfunc (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) {\n\tif l.level > lv {\n\t\treturn\n\t}\n\tlevel := lv.toString()\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(level)\n\n\tif len(fmtArgs) > 0 {\n\t\t_, _ = fmt.Fprintf(buf, format, fmtArgs...)\n\t} else {\n\t\t_, _ = fmt.Fprint(buf, format)\n\t}\n\n\t_ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error\n\tif lv == LevelPanic {\n\t\tpanic(buf.String())\n\t}\n\tbuf.Reset()\n\tbytebufferpool.Put(buf)\n\tif lv == LevelFatal {\n\t\tos.Exit(1) //nolint:revive // we want to exit the program when Fatal is called\n\t}\n}\n\n// privateLogw logs a message at a given level log the default logger.\n// when the level is fatal, it will exit the program.\nfunc (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any) {\n\tif l.level > lv {\n\t\treturn\n\t}\n\tlevel := lv.toString()\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(level)\n\n\t// Write format privateLog buffer\n\tif format != \"\" {\n\t\tbuf.WriteString(format)\n\t}\n\t// Write keys and values privateLog buffer\n\tif len(keysAndValues) > 0 {\n\t\tif (len(keysAndValues) & 1) == 1 {\n\t\t\tkeysAndValues = append(keysAndValues, \"KEYVALS UNPAIRED\")\n\t\t}\n\n\t\tfor i := 0; i < len(keysAndValues); i += 2 {\n\t\t\tif i > 0 || format != \"\" {\n\t\t\t\tbuf.WriteByte(' ')\n\t\t\t}\n\t\t\tswitch key := keysAndValues[i].(type) {\n\t\t\tcase string:\n\t\t\t\tbuf.WriteString(key)\n\t\t\tdefault:\n\t\t\t\t_, _ = fmt.Fprint(buf, key)\n\t\t\t}\n\t\t\tbuf.WriteByte('=')\n\t\t\tbuf.WriteString(utils.ToString(keysAndValues[i+1]))\n\t\t}\n\t}\n\n\t_ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error\n\tif lv == LevelPanic {\n\t\tpanic(buf.String())\n\t}\n\tbuf.Reset()\n\tbytebufferpool.Put(buf)\n\tif lv == LevelFatal {\n\t\tos.Exit(1) //nolint:revive // we want to exit the program when Fatal is called\n\t}\n}\n\n// Trace logs the given values at trace level.\nfunc (l *defaultLogger) Trace(v ...any) {\n\tl.privateLog(LevelTrace, v)\n}\n\n// Debug logs the given values at debug level.\nfunc (l *defaultLogger) Debug(v ...any) {\n\tl.privateLog(LevelDebug, v)\n}\n\n// Info logs the given values at info level.\nfunc (l *defaultLogger) Info(v ...any) {\n\tl.privateLog(LevelInfo, v)\n}\n\n// Warn logs the given values at warn level.\nfunc (l *defaultLogger) Warn(v ...any) {\n\tl.privateLog(LevelWarn, v)\n}\n\n// Error logs the given values at error level.\nfunc (l *defaultLogger) Error(v ...any) {\n\tl.privateLog(LevelError, v)\n}\n\n// Fatal logs the given values at fatal level and terminates the process.\nfunc (l *defaultLogger) Fatal(v ...any) {\n\tl.privateLog(LevelFatal, v)\n}\n\n// Panic logs the given values at panic level and panics.\nfunc (l *defaultLogger) Panic(v ...any) {\n\tl.privateLog(LevelPanic, v)\n}\n\n// Tracef formats according to a format specifier and logs at trace level.\nfunc (l *defaultLogger) Tracef(format string, v ...any) {\n\tl.privateLogf(LevelTrace, format, v)\n}\n\n// Debugf formats according to a format specifier and logs at debug level.\nfunc (l *defaultLogger) Debugf(format string, v ...any) {\n\tl.privateLogf(LevelDebug, format, v)\n}\n\n// Infof formats according to a format specifier and logs at info level.\nfunc (l *defaultLogger) Infof(format string, v ...any) {\n\tl.privateLogf(LevelInfo, format, v)\n}\n\n// Warnf formats according to a format specifier and logs at warn level.\nfunc (l *defaultLogger) Warnf(format string, v ...any) {\n\tl.privateLogf(LevelWarn, format, v)\n}\n\n// Errorf formats according to a format specifier and logs at error level.\nfunc (l *defaultLogger) Errorf(format string, v ...any) {\n\tl.privateLogf(LevelError, format, v)\n}\n\n// Fatalf formats according to a format specifier, logs at fatal level, and terminates the process.\nfunc (l *defaultLogger) Fatalf(format string, v ...any) {\n\tl.privateLogf(LevelFatal, format, v)\n}\n\n// Panicf formats according to a format specifier, logs at panic level, and panics.\nfunc (l *defaultLogger) Panicf(format string, v ...any) {\n\tl.privateLogf(LevelPanic, format, v)\n}\n\n// Tracew logs at trace level with a message and key/value pairs.\nfunc (l *defaultLogger) Tracew(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelTrace, msg, keysAndValues)\n}\n\n// Debugw logs at debug level with a message and key/value pairs.\nfunc (l *defaultLogger) Debugw(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelDebug, msg, keysAndValues)\n}\n\n// Infow logs at info level with a message and key/value pairs.\nfunc (l *defaultLogger) Infow(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelInfo, msg, keysAndValues)\n}\n\n// Warnw logs at warn level with a message and key/value pairs.\nfunc (l *defaultLogger) Warnw(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelWarn, msg, keysAndValues)\n}\n\n// Errorw logs at error level with a message and key/value pairs.\nfunc (l *defaultLogger) Errorw(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelError, msg, keysAndValues)\n}\n\n// Fatalw logs at fatal level with a message and key/value pairs, then terminates the process.\nfunc (l *defaultLogger) Fatalw(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelFatal, msg, keysAndValues)\n}\n\n// Panicw logs at panic level with a message and key/value pairs, then panics.\nfunc (l *defaultLogger) Panicw(msg string, keysAndValues ...any) {\n\tl.privateLogw(LevelPanic, msg, keysAndValues)\n}\n\n// WithContext returns a logger that shares the underlying output but adjusts the call depth.\nfunc (l *defaultLogger) WithContext(_ context.Context) CommonLogger {\n\treturn &defaultLogger{\n\t\tstdlog: l.stdlog,\n\t\tlevel:  l.level,\n\t\tdepth:  l.depth - 1,\n\t}\n}\n\n// SetLevel updates the minimum level that will be emitted by the logger.\nfunc (l *defaultLogger) SetLevel(level Level) {\n\tl.level = level\n}\n\n// SetOutput replaces the underlying writer used by the logger.\nfunc (l *defaultLogger) SetOutput(writer io.Writer) {\n\tl.stdlog.SetOutput(writer)\n}\n\n// Logger returns the logger instance. It can be used to adjust the logger configurations in case of need.\nfunc (l *defaultLogger) Logger() *log.Logger {\n\treturn l.stdlog\n}\n\n// DefaultLogger returns the default logger.\nfunc DefaultLogger[T any]() AllLogger[T] {\n\tif l, ok := logger.(AllLogger[T]); ok {\n\t\treturn l\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "log/default_test.go",
    "content": "package log\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst work = \"work\"\n\nfunc initDefaultLogger() {\n\tlogger = &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", 0),\n\t\tdepth:  4,\n\t}\n}\n\ntype byteSliceWriter struct {\n\tb []byte\n}\n\nfunc (w *byteSliceWriter) Write(p []byte) (int, error) {\n\tw.b = append(w.b, p...)\n\treturn len(p), nil\n}\n\nfunc Test_WithContextCaller(t *testing.T) {\n\tlogger = &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", log.Lshortfile),\n\t\tdepth:  4,\n\t}\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\tctx := context.TODO()\n\n\tWithContext(ctx).Info(\"\")\n\tInfo(\"\")\n\n\trequire.Equal(t, \"default_test.go:41: [Info] \\ndefault_test.go:42: [Info] \\n\", string(w.b))\n}\n\nfunc Test_DefaultLogger(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tTrace(\"trace work\")\n\tDebug(\"received work order\")\n\tInfo(\"starting work\")\n\tWarn(\"work may fail\")\n\tError(\"work failed\")\n\n\trequire.Panics(t, func() {\n\t\tPanic(\"work panic\")\n\t})\n\n\trequire.Equal(t, \"[Trace] trace work\\n\"+\n\t\t\"[Debug] received work order\\n\"+\n\t\t\"[Info] starting work\\n\"+\n\t\t\"[Warn] work may fail\\n\"+\n\t\t\"[Error] work failed\\n\"+\n\t\t\"[Panic] work panic\\n\", string(w.b))\n}\n\nfunc Test_DefaultFormatLogger(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tTracef(\"trace %s\", work)\n\tDebugf(\"received %s order\", work)\n\tInfof(\"starting %s\", work)\n\tWarnf(\"%s may fail\", work)\n\tErrorf(\"%s failed\", work)\n\n\trequire.Panics(t, func() {\n\t\tPanicf(\"%s panic\", work)\n\t})\n\n\trequire.Equal(t, \"[Trace] trace work\\n\"+\n\t\t\"[Debug] received work order\\n\"+\n\t\t\"[Info] starting work\\n\"+\n\t\t\"[Warn] work may fail\\n\"+\n\t\t\"[Error] work failed\\n\"+\n\t\t\"[Panic] work panic\\n\", string(w.b))\n}\n\nfunc Test_CtxLogger(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tctx := context.Background()\n\n\tWithContext(ctx).Tracef(\"trace %s\", work)\n\tWithContext(ctx).Debugf(\"received %s order\", work)\n\tWithContext(ctx).Infof(\"starting %s\", work)\n\tWithContext(ctx).Warnf(\"%s may fail\", work)\n\tWithContext(ctx).Errorf(\"%s failed %d\", work, 50)\n\n\trequire.Panics(t, func() {\n\t\tWithContext(ctx).Panicf(\"%s panic\", work)\n\t})\n\n\trequire.Equal(t, \"[Trace] trace work\\n\"+\n\t\t\"[Debug] received work order\\n\"+\n\t\t\"[Info] starting work\\n\"+\n\t\t\"[Warn] work may fail\\n\"+\n\t\t\"[Error] work failed 50\\n\"+\n\t\t\"[Panic] work panic\\n\", string(w.b))\n}\n\nfunc Test_LogfKeyAndValues(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tformat        string\n\t\twantOutput    string\n\t\tfmtArgs       []any\n\t\tkeysAndValues []any\n\t\tlevel         Level\n\t}{\n\t\t{\n\t\t\tname:          \"test logf with debug level and key-values\",\n\t\t\tlevel:         LevelDebug,\n\t\t\tformat:        \"\",\n\t\t\tfmtArgs:       nil,\n\t\t\tkeysAndValues: []any{\"name\", \"Bob\", \"age\", 30},\n\t\t\twantOutput:    \"[Debug] name=Bob age=30\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with info level and key-values\",\n\t\t\tlevel:         LevelInfo,\n\t\t\tformat:        \"\",\n\t\t\tfmtArgs:       nil,\n\t\t\tkeysAndValues: []any{\"status\", \"ok\", \"code\", 200},\n\t\t\twantOutput:    \"[Info] status=ok code=200\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with warn level and key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tfmtArgs:       nil,\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t\twantOutput:    \"[Warn] error=not found id=123\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with format and key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"test\",\n\t\t\tfmtArgs:       nil,\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t\twantOutput:    \"[Warn] test error=not found id=123\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with one key\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tfmtArgs:       nil,\n\t\t\tkeysAndValues: []any{\"error\"},\n\t\t\twantOutput:    \"[Warn] error=KEYVALS UNPAIRED\\n\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tl := &defaultLogger{\n\t\t\t\tstdlog: log.New(&buf, \"\", 0),\n\t\t\t\tlevel:  tt.level,\n\t\t\t\tdepth:  4,\n\t\t\t}\n\t\t\tl.privateLogw(tt.level, tt.format, tt.keysAndValues)\n\t\t\trequire.Equal(t, tt.wantOutput, buf.String())\n\t\t})\n\t}\n}\n\nfunc Test_SetLevel(t *testing.T) {\n\tsetLogger := &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n\t\tdepth:  4,\n\t}\n\n\tsetLogger.SetLevel(LevelTrace)\n\trequire.Equal(t, LevelTrace, setLogger.level)\n\trequire.Equal(t, LevelTrace.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelDebug)\n\trequire.Equal(t, LevelDebug, setLogger.level)\n\trequire.Equal(t, LevelDebug.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelInfo)\n\trequire.Equal(t, LevelInfo, setLogger.level)\n\trequire.Equal(t, LevelInfo.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelWarn)\n\trequire.Equal(t, LevelWarn, setLogger.level)\n\trequire.Equal(t, LevelWarn.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelError)\n\trequire.Equal(t, LevelError, setLogger.level)\n\trequire.Equal(t, LevelError.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelFatal)\n\trequire.Equal(t, LevelFatal, setLogger.level)\n\trequire.Equal(t, LevelFatal.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(LevelPanic)\n\trequire.Equal(t, LevelPanic, setLogger.level)\n\trequire.Equal(t, LevelPanic.toString(), setLogger.level.toString())\n\n\tsetLogger.SetLevel(8)\n\trequire.Equal(t, 8, int(setLogger.level))\n\trequire.Equal(t, \"[?8] \", setLogger.level.toString())\n}\n\nfunc Test_Logger(t *testing.T) {\n\tunderlyingLogger := log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds)\n\tsetLogger := &defaultLogger{\n\t\tstdlog: underlyingLogger,\n\t\tdepth:  4,\n\t}\n\n\trequire.Equal(t, underlyingLogger, setLogger.Logger())\n\n\tlogger := setLogger.Logger()\n\tlogger.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)\n\trequire.Equal(t, log.LstdFlags|log.Lshortfile|log.Lmicroseconds, setLogger.stdlog.Flags())\n}\n\nfunc Test_Debugw(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"debug work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\tDebugw(msg, keysAndValues...)\n\n\trequire.Equal(t, \"[Debug] debug work key1=value1 key2=value2\\n\", string(w.b))\n}\n\nfunc Test_Infow(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"info work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\tInfow(msg, keysAndValues...)\n\n\trequire.Equal(t, \"[Info] info work key1=value1 key2=value2\\n\", string(w.b))\n}\n\nfunc Test_Warnw(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"warning work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\tWarnw(msg, keysAndValues...)\n\n\trequire.Equal(t, \"[Warn] warning work key1=value1 key2=value2\\n\", string(w.b))\n}\n\nfunc Test_Errorw(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"error work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\tErrorw(msg, keysAndValues...)\n\n\trequire.Equal(t, \"[Error] error work key1=value1 key2=value2\\n\", string(w.b))\n}\n\nfunc Test_Panicw(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"panic work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\trequire.Panics(t, func() {\n\t\tPanicw(msg, keysAndValues...)\n\t})\n\n\trequire.Equal(t, \"[Panic] panic work key1=value1 key2=value2\\n\", string(w.b))\n}\n\nfunc Test_Tracew(t *testing.T) {\n\tinitDefaultLogger()\n\n\tvar w byteSliceWriter\n\tSetOutput(&w)\n\n\tmsg := \"trace work\"\n\tkeysAndValues := []any{\"key1\", \"value1\", \"key2\", \"value2\"}\n\n\tTracew(msg, keysAndValues...)\n\n\trequire.Equal(t, \"[Trace] trace work key1=value1 key2=value2\\n\", string(w.b))\n}\n\ntype stringKey struct {\n\tvalue string\n}\n\nfunc (k stringKey) String() string {\n\treturn \"key:\" + k.value\n}\n\nfunc Test_DefaultLoggerNonStringKeys(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Tracew with non-string keys\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tl := &defaultLogger{\n\t\t\tstdlog: log.New(&buf, \"\", 0),\n\t\t\tlevel:  LevelTrace,\n\t\t\tdepth:  4,\n\t\t}\n\n\t\trequire.NotPanics(t, func() {\n\t\t\tl.Tracew(\"trace\", 123, \"value\", stringKey{value: \"alpha\"}, 42)\n\t\t})\n\n\t\trequire.Equal(t, \"[Trace] trace 123=value key:alpha=42\\n\", buf.String())\n\t})\n\n\tt.Run(\"Infow with non-string keys\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tl := &defaultLogger{\n\t\t\tstdlog: log.New(&buf, \"\", 0),\n\t\t\tlevel:  LevelTrace,\n\t\t\tdepth:  4,\n\t\t}\n\n\t\trequire.NotPanics(t, func() {\n\t\t\tl.Infow(\"info\", 456, \"value\", stringKey{value: \"beta\"}, 7)\n\t\t})\n\n\t\trequire.Equal(t, \"[Info] info 456=value key:beta=7\\n\", buf.String())\n\t})\n}\n\nfunc Benchmark_LogfKeyAndValues(b *testing.B) {\n\ttests := []struct {\n\t\tname          string\n\t\tformat        string\n\t\tkeysAndValues []any\n\t\tlevel         Level\n\t}{\n\t\t{\n\t\t\tname:          \"test logf with debug level and key-values\",\n\t\t\tlevel:         LevelDebug,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"name\", \"Bob\", \"age\", 30},\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with info level and key-values\",\n\t\t\tlevel:         LevelInfo,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"status\", \"ok\", \"code\", 200},\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with warn level and key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with format and key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"test\",\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t},\n\t\t{\n\t\t\tname:          \"test logf with one key\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"error\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(tt.name, func(bb *testing.B) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tl := &defaultLogger{\n\t\t\t\tstdlog: log.New(&buf, \"\", 0),\n\t\t\t\tlevel:  tt.level,\n\t\t\t\tdepth:  4,\n\t\t\t}\n\n\t\t\tbb.ReportAllocs()\n\n\t\t\tfor bb.Loop() {\n\t\t\t\tl.privateLogw(tt.level, tt.format, tt.keysAndValues)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Benchmark_LogfKeyAndValues_Parallel(b *testing.B) {\n\ttests := []struct {\n\t\tname          string\n\t\tformat        string\n\t\tkeysAndValues []any\n\t\tlevel         Level\n\t}{\n\t\t{\n\t\t\tname:          \"debug level with key-values\",\n\t\t\tlevel:         LevelDebug,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"name\", \"Bob\", \"age\", 30},\n\t\t},\n\t\t{\n\t\t\tname:          \"info level with key-values\",\n\t\t\tlevel:         LevelInfo,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"status\", \"ok\", \"code\", 200},\n\t\t},\n\t\t{\n\t\t\tname:          \"warn level with key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t},\n\t\t{\n\t\t\tname:          \"warn level with format and key-values\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"test\",\n\t\t\tkeysAndValues: []any{\"error\", \"not found\", \"id\", 123},\n\t\t},\n\t\t{\n\t\t\tname:          \"warn level with one key\",\n\t\t\tlevel:         LevelWarn,\n\t\t\tformat:        \"\",\n\t\t\tkeysAndValues: []any{\"error\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(tt.name, func(bb *testing.B) {\n\t\t\tbb.ReportAllocs()\n\t\t\tbb.ResetTimer()\n\t\t\tbb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\tl := &defaultLogger{\n\t\t\t\t\tstdlog: log.New(&buf, \"\", 0),\n\t\t\t\t\tlevel:  tt.level,\n\t\t\t\t\tdepth:  4,\n\t\t\t\t}\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tl.privateLogw(tt.level, tt.format, tt.keysAndValues)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "log/fiberlog.go",
    "content": "package log\n\nimport (\n\t\"context\"\n\t\"io\"\n)\n\n// Fatal calls the default logger's Fatal method and then os.Exit(1).\nfunc Fatal(v ...any) {\n\tlogger.Fatal(v...)\n}\n\n// Error calls the default logger's Error method.\nfunc Error(v ...any) {\n\tlogger.Error(v...)\n}\n\n// Warn calls the default logger's Warn method.\nfunc Warn(v ...any) {\n\tlogger.Warn(v...)\n}\n\n// Info calls the default logger's Info method.\nfunc Info(v ...any) {\n\tlogger.Info(v...)\n}\n\n// Debug calls the default logger's Debug method.\nfunc Debug(v ...any) {\n\tlogger.Debug(v...)\n}\n\n// Trace calls the default logger's Trace method.\nfunc Trace(v ...any) {\n\tlogger.Trace(v...)\n}\n\n// Panic calls the default logger's Panic method.\nfunc Panic(v ...any) {\n\tlogger.Panic(v...)\n}\n\n// Fatalf calls the default logger's Fatalf method and then os.Exit(1).\nfunc Fatalf(format string, v ...any) {\n\tlogger.Fatalf(format, v...)\n}\n\n// Errorf calls the default logger's Errorf method.\nfunc Errorf(format string, v ...any) {\n\tlogger.Errorf(format, v...)\n}\n\n// Warnf calls the default logger's Warnf method.\nfunc Warnf(format string, v ...any) {\n\tlogger.Warnf(format, v...)\n}\n\n// Infof calls the default logger's Infof method.\nfunc Infof(format string, v ...any) {\n\tlogger.Infof(format, v...)\n}\n\n// Debugf calls the default logger's Debugf method.\nfunc Debugf(format string, v ...any) {\n\tlogger.Debugf(format, v...)\n}\n\n// Tracef calls the default logger's Tracef method.\nfunc Tracef(format string, v ...any) {\n\tlogger.Tracef(format, v...)\n}\n\n// Panicf calls the default logger's Tracef method.\nfunc Panicf(format string, v ...any) {\n\tlogger.Panicf(format, v...)\n}\n\n// Tracew logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Tracew(msg string, keysAndValues ...any) {\n\tlogger.Tracew(msg, keysAndValues...)\n}\n\n// Debugw logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Debugw(msg string, keysAndValues ...any) {\n\tlogger.Debugw(msg, keysAndValues...)\n}\n\n// Infow logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Infow(msg string, keysAndValues ...any) {\n\tlogger.Infow(msg, keysAndValues...)\n}\n\n// Warnw logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Warnw(msg string, keysAndValues ...any) {\n\tlogger.Warnw(msg, keysAndValues...)\n}\n\n// Errorw logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Errorw(msg string, keysAndValues ...any) {\n\tlogger.Errorw(msg, keysAndValues...)\n}\n\n// Fatalw logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Fatalw(msg string, keysAndValues ...any) {\n\tlogger.Fatalw(msg, keysAndValues...)\n}\n\n// Panicw logs a message with some additional context. The variadic key-value\n// pairs are treated as they are privateLog With.\nfunc Panicw(msg string, keysAndValues ...any) {\n\tlogger.Panicw(msg, keysAndValues...)\n}\n\n// WithContext binds the default logger to the provided context and returns the\n// contextualized logger.\nfunc WithContext(ctx context.Context) CommonLogger {\n\treturn logger.WithContext(ctx)\n}\n\n// SetLogger sets the default logger and the system logger.\n// Note that this method is not concurrent-safe and must not be called\n// after the use of DefaultLogger and global functions from this package.\nfunc SetLogger[T any](v AllLogger[T]) {\n\tlogger = v\n}\n\n// SetOutput sets the output of default logger and system logger. By default, it is stderr.\nfunc SetOutput(w io.Writer) {\n\tlogger.SetOutput(w)\n}\n\n// SetLevel sets the level of logs below which logs will not be output.\n// The default logger is LevelTrace.\n// Note that this method is not concurrent-safe.\nfunc SetLevel(lv Level) {\n\tlogger.SetLevel(lv)\n}\n"
  },
  {
    "path": "log/fiberlog_test.go",
    "content": "package log\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_DefaultSystemLogger(t *testing.T) {\n\tt.Parallel()\n\tdefaultL := DefaultLogger[*log.Logger]()\n\trequire.Equal(t, logger, defaultL)\n}\n\nfunc Test_SetLogger(t *testing.T) {\n\tsetLog := &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n\t\tdepth:  6,\n\t}\n\n\tSetLogger(setLog)\n\trequire.Equal(t, logger, setLog)\n}\n\nfunc Test_Fiberlog_SetLevel(t *testing.T) {\n\tmockLogger := &defaultLogger{}\n\tSetLogger(mockLogger)\n\n\t// Test cases\n\ttestCases := []struct {\n\t\tname     string\n\t\tlevel    Level\n\t\texpected Level\n\t}{\n\t\t{\n\t\t\tname:     \"Test case 1\",\n\t\t\tlevel:    LevelDebug,\n\t\t\texpected: LevelDebug,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 2\",\n\t\t\tlevel:    LevelInfo,\n\t\t\texpected: LevelInfo,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 3\",\n\t\t\tlevel:    LevelWarn,\n\t\t\texpected: LevelWarn,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 4\",\n\t\t\tlevel:    LevelError,\n\t\t\texpected: LevelError,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 5\",\n\t\t\tlevel:    LevelFatal,\n\t\t\texpected: LevelFatal,\n\t\t},\n\t}\n\n\t// Run tests\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tSetLevel(tc.level)\n\t\t\trequire.Equal(t, tc.expected, mockLogger.level)\n\t\t})\n\t}\n}\n\nfunc Benchmark_DefaultSystemLogger(b *testing.B) {\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\t_ = DefaultLogger[*log.Logger]()\n\t}\n}\n\nfunc Benchmark_SetLogger(b *testing.B) {\n\tsetLog := &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n\t\tdepth:  6,\n\t}\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tSetLogger(setLog)\n\t}\n}\n\nfunc Benchmark_Fiberlog_SetLevel(b *testing.B) {\n\tmockLogger := &defaultLogger{}\n\tSetLogger(mockLogger)\n\n\t// Test cases\n\ttestCases := []struct {\n\t\tname     string\n\t\tlevel    Level\n\t\texpected Level\n\t}{\n\t\t{\n\t\t\tname:     \"Test case 1\",\n\t\t\tlevel:    LevelDebug,\n\t\t\texpected: LevelDebug,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 2\",\n\t\t\tlevel:    LevelInfo,\n\t\t\texpected: LevelInfo,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 3\",\n\t\t\tlevel:    LevelWarn,\n\t\t\texpected: LevelWarn,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 4\",\n\t\t\tlevel:    LevelError,\n\t\t\texpected: LevelError,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 5\",\n\t\t\tlevel:    LevelFatal,\n\t\t\texpected: LevelFatal,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tb.ReportAllocs()\n\t\tb.Run(tc.name, func(b *testing.B) {\n\t\t\tfor b.Loop() {\n\t\t\t\tSetLevel(tc.level)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Benchmark_DefaultSystemLogger_Parallel(b *testing.B) {\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\t_ = DefaultLogger[*log.Logger]()\n\t\t}\n\t})\n}\n\nfunc Benchmark_SetLogger_Parallel(b *testing.B) {\n\tsetLog := &defaultLogger{\n\t\tstdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n\t\tdepth:  6,\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tSetLogger(setLog)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Fiberlog_SetLevel_Parallel(b *testing.B) {\n\tmockLogger := &defaultLogger{}\n\tSetLogger(mockLogger)\n\n\t// Test cases\n\ttestCases := []struct {\n\t\tname     string\n\t\tlevel    Level\n\t\texpected Level\n\t}{\n\t\t{\n\t\t\tname:     \"Test case 1\",\n\t\t\tlevel:    LevelDebug,\n\t\t\texpected: LevelDebug,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 2\",\n\t\t\tlevel:    LevelInfo,\n\t\t\texpected: LevelInfo,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 3\",\n\t\t\tlevel:    LevelWarn,\n\t\t\texpected: LevelWarn,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 4\",\n\t\t\tlevel:    LevelError,\n\t\t\texpected: LevelError,\n\t\t},\n\t\t{\n\t\t\tname:     \"Test case 5\",\n\t\t\tlevel:    LevelFatal,\n\t\t\texpected: LevelFatal,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tb.Run(tc.name+\"_Parallel\", func(bb *testing.B) {\n\t\t\tbb.ReportAllocs()\n\t\t\tbb.ResetTimer()\n\t\t\tbb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tSetLevel(tc.level)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "log/log.go",
    "content": "package log\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n)\n\n// baseLogger defines the minimal logger functionality required by the package.\n// It allows storing any logger implementation regardless of its generic type.\ntype baseLogger interface {\n\tCommonLogger\n\tSetLevel(Level)\n\tSetOutput(io.Writer)\n\tWithContext(ctx context.Context) CommonLogger\n}\n\nvar logger baseLogger = &defaultLogger{\n\tstdlog: log.New(os.Stderr, \"\", log.LstdFlags|log.Lshortfile|log.Lmicroseconds),\n\tdepth:  4,\n}\n\n// Logger is a logger interface that provides logging function with levels.\ntype Logger interface {\n\tTrace(v ...any)\n\tDebug(v ...any)\n\tInfo(v ...any)\n\tWarn(v ...any)\n\tError(v ...any)\n\tFatal(v ...any)\n\tPanic(v ...any)\n}\n\n// FormatLogger is a logger interface that output logs with a format.\ntype FormatLogger interface {\n\tTracef(format string, v ...any)\n\tDebugf(format string, v ...any)\n\tInfof(format string, v ...any)\n\tWarnf(format string, v ...any)\n\tErrorf(format string, v ...any)\n\tFatalf(format string, v ...any)\n\tPanicf(format string, v ...any)\n}\n\n// WithLogger is a logger interface that output logs with a message and key-value pairs.\ntype WithLogger interface {\n\tTracew(msg string, keysAndValues ...any)\n\tDebugw(msg string, keysAndValues ...any)\n\tInfow(msg string, keysAndValues ...any)\n\tWarnw(msg string, keysAndValues ...any)\n\tErrorw(msg string, keysAndValues ...any)\n\tFatalw(msg string, keysAndValues ...any)\n\tPanicw(msg string, keysAndValues ...any)\n}\n\n// CommonLogger is the set of logging operations available across Fiber's\n// logging implementations.\ntype CommonLogger interface {\n\tLogger\n\tFormatLogger\n\tWithLogger\n}\n\n// ConfigurableLogger provides methods to config a logger.\ntype ConfigurableLogger[T any] interface {\n\t// SetLevel sets logging level.\n\t//\n\t// Available levels: Trace, Debug, Info, Warn, Error, Fatal, Panic.\n\tSetLevel(level Level)\n\n\t// SetOutput sets the logger output.\n\tSetOutput(w io.Writer)\n\n\t// Logger returns the logger instance. It can be used to adjust the logger configurations in case of need.\n\tLogger() T\n}\n\n// AllLogger is the combination of Logger, FormatLogger, CtxLogger and ConfigurableLogger.\n// Custom extensions can be made through AllLogger\ntype AllLogger[T any] interface {\n\tCommonLogger\n\tConfigurableLogger[T]\n\n\t// WithContext returns a new logger with the given context.\n\tWithContext(ctx context.Context) CommonLogger\n}\n\n// Level defines the priority of a log message.\n// When a logger is configured with a level, any log message with a lower\n// log level (smaller by integer comparison) will not be output.\ntype Level int\n\n// The levels of logs.\nconst (\n\tLevelTrace Level = iota\n\tLevelDebug\n\tLevelInfo\n\tLevelWarn\n\tLevelError\n\tLevelFatal\n\tLevelPanic\n)\n\nvar strs = []string{\n\t\"[Trace] \",\n\t\"[Debug] \",\n\t\"[Info] \",\n\t\"[Warn] \",\n\t\"[Error] \",\n\t\"[Fatal] \",\n\t\"[Panic] \",\n}\n\nfunc (lv Level) toString() string {\n\tif lv >= LevelTrace && lv <= LevelPanic {\n\t\treturn strs[lv]\n\t}\n\treturn fmt.Sprintf(\"[?%d] \", lv)\n}\n"
  },
  {
    "path": "middleware/adaptor/adaptor.go",
    "content": "package adaptor\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sync\"\n\t\"unsafe\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n\t\"github.com/valyala/fasthttp/fasthttpadaptor\"\n)\n\n// disableLogger implements the fasthttp Logger interface and discards log output.\ntype disableLogger struct{}\n\n// Printf implements the fasthttp Logger interface and discards log output.\nfunc (*disableLogger) Printf(string, ...any) {\n}\n\nvar ctxPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new(fasthttp.RequestCtx)\n\t},\n}\n\n// LocalContextKey is the key used to store the user's context.Context in the fasthttp request context.\n// Adapted http.Handler functions can retrieve this context using r.Context().Value(adaptor.LocalContextKey)\nvar localContextKey = &struct{}{}\n\nconst bufferSize = 32 * 1024\n\nvar bufferPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new([bufferSize]byte)\n\t},\n}\n\nvar (\n\tErrRemoteAddrEmpty   = errors.New(\"remote address cannot be empty\")\n\tErrRemoteAddrTooLong = errors.New(\"remote address too long\")\n)\n\n// HTTPHandlerFunc wraps net/http handler func to fiber handler\nfunc HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler {\n\treturn HTTPHandler(h)\n}\n\n// HTTPHandler wraps net/http handler to fiber handler\nfunc HTTPHandler(h http.Handler) fiber.Handler {\n\thandler := fasthttpadaptor.NewFastHTTPHandler(h)\n\treturn func(c fiber.Ctx) error {\n\t\thandler(c.RequestCtx())\n\t\treturn nil\n\t}\n}\n\n// HTTPHandlerWithContext is like HTTPHandler, but additionally stores Fiber’s user context in the request context\nfunc HTTPHandlerWithContext(h http.Handler) fiber.Handler {\n\thandler := fasthttpadaptor.NewFastHTTPHandler(h)\n\treturn func(c fiber.Ctx) error {\n\t\t// Store the Fiber user context (c.Context()) in the fasthttp request context\n\t\t// so adapted net/http handlers can retrieve it via adaptor.LocalContextFromHTTPRequest(r)\n\t\tc.RequestCtx().SetUserValue(localContextKey, c.Context())\n\n\t\thandler(c.RequestCtx())\n\t\treturn nil\n\t}\n}\n\n// LocalContextFromHTTPRequest extracts the Fiber user context previously stored into r.Context() by the adaptor.\nfunc LocalContextFromHTTPRequest(r *http.Request) (context.Context, bool) {\n\tif r == nil {\n\t\treturn nil, false\n\t}\n\n\tctx, err := r.Context().Value(localContextKey).(context.Context)\n\treturn ctx, err\n}\n\n// ConvertRequest converts a fiber.Ctx to a http.Request.\n// forServer should be set to true when the http.Request is going to be passed to a http.Handler.\nfunc ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error) {\n\tvar req http.Request\n\tif err := fasthttpadaptor.ConvertRequest(c.RequestCtx(), &req, forServer); err != nil {\n\t\treturn nil, err //nolint:wrapcheck // This must not be wrapped\n\t}\n\treturn &req, nil\n}\n\n// CopyContextToFiberContext copies the values of context.Context to a fasthttp.RequestCtx.\n// This function safely handles struct fields, using unsafe operations only when necessary for unexported fields.\n//\n// Deprecated: This function uses reflection and unsafe pointers; consider using explicit context passing.\nfunc CopyContextToFiberContext(src any, requestContext *fasthttp.RequestCtx) {\n\tif requestContext == nil {\n\t\treturn\n\t}\n\n\tv := reflect.ValueOf(src)\n\tif !v.IsValid() {\n\t\treturn\n\t}\n\t// Deref pointer chains\n\tfor v.Kind() == reflect.Ptr {\n\t\tif v.IsNil() {\n\t\t\treturn\n\t\t}\n\t\tv = v.Elem()\n\t}\n\tt := v.Type()\n\tif t.Kind() != reflect.Struct {\n\t\treturn\n\t}\n\t// Ensure addressable for safe unsafe-access of unexported fields\n\tif !v.CanAddr() {\n\t\ttmp := reflect.New(t)\n\t\ttmp.Elem().Set(v)\n\t\tv = tmp.Elem()\n\t}\n\tcontextValues := v\n\tcontextKeys := t\n\n\tvar lastKey any\n\tfor i := 0; i < contextValues.NumField(); i++ {\n\t\treflectValue := contextValues.Field(i)\n\t\treflectField := contextKeys.Field(i)\n\n\t\tif reflectField.Name == \"noCopy\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// Avoid unsafe access for unexported fields; use safe reflection where possible\n\t\tif !reflectValue.CanInterface() {\n\t\t\t/* #nosec */\n\t\t\treflectValue = reflect.NewAt(reflectValue.Type(), unsafe.Pointer(reflectValue.UnsafeAddr())).Elem()\n\t\t}\n\n\t\tswitch reflectField.Name {\n\t\tcase \"Context\":\n\t\t\tCopyContextToFiberContext(reflectValue.Interface(), requestContext)\n\t\tcase \"key\":\n\t\t\tlastKey = reflectValue.Interface()\n\t\tcase \"val\":\n\t\t\tif lastKey != nil {\n\t\t\t\trequestContext.SetUserValue(lastKey, reflectValue.Interface())\n\t\t\t\tlastKey = nil\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// HTTPMiddleware wraps net/http middleware to fiber middleware\nfunc HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tvar next bool\n\t\tnextHandler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {\n\t\t\tnext = true\n\t\t\tc.Request().Header.SetMethod(r.Method)\n\t\t\tc.Request().SetRequestURI(r.RequestURI)\n\t\t\tc.Request().SetHost(r.Host)\n\t\t\tc.Request().Header.SetHost(r.Host)\n\n\t\t\t// Remove all cookies before setting, see https://github.com/valyala/fasthttp/pull/1864\n\t\t\tc.Request().Header.DelAllCookies()\n\t\t\tfor key, val := range r.Header {\n\t\t\t\tfor _, v := range val {\n\t\t\t\t\tc.Request().Header.Set(key, v)\n\t\t\t\t}\n\t\t\t}\n\t\t\tCopyContextToFiberContext(r.Context(), c.RequestCtx())\n\t\t})\n\n\t\tif err := HTTPHandler(mw(nextHandler))(c); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif next {\n\t\t\treturn c.Next()\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// FiberHandler wraps fiber handler to net/http handler\nfunc FiberHandler(h fiber.Handler) http.Handler {\n\treturn FiberHandlerFunc(h)\n}\n\n// FiberHandlerFunc wraps fiber handler to net/http handler func\nfunc FiberHandlerFunc(h fiber.Handler) http.HandlerFunc {\n\treturn handlerFunc(fiber.New(), h)\n}\n\n// FiberApp wraps fiber app to net/http handler func\nfunc FiberApp(app *fiber.App) http.HandlerFunc {\n\treturn handlerFunc(app)\n}\n\nfunc isUnixNetwork(network string) bool {\n\treturn network == \"unix\" || network == \"unixgram\" || network == \"unixpacket\"\n}\n\nfunc resolveRemoteAddr(remoteAddr string, localAddr any) (net.Addr, error) {\n\tif addr, ok := localAddr.(net.Addr); ok && isUnixNetwork(addr.Network()) {\n\t\treturn addr, nil\n\t}\n\n\t// Validate input to prevent malformed addresses\n\tif remoteAddr == \"\" {\n\t\treturn nil, ErrRemoteAddrEmpty\n\t}\n\n\tresolved, err := net.ResolveTCPAddr(\"tcp\", remoteAddr)\n\tif err == nil {\n\t\treturn resolved, nil\n\t}\n\n\tvar addrErr *net.AddrError\n\tif errors.As(err, &addrErr) && addrErr.Err == \"missing port in address\" {\n\t\tif len(remoteAddr) > 253 { // Max hostname length\n\t\t\treturn nil, ErrRemoteAddrTooLong\n\t\t}\n\t\tremoteAddr = net.JoinHostPort(remoteAddr, \"80\")\n\t\tresolved, err2 := net.ResolveTCPAddr(\"tcp\", remoteAddr)\n\t\tif err2 != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to resolve TCP address after adding port: %w\", err2)\n\t\t}\n\t\treturn resolved, nil\n\t}\n\treturn nil, fmt.Errorf(\"failed to resolve TCP address: %w\", err)\n}\n\nfunc handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\treq := fasthttp.AcquireRequest()\n\t\tdefer fasthttp.ReleaseRequest(req)\n\n\t\t// Convert net/http -> fasthttp request with size limit\n\t\tmaxBodySize := int64(app.Config().BodyLimit)\n\t\tif r.Body != nil {\n\t\t\tif r.ContentLength > maxBodySize {\n\t\t\t\thttp.Error(w, utils.StatusMessage(fiber.StatusRequestEntityTooLarge), fiber.StatusRequestEntityTooLarge)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlimitedReader := io.LimitReader(r.Body, maxBodySize)\n\t\t\tn, err := io.Copy(req.BodyWriter(), limitedReader)\n\t\t\treq.Header.SetContentLength(int(n))\n\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\treq.Header.SetMethod(r.Method)\n\t\treq.SetRequestURI(r.RequestURI)\n\t\treq.SetHost(r.Host)\n\t\treq.Header.SetHost(r.Host)\n\n\t\tfor key, val := range r.Header {\n\t\t\tfor _, v := range val {\n\t\t\t\treq.Header.Set(key, v)\n\t\t\t}\n\t\t}\n\n\t\tremoteAddr, err := resolveRemoteAddr(r.RemoteAddr, r.Context().Value(http.LocalAddrContextKey))\n\t\tif err != nil {\n\t\t\tremoteAddr = nil // Fallback to nil\n\t\t}\n\n\t\t// New fasthttp Ctx from pool\n\t\tfctx := ctxPool.Get().(*fasthttp.RequestCtx) //nolint:forcetypeassert,errcheck // not needed\n\t\tfctx.Response.Reset()\n\t\tfctx.Request.Reset()\n\t\tdefer ctxPool.Put(fctx)\n\t\tfctx.Init(req, remoteAddr, &disableLogger{})\n\n\t\tif len(h) > 0 {\n\t\t\t// New fiber Ctx\n\t\t\tctx := app.AcquireCtx(fctx)\n\t\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t\t// Execute fiber Ctx\n\t\t\terr := h[0](ctx)\n\t\t\tif err != nil {\n\t\t\t\t_ = app.Config().ErrorHandler(ctx, err) //nolint:errcheck // not needed\n\t\t\t}\n\t\t} else {\n\t\t\t// Execute fasthttp Ctx though app.Handler\n\t\t\tapp.Handler()(fctx)\n\t\t}\n\n\t\t// Convert fasthttp Ctx -> net/http\n\t\tfor k, v := range fctx.Response.Header.All() {\n\t\t\tw.Header().Add(string(k), string(v))\n\t\t}\n\t\tw.WriteHeader(fctx.Response.StatusCode())\n\n\t\t// Check if streaming is not possible or unnecessary.\n\t\tbodyStream := fctx.Response.BodyStream()\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok || bodyStream == nil {\n\t\t\t_, _ = w.Write(fctx.Response.Body()) //nolint:errcheck // not needed\n\t\t\treturn\n\t\t}\n\n\t\t// Stream fctx.Response.BodyStream() -> w\n\t\t// in chunks.\n\t\tbufPtr, ok := bufferPool.Get().(*[bufferSize]byte)\n\t\tif !ok {\n\t\t\tpanic(fmt.Errorf(\"failed to type-assert to *[%d]byte\", bufferSize))\n\t\t}\n\t\tdefer bufferPool.Put(bufPtr)\n\n\t\tbuf := bufPtr[:]\n\t\tfor {\n\t\t\tn, err := bodyStream.Read(buf)\n\t\t\tif n > 0 {\n\t\t\t\tif _, writeErr := w.Write(buf[:n]); writeErr != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tflusher.Flush()\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/adaptor/adaptor_test.go",
    "content": "//nolint:contextcheck,revive // Much easier to just ignore memory leaks in tests\npackage adaptor\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nconst (\n\texpectedRequestURI = \"/foo/bar?baz=123\"\n\texpectedBody       = \"body 123 foo bar baz\"\n\texpectedHost       = \"foobar.com\"\n\texpectedRemoteAddr = \"1.2.3.4:6789\"\n)\n\nfunc Test_HTTPHandler(t *testing.T) {\n\tt.Parallel()\n\n\texpectedMethod := fiber.MethodPost\n\texpectedProto := \"HTTP/1.1\"\n\texpectedProtoMajor := 1\n\texpectedProtoMinor := 1\n\texpectedContentLength := len(expectedBody)\n\texpectedHeader := map[string]string{\n\t\t\"Foo-Bar\":         \"baz\",\n\t\t\"Abc\":             \"defg\",\n\t\t\"XXX-Remote-Addr\": \"123.43.4543.345\",\n\t}\n\texpectedURL, err := url.ParseRequestURI(expectedRequestURI)\n\trequire.NoError(t, err)\n\n\ttype contextKeyType string\n\texpectedContextKey := contextKeyType(\"contextKey\")\n\texpectedContextValue := \"contextValue\"\n\n\tcallsCount := 0\n\tnethttpH := func(w http.ResponseWriter, r *http.Request) {\n\t\tcallsCount++\n\t\tassert.Equal(t, expectedMethod, r.Method, \"Method\")\n\t\tassert.Equal(t, expectedProto, r.Proto, \"Proto\")\n\t\tassert.Equal(t, expectedProtoMajor, r.ProtoMajor, \"ProtoMajor\")\n\t\tassert.Equal(t, expectedProtoMinor, r.ProtoMinor, \"ProtoMinor\")\n\t\tassert.Equal(t, expectedRequestURI, r.RequestURI, \"RequestURI\")\n\t\tassert.Equal(t, expectedContentLength, int(r.ContentLength), \"ContentLength\")\n\t\tassert.Empty(t, r.TransferEncoding, \"TransferEncoding\")\n\t\tassert.Equal(t, expectedHost, r.Host, \"Host\")\n\t\tassert.Equal(t, expectedRemoteAddr, r.RemoteAddr, \"RemoteAddr\")\n\n\t\tbody, readErr := io.ReadAll(r.Body)\n\t\tassert.NoError(t, readErr)\n\t\tassert.Equal(t, expectedBody, string(body), \"Body\")\n\t\tassert.Equal(t, expectedURL, r.URL, \"URL\")\n\t\tassert.Equal(t, expectedContextValue, r.Context().Value(expectedContextKey), \"Context\")\n\n\t\tfor k, expectedV := range expectedHeader {\n\t\t\tv := r.Header.Get(k)\n\t\t\tassert.Equal(t, expectedV, v, \"Header\")\n\t\t}\n\n\t\tw.Header().Set(\"Header1\", \"value1\")\n\t\tw.Header().Set(\"Header2\", \"value2\")\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tfmt.Fprintf(w, \"request body is %q\", body)\n\t}\n\tfiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH))\n\tfiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue)\n\n\tvar fctx fasthttp.RequestCtx\n\tvar req fasthttp.Request\n\n\treq.Header.SetMethod(expectedMethod)\n\treq.SetRequestURI(expectedRequestURI)\n\treq.Header.SetHost(expectedHost)\n\treq.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck // not needed\n\tfor k, v := range expectedHeader {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tremoteAddr, err := net.ResolveTCPAddr(\"tcp\", expectedRemoteAddr)\n\trequire.NoError(t, err)\n\n\tfctx.Init(&req, remoteAddr, &disableLogger{})\n\tapp := fiber.New()\n\tctx := app.AcquireCtx(&fctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\terr = fiberH(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, callsCount, \"callsCount\")\n\n\tresp := &fctx.Response\n\trequire.Equal(t, http.StatusBadRequest, resp.StatusCode(), \"StatusCode\")\n\trequire.Equal(t, \"value1\", string(resp.Header.Peek(\"Header1\")), \"Header1\")\n\trequire.Equal(t, \"value2\", string(resp.Header.Peek(\"Header2\")), \"Header2\")\n\n\texpectedResponseBody := fmt.Sprintf(\"request body is %q\", expectedBody)\n\trequire.Equal(t, expectedResponseBody, string(resp.Body()), \"Body\")\n}\n\nfunc Test_HTTPHandler_Flush(t *testing.T) {\n\tt.Parallel()\n\n\texpectedMethod := fiber.MethodPost\n\texpectedProto := \"HTTP/1.1\"\n\texpectedProtoMajor := 1\n\texpectedProtoMinor := 1\n\texpectedContentLength := len(expectedBody)\n\texpectedHeader := map[string]string{\n\t\t\"Foo-Bar\":         \"baz\",\n\t\t\"Abc\":             \"defg\",\n\t\t\"XXX-Remote-Addr\": \"123.43.4543.345\",\n\t}\n\texpectedURL, err := url.ParseRequestURI(expectedRequestURI)\n\trequire.NoError(t, err)\n\n\ttype contextKeyType string\n\texpectedContextKey := contextKeyType(\"contextKey\")\n\texpectedContextValue := \"contextValue\"\n\n\tcallsCount := 0\n\tnethttpH := func(w http.ResponseWriter, r *http.Request) {\n\t\tcallsCount++\n\t\tassert.Equal(t, expectedMethod, r.Method, \"Method\")\n\t\tassert.Equal(t, expectedProto, r.Proto, \"Proto\")\n\t\tassert.Equal(t, expectedProtoMajor, r.ProtoMajor, \"ProtoMajor\")\n\t\tassert.Equal(t, expectedProtoMinor, r.ProtoMinor, \"ProtoMinor\")\n\t\tassert.Equal(t, expectedRequestURI, r.RequestURI, \"RequestURI\")\n\t\tassert.Equal(t, expectedContentLength, int(r.ContentLength), \"ContentLength\")\n\t\tassert.Empty(t, r.TransferEncoding, \"TransferEncoding\")\n\t\tassert.Equal(t, expectedHost, r.Host, \"Host\")\n\t\tassert.Equal(t, expectedRemoteAddr, r.RemoteAddr, \"RemoteAddr\")\n\n\t\tbody, readErr := io.ReadAll(r.Body)\n\t\tassert.NoError(t, readErr)\n\t\tassert.Equal(t, expectedBody, string(body), \"Body\")\n\t\tassert.Equal(t, expectedURL, r.URL, \"URL\")\n\t\tassert.Equal(t, expectedContextValue, r.Context().Value(expectedContextKey), \"Context\")\n\n\t\tfor k, expectedV := range expectedHeader {\n\t\t\tv := r.Header.Get(k)\n\t\t\tassert.Equal(t, expectedV, v, \"Header\")\n\t\t}\n\n\t\tw.Header().Set(\"Header1\", \"value1\")\n\t\tw.Header().Set(\"Header2\", \"value2\")\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tfmt.Fprintf(w, \"request body is \")\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\tt.Fatal(\"w does not implement http.Flusher\")\n\t\t}\n\t\tflusher.Flush()\n\t\tfmt.Fprintf(w, \"%q\", body)\n\t}\n\tfiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH))\n\tfiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue)\n\n\tvar fctx fasthttp.RequestCtx\n\tvar req fasthttp.Request\n\n\treq.Header.SetMethod(expectedMethod)\n\treq.SetRequestURI(expectedRequestURI)\n\treq.Header.SetHost(expectedHost)\n\treq.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck // not needed\n\tfor k, v := range expectedHeader {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tremoteAddr, err := net.ResolveTCPAddr(\"tcp\", expectedRemoteAddr)\n\trequire.NoError(t, err)\n\n\tfctx.Init(&req, remoteAddr, &disableLogger{})\n\tapp := fiber.New()\n\tctx := app.AcquireCtx(&fctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\terr = fiberH(ctx)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, callsCount, \"callsCount\")\n\n\tresp := &fctx.Response\n\trequire.Equal(t, http.StatusBadRequest, resp.StatusCode(), \"StatusCode\")\n\trequire.Equal(t, \"value1\", string(resp.Header.Peek(\"Header1\")), \"Header1\")\n\trequire.Equal(t, \"value2\", string(resp.Header.Peek(\"Header2\")), \"Header2\")\n\n\texpectedResponseBody := fmt.Sprintf(\"request body is %q\", expectedBody)\n\trequire.Equal(t, expectedResponseBody, string(resp.Body()), \"Body\")\n}\n\nfunc Test_HTTPHandler_Flush_App_Test(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\tt.Fatal(\"w does not implement http.Flusher\")\n\t\t}\n\t\tw.WriteHeader(fiber.StatusOK)\n\t\tfmt.Fprintf(w, \"Hello \")\n\t\tflusher.Flush()\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tfmt.Fprintf(w, \"World!\")\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // not needed\n\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Hello World!\", string(body))\n}\n\nfunc Test_HTTPHandler_App_Test_Interrupted(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tflusher, ok := w.(http.Flusher)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"w does not implement http.Flusher\")\n\t\t}\n\t\tw.WriteHeader(fiber.StatusOK)\n\t\tfmt.Fprintf(w, \"Hello \")\n\t\tflusher.Flush()\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tfmt.Fprintf(w, \"World!\")\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       200 * time.Millisecond,\n\t\tFailOnTimeout: true, // Changed to true to test interrupted behavior\n\t})\n\t// With FailOnTimeout: true, we should get a timeout error\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n\trequire.Nil(t, resp)\n}\n\nfunc Test_LocalContextFromHTTPRequest(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"nil request\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tctx, ok := LocalContextFromHTTPRequest(nil)\n\t\trequire.False(t, ok)\n\t\trequire.Nil(t, ctx)\n\t})\n\n\tt.Run(\"request without stored context key\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\n\t\tctx, ok := LocalContextFromHTTPRequest(req)\n\t\trequire.False(t, ok)\n\t\trequire.Nil(t, ctx)\n\t})\n\n\tt.Run(\"request with stored context key\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpectedCtx := context.WithValue(context.Background(), contextKey(\"k\"), \"v\")\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody).WithContext(\n\t\t\tcontext.WithValue(context.Background(), localContextKey, expectedCtx),\n\t\t)\n\n\t\tctx, ok := LocalContextFromHTTPRequest(req)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, expectedCtx, ctx)\n\t})\n}\n\nfunc Test_HTTPHandlerWithContext_local_context(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// unique type for avoiding collisions in context\n\ttype key struct{}\n\tvar testKey key\n\n\tconst testVal string = \"test-value\"\n\n\t// a middleware to add a value to the local context\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tctx := context.WithValue(c.Context(), testKey, testVal)\n\t\tc.SetContext(ctx)\n\t\treturn c.Next()\n\t})\n\n\t// a handler that checks if the value has been appended to the local context\n\tapp.Get(\"/\", HTTPHandlerWithContext(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tctx, ok := LocalContextFromHTTPRequest(r)\n\t\tif !ok {\n\t\t\thttp.Error(w, \"local context not found\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tval, ok := ctx.Value(testKey).(string)\n\t\tif !ok {\n\t\t\thttp.Error(w, \"invalid context value\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tif _, err := w.Write([]byte(val)); err != nil {\n\t\t\tt.Logf(\"write failed: %v\", err)\n\t\t}\n\t})))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       200 * time.Millisecond,\n\t\tFailOnTimeout: false,\n\t})\n\trequire.NoError(t, err)\n\n\tdefer resp.Body.Close() //nolint:errcheck // no need\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testVal, string(body))\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\ntype contextKey string\n\nfunc (c contextKey) String() string {\n\treturn \"test-\" + string(c)\n}\n\nvar (\n\tTestContextKey       = contextKey(\"TestContextKey\")\n\tTestContextSecondKey = contextKey(\"TestContextSecondKey\")\n)\n\nfunc Test_HTTPMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname       string\n\t\turl        string\n\t\tmethod     string\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\tname:       \"Should return 200\",\n\t\t\turl:        \"/\",\n\t\t\tmethod:     \"POST\",\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tname:       \"Should return 405\",\n\t\t\turl:        \"/\",\n\t\t\tmethod:     \"GET\",\n\t\t\tstatusCode: 405,\n\t\t},\n\t\t{\n\t\t\tname:       \"Should return 400\",\n\t\t\turl:        \"/unknown\",\n\t\t\tmethod:     \"POST\",\n\t\t\tstatusCode: 404,\n\t\t},\n\t}\n\n\tnethttpMW := func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tr = r.WithContext(context.WithValue(r.Context(), TestContextKey, \"okay\"))\n\t\t\tr = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, \"not_okay\"))\n\t\t\tr = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, \"okay\"))\n\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n\n\tapp := fiber.New()\n\tapp.Use(HTTPMiddleware(nethttpMW))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tvalue := c.RequestCtx().Value(TestContextKey)\n\t\tval, ok := value.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"unexpected error on type-assertion\")\n\t\t}\n\t\tif value != nil {\n\t\t\tc.Set(\"context_okay\", val)\n\t\t}\n\t\tvalue = c.RequestCtx().Value(TestContextSecondKey)\n\t\tif value != nil {\n\t\t\tval, ok := value.(string)\n\t\t\tif !ok {\n\t\t\t\tt.Error(\"unexpected error on type-assertion\")\n\t\t\t}\n\t\t\tc.Set(\"context_second_okay\", val)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tfor _, tt := range tests {\n\t\treq, err := http.NewRequestWithContext(context.Background(), tt.method, tt.url, http.NoBody)\n\t\treq.Host = expectedHost\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tt.statusCode, resp.StatusCode, \"StatusCode\")\n\t}\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, \"/\", http.NoBody)\n\treq.Host = expectedHost\n\trequire.NoError(t, err)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"okay\", resp.Header.Get(\"context_okay\"))\n\trequire.Equal(t, \"okay\", resp.Header.Get(\"context_second_okay\"))\n}\n\nfunc Test_HTTPMiddlewareWithCookies(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tcookieHeader    = \"Cookie\"\n\t\tsetCookieHeader = \"Set-Cookie\"\n\t\tcookieOneName   = \"cookieOne\"\n\t\tcookieTwoName   = \"cookieTwo\"\n\t\tcookieOneValue  = \"valueCookieOne\"\n\t\tcookieTwoValue  = \"valueCookieTwo\"\n\t)\n\tnethttpMW := func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n\n\tapp := fiber.New()\n\tapp.Use(HTTPMiddleware(nethttpMW))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t// RETURNING CURRENT COOKIES TO RESPONSE\n\t\tcookies := strings.Split(c.Get(cookieHeader), \"; \")\n\t\tfor _, cookie := range cookies {\n\t\t\tc.Set(setCookieHeader, cookie)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\t// Test case for POST request with cookies\n\tt.Run(\"POST request with cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, \"/\", http.NoBody)\n\t\trequire.NoError(t, err)\n\t\treq.AddCookie(&http.Cookie{Name: cookieOneName, Value: cookieOneValue})\n\t\treq.AddCookie(&http.Cookie{Name: cookieTwoName, Value: cookieTwoValue})\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tcookies := resp.Cookies()\n\t\trequire.Len(t, cookies, 2)\n\t\tfor _, cookie := range cookies {\n\t\t\tswitch cookie.Name {\n\t\t\tcase cookieOneName:\n\t\t\t\trequire.Equal(t, cookieOneValue, cookie.Value)\n\t\t\tcase cookieTwoName:\n\t\t\t\trequire.Equal(t, cookieTwoValue, cookie.Value)\n\t\t\tdefault:\n\t\t\t\tt.Error(\"unexpected cookie key\")\n\t\t\t}\n\t\t}\n\t})\n\n\t// New test case for GET request\n\tt.Run(\"GET request\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/\", http.NoBody)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)\n\t})\n\n\t// New test case for request without cookies\n\tt.Run(\"POST request without cookies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, \"/\", http.NoBody)\n\t\trequire.NoError(t, err)\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\t\trequire.Empty(t, resp.Cookies())\n\t})\n}\n\nfunc Test_FiberHandler(t *testing.T) {\n\tt.Parallel()\n\n\ttestFiberToHandlerFunc(t, false)\n}\n\nfunc Test_FiberHandler_BodyLimit(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname           string\n\t\tbodyLimit      int\n\t\tbodySize       int\n\t\texpectedStatus int\n\t}{\n\t\t{\n\t\t\tname:           \"DefaultLimitExceededReturns413\",\n\t\t\tbodySize:       fiber.DefaultBodyLimit + 1024,\n\t\t\texpectedStatus: fiber.StatusRequestEntityTooLarge,\n\t\t},\n\t\t{\n\t\t\tname:           \"CustomLimitExceededReturns413\",\n\t\t\tbodyLimit:      1 * 1024 * 1024,\n\t\t\tbodySize:       (1 * 1024 * 1024) + 1,\n\t\t\texpectedStatus: fiber.StatusRequestEntityTooLarge,\n\t\t},\n\t\t{\n\t\t\tname:           \"CustomLimitAllowsLargerPayload\",\n\t\t\tbodyLimit:      2 * fiber.DefaultBodyLimit,\n\t\t\tbodySize:       fiber.DefaultBodyLimit + 512,\n\t\t\texpectedStatus: fiber.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"ZeroLimitConfigFallsBackToDefault\",\n\t\t\tbodyLimit:      0,\n\t\t\tbodySize:       fiber.DefaultBodyLimit - 256,\n\t\t\texpectedStatus: fiber.StatusOK,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New(fiber.Config{\n\t\t\t\tBodyLimit: tt.bodyLimit,\n\t\t\t})\n\n\t\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t})\n\n\t\t\thandlerFunc := FiberApp(app)\n\t\t\tbody := bytes.Repeat([]byte(\"a\"), tt.bodySize)\n\t\t\treq := httptest.NewRequest(http.MethodPost, \"/\", bytes.NewReader(body))\n\t\t\treq.ContentLength = int64(len(body))\n\t\t\tresp := httptest.NewRecorder()\n\n\t\t\thandlerFunc.ServeHTTP(resp, req)\n\n\t\t\trequire.Equal(t, tt.expectedStatus, resp.Code)\n\t\t})\n\t}\n}\n\nfunc Test_FiberApp(t *testing.T) {\n\tt.Parallel()\n\n\ttestFiberToHandlerFunc(t, false, fiber.New())\n}\n\nfunc Test_FiberHandlerDefaultPort(t *testing.T) {\n\tt.Parallel()\n\n\ttestFiberToHandlerFunc(t, true)\n}\n\nfunc Test_FiberAppDefaultPort(t *testing.T) {\n\tt.Parallel()\n\n\ttestFiberToHandlerFunc(t, true, fiber.New())\n}\n\nfunc testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.App) {\n\tt.Helper()\n\n\texpectedMethod := fiber.MethodPost\n\texpectedContentLength := len(expectedBody)\n\texpectedRemoteAddr := \"1.2.3.4:6789\"\n\tif checkDefaultPort {\n\t\texpectedRemoteAddr = \"1.2.3.4:80\"\n\t}\n\texpectedHeader := map[string]string{\n\t\t\"Foo-Bar\":         \"baz\",\n\t\t\"Abc\":             \"defg\",\n\t\t\"XXX-Remote-Addr\": \"123.43.4543.345\",\n\t}\n\texpectedURL, err := url.ParseRequestURI(expectedRequestURI)\n\trequire.NoError(t, err)\n\n\tcallsCount := 0\n\tfiberH := func(c fiber.Ctx) error {\n\t\tcallsCount++\n\t\trequire.Equal(t, expectedMethod, c.Method(), \"Method\")\n\t\trequire.Equal(t, expectedRequestURI, string(c.RequestCtx().RequestURI()), \"RequestURI\")\n\t\trequire.Equal(t, expectedContentLength, c.RequestCtx().Request.Header.ContentLength(), \"ContentLength\")\n\t\trequire.Equal(t, expectedHost, c.Hostname(), \"Host\")\n\t\trequire.Equal(t, expectedHost, string(c.Request().Header.Host()), \"Host\")\n\t\trequire.Equal(t, \"http://\"+expectedHost, c.BaseURL(), \"BaseURL\")\n\t\trequire.Equal(t, expectedRemoteAddr, c.RequestCtx().RemoteAddr().String(), \"RemoteAddr\")\n\n\t\tbody := string(c.Body())\n\t\trequire.Equal(t, expectedBody, body, \"Body\")\n\t\trequire.Equal(t, expectedURL.String(), c.OriginalURL(), \"URL\")\n\n\t\tfor k, expectedV := range expectedHeader {\n\t\t\tv := c.Get(k)\n\t\t\trequire.Equal(t, expectedV, v, \"Header\")\n\t\t}\n\n\t\tc.Set(\"Header1\", \"value1\")\n\t\tc.Set(\"Header2\", \"value2\")\n\t\tc.Status(fiber.StatusBadRequest)\n\t\t_, err := c.Write(fmt.Appendf(nil, \"request body is %q\", body))\n\t\treturn err\n\t}\n\n\tvar handlerFunc http.HandlerFunc\n\tif len(app) > 0 {\n\t\tapp[0].Post(\"/foo/bar\", fiberH)\n\t\thandlerFunc = FiberApp(app[0])\n\t} else {\n\t\thandlerFunc = FiberHandlerFunc(fiberH)\n\t}\n\n\tvar r http.Request\n\n\tr.Method = expectedMethod\n\tr.Body = &netHTTPBody{b: []byte(expectedBody)}\n\tr.RequestURI = expectedRequestURI\n\tr.ContentLength = int64(expectedContentLength)\n\tr.Host = expectedHost\n\tr.RemoteAddr = expectedRemoteAddr\n\tif checkDefaultPort {\n\t\tr.RemoteAddr = \"1.2.3.4\"\n\t}\n\n\thdr := make(http.Header)\n\tfor k, v := range expectedHeader {\n\t\thdr.Set(k, v)\n\t}\n\tr.Header = hdr\n\n\tvar w netHTTPResponseWriter\n\thandlerFunc.ServeHTTP(&w, &r)\n\n\trequire.Equal(t, http.StatusBadRequest, w.StatusCode(), \"StatusCode\")\n\trequire.Equal(t, \"value1\", w.Header().Get(\"Header1\"), \"Header1\")\n\trequire.Equal(t, \"value2\", w.Header().Get(\"Header2\"), \"Header2\")\n\n\texpectedResponseBody := fmt.Sprintf(\"request body is %q\", expectedBody)\n\trequire.Equal(t, expectedResponseBody, string(w.body), \"Body\")\n}\n\nfunc setFiberContextValueMiddleware(next fiber.Handler, key, value any) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tc.Locals(key, value)\n\t\treturn next(c)\n\t}\n}\n\nfunc Test_FiberHandler_RequestNilBody(t *testing.T) {\n\tt.Parallel()\n\n\texpectedMethod := fiber.MethodGet\n\texpectedRequestURI := \"/foo/bar\"\n\texpectedContentLength := 0\n\n\tcallsCount := 0\n\tfiberH := func(c fiber.Ctx) error {\n\t\tcallsCount++\n\t\trequire.Equal(t, expectedMethod, c.Method(), \"Method\")\n\t\trequire.Equal(t, expectedRequestURI, string(c.RequestCtx().RequestURI()), \"RequestURI\")\n\t\trequire.Equal(t, expectedContentLength, c.RequestCtx().Request.Header.ContentLength(), \"ContentLength\")\n\n\t\t_, err := c.WriteString(\"request body is nil\")\n\t\treturn err\n\t}\n\tnethttpH := FiberHandler(fiberH)\n\n\tvar r http.Request\n\n\tr.Method = expectedMethod\n\tr.RequestURI = expectedRequestURI\n\n\tvar w netHTTPResponseWriter\n\tnethttpH.ServeHTTP(&w, &r)\n\n\texpectedResponseBody := \"request body is nil\"\n\trequire.Equal(t, expectedResponseBody, string(w.body), \"Body\")\n}\n\ntype netHTTPBody struct {\n\tb []byte\n}\n\nfunc (r *netHTTPBody) Read(p []byte) (int, error) {\n\tif len(r.b) == 0 {\n\t\treturn 0, io.EOF\n\t}\n\tn := copy(p, r.b)\n\tr.b = r.b[n:]\n\treturn n, nil\n}\n\nfunc (r *netHTTPBody) Close() error {\n\tr.b = r.b[:0]\n\treturn nil\n}\n\nfunc createTestRequest(method, uri, remoteAddr string, body io.Reader) *http.Request {\n\tr := &http.Request{\n\t\tMethod:     method,\n\t\tRequestURI: uri,\n\t\tRemoteAddr: remoteAddr,\n\t\tHeader:     make(http.Header),\n\t\tBody:       http.NoBody,\n\t}\n\tif body != nil {\n\t\tif rc, ok := body.(io.ReadCloser); ok {\n\t\t\tr.Body = rc\n\t\t} else {\n\t\t\tr.Body = io.NopCloser(body)\n\t\t}\n\t}\n\treturn r\n}\n\nfunc executeHandlerTest(_ *testing.T, handler http.HandlerFunc, req *http.Request) *netHTTPResponseWriter {\n\tw := &netHTTPResponseWriter{}\n\thandler.ServeHTTP(w, req)\n\treturn w\n}\n\ntype netHTTPResponseWriter struct {\n\th          http.Header\n\tbody       []byte\n\tstatusCode int\n}\n\nfunc (w *netHTTPResponseWriter) StatusCode() int {\n\tif w.statusCode == 0 {\n\t\treturn http.StatusOK\n\t}\n\treturn w.statusCode\n}\n\nfunc (w *netHTTPResponseWriter) Header() http.Header {\n\tif w.h == nil {\n\t\tw.h = make(http.Header)\n\t}\n\treturn w.h\n}\n\nfunc (w *netHTTPResponseWriter) WriteHeader(statusCode int) {\n\tw.statusCode = statusCode\n}\n\nfunc (w *netHTTPResponseWriter) Write(p []byte) (int, error) {\n\tw.body = append(w.body, p...)\n\treturn len(p), nil\n}\n\nfunc (w *netHTTPResponseWriter) Flush() {}\n\nfunc Test_ConvertRequest(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"successful conversion\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\thttpReq, err := ConvertRequest(c, false)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn c.SendString(\"Request URL: \" + httpReq.URL.String())\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test?hello=world&another=test\", http.NoBody))\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\trequire.Equal(t, http.StatusOK, resp.StatusCode, \"Status code\")\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"Request URL: /test?hello=world&another=test\", string(body))\n\t})\n\n\tt.Run(\"conversion error handling\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Test error case by creating a context with an invalid URL that will cause fasthttpadaptor.ConvertRequest to fail\n\t\tapp := fiber.New()\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// Create a malformed request URI that should cause conversion to fail\n\t\tctx.Request().SetRequestURI(\"http://[::1:bad:url\") // Invalid URL format\n\t\tctx.Request().Header.SetMethod(fiber.MethodGet)\n\n\t\t_, err := ConvertRequest(ctx, true) // Use forServer=true which does more validation\n\t\tif err == nil {\n\t\t\t// If the above doesn't fail, try a different approach\n\t\t\tctx.Request().SetRequestURI(\"\\x00\\x01\\x02\") // Invalid characters in URI\n\t\t\t_, err = ConvertRequest(ctx, true)\n\t\t}\n\t\t// Note: This test may pass if fasthttpadaptor is very permissive\n\t\t// The important thing is that our function doesn't panic\n\t\tif err != nil {\n\t\t\trequire.Error(t, err, \"Expected error from fasthttpadaptor.ConvertRequest\")\n\t\t}\n\t})\n}\n\nfunc Test_CopyContextToFiberContext(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"unsupported context type\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Test with non-struct context (should return early)\n\t\tvar fctx fasthttp.RequestCtx\n\t\tstringContext := \"not a struct\"\n\n\t\t// This should not panic and should handle the non-struct gracefully\n\t\tCopyContextToFiberContext(&stringContext, &fctx)\n\t\t// No assertions needed - just ensuring it doesn't panic\n\t})\n\n\tt.Run(\"context with unknown field\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Test the default case (continue statement coverage)\n\t\ttype customContext struct {\n\t\t\tUnknownField string\n\t\t}\n\n\t\tvar fctx fasthttp.RequestCtx\n\t\tctx := customContext{UnknownField: \"test\"}\n\n\t\t// This should hit the default case and continue\n\t\tCopyContextToFiberContext(&ctx, &fctx)\n\t\t// No assertions needed - just ensuring it doesn't panic and continues\n\t})\n\n\tt.Run(\"invalid src\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar fctx fasthttp.RequestCtx\n\t\tCopyContextToFiberContext(nil, &fctx)\n\t\t// Add assertion to ensure no panic and coverage is detected\n\t\tassert.NotNil(t, &fctx)\n\t})\n\n\tt.Run(\"nil request context\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tctx := context.WithValue(context.Background(), contextKey(\"nil-request-context\"), \"value\")\n\t\trequire.NotPanics(t, func() {\n\t\t\tCopyContextToFiberContext(ctx, nil)\n\t\t})\n\t})\n\n\tt.Run(\"nil pointer\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar nilPtr *context.Context // Nil pointer to a context\n\t\tvar fctx fasthttp.RequestCtx\n\t\tCopyContextToFiberContext(nilPtr, &fctx)\n\t\t// Add assertion to ensure no panic and coverage is detected\n\t\tassert.NotNil(t, &fctx)\n\t})\n\n\tt.Run(\"copies key value pairs\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar fctx fasthttp.RequestCtx\n\t\tkey := contextKey(\"copy-key\")\n\t\texpectedValue := \"copy-value\"\n\t\tctx := context.WithValue(context.Background(), key, expectedValue)\n\n\t\tCopyContextToFiberContext(ctx, &fctx)\n\n\t\trequire.Equal(t, expectedValue, fctx.UserValue(key))\n\t})\n\n\tt.Run(\"nested context wrappers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar fctx fasthttp.RequestCtx\n\t\tkeyA := contextKey(\"nested-a\")\n\t\tkeyB := contextKey(\"nested-b\")\n\t\tbaseCtx := context.WithValue(context.Background(), keyA, \"value-a\")\n\t\tcancelCtx, cancel := context.WithCancel(baseCtx)\n\t\tt.Cleanup(cancel)\n\t\twrappedCtx := context.WithValue(cancelCtx, keyB, \"value-b\")\n\n\t\tCopyContextToFiberContext(wrappedCtx, &fctx)\n\n\t\trequire.Equal(t, \"value-a\", fctx.UserValue(keyA))\n\t\trequire.Equal(t, \"value-b\", fctx.UserValue(keyB))\n\t})\n\n\tt.Run(\"multi-level pointer\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar fctx fasthttp.RequestCtx\n\t\tctx := context.Background()\n\t\tptr := &ctx\n\t\tdoublePtr := &ptr\n\t\t// Test deref pointer chains\n\t\tCopyContextToFiberContext(doublePtr, &fctx)\n\t\t// No assertions needed - just ensuring it doesn't panic\n\t})\n\n\tt.Run(\"non-addressable struct\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar fctx fasthttp.RequestCtx\n\t\ttype testStruct struct {\n\t\t\tField string\n\t\t}\n\t\t// Pass struct value directly to test addressability check\n\t\tCopyContextToFiberContext(testStruct{Field: \"test\"}, &fctx)\n\t\t// No assertions needed - just ensuring it doesn't panic and creates temporary\n\t})\n}\n\nfunc Test_HTTPMiddleware_ErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\t// Test middleware that returns an error from HTTPHandler\n\terrorMiddleware := func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t// This will cause an error in the underlying handler\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\tnext.ServeHTTP(w, r)\n\t\t})\n\t}\n\n\tfiberHandler := func(c fiber.Ctx) error {\n\t\treturn fiber.NewError(fiber.StatusBadRequest, \"test error\")\n\t}\n\n\tapp := fiber.New()\n\tapp.Use(HTTPMiddleware(errorMiddleware))\n\tapp.Get(\"/error\", fiberHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/error\", http.NoBody))\n\trequire.NoError(t, err)\n\t// The error should be handled by the error handler\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_FiberHandler_IOError(t *testing.T) {\n\tt.Parallel()\n\n\t// Test io.Copy error by using a failing reader\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"should not reach here\")\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\t// Create a reader that fails\n\tfailingReader := &failingReader{}\n\n\tr := &http.Request{\n\t\tMethod:        http.MethodPost,\n\t\tRequestURI:    \"/test\",\n\t\tBody:          failingReader,\n\t\tContentLength: 100, // Set content length so it tries to read\n\t\tHeader:        make(http.Header),\n\t}\n\n\tw := &netHTTPResponseWriter{}\n\thandlerFunc.ServeHTTP(w, r)\n\n\t// Should return 500 due to io.Copy error\n\trequire.Equal(t, http.StatusInternalServerError, w.StatusCode())\n}\n\nfunc Test_FiberHandler_WithErrorInHandler(t *testing.T) {\n\tt.Parallel()\n\n\t// Test error handling in fiber handler\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn fiber.NewError(fiber.StatusTeapot, \"I'm a teapot\")\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\tr := &http.Request{\n\t\tMethod:     http.MethodGet,\n\t\tRequestURI: \"/test\",\n\t\tHeader:     make(http.Header),\n\t\tBody:       http.NoBody,\n\t}\n\n\tw := &netHTTPResponseWriter{}\n\thandlerFunc.ServeHTTP(w, r)\n\n\t// Should return the error status\n\trequire.Equal(t, fiber.StatusTeapot, w.StatusCode())\n}\n\nfunc Test_FiberHandler_WithSendStreamWriter(t *testing.T) {\n\tt.Parallel()\n\n\t// Test streaming functionality in FiberHandler using SendStreamWriter.\n\tfiberH := func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusTeapot)\n\t\treturn c.SendStreamWriter(func(w *bufio.Writer) {\n\t\t\tw.WriteString(\"Hello \")            //nolint:errcheck // not needed\n\t\t\tw.Flush()                          //nolint:errcheck // not needed\n\t\t\ttime.Sleep(200 * time.Millisecond) // Simulate a long operation\n\t\t\tw.WriteString(\"World!\")            //nolint:errcheck // not needed\n\t\t})\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\tr := &http.Request{\n\t\tMethod:     http.MethodGet,\n\t\tRequestURI: \"/test\",\n\t\tHeader:     make(http.Header),\n\t\tBody:       http.NoBody,\n\t}\n\n\tw := &netHTTPResponseWriter{}\n\thandlerFunc.ServeHTTP(w, r)\n\n\t// Should return the error status\n\trequire.Equal(t, fiber.StatusTeapot, w.StatusCode())\n\trequire.Equal(t, \"Hello World!\", string(w.body))\n}\n\nfunc Test_FiberHandler_WithInterruptedSendStreamWriter(t *testing.T) {\n\tt.Parallel()\n\n\t// Test streaming functionality to ensure data is sent even during a timeout.\n\tfiberH := func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusTeapot)\n\t\treturn c.SendStreamWriter(func(w *bufio.Writer) {\n\t\t\tw.WriteString(\"Hello \")            //nolint:errcheck // not needed\n\t\t\tw.Flush()                          //nolint:errcheck // not needed\n\t\t\ttime.Sleep(500 * time.Millisecond) // Simulate a long operation\n\t\t\tw.WriteString(\"World!\")            //nolint:errcheck // not needed\n\t\t})\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\t// Start a mock HTTP server using the handlerFunc\n\tserver := &http.Server{\n\t\tHandler:      handlerFunc,\n\t\tReadTimeout:  5 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t}\n\tlistener, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\taddr := fmt.Sprintf(\"http://%s\", listener.Addr())\n\n\tgo func() {\n\t\tserver.Serve(listener) //nolint:errcheck // not needed\n\t}()\n\tdefer func() {\n\t\trequire.NoError(t, server.Close())\n\t}()\n\n\tcc := &http.Client{\n\t\tTimeout: 200 * time.Millisecond,\n\t}\n\tresp, err := cc.Get(addr) //nolint:noctx // ctx is not needed\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\tbody, readErr := io.ReadAll(resp.Body)\n\trequire.ErrorIs(t, readErr, context.DeadlineExceeded)\n\trequire.Equal(t, \"Hello \", string(body))\n}\n\n// failingReader always returns an error when Read is called\ntype failingReader struct{}\n\nfunc (f *failingReader) Read(p []byte) (int, error) {\n\treturn 0, errors.New(\"simulated read error\")\n}\n\nfunc (f *failingReader) Close() error {\n\treturn nil\n}\n\n// Benchmark for FiberHandlerFunc\nfunc Benchmark_FiberHandlerFunc(b *testing.B) {\n\tbenchmarks := []struct {\n\t\tname        string\n\t\tbodyContent []byte\n\t}{\n\t\t{\n\t\t\tname:        \"No Content\",\n\t\t\tbodyContent: nil, // No body content case\n\t\t},\n\t\t{\n\t\t\tname:        \"100KB\",\n\t\t\tbodyContent: make([]byte, 100*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"500KB\",\n\t\t\tbodyContent: make([]byte, 500*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"1MB\",\n\t\t\tbodyContent: make([]byte, 1*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"5MB\",\n\t\t\tbodyContent: make([]byte, 5*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"10MB\",\n\t\t\tbodyContent: make([]byte, 10*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"25MB\",\n\t\t\tbodyContent: make([]byte, 25*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"50MB\",\n\t\t\tbodyContent: make([]byte, 50*1024*1024),\n\t\t},\n\t}\n\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\tfor _, bm := range benchmarks {\n\t\tb.Run(bm.name, func(b *testing.B) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\tvar bodyBuffer *bytes.Buffer\n\n\t\t\t// Handle the \"No Content\" case where bodyContent is nil\n\t\t\tif bm.bodyContent != nil {\n\t\t\t\tbodyBuffer = bytes.NewBuffer(bm.bodyContent)\n\t\t\t} else {\n\t\t\t\tbodyBuffer = bytes.NewBuffer([]byte{}) // Empty buffer for no content\n\t\t\t}\n\n\t\t\tr := http.Request{\n\t\t\t\tMethod: http.MethodPost,\n\t\t\t\tBody:   nil,\n\t\t\t}\n\n\t\t\t// Replace the empty Body with our buffer\n\t\t\tr.Body = io.NopCloser(bodyBuffer)\n\t\t\tdefer r.Body.Close() //nolint:errcheck // not needed\n\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor b.Loop() {\n\t\t\t\thandlerFunc.ServeHTTP(w, &r)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Benchmark_FiberHandlerFunc_Parallel(b *testing.B) {\n\tbenchmarks := []struct {\n\t\tname        string\n\t\tbodyContent []byte\n\t}{\n\t\t{\n\t\t\tname:        \"No Content\",\n\t\t\tbodyContent: nil, // No body content case\n\t\t},\n\t\t{\n\t\t\tname:        \"100KB\",\n\t\t\tbodyContent: make([]byte, 100*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"500KB\",\n\t\t\tbodyContent: make([]byte, 500*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"1MB\",\n\t\t\tbodyContent: make([]byte, 1*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"5MB\",\n\t\t\tbodyContent: make([]byte, 5*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"10MB\",\n\t\t\tbodyContent: make([]byte, 10*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"25MB\",\n\t\t\tbodyContent: make([]byte, 25*1024*1024),\n\t\t},\n\t\t{\n\t\t\tname:        \"50MB\",\n\t\t\tbodyContent: make([]byte, 50*1024*1024),\n\t\t},\n\t}\n\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\tfor _, bm := range benchmarks {\n\t\tb.Run(bm.name, func(b *testing.B) {\n\t\t\tvar bodyBuffer *bytes.Buffer\n\n\t\t\t// Handle the \"No Content\" case where bodyContent is nil\n\t\t\tif bm.bodyContent != nil {\n\t\t\t\tbodyBuffer = bytes.NewBuffer(bm.bodyContent)\n\t\t\t} else {\n\t\t\t\tbodyBuffer = bytes.NewBuffer([]byte{}) // Empty buffer for no content\n\t\t\t}\n\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\tr := http.Request{\n\t\t\t\t\tMethod: http.MethodPost,\n\t\t\t\t\tBody:   nil,\n\t\t\t\t}\n\n\t\t\t\t// Replace the empty Body with our buffer\n\t\t\t\tr.Body = io.NopCloser(bodyBuffer)\n\t\t\t\tdefer r.Body.Close() //nolint:errcheck // not needed\n\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\thandlerFunc(w, &r)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc Benchmark_HTTPHandler(b *testing.B) {\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(\"ok\")) //nolint:errcheck // not needed\n\t})\n\n\tvar err error\n\tapp := fiber.New()\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer func() {\n\t\tapp.ReleaseCtx(ctx)\n\t}()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfiberHandler := HTTPHandler(handler)\n\n\tfor b.Loop() {\n\t\tctx.Request().Reset()\n\t\tctx.Response().Reset()\n\t\tctx.Request().SetRequestURI(\"/test\")\n\t\tctx.Request().Header.SetMethod(\"GET\")\n\n\t\terr = fiberHandler(ctx)\n\t}\n\n\trequire.NoError(b, err)\n}\n\nfunc Benchmark_HTTPHandlerWithContext(b *testing.B) {\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(\"ok\")) //nolint:errcheck // not needed\n\t})\n\n\tvar err error\n\tapp := fiber.New()\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tdefer func() {\n\t\tapp.ReleaseCtx(ctx)\n\t}()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\ttype key struct{}\n\tvar testKey key\n\tctx.SetContext(context.WithValue(ctx.Context(), testKey, \"gofiber\"))\n\n\tfiberHandler := HTTPHandlerWithContext(handler)\n\n\tfor b.Loop() {\n\t\tctx.Request().Reset()\n\t\tctx.Response().Reset()\n\t\tctx.Request().SetRequestURI(\"/test\")\n\t\tctx.Request().Header.SetMethod(\"GET\")\n\n\t\terr = fiberHandler(ctx)\n\t}\n\trequire.NoError(b, err)\n}\n\nfunc Test_resolveRemoteAddr(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpectedErr   error\n\t\tlocalAddr     any\n\t\tname          string\n\t\tremoteAddr    string\n\t\terrorContains string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:        \"valid TCP address with port\",\n\t\t\tremoteAddr:  \"192.168.1.1:8080\",\n\t\t\tlocalAddr:   nil,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"valid TCP address without port - should add default port 80\",\n\t\t\tremoteAddr:  \"192.168.1.1\",\n\t\t\tlocalAddr:   nil,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"unix socket - should return local addr\",\n\t\t\tremoteAddr:  \"irrelevant\",\n\t\t\tlocalAddr:   &net.UnixAddr{Name: \"/tmp/test.sock\", Net: \"unix\"},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid address - should fail\",\n\t\t\tremoteAddr:    \"[invalid:address:format\",\n\t\t\tlocalAddr:     nil,\n\t\t\texpectError:   true,\n\t\t\terrorContains: \"failed to resolve TCP address:\",\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid address after adding port - should fail\",\n\t\t\tremoteAddr:    \"[invalid\",\n\t\t\tlocalAddr:     nil,\n\t\t\texpectError:   true,\n\t\t\terrorContains: \"failed to resolve TCP address after adding port:\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty address - should fail\",\n\t\t\tremoteAddr:  \"\",\n\t\t\tlocalAddr:   nil,\n\t\t\texpectError: true,\n\t\t\texpectedErr: ErrRemoteAddrEmpty,\n\t\t},\n\t\t{\n\t\t\tname:        \"too long address - should fail\",\n\t\t\tremoteAddr:  strings.Repeat(\"a\", 254),\n\t\t\tlocalAddr:   nil,\n\t\t\texpectError: true,\n\t\t\texpectedErr: ErrRemoteAddrTooLong,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\taddr, err := resolveRemoteAddr(tt.remoteAddr, tt.localAddr)\n\n\t\t\texpectError := tt.expectedErr != nil || tt.errorContains != \"\"\n\t\t\tif expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tt.expectedErr != nil {\n\t\t\t\t\trequire.ErrorIs(t, err, tt.expectedErr)\n\t\t\t\t}\n\t\t\t\tif tt.errorContains != \"\" {\n\t\t\t\t\trequire.Contains(t, err.Error(), tt.errorContains)\n\t\t\t\t}\n\t\t\t\trequire.Nil(t, addr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, addr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_isUnixNetwork(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tnetwork  string\n\t\texpected bool\n\t}{\n\t\t{\"unix\", \"unix\", true},\n\t\t{\"unixgram\", \"unixgram\", true},\n\t\t{\"unixpacket\", \"unixpacket\", true},\n\t\t{\"tcp\", \"tcp\", false},\n\t\t{\"tcp4\", \"tcp4\", false},\n\t\t{\"tcp6\", \"tcp6\", false},\n\t\t{\"udp\", \"udp\", false},\n\t\t{\"empty\", \"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := isUnixNetwork(tt.network)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc Test_FiberHandler_ErrorFallback(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case where resolveRemoteAddr fails and falls back to nil\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"success\")\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\t// Use helper function for cleaner test setup\n\treq := createTestRequest(http.MethodGet, \"/test\", \"[invalid:address:format\", nil)\n\tw := executeHandlerTest(t, handlerFunc, req)\n\n\t// Should still work despite the invalid remote address\n\trequire.Equal(t, http.StatusOK, w.StatusCode())\n\trequire.Equal(t, \"success\", string(w.body))\n}\n\nfunc Test_FiberHandler_WithUnixSocket(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case where request has unix socket context\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"unix socket success\")\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\t// Create a context with unix socket local address\n\tunixAddr := &net.UnixAddr{Name: \"/tmp/test.sock\", Net: \"unix\"}\n\tctx := context.WithValue(context.Background(), http.LocalAddrContextKey, unixAddr)\n\n\tr := &http.Request{\n\t\tMethod:     http.MethodGet,\n\t\tRequestURI: \"/test\",\n\t\tRemoteAddr: \"someremoteaddr\", // This will be ignored due to unix socket\n\t\tHeader:     make(http.Header),\n\t\tBody:       http.NoBody,\n\t}\n\tr = r.WithContext(ctx)\n\n\tw := &netHTTPResponseWriter{}\n\thandlerFunc.ServeHTTP(w, r)\n\n\trequire.Equal(t, http.StatusOK, w.StatusCode())\n\trequire.Equal(t, \"unix socket success\", string(w.body))\n}\n\nfunc Test_FiberHandler_BodySizeLimit(t *testing.T) {\n\tt.Parallel()\n\n\t// Test body size limit enforcement\n\tfiberH := func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"processed\")\n\t}\n\thandlerFunc := FiberHandlerFunc(fiberH)\n\n\t// Create a large body exceeding limit\n\tlargeBody := make([]byte, 15*1024*1024) // 15MB > 10MB limit\n\treq := createTestRequest(http.MethodPost, \"/test\", \"127.0.0.1:8080\", bytes.NewReader(largeBody))\n\treq.ContentLength = int64(len(largeBody))\n\n\tw := executeHandlerTest(t, handlerFunc, req)\n\n\t// Should return 413 due to size limit\n\trequire.Equal(t, http.StatusRequestEntityTooLarge, w.StatusCode())\n}\n\nfunc Test_CopyContextToFiberContext_Safe(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"safe handling of unexported fields\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Test that unexported fields are handled safely\n\t\ttype testContext struct {\n\t\t\texportedField string\n\t\t\tunexported    string // unexported\n\t\t}\n\n\t\tvar fctx fasthttp.RequestCtx\n\t\tctx := testContext{exportedField: \"exported\", unexported: \"unexported\"}\n\n\t\t// Should not panic and handle safely\n\t\tCopyContextToFiberContext(&ctx, &fctx)\n\t\t// No specific assertion, just ensure no panic\n\t})\n}\n\nfunc TestUnixSocketAdaptor(t *testing.T) {\n\tdir := t.TempDir()\n\tsocketPath := filepath.Join(dir, \"test.sock\")\n\tdefer func() {\n\t\tif err := os.Remove(socketPath); err != nil {\n\t\t\tt.Logf(\"cleanup failed: %v\", err)\n\t\t}\n\t}()\n\n\tapp := fiber.New()\n\tapp.Get(\"/hello\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\thandler := FiberApp(app)\n\n\tlistener, err := net.Listen(\"unix\", socketPath)\n\tif err != nil {\n\t\t// Skip on platforms where the \"unix\" network is unsupported\n\t\tif strings.Contains(err.Error(), \"unknown network\") ||\n\t\t\tstrings.Contains(err.Error(), \"address family not supported\") {\n\t\t\tt.Skipf(\"Unix domain sockets not supported on this platform: %v\", err)\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tif closeErr := listener.Close(); closeErr != nil {\n\t\t\tt.Logf(\"listener close failed: %v\", closeErr)\n\t\t}\n\t}()\n\n\t// start server with timeouts\n\tsrv := &http.Server{\n\t\tHandler:      handler,\n\t\tReadTimeout:  5 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t}\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tif serveErr := srv.Serve(listener); serveErr != nil && serveErr != http.ErrServerClosed {\n\t\t\tt.Errorf(\"http server failed: %v\", serveErr)\n\t\t}\n\t\tclose(done)\n\t}()\n\n\tconn, err := net.Dial(\"unix\", socketPath)\n\trequire.NoError(t, err)\n\tdefer func() {\n\t\tif closeErr := conn.Close(); closeErr != nil {\n\t\t\tt.Logf(\"conn close failed: %v\", closeErr)\n\t\t}\n\t}()\n\n\t// set deadline for both write + read (2s)\n\trequire.NoError(t, conn.SetDeadline(time.Now().Add(2*time.Second)))\n\n\t// write request\n\t_, err = conn.Write([]byte(\"GET /hello HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\"))\n\trequire.NoError(t, err)\n\n\t// read response\n\tbuf := make([]byte, 1024)\n\tn, err := conn.Read(buf)\n\trequire.NoError(t, err)\n\n\t// clear deadline to avoid affecting further calls\n\trequire.NoError(t, conn.SetDeadline(time.Time{}))\n\n\traw := string(buf[:n])\n\tt.Logf(\"Raw response:\\n%s\", raw)\n\trequire.Contains(t, raw, \"HTTP/1.1 200 OK\")\n\trequire.Contains(t, raw, \"ok\")\n\n\t// now shutdown the server explicitly before waiting for done\n\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\tdefer cancel()\n\trequire.NoError(t, srv.Shutdown(ctx))\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"server shutdown timed out\")\n\t}\n}\n"
  },
  {
    "path": "middleware/basicauth/basicauth.go",
    "content": "package basicauth\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"golang.org/x/text/unicode/norm\"\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\n// The key for the username value stored in the context\nconst (\n\tusernameKey contextKey = iota\n)\n\nconst basicScheme = \"Basic\"\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\tvar cerr base64.CorruptInputError\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Get authorization header and ensure it matches the Basic scheme\n\t\trawAuth := c.Get(fiber.HeaderAuthorization)\n\t\tif rawAuth == \"\" {\n\t\t\treturn cfg.Unauthorized(c)\n\t\t}\n\t\tif len(rawAuth) > cfg.HeaderLimit {\n\t\t\treturn c.SendStatus(fiber.StatusRequestHeaderFieldsTooLarge)\n\t\t}\n\t\tif containsInvalidHeaderChars(rawAuth) {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\t\tauth := utils.TrimSpace(rawAuth)\n\t\tif auth == \"\" {\n\t\t\treturn cfg.Unauthorized(c)\n\t\t}\n\t\tif len(auth) < len(basicScheme) || !utils.EqualFold(auth[:len(basicScheme)], basicScheme) {\n\t\t\treturn cfg.Unauthorized(c)\n\t\t}\n\t\trest := auth[len(basicScheme):]\n\t\tif len(rest) < 2 || rest[0] != ' ' || rest[1] == ' ' {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\t\trest = rest[1:]\n\t\tif strings.IndexFunc(rest, unicode.IsSpace) != -1 {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\n\t\t// Decode the header contents\n\t\traw, err := base64.StdEncoding.DecodeString(rest)\n\t\tif err != nil {\n\t\t\tif errors.As(err, &cerr) {\n\t\t\t\traw, err = base64.RawStdEncoding.DecodeString(rest)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn cfg.BadRequest(c)\n\t\t\t}\n\t\t}\n\t\tif !utf8.Valid(raw) {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\t\tif !norm.NFC.IsNormal(raw) {\n\t\t\traw = norm.NFC.Bytes(raw)\n\t\t}\n\n\t\t// Get the credentials\n\t\tvar creds string\n\t\tif c.App().Config().Immutable {\n\t\t\tcreds = string(raw)\n\t\t} else {\n\t\t\tcreds = utils.UnsafeString(raw)\n\t\t}\n\n\t\t// Check if the credentials are in the correct form\n\t\t// which is \"username:password\".\n\t\tusername, password, found := strings.Cut(creds, \":\")\n\t\tif !found {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\n\t\tif containsCTL(username) || containsCTL(password) {\n\t\t\treturn cfg.BadRequest(c)\n\t\t}\n\n\t\tif cfg.Authorizer(username, password, c) {\n\t\t\tfiber.StoreInContext(c, usernameKey, username)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Authentication failed\n\t\treturn cfg.Unauthorized(c)\n\t}\n}\n\nfunc containsCTL(s string) bool {\n\treturn strings.IndexFunc(s, unicode.IsControl) != -1\n}\n\nfunc containsInvalidHeaderChars(s string) bool {\n\treturn strings.IndexFunc(s, func(r rune) bool {\n\t\treturn (r < 0x20 && r != '\\t') || r == 0x7F || r >= 0x80\n\t}) != -1\n}\n\n// UsernameFromContext returns the username found in the context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// It returns an empty string if the username does not exist.\nfunc UsernameFromContext(ctx any) string {\n\tif username, ok := fiber.ValueFromContext[string](ctx, usernameKey); ok {\n\t\treturn username\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "middleware/basicauth/basicauth_test.go",
    "content": "package basicauth\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nfunc sha256Hash(p string) string {\n\tsum := sha256.Sum256([]byte(p))\n\treturn \"{SHA256}\" + base64.StdEncoding.EncodeToString(sum[:])\n}\n\nfunc sha512Hash(p string) string {\n\tsum := sha512.Sum512([]byte(p))\n\treturn \"{SHA512}\" + base64.StdEncoding.EncodeToString(sum[:])\n}\n\n// go test -run Test_BasicAuth_Next\nfunc Test_BasicAuth_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Middleware_BasicAuth(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\thashedAdmin, err := bcrypt.GenerateFromPassword([]byte(\"123456\"), bcrypt.MinCost)\n\trequire.NoError(t, err)\n\n\tapp.Use(New(Config{\n\t\tUsers: map[string]string{\n\t\t\t\"john\":  hashedJohn,\n\t\t\t\"admin\": string(hashedAdmin),\n\t\t},\n\t}))\n\n\tapp.Get(\"/testauth\", func(c fiber.Ctx) error {\n\t\tusername := UsernameFromContext(c)\n\t\treturn c.SendString(username)\n\t})\n\n\ttests := []struct {\n\t\turl        string\n\t\tusername   string\n\t\tpassword   string\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\turl:        \"/testauth\",\n\t\t\tstatusCode: 200,\n\t\t\tusername:   \"john\",\n\t\t\tpassword:   \"doe\",\n\t\t},\n\t\t{\n\t\t\turl:        \"/testauth\",\n\t\t\tstatusCode: 200,\n\t\t\tusername:   \"admin\",\n\t\t\tpassword:   \"123456\",\n\t\t},\n\t\t{\n\t\t\turl:        \"/testauth\",\n\t\t\tstatusCode: 401,\n\t\t\tusername:   \"ee\",\n\t\t\tpassword:   \"123456\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\t// Base64 encode credentials for http auth header\n\t\tcreds := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, \"%s:%s\", tt.username, tt.password))\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/testauth\", http.NoBody)\n\t\treq.Header.Add(\"Authorization\", \"Basic \"+creds)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tt.statusCode, resp.StatusCode)\n\n\t\tif tt.statusCode == 200 {\n\t\t\trequire.Equal(t, tt.username, string(body))\n\t\t}\n\t}\n}\n\nfunc Test_BasicAuth_UsernameFromContext_Types(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New(Config{\n\t\tUsers: map[string]string{\n\t\t\t\"john\": sha256Hash(\"doe\"),\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\trequire.Equal(t, \"john\", UsernameFromContext(c))\n\t\tcustomCtx, ok := c.(fiber.CustomCtx)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, \"john\", UsernameFromContext(customCtx))\n\t\trequire.Equal(t, \"john\", UsernameFromContext(c.RequestCtx()))\n\t\trequire.Equal(t, \"john\", UsernameFromContext(c.Context()))\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_BasicAuth_AuthorizerCtx(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tcalled := false\n\tapp.Use(New(Config{\n\t\tAuthorizer: func(user, pass string, c fiber.Ctx) bool {\n\t\t\tcalled = true\n\t\t\trequire.Equal(t, \"john\", user)\n\t\t\trequire.Equal(t, \"doe\", pass)\n\t\t\trequire.Equal(t, \"/ctx\", c.Path())\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tapp.Get(\"/ctx\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) })\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\treq := httptest.NewRequest(fiber.MethodGet, \"/ctx\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.True(t, called)\n}\n\nfunc Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\trequire.Equal(t, `Basic realm=\"Restricted\", charset=\"UTF-8\"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_BasicAuth_WWWAuthenticateHeader_UTF8(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}, Charset: \"utf-8\"}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\trequire.Equal(t, `Basic realm=\"Restricted\", charset=\"UTF-8\"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_BasicAuth_InvalidHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic notbase64\")\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_BasicAuth_MissingScheme(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\trequire.Equal(t, `Basic realm=\"Restricted\", charset=\"UTF-8\"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_BasicAuth_MissingColon(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john\"))\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_BasicAuth_EmptyAuthorization(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\tcases := []string{\"\", \"   \"}\n\tfor _, h := range cases {\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, h)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\t}\n}\n\nfunc Test_BasicAuth_HeaderWhitespace(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\n\tcases := []struct {\n\t\theader string\n\t\tstatus int\n\t}{\n\t\t{\"Basic \" + creds, fiber.StatusTeapot},\n\t\t{\" Basic \" + creds, fiber.StatusTeapot},\n\t\t{\"Basic  \" + creds, fiber.StatusBadRequest},\n\t\t{\"Basic   \" + creds, fiber.StatusBadRequest},\n\t\t{\"Basic\\t\" + creds, fiber.StatusBadRequest},\n\t\t{\"Basic \\t\" + creds, fiber.StatusBadRequest},\n\t\t{\"Basic\\u00A0\" + creds, fiber.StatusBadRequest},\n\t\t{\"Basic\\u3000\" + creds, fiber.StatusBadRequest},\n\t\t{\"\\tBasic \" + creds + \"\\t\", fiber.StatusTeapot},\n\t\t{\"Basic \" + creds[:4] + \" \" + creds[4:], fiber.StatusBadRequest},\n\t}\n\n\tfor _, tt := range cases {\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, tt.header)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tt.status, resp.StatusCode)\n\t}\n}\n\nfunc Test_BasicAuth_ControlChars(t *testing.T) {\n\tt.Parallel()\n\tcalled := false\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tAuthorizer: func(_, _ string, _ fiber.Ctx) bool {\n\t\t\tcalled = true\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tcreds := []string{\n\t\tbase64.StdEncoding.EncodeToString([]byte(\"john:\\x01doe\")),\n\t\tbase64.StdEncoding.EncodeToString([]byte(\"jo\\x7Fhn:doe\")),\n\t\tbase64.StdEncoding.EncodeToString([]byte{'j', 'o', 'h', 'n', ':', 0x85, 'd', 'o', 'e'}),\n\t\tbase64.StdEncoding.EncodeToString([]byte{'j', 'o', 'h', 'n', ':', 0x9F, 'd', 'o', 'e'}),\n\t}\n\n\tfor _, c := range creds {\n\t\tcalled = false\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+c)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\t\trequire.Empty(t, resp.Header.Get(fiber.HeaderWWWAuthenticate))\n\t\trequire.False(t, called)\n\t}\n}\n\nfunc Test_BasicAuth_UnpaddedBase64(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\tcreds = strings.TrimRight(creds, \"=\")\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\nfunc Test_BasicAuth_NonASCIIHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\thandler := app.Handler()\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.SetRequestURI(\"/\")\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.Header.SetBytesKV([]byte(fiber.HeaderAuthorization), []byte(\"Basic \\x80\"+creds))\n\thandler(fctx)\n\trequire.Equal(t, fiber.StatusBadRequest, fctx.Response.StatusCode())\n}\n\nfunc Test_BasicAuth_InvalidUTF8(t *testing.T) {\n\tt.Parallel()\n\tcalled := false\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tCharset: \"UTF-8\",\n\t\tAuthorizer: func(_, _ string, _ fiber.Ctx) bool {\n\t\t\tcalled = true\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte{'j', 'o', 'h', 'n', ':', 0xff, 'd', 'o', 'e'})\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\trequire.False(t, called)\n}\n\nfunc Test_BasicAuth_UTF8Normalization(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tdecomposed := \"e\\u0301\" // e + combining acute accent\n\tcalled := false\n\tapp.Use(New(Config{\n\t\tCharset: \"UTF-8\",\n\t\tAuthorizer: func(u, p string, _ fiber.Ctx) bool {\n\t\t\tcalled = true\n\t\t\trequire.Equal(t, \"é\", u)\n\t\t\trequire.Equal(t, \"doe\", p)\n\t\t\treturn true\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(decomposed + \":doe\"))\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.True(t, called)\n}\n\nfunc Test_BasicAuth_HeaderControlCharEdges(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\n\thandler := app.Handler()\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\theaders := [][]byte{\n\t\t[]byte(\"\\rBasic \" + creds),\n\t\t[]byte(\"\\nBasic \" + creds),\n\t\t[]byte(\"Basic \" + creds + \"\\r\"),\n\t\t[]byte(\"Basic \" + creds + \"\\n\"),\n\t}\n\n\tfor _, h := range headers {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.SetRequestURI(\"/\")\n\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\tfctx.Request.Header.SetBytesKV([]byte(fiber.HeaderAuthorization), h)\n\t\thandler(fctx)\n\t\trequire.Equal(t, fiber.StatusBadRequest, fctx.Response.StatusCode())\n\t}\n}\n\nfunc Test_BasicAuth_Charset(t *testing.T) {\n\tt.Parallel()\n\trequire.Panics(t, func() { New(Config{Charset: \"ISO-8859-1\"}) })\n\trequire.NotPanics(t, func() { New(Config{Charset: \"utf-8\"}) })\n\trequire.NotPanics(t, func() { New(Config{Charset: \"UTF-8\"}) })\n\trequire.NotPanics(t, func() { New(Config{}) })\n}\n\nfunc Test_BasicAuth_HeaderLimit(t *testing.T) {\n\tt.Parallel()\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\thashedJohn := sha256Hash(\"doe\")\n\n\tt.Run(\"too large\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}, HeaderLimit: 10}))\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusRequestHeaderFieldsTooLarge, resp.StatusCode)\n\t})\n\n\tt.Run(\"allowed\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}, HeaderLimit: 100}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Middleware_BasicAuth -benchmem -count=4\nfunc Benchmark_Middleware_BasicAuth(b *testing.B) {\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\n\tapp.Use(New(Config{\n\t\tUsers: map[string]string{\n\t\t\t\"john\": hashedJohn,\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\tfctx.Request.Header.Set(fiber.HeaderAuthorization, \"basic am9objpkb2U=\") // john:doe\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Middleware_BasicAuth -benchmem -count=4\nfunc Benchmark_Middleware_BasicAuth_Upper(b *testing.B) {\n\tapp := fiber.New()\n\n\thashedJohn := sha256Hash(\"doe\")\n\n\tapp.Use(New(Config{\n\t\tUsers: map[string]string{\n\t\t\t\"john\": hashedJohn,\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\tfctx.Request.Header.Set(fiber.HeaderAuthorization, \"Basic am9objpkb2U=\") // john:doe\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n}\n\nfunc Test_BasicAuth_Immutable(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New(fiber.Config{Immutable: true})\n\n\thashedJohn := sha256Hash(\"doe\")\n\tapp.Use(New(Config{Users: map[string]string{\"john\": hashedJohn}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:doe\"))\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\nfunc Test_parseHashedPassword(t *testing.T) {\n\tt.Parallel()\n\tpass := \"secret\"\n\tsha := sha256.Sum256([]byte(pass))\n\tb64 := base64.StdEncoding.EncodeToString(sha[:])\n\thexDigest := hex.EncodeToString(sha[:])\n\tbcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)\n\trequire.NoError(t, err)\n\n\tcases := []struct {\n\t\tname   string\n\t\thashed string\n\t}{\n\t\t{\"bcrypt\", string(bcryptHash)},\n\t\t{\"sha512\", sha512Hash(pass)},\n\t\t{\"sha256\", sha256Hash(pass)},\n\t\t{\"sha256-hex\", hexDigest},\n\t\t{\"sha256-b64\", b64},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tverify, err := parseHashedPassword(tt.hashed)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.True(t, verify(pass))\n\t\t\trequire.False(t, verify(\"wrong\"))\n\t\t})\n\t}\n}\n\nfunc Test_BasicAuth_HashVariants(t *testing.T) {\n\tt.Parallel()\n\tpass := \"doe\"\n\tbcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)\n\trequire.NoError(t, err)\n\tcases := []struct {\n\t\tname   string\n\t\thashed string\n\t}{\n\t\t{\"bcrypt\", string(bcryptHash)},\n\t\t{\"sha512\", sha512Hash(pass)},\n\t\t{\"sha256\", sha256Hash(pass)},\n\t\t{\"sha256-hex\", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},\n\t}\n\n\tfor _, tt := range cases {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Users: map[string]string{\"john\": tt.hashed}}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\n\t\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:\" + pass))\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\t}\n}\n\nfunc Test_BasicAuth_HashVariants_Invalid(t *testing.T) {\n\tt.Parallel()\n\tpass := \"doe\"\n\twrong := \"wrong\"\n\tbcryptHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.MinCost)\n\trequire.NoError(t, err)\n\tcases := []struct {\n\t\tname   string\n\t\thashed string\n\t}{\n\t\t{\"bcrypt\", string(bcryptHash)},\n\t\t{\"sha512\", sha512Hash(pass)},\n\t\t{\"sha256\", sha256Hash(pass)},\n\t\t{\"sha256-hex\", func() string { h := sha256.Sum256([]byte(pass)); return hex.EncodeToString(h[:]) }()},\n\t}\n\n\tfor _, tt := range cases {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Users: map[string]string{\"john\": tt.hashed}}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })\n\n\t\tcreds := base64.StdEncoding.EncodeToString([]byte(\"john:\" + wrong))\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Basic \"+creds)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\t}\n}\n"
  },
  {
    "path": "middleware/basicauth/config.go",
    "content": "package basicauth\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"crypto/subtle\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"golang.org/x/crypto/bcrypt\"\n)\n\nvar ErrInvalidSHA256PasswordLength = errors.New(\"decode SHA256 password: invalid length\")\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Users defines the allowed credentials\n\t//\n\t// Required. Default: map[string]string{}\n\tUsers map[string]string\n\n\t// Authorizer defines a function you can pass\n\t// to check the credentials however you want.\n\t// It will be called with a username, password and\n\t// the current fiber context and is expected to return\n\t// true or false to indicate that the credentials were\n\t// approved or not.\n\t//\n\t// Optional. Default: nil.\n\tAuthorizer func(string, string, fiber.Ctx) bool\n\n\t// Unauthorized defines the response body for unauthorized responses.\n\t// By default it will return with a 401 Unauthorized and the correct WWW-Auth header\n\t//\n\t// Optional. Default: nil\n\tUnauthorized fiber.Handler\n\n\t// BadRequest defines the response body for malformed Authorization headers.\n\t// By default it will return with a 400 Bad Request without the WWW-Authenticate header.\n\t//\n\t// Optional. Default: nil\n\tBadRequest fiber.Handler\n\n\t// Realm is a string to define realm attribute of BasicAuth.\n\t// the realm identifies the system to authenticate against\n\t// and can be used by clients to save credentials\n\t//\n\t// Optional. Default: \"Restricted\".\n\tRealm string\n\n\t// Charset defines the value for the charset parameter in the\n\t// WWW-Authenticate header. According to RFC 7617 clients can use\n\t// this value to interpret credentials correctly. Only the value\n\t// \"UTF-8\" is allowed; any other value will panic.\n\t//\n\t// Optional. Default: \"UTF-8\".\n\tCharset string\n\n\t// HeaderLimit specifies the maximum allowed length of the\n\t// Authorization header. Requests exceeding this limit will\n\t// be rejected.\n\t//\n\t// Optional. Default: 8192.\n\tHeaderLimit int\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:         nil,\n\tUsers:        map[string]string{},\n\tRealm:        \"Restricted\",\n\tCharset:      \"UTF-8\",\n\tHeaderLimit:  8192,\n\tAuthorizer:   nil,\n\tUnauthorized: nil,\n\tBadRequest:   nil,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\n\tif cfg.Users == nil {\n\t\tcfg.Users = ConfigDefault.Users\n\t}\n\n\tif cfg.Realm == \"\" {\n\t\tcfg.Realm = ConfigDefault.Realm\n\t}\n\n\tswitch {\n\tcase cfg.Charset == \"\":\n\t\tcfg.Charset = ConfigDefault.Charset\n\tcase utils.EqualFold(cfg.Charset, \"UTF-8\"):\n\t\tcfg.Charset = \"UTF-8\"\n\tdefault:\n\t\tpanic(\"basicauth: charset must be UTF-8\")\n\t}\n\n\tif cfg.HeaderLimit <= 0 {\n\t\tcfg.HeaderLimit = ConfigDefault.HeaderLimit\n\t}\n\n\tif cfg.Authorizer == nil {\n\t\tverifiers := make(map[string]func(string) bool, len(cfg.Users))\n\t\tfor u, hpw := range cfg.Users {\n\t\t\tv, err := parseHashedPassword(hpw)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tverifiers[u] = v\n\t\t}\n\t\tcfg.Authorizer = func(user, pass string, _ fiber.Ctx) bool {\n\t\t\tverify, ok := verifiers[user]\n\t\t\treturn ok && verify(pass)\n\t\t}\n\t}\n\n\tif cfg.Unauthorized == nil {\n\t\tcfg.Unauthorized = func(c fiber.Ctx) error {\n\t\t\theader := \"Basic realm=\" + strconv.Quote(cfg.Realm)\n\t\t\tif cfg.Charset != \"\" {\n\t\t\t\theader += \", charset=\" + strconv.Quote(cfg.Charset)\n\t\t\t}\n\t\t\tc.Set(fiber.HeaderWWWAuthenticate, header)\n\t\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\t\tc.Set(fiber.HeaderVary, fiber.HeaderAuthorization)\n\t\t\treturn c.SendStatus(fiber.StatusUnauthorized)\n\t\t}\n\t}\n\n\tif cfg.BadRequest == nil {\n\t\tcfg.BadRequest = func(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusBadRequest)\n\t\t}\n\t}\n\treturn cfg\n}\n\nfunc parseHashedPassword(h string) (func(string) bool, error) {\n\tswitch {\n\tcase strings.HasPrefix(h, \"$2\"):\n\t\thash := []byte(h)\n\t\treturn func(p string) bool {\n\t\t\treturn bcrypt.CompareHashAndPassword(hash, []byte(p)) == nil\n\t\t}, nil\n\tcase strings.HasPrefix(h, \"{SHA512}\"):\n\t\tb, err := base64.StdEncoding.DecodeString(h[len(\"{SHA512}\"):])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode SHA512 password: %w\", err)\n\t\t}\n\t\treturn func(p string) bool {\n\t\t\tsum := sha512.Sum512([]byte(p))\n\t\t\treturn subtle.ConstantTimeCompare(sum[:], b) == 1\n\t\t}, nil\n\tcase strings.HasPrefix(h, \"{SHA256}\"):\n\t\tb, err := base64.StdEncoding.DecodeString(h[len(\"{SHA256}\"):])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode SHA256 password: %w\", err)\n\t\t}\n\t\treturn func(p string) bool {\n\t\t\tsum := sha256.Sum256([]byte(p))\n\t\t\treturn subtle.ConstantTimeCompare(sum[:], b) == 1\n\t\t}, nil\n\tdefault:\n\t\tb, err := hex.DecodeString(h)\n\t\tif err != nil || len(b) != sha256.Size {\n\t\t\tif b, err = base64.StdEncoding.DecodeString(h); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"decode SHA256 password: %w\", err)\n\t\t\t}\n\t\t\tif len(b) != sha256.Size {\n\t\t\t\treturn nil, ErrInvalidSHA256PasswordLength\n\t\t\t}\n\t\t}\n\t\treturn func(p string) bool {\n\t\t\tsum := sha256.Sum256([]byte(p))\n\t\t\treturn subtle.ConstantTimeCompare(sum[:], b) == 1\n\t\t}, nil\n\t}\n}\n"
  },
  {
    "path": "middleware/cache/cache.go",
    "content": "// Special thanks to @codemicro for moving this to fiber core\n// Original middleware: github.com/codemicro/fiber-cache\npackage cache\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// timestampUpdatePeriod is the period which is used to check the cache expiration.\n// It should not be too long to provide more or less acceptable expiration error, and in the same\n// time it should not be too short to avoid overwhelming of the system\nconst timestampUpdatePeriod = 300 * time.Millisecond\n\n// buffer size for hexpool\nconst hexLen = sha256.Size * 2\n\n// cache status\n// unreachable: when cache is bypass, or invalid\n// hit: cache is served\n// miss: do not have cache record\nconst (\n\tcacheUnreachable = \"unreachable\"\n\tcacheHit         = \"hit\"\n\tcacheMiss        = \"miss\"\n)\n\ntype expirationSource uint8\n\nconst (\n\texpirationSourceConfig expirationSource = iota\n\texpirationSourceMaxAge\n\texpirationSourceSMaxAge\n\texpirationSourceExpires\n\texpirationSourceGenerator\n)\n\n// directives\nconst (\n\tnoCache          = \"no-cache\"\n\tnoStore          = \"no-store\"\n\tprivateDirective = \"private\"\n)\n\ntype requestCacheDirectives struct {\n\tmaxAge   uint64\n\tmaxStale uint64\n\tminFresh uint64\n\n\tmaxAgeSet    bool\n\tmaxStaleSet  bool\n\tmaxStaleAny  bool\n\tminFreshSet  bool\n\tnoStore      bool\n\tnoCache      bool\n\tonlyIfCached bool\n}\n\nvar ignoreHeaders = map[string]struct{}{\n\t\"Age\":                 {},\n\t\"Cache-Control\":       {}, // already stored explicitly by the cache manager\n\t\"Connection\":          {},\n\t\"Content-Encoding\":    {}, // already stored explicitly by the cache manager\n\t\"Content-Type\":        {}, // already stored explicitly by the cache manager\n\t\"Date\":                {},\n\t\"ETag\":                {}, // already stored explicitly by the cache manager\n\t\"Expires\":             {}, // already stored explicitly by the cache manager\n\t\"Keep-Alive\":          {},\n\t\"Proxy-Authenticate\":  {},\n\t\"Proxy-Authorization\": {},\n\t\"TE\":                  {},\n\t\"Trailers\":            {},\n\t\"Transfer-Encoding\":   {},\n\t\"Upgrade\":             {},\n}\n\nvar cacheableStatusCodes = map[int]struct{}{\n\tfiber.StatusOK:                          {},\n\tfiber.StatusNonAuthoritativeInformation: {},\n\tfiber.StatusNoContent:                   {},\n\tfiber.StatusPartialContent:              {},\n\tfiber.StatusMultipleChoices:             {},\n\tfiber.StatusMovedPermanently:            {},\n\tfiber.StatusPermanentRedirect:           {},\n\tfiber.StatusNotFound:                    {},\n\tfiber.StatusMethodNotAllowed:            {},\n\tfiber.StatusGone:                        {},\n\tfiber.StatusRequestURITooLong:           {},\n\tfiber.StatusNotImplemented:              {},\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\ttype evictionCandidate struct {\n\t\tkey     string\n\t\tsize    uint\n\t\texp     uint64\n\t\theapIdx int\n\t}\n\n\tredactKeys := !cfg.DisableValueRedaction\n\n\tmaskKey := func(key string) string {\n\t\tif redactKeys {\n\t\t\treturn redactedKey\n\t\t}\n\t\treturn key\n\t}\n\n\t// Nothing to cache\n\tif int(cfg.Expiration.Seconds()) < 0 {\n\t\treturn func(c fiber.Ctx) error {\n\t\t\treturn c.Next()\n\t\t}\n\t}\n\n\tvar (\n\t\t// Cache settings\n\t\tmux       = &sync.RWMutex{}\n\t\ttimestamp = safeUnixSeconds(time.Now())\n\t)\n\t// Create manager to simplify storage operations ( see manager.go )\n\tmanager := newManager(cfg.Storage, redactKeys)\n\t// Create indexed heap for tracking expirations ( see heap.go )\n\theap := &indexedHeap{}\n\t// count stored bytes (sizes of response bodies)\n\tvar storedBytes uint\n\t// Pool for hex encoding buffers\n\thexBufPool := &sync.Pool{\n\t\tNew: func() any {\n\t\t\tbuf := make([]byte, hexLen)\n\t\t\treturn &buf\n\t\t},\n\t}\n\thashAuthorization := makeHashAuthFunc(hexBufPool)\n\tbuildVaryKey := makeBuildVaryKeyFunc(hexBufPool)\n\n\t// Update timestamp in the configured interval\n\tgo func() {\n\t\tticker := time.NewTicker(timestampUpdatePeriod)\n\t\tdefer ticker.Stop()\n\t\tfor range ticker.C {\n\t\t\tatomic.StoreUint64(&timestamp, safeUnixSeconds(time.Now()))\n\t\t}\n\t}()\n\n\t// Delete key from both manager and storage\n\tdeleteKey := func(ctx context.Context, dkey string) error {\n\t\tif err := manager.del(ctx, dkey); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// External storage saves body data with different key\n\t\tif cfg.Storage != nil {\n\t\t\tif err := manager.del(ctx, dkey+\"_body\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tremoveHeapEntry := func(entryKey string, heapIdx int) {\n\t\tif cfg.MaxBytes == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tif heapIdx < 0 || heapIdx >= len(heap.indices) {\n\t\t\treturn\n\t\t}\n\n\t\tindexedIdx := heap.indices[heapIdx]\n\t\tif indexedIdx < 0 || indexedIdx >= len(heap.entries) {\n\t\t\treturn\n\t\t}\n\n\t\tentry := heap.entries[indexedIdx]\n\t\tif entry.idx != heapIdx || entry.key != entryKey {\n\t\t\treturn\n\t\t}\n\n\t\t_, size := heap.remove(heapIdx)\n\t\tstoredBytes -= size\n\t}\n\n\trefreshHeapIndex := func(ctx context.Context, candidate evictionCandidate) error {\n\t\tentry, err := manager.get(ctx, candidate.key)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, errCacheMiss) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"cache: failed to reload key %q after eviction failure: %w\", maskKey(candidate.key), err)\n\t\t}\n\n\t\tentry.heapidx = candidate.heapIdx\n\n\t\tremainingTTL := max(time.Until(secondsToTime(entry.exp)), 0)\n\n\t\tif err := manager.set(ctx, candidate.key, entry, remainingTTL); err != nil {\n\t\t\treturn fmt.Errorf(\"cache: failed to restore heap index for key %q: %w\", maskKey(candidate.key), err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\thasAuthorization := len(c.Request().Header.Peek(fiber.HeaderAuthorization)) > 0\n\t\treqCacheControl := c.Request().Header.Peek(fiber.HeaderCacheControl)\n\t\treqDirectives := parseRequestCacheControl(reqCacheControl)\n\t\tif !reqDirectives.noCache {\n\t\t\treqPragma := utils.UnsafeString(c.Request().Header.Peek(fiber.HeaderPragma))\n\t\t\tif hasDirective(reqPragma, noCache) {\n\t\t\t\treqDirectives.noCache = true\n\t\t\t}\n\t\t}\n\n\t\t// Refrain from caching\n\t\tif reqDirectives.noStore {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\trequestMethod := c.Method()\n\n\t\t// Only cache selected methods\n\t\tif !slices.Contains(cfg.Methods, requestMethod) {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Get key from request\n\t\tbaseKey := cfg.KeyGenerator(c) + \"_\" + requestMethod\n\t\tmanifestKey := baseKey + \"|vary\"\n\t\tif hasAuthorization {\n\t\t\tauthHash := hashAuthorization(c.Request().Header.Peek(fiber.HeaderAuthorization))\n\t\t\tbaseKey += \"_auth_\" + authHash\n\t\t\tmanifestKey = baseKey + \"|vary\"\n\t\t}\n\t\tkey := baseKey\n\n\t\treqCtx := c.Context()\n\n\t\tvaryNames, hasVaryManifest, err := loadVaryManifest(reqCtx, manager, manifestKey)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(varyNames) > 0 {\n\t\t\tkey += buildVaryKey(varyNames, &c.Request().Header)\n\t\t}\n\n\t\t// Get entry from pool\n\t\te, err := manager.get(reqCtx, key)\n\t\tif err != nil && !errors.Is(err, errCacheMiss) {\n\t\t\treturn err\n\t\t}\n\t\tentryAge := uint64(0)\n\t\trevalidate := false\n\t\toldHeapIdx := -1 // Track old heap index for replacement during revalidation\n\n\t\thandleMinFresh := func(now uint64) {\n\t\t\tif e == nil || !reqDirectives.minFreshSet {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tremainingFreshness := remainingFreshness(e, now)\n\t\t\tif remainingFreshness < reqDirectives.minFresh {\n\t\t\t\trevalidate = true\n\t\t\t\toldHeapIdx = e.heapidx\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\te = nil\n\t\t\t}\n\t\t}\n\n\t\t// Lock entry\n\t\tmux.Lock()\n\t\tlocked := true\n\t\tunlock := func() {\n\t\t\tif locked {\n\t\t\t\tmux.Unlock()\n\t\t\t\tlocked = false\n\t\t\t}\n\t\t}\n\t\trelock := func() {\n\t\t\tif !locked {\n\t\t\t\tmux.Lock()\n\t\t\t\tlocked = true\n\t\t\t}\n\t\t}\n\t\t// Get timestamp\n\t\tts := atomic.LoadUint64(&timestamp)\n\n\t\t// Cache Entry found\n\t\tif e != nil {\n\t\t\tentryAge = cachedResponseAge(e, ts)\n\t\t\tif reqDirectives.maxAgeSet && (reqDirectives.maxAge == 0 || entryAge > reqDirectives.maxAge) {\n\t\t\t\trevalidate = true\n\t\t\t\toldHeapIdx = e.heapidx\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\te = nil\n\t\t\t}\n\n\t\t\thandleMinFresh(ts)\n\t\t}\n\n\t\tif e != nil && e.ttl == 0 && e.forceRevalidate {\n\t\t\trevalidate = true\n\t\t\toldHeapIdx = e.heapidx\n\t\t\tif cfg.Storage != nil {\n\t\t\t\tmanager.release(e)\n\t\t\t}\n\t\t\te = nil\n\t\t}\n\n\t\tif e != nil && e.ttl == 0 && e.exp != 0 && ts >= e.exp {\n\t\t\tunlock()\n\t\t\tif err := deleteKey(reqCtx, key); err != nil {\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"cache: failed to delete expired key %q: %w\", maskKey(key), err)\n\t\t\t}\n\t\t\trelock()\n\t\t\tremoveHeapEntry(key, e.heapidx)\n\t\t\tif cfg.Storage != nil {\n\t\t\t\tmanager.release(e)\n\t\t\t}\n\t\t\te = nil\n\t\t\tunlock()\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\tgoto continueRequest\n\t\t}\n\n\t\tif e != nil {\n\t\t\tentryHasPrivate := e != nil && e.private\n\t\t\tif !entryHasPrivate && cfg.StoreResponseHeaders && len(e.headers) > 0 {\n\t\t\t\tif cc, ok := lookupCachedHeader(e.headers, fiber.HeaderCacheControl); ok && hasDirective(utils.UnsafeString(cc), privateDirective) {\n\t\t\t\t\tentryHasPrivate = true\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestNoCache := reqDirectives.noCache\n\n\t\t\t// Invalidate cache if requested\n\t\t\tif cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) {\n\t\t\t\te.exp = ts - 1\n\t\t\t}\n\n\t\t\tentryHasExpiration := e != nil && e.exp != 0\n\t\t\tentryExpired := entryHasExpiration && ts >= e.exp\n\t\t\tstaleness := uint64(0)\n\t\t\tif entryExpired {\n\t\t\t\tstaleness = ts - e.exp\n\t\t\t}\n\t\t\tallowStale := entryExpired && (reqDirectives.maxStaleAny || (reqDirectives.maxStaleSet && staleness <= reqDirectives.maxStale))\n\n\t\t\tif entryExpired && e.revalidate {\n\t\t\t\trevalidate = true\n\t\t\t\toldHeapIdx = e.heapidx\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\te = nil\n\t\t\t}\n\n\t\t\thandleMinFresh(ts)\n\n\t\t\tif revalidate {\n\t\t\t\tunlock()\n\t\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\t\tif reqDirectives.onlyIfCached {\n\t\t\t\t\treturn c.SendStatus(fiber.StatusGatewayTimeout)\n\t\t\t\t}\n\t\t\t\tgoto continueRequest\n\t\t\t}\n\n\t\t\tservedStale := false\n\n\t\t\tswitch {\n\t\t\tcase entryExpired && !allowStale:\n\t\t\t\tunlock()\n\t\t\t\tif err := deleteKey(reqCtx, key); err != nil {\n\t\t\t\t\tif e != nil {\n\t\t\t\t\t\tmanager.release(e)\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Errorf(\"cache: failed to delete expired key %q: %w\", maskKey(key), err)\n\t\t\t\t}\n\t\t\t\trelock()\n\t\t\t\tidx := e.heapidx\n\t\t\t\tmanager.release(e)\n\t\t\t\tremoveHeapEntry(key, idx)\n\t\t\t\te = nil\n\t\t\tcase entryHasPrivate:\n\t\t\t\tunlock()\n\t\t\t\tif err := deleteKey(reqCtx, key); err != nil {\n\t\t\t\t\tif e != nil {\n\t\t\t\t\t\tmanager.release(e)\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Errorf(\"cache: failed to delete private response for key %q: %w\", maskKey(key), err)\n\t\t\t\t}\n\t\t\t\trelock()\n\t\t\t\tremoveHeapEntry(key, e.heapidx)\n\t\t\t\tif cfg.Storage != nil && e != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\te = nil\n\t\t\t\tunlock()\n\t\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\t\tif reqDirectives.onlyIfCached {\n\t\t\t\t\treturn c.SendStatus(fiber.StatusGatewayTimeout)\n\t\t\t\t}\n\t\t\t\treturn c.Next()\n\t\t\tcase entryHasExpiration && !requestNoCache:\n\t\t\t\tservedStale = entryExpired\n\t\t\t\tif hasAuthorization && !e.shareable {\n\t\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\t\tmanager.release(e)\n\t\t\t\t\t}\n\t\t\t\t\tunlock()\n\t\t\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\t\t\treturn c.Next()\n\t\t\t\t}\n\n\t\t\t\t// Separate body value to avoid msgp serialization\n\t\t\t\t// We can store raw bytes with Storage 👍\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tunlock()\n\t\t\t\t\trawBody, err := manager.getRaw(reqCtx, key+\"_body\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tmanager.release(e)\n\t\t\t\t\t\treturn cacheBodyFetchError(maskKey, key, err)\n\t\t\t\t\t}\n\t\t\t\t\te.body = rawBody\n\t\t\t\t} else {\n\t\t\t\t\tunlock()\n\t\t\t\t}\n\t\t\t\t// Set response headers from cache\n\t\t\t\tc.Response().SetBodyRaw(e.body)\n\t\t\t\tc.Response().SetStatusCode(e.status)\n\t\t\t\tc.Response().Header.SetContentTypeBytes(e.ctype)\n\t\t\t\tif len(e.cencoding) > 0 {\n\t\t\t\t\tc.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)\n\t\t\t\t}\n\t\t\t\tif len(e.cacheControl) > 0 {\n\t\t\t\t\tc.Response().Header.SetBytesV(fiber.HeaderCacheControl, e.cacheControl)\n\t\t\t\t}\n\t\t\t\tif len(e.expires) > 0 {\n\t\t\t\t\tc.Response().Header.SetBytesV(fiber.HeaderExpires, e.expires)\n\t\t\t\t}\n\t\t\t\tif len(e.etag) > 0 {\n\t\t\t\t\tc.Response().Header.SetBytesV(fiber.HeaderETag, e.etag)\n\t\t\t\t}\n\t\t\t\tclampedDate := clampDateSeconds(e.date, ts)\n\t\t\t\tdateValue := fasthttp.AppendHTTPDate(nil, secondsToTime(clampedDate))\n\t\t\t\tc.Response().Header.SetBytesV(fiber.HeaderDate, dateValue)\n\t\t\t\tfor i := range e.headers {\n\t\t\t\t\th := e.headers[i]\n\t\t\t\t\tc.Response().Header.SetBytesKV(h.key, h.value)\n\t\t\t\t}\n\t\t\t\t// Set Cache-Control header if not disabled and not already set\n\t\t\t\tif !cfg.DisableCacheControl && len(c.Response().Header.Peek(fiber.HeaderCacheControl)) == 0 {\n\t\t\t\t\tremaining := uint64(0)\n\t\t\t\t\tif e.exp > ts {\n\t\t\t\t\t\tremaining = e.exp - ts\n\t\t\t\t\t}\n\t\t\t\t\tmaxAge := utils.FormatUint(remaining)\n\t\t\t\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=\"+maxAge)\n\t\t\t\t}\n\n\t\t\t\tconst maxDeltaSeconds = uint64(math.MaxInt32)\n\t\t\t\tageSeconds := min(entryAge, maxDeltaSeconds)\n\n\t\t\t\t// RFC-compliant Age header (RFC 9111)\n\t\t\t\tage := utils.FormatUint(ageSeconds)\n\t\t\t\tc.Response().Header.Set(fiber.HeaderAge, age)\n\t\t\t\tappendWarningHeaders(&c.Response().Header, servedStale, isHeuristicFreshness(e, &cfg, entryAge))\n\n\t\t\t\tc.Set(cfg.CacheHeader, cacheHit)\n\n\t\t\t\t// release item allocated from storage\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\n\t\t\t\t// Return response\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\t// no cached response to serve\n\t\t\t}\n\t\t}\n\n\t\tif e == nil && revalidate {\n\t\t\tunlock()\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\tif reqDirectives.onlyIfCached {\n\t\t\t\treturn c.SendStatus(fiber.StatusGatewayTimeout)\n\t\t\t}\n\t\t\tgoto continueRequest\n\t\t}\n\n\t\tif e == nil && reqDirectives.onlyIfCached {\n\t\t\tunlock()\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn c.SendStatus(fiber.StatusGatewayTimeout)\n\t\t}\n\n\t\t// make sure we're not blocking concurrent requests - do unlock\n\t\tunlock()\n\n\tcontinueRequest:\n\t\t// Continue stack, return err to Fiber if exist\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcacheControlBytes := c.Response().Header.Peek(fiber.HeaderCacheControl)\n\t\trespCacheControl := parseResponseCacheControl(cacheControlBytes)\n\t\tvaryHeader := utils.UnsafeString(c.Response().Header.Peek(fiber.HeaderVary))\n\t\thasPrivate := respCacheControl.hasPrivate\n\t\thasNoCache := respCacheControl.hasNoCache\n\t\tvaryNames, varyHasStar := parseVary(varyHeader)\n\n\t\t// Respect server cache-control: no-store\n\t\tif respCacheControl.hasNoStore {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\tif hasPrivate || hasNoCache || varyHasStar {\n\t\t\tif e != nil {\n\t\t\t\tif err := deleteKey(reqCtx, key); err != nil {\n\t\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\t\tmanager.release(e)\n\t\t\t\t\t}\n\t\t\t\t\treturn fmt.Errorf(\"cache: failed to delete cached response for key %q: %w\", maskKey(key), err)\n\t\t\t\t}\n\t\t\t\tmux.Lock()\n\t\t\t\tremoveHeapEntry(key, e.heapidx)\n\t\t\t\tif cfg.Storage != nil {\n\t\t\t\t\tmanager.release(e)\n\t\t\t\t}\n\t\t\t\te = nil\n\t\t\t\tmux.Unlock()\n\t\t\t}\n\n\t\t\tif hasVaryManifest {\n\t\t\t\tif err := manager.del(reqCtx, manifestKey); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"cache: failed to delete stale vary manifest %q: %w\", maskKey(manifestKey), err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\tshouldStoreVaryManifest := len(varyNames) > 0\n\t\tif len(varyNames) > 0 {\n\t\t\tif key == baseKey {\n\t\t\t\tkey += buildVaryKey(varyNames, &c.Request().Header)\n\t\t\t}\n\t\t} else if hasVaryManifest {\n\t\t\tif err := manager.del(reqCtx, manifestKey); err != nil {\n\t\t\t\treturn fmt.Errorf(\"cache: failed to delete stale vary manifest %q: %w\", maskKey(manifestKey), err)\n\t\t\t}\n\t\t}\n\n\t\tisSharedCacheAllowed := allowsSharedCacheDirectives(respCacheControl)\n\t\tif hasAuthorization && !isSharedCacheAllowed {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\tsharedCacheMode := !hasAuthorization || isSharedCacheAllowed\n\n\t\t// Don't cache response if status code is not cacheable\n\t\tif _, ok := cacheableStatusCodes[c.Response().StatusCode()]; !ok {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Don't cache response if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Don't try to cache if body won't fit into cache\n\t\tbodySize := uint(len(c.Response().Body()))\n\t\tif cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Eviction loop: atomically reserve space for new entry and evict old entries.\n\t\t// Strategy:\n\t\t// 1. Under lock: reserve space by pre-incrementing storedBytes, then collect entries to evict\n\t\t// 2. Outside lock: perform I/O deletions\n\t\t// 3. On deletion failure: restore storedBytes and return error\n\t\t// 4. Track reservation with a flag; unreserve on early return via defer\n\t\tvar spaceReserved bool\n\t\tdefer func() {\n\t\t\t// If we reserved space but the entry was not successfully added to heap, unreserve it\n\t\t\tif cfg.MaxBytes > 0 && spaceReserved {\n\t\t\t\tmux.Lock()\n\t\t\t\tstoredBytes -= bodySize\n\t\t\t\tmux.Unlock()\n\t\t\t}\n\t\t}()\n\n\t\tif cfg.MaxBytes > 0 {\n\t\t\tmux.Lock()\n\t\t\t// Reserve space for the new entry first\n\t\t\tstoredBytes += bodySize\n\t\t\tspaceReserved = true\n\n\t\t\t// Now evict entries until we're under the limit\n\t\t\tvar keysToRemove []string\n\t\t\tvar sizesToRemove []uint\n\t\t\tvar candidates []evictionCandidate\n\n\t\t\tfor storedBytes > cfg.MaxBytes {\n\t\t\t\tif heap.Len() == 0 {\n\t\t\t\t\t// Can't evict more, unreserve space and fail\n\t\t\t\t\tstoredBytes -= bodySize\n\t\t\t\t\t// Set spaceReserved to false so the deferred cleanup does not unreserve again\n\t\t\t\t\tspaceReserved = false\n\t\t\t\t\tmux.Unlock()\n\t\t\t\t\treturn errors.New(\"cache: insufficient space and no entries to evict\")\n\t\t\t\t}\n\t\t\t\tnext := heap.entries[0]\n\t\t\t\tkeyToRemove, size := heap.removeFirst()\n\t\t\t\tkeysToRemove = append(keysToRemove, keyToRemove)\n\t\t\t\tsizesToRemove = append(sizesToRemove, size)\n\t\t\t\tcandidates = append(candidates, evictionCandidate{\n\t\t\t\t\tkey:  keyToRemove,\n\t\t\t\t\tsize: size,\n\t\t\t\t\texp:  next.exp,\n\t\t\t\t})\n\t\t\t\tstoredBytes -= size\n\t\t\t}\n\t\t\tmux.Unlock()\n\n\t\t\t// Perform deletions outside the lock\n\t\t\tif len(keysToRemove) > 0 {\n\t\t\t\tfor i, keyToRemove := range keysToRemove {\n\t\t\t\t\tdelErr := deleteKey(reqCtx, keyToRemove)\n\t\t\t\t\tif delErr == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Deletion failed: restore storedBytes for failed deletions\n\t\t\t\t\tmux.Lock()\n\t\t\t\t\t// Restore sizes of entries we failed to delete\n\t\t\t\t\tfor j := i; j < len(sizesToRemove); j++ {\n\t\t\t\t\t\tstoredBytes += sizesToRemove[j]\n\t\t\t\t\t}\n\t\t\t\t\t// Unreserve space for the new entry\n\t\t\t\t\tstoredBytes -= bodySize\n\t\t\t\t\tspaceReserved = false\n\n\t\t\t\t\t// Re-add entries to the heap to keep expiration tracking consistent\n\t\t\t\t\tvar restored []evictionCandidate\n\t\t\t\t\tfor j := i; j < len(candidates); j++ {\n\t\t\t\t\t\tcandidate := candidates[j]\n\t\t\t\t\t\tcandidate.heapIdx = heap.put(candidate.key, candidate.exp, candidate.size)\n\t\t\t\t\t\trestored = append(restored, candidate)\n\t\t\t\t\t}\n\t\t\t\t\tmux.Unlock()\n\n\t\t\t\t\tvar restoreErr error\n\t\t\t\t\tfor _, candidate := range restored {\n\t\t\t\t\t\tif err := refreshHeapIndex(reqCtx, candidate); err != nil {\n\t\t\t\t\t\t\trestoreErr = errors.Join(restoreErr, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif restoreErr != nil {\n\t\t\t\t\t\treturn errors.Join(fmt.Errorf(\"cache: failed to delete key %q while evicting: %w\", maskKey(keyToRemove), delErr), restoreErr)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn fmt.Errorf(\"cache: failed to delete key %q while evicting: %w\", maskKey(keyToRemove), delErr)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\te = manager.acquire()\n\t\t// Cache response\n\t\te.body = utils.CopyBytes(c.Response().Body())\n\t\te.status = c.Response().StatusCode()\n\t\te.ctype = utils.CopyBytes(c.Response().Header.ContentType())\n\t\te.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding))\n\t\te.private = false\n\t\te.cacheControl = utils.CopyBytes(cacheControlBytes)\n\t\te.expires = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderExpires))\n\t\te.etag = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderETag))\n\t\te.date = 0\n\n\t\tageVal := uint64(0)\n\t\tif b := c.Response().Header.Peek(fiber.HeaderAge); len(b) > 0 {\n\t\t\tif v, err := fasthttp.ParseUint(b); err == nil {\n\t\t\t\tif v >= 0 {\n\t\t\t\t\tageVal = uint64(v)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tc.Response().Header.Set(fiber.HeaderAge, \"0\")\n\t\t}\n\t\te.age = ageVal\n\t\te.shareable = isSharedCacheAllowed\n\t\tnow := time.Now().UTC()\n\t\tnowUnix := safeUnixSeconds(now)\n\t\tdateHeader := c.Response().Header.Peek(fiber.HeaderDate)\n\t\tparsedDate, _ := parseHTTPDate(dateHeader)\n\t\te.date = clampDateSeconds(parsedDate, nowUnix)\n\t\tdateBytes := fasthttp.AppendHTTPDate(nil, secondsToTime(e.date))\n\t\tc.Response().Header.SetBytesV(fiber.HeaderDate, dateBytes)\n\n\t\t// Store all response headers\n\t\t// (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1)\n\t\tif cfg.StoreResponseHeaders {\n\t\t\tallHeaders := c.Response().Header.All()\n\t\t\te.headers = e.headers[:0]\n\t\t\tfor key, value := range allHeaders {\n\t\t\t\tkeyStr := string(key)\n\t\t\t\tif _, ok := ignoreHeaders[keyStr]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\te.headers = append(e.headers, cachedHeader{\n\t\t\t\t\tkey:   utils.CopyBytes(utils.UnsafeBytes(keyStr)),\n\t\t\t\t\tvalue: utils.CopyBytes(value),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\texpirationSource := expirationSourceConfig\n\t\texpiresParseError := false\n\t\tmustRevalidate := respCacheControl.mustRevalidate || respCacheControl.proxyRevalidate\n\t\t// default cache expiration\n\t\texpiration := cfg.Expiration\n\t\tif sharedCacheMode && respCacheControl.sMaxAgeSet {\n\t\t\texpiration = secondsToDuration(respCacheControl.sMaxAge)\n\t\t\texpirationSource = expirationSourceSMaxAge\n\t\t}\n\t\tif expirationSource == expirationSourceConfig {\n\t\t\tif respCacheControl.maxAgeSet {\n\t\t\t\texpiration = secondsToDuration(respCacheControl.maxAge)\n\t\t\t\texpirationSource = expirationSourceMaxAge\n\t\t\t} else if expiresBytes := c.Response().Header.Peek(fiber.HeaderExpires); len(expiresBytes) > 0 {\n\t\t\t\texpiresAt, err := fasthttp.ParseHTTPDate(expiresBytes)\n\t\t\t\tif err != nil {\n\t\t\t\t\texpiration = time.Nanosecond\n\t\t\t\t\texpiresParseError = true\n\t\t\t\t} else {\n\t\t\t\t\texpiration = time.Until(expiresAt)\n\t\t\t\t}\n\t\t\t\texpirationSource = expirationSourceExpires\n\t\t\t}\n\t\t}\n\t\t// Calculate expiration by response header or other setting\n\t\tif cfg.ExpirationGenerator != nil {\n\t\t\texpiration = cfg.ExpirationGenerator(c, &cfg)\n\t\t\texpirationSource = expirationSourceGenerator\n\t\t}\n\t\te.forceRevalidate = expiresParseError\n\t\te.revalidate = mustRevalidate\n\n\t\tstorageExpiration := expiration\n\t\tif expiresParseError || storageExpiration < cfg.Expiration {\n\t\t\tstorageExpiration = cfg.Expiration\n\t\t}\n\n\t\tif expiration <= 0 && !expiresParseError {\n\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\treturn nil\n\t\t}\n\n\t\tts = atomic.LoadUint64(&timestamp)\n\t\tresponseTS := max(ts, nowUnix)\n\n\t\tmaxAgeSeconds := uint64(time.Duration(math.MaxInt64) / time.Second)\n\t\tvar ageDuration time.Duration\n\t\tapparentAge := e.age\n\t\tif e.date > 0 && responseTS > e.date {\n\t\t\tdateAge := responseTS - e.date\n\t\t\tif dateAge > apparentAge {\n\t\t\t\tapparentAge = dateAge\n\t\t\t}\n\t\t}\n\t\tif expirationSource != expirationSourceExpires {\n\t\t\tif apparentAge > maxAgeSeconds {\n\t\t\t\tageDuration = expiration + time.Second\n\t\t\t} else {\n\t\t\t\tageDuration = time.Duration(apparentAge) * time.Second\n\t\t\t}\n\t\t}\n\t\tremainingExpiration := expiration - ageDuration\n\t\tif remainingExpiration <= 0 {\n\t\t\tif expirationSource != expirationSourceExpires {\n\t\t\t\tc.Set(cfg.CacheHeader, cacheUnreachable)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tremainingExpiration = 0\n\t\t}\n\n\t\tif shouldStoreVaryManifest {\n\t\t\tif err := storeVaryManifest(reqCtx, manager, manifestKey, varyNames, storageExpiration); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\te.exp = responseTS + uint64(remainingExpiration.Seconds())\n\t\te.ttl = uint64(expiration.Seconds())\n\t\tif expiresParseError {\n\t\t\te.exp = ts + 1\n\t\t}\n\n\t\t// Store entry in heap (space already reserved in eviction phase)\n\t\tvar heapIdx int\n\t\tif cfg.MaxBytes > 0 {\n\t\t\tmux.Lock()\n\t\t\theapIdx = heap.put(key, e.exp, bodySize)\n\t\t\te.heapidx = heapIdx\n\t\t\t// Note: storedBytes was incremented during reservation, and evictions\n\t\t\t// have already been accounted for, so no additional increment is needed\n\t\t\tspaceReserved = false // Clear flag to prevent defer from unreserving\n\t\t\tmux.Unlock()\n\t\t}\n\n\t\tcleanupOnStoreError := func(ctx context.Context, releaseEntry, rawStored bool) error {\n\t\t\tvar cleanupErr error\n\t\t\tif cfg.MaxBytes > 0 {\n\t\t\t\tmux.Lock()\n\t\t\t\t_, size := heap.remove(heapIdx)\n\t\t\t\tstoredBytes -= size\n\t\t\t\tmux.Unlock()\n\t\t\t}\n\t\t\tif releaseEntry {\n\t\t\t\tmanager.release(e)\n\t\t\t}\n\t\t\tif rawStored {\n\t\t\t\trawKey := key + \"_body\"\n\t\t\t\tif err := manager.del(ctx, rawKey); err != nil {\n\t\t\t\t\tcleanupErr = errors.Join(cleanupErr, fmt.Errorf(\"cache: failed to delete raw key %q after store error: %w\", maskKey(rawKey), err))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn cleanupErr\n\t\t}\n\n\t\t// For external Storage we store raw body separated\n\t\tif cfg.Storage != nil {\n\t\t\tif err := manager.setRaw(reqCtx, key+\"_body\", e.body, storageExpiration); err != nil {\n\t\t\t\tif cleanupErr := cleanupOnStoreError(reqCtx, true, false); cleanupErr != nil {\n\t\t\t\t\terr = errors.Join(err, cleanupErr)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// avoid body msgp encoding\n\t\t\te.body = nil\n\t\t\tif err := manager.set(reqCtx, key, e, storageExpiration); err != nil {\n\t\t\t\tif cleanupErr := cleanupOnStoreError(reqCtx, false, true); cleanupErr != nil {\n\t\t\t\t\terr = errors.Join(err, cleanupErr)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// Store entry in memory\n\t\t\tif err := manager.set(reqCtx, key, e, storageExpiration); err != nil {\n\t\t\t\tif cleanupErr := cleanupOnStoreError(reqCtx, true, false); cleanupErr != nil {\n\t\t\t\t\terr = errors.Join(err, cleanupErr)\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// If revalidating, remove old heap entry now that replacement is successfully stored\n\t\tif cfg.MaxBytes > 0 && revalidate && oldHeapIdx >= 0 {\n\t\t\tmux.Lock()\n\t\t\tremoveHeapEntry(key, oldHeapIdx)\n\t\t\tmux.Unlock()\n\t\t}\n\n\t\tc.Set(cfg.CacheHeader, cacheMiss)\n\n\t\t// Finish response\n\t\treturn nil\n\t}\n}\n\n// hasDirective checks if a cache-control header contains a directive (case-insensitive)\nfunc hasDirective(cc, directive string) bool {\n\tccLen := len(cc)\n\tdirLen := len(directive)\n\tfor i := 0; i <= ccLen-dirLen; i++ {\n\t\tif !utils.EqualFold(cc[i:i+dirLen], directive) {\n\t\t\tcontinue\n\t\t}\n\t\tif i > 0 {\n\t\t\tprev := cc[i-1]\n\t\t\tif prev != ' ' && prev != ',' {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif i+dirLen == ccLen || cc[i+dirLen] == ',' {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc cacheBodyFetchError(mask func(string) string, key string, err error) error {\n\tif errors.Is(err, errCacheMiss) {\n\t\treturn fmt.Errorf(\"cache: no cached body for key %q: %w\", mask(key), err)\n\t}\n\treturn err\n}\n\nfunc parseUintDirective(val []byte) (uint64, bool) {\n\tif len(val) == 0 {\n\t\treturn 0, false\n\t}\n\tparsed, err := fasthttp.ParseUint(val)\n\tif err != nil || parsed < 0 {\n\t\treturn 0, false\n\t}\n\treturn uint64(parsed), true\n}\n\nfunc parseCacheControlDirectives(cc []byte, fn func(key, value []byte)) {\n\tfor i := 0; i < len(cc); {\n\t\t// skip leading separators/spaces\n\t\tfor i < len(cc) && (cc[i] == ' ' || cc[i] == ',') {\n\t\t\ti++\n\t\t}\n\t\tif i >= len(cc) {\n\t\t\tbreak\n\t\t}\n\n\t\tstart := i\n\t\tfor i < len(cc) && cc[i] != ',' {\n\t\t\ti++\n\t\t}\n\t\tpartEnd := i\n\t\tfor partEnd > start && cc[partEnd-1] == ' ' {\n\t\t\tpartEnd--\n\t\t}\n\n\t\tkeyStart := start\n\t\tfor keyStart < partEnd && cc[keyStart] == ' ' {\n\t\t\tkeyStart++\n\t\t}\n\t\tif keyStart >= partEnd {\n\t\t\tcontinue\n\t\t}\n\n\t\tkeyEnd := keyStart\n\t\tfor keyEnd < partEnd && cc[keyEnd] != '=' {\n\t\t\tkeyEnd++\n\t\t}\n\t\t// Trim trailing spaces from key\n\t\tkeyEndTrimmed := keyEnd\n\t\tfor keyEndTrimmed > keyStart && cc[keyEndTrimmed-1] == ' ' {\n\t\t\tkeyEndTrimmed--\n\t\t}\n\t\tkey := cc[keyStart:keyEndTrimmed]\n\n\t\tvar value []byte\n\t\tif keyEnd < partEnd && cc[keyEnd] == '=' {\n\t\t\tvalueStart := keyEnd + 1\n\t\t\tfor valueStart < partEnd && cc[valueStart] == ' ' {\n\t\t\t\tvalueStart++\n\t\t\t}\n\t\t\tvalueEnd := partEnd\n\t\t\tfor valueEnd > valueStart && cc[valueEnd-1] == ' ' {\n\t\t\t\tvalueEnd--\n\t\t\t}\n\t\t\tif valueStart <= valueEnd {\n\t\t\t\tvalue = cc[valueStart:valueEnd]\n\t\t\t\t// Handle quoted-string values per RFC 9111 Section 5.2\n\t\t\t\tif len(value) >= 2 && value[0] == '\"' && value[len(value)-1] == '\"' {\n\t\t\t\t\tvalue = unquoteCacheDirective(value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfn(key, value)\n\t\ti++ // skip comma\n\t}\n}\n\n// unquoteCacheDirective removes quotes and handles escaped characters in quoted-string values.\n// Per RFC 9111 Section 5.2, quoted-string values follow RFC 9110 Section 5.6.4.\nfunc unquoteCacheDirective(quoted []byte) []byte {\n\tif len(quoted) < 2 {\n\t\treturn quoted\n\t}\n\n\t// Remove surrounding quotes\n\tinner := quoted[1 : len(quoted)-1]\n\n\t// Check if there are any escaped characters (backslash followed by another character)\n\thasEscapes := false\n\tfor i := 0; i < len(inner)-1; i++ {\n\t\tif inner[i] == '\\\\' {\n\t\t\thasEscapes = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If no escapes, return the inner content directly\n\tif !hasEscapes {\n\t\treturn inner\n\t}\n\n\t// Process escaped characters\n\tresult := make([]byte, 0, len(inner))\n\tfor i := 0; i < len(inner); i++ {\n\t\tif inner[i] == '\\\\' && i+1 < len(inner) {\n\t\t\t// Skip the backslash and take the next character\n\t\t\ti++\n\t\t\tresult = append(result, inner[i])\n\t\t} else {\n\t\t\tresult = append(result, inner[i])\n\t\t}\n\t}\n\n\treturn result\n}\n\ntype responseCacheControl struct {\n\tmaxAge          uint64\n\tsMaxAge         uint64\n\tmaxAgeSet       bool\n\tsMaxAgeSet      bool\n\thasNoCache      bool\n\thasNoStore      bool\n\thasPrivate      bool\n\thasPublic       bool\n\tmustRevalidate  bool\n\tproxyRevalidate bool\n}\n\nfunc parseResponseCacheControl(cc []byte) responseCacheControl {\n\tparsed := responseCacheControl{}\n\tparseCacheControlDirectives(cc, func(key, value []byte) {\n\t\tswitch {\n\t\tcase utils.EqualFold(utils.UnsafeString(key), noStore):\n\t\t\tparsed.hasNoStore = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), noCache):\n\t\t\tparsed.hasNoCache = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), privateDirective):\n\t\t\tparsed.hasPrivate = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"public\"):\n\t\t\tparsed.hasPublic = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"max-age\"):\n\t\t\tif v, ok := parseUintDirective(value); ok {\n\t\t\t\tparsed.maxAgeSet = true\n\t\t\t\tparsed.maxAge = v\n\t\t\t}\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"s-maxage\"):\n\t\t\tif v, ok := parseUintDirective(value); ok {\n\t\t\t\tparsed.sMaxAgeSet = true\n\t\t\t\tparsed.sMaxAge = v\n\t\t\t}\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"must-revalidate\"):\n\t\t\tparsed.mustRevalidate = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"proxy-revalidate\"):\n\t\t\tparsed.proxyRevalidate = true\n\t\tdefault:\n\t\t\t// ignore unknown directives\n\t\t}\n\t})\n\treturn parsed\n}\n\n// parseMaxAge extracts the max-age directive from a Cache-Control header.\nfunc parseMaxAge(cc string) (time.Duration, bool) {\n\tparsed := parseResponseCacheControl(utils.UnsafeBytes(cc))\n\tif !parsed.maxAgeSet {\n\t\treturn 0, false\n\t}\n\treturn secondsToDuration(parsed.maxAge), true\n}\n\nfunc parseRequestCacheControl(cc []byte) requestCacheDirectives {\n\tdirectives := requestCacheDirectives{}\n\tparseCacheControlDirectives(cc, func(key, value []byte) {\n\t\tswitch {\n\t\tcase utils.EqualFold(utils.UnsafeString(key), noStore):\n\t\t\tdirectives.noStore = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), noCache):\n\t\t\tdirectives.noCache = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"only-if-cached\"):\n\t\t\tdirectives.onlyIfCached = true\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"max-age\"):\n\t\t\tif sec, ok := parseUintDirective(value); ok {\n\t\t\t\tdirectives.maxAgeSet = true\n\t\t\t\tdirectives.maxAge = sec\n\t\t\t}\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"max-stale\"):\n\t\t\tdirectives.maxStaleSet = true\n\t\t\tdirectives.maxStaleAny = len(value) == 0\n\t\t\tif !directives.maxStaleAny {\n\t\t\t\tif sec, ok := parseUintDirective(value); ok {\n\t\t\t\t\tdirectives.maxStale = sec\n\t\t\t\t}\n\t\t\t}\n\t\tcase utils.EqualFold(utils.UnsafeString(key), \"min-fresh\"):\n\t\t\tif sec, ok := parseUintDirective(value); ok {\n\t\t\t\tdirectives.minFreshSet = true\n\t\t\t\tdirectives.minFresh = sec\n\t\t\t}\n\t\tdefault:\n\t\t\t// ignore unknown directives\n\t\t}\n\t})\n\treturn directives\n}\n\nfunc parseRequestCacheControlString(cc string) requestCacheDirectives {\n\treturn parseRequestCacheControl(utils.UnsafeBytes(cc))\n}\n\nfunc cachedResponseAge(e *item, now uint64) uint64 {\n\tclampedDate := clampDateSeconds(e.date, now)\n\n\tresident := uint64(0)\n\tif e.exp != 0 {\n\t\tif e.exp <= now {\n\t\t\tresident = e.ttl + (now - e.exp)\n\t\t} else {\n\t\t\tresident = e.ttl - (e.exp - now)\n\t\t}\n\t}\n\n\tdateAge := uint64(0)\n\tif clampedDate != 0 && now > clampedDate {\n\t\tdateAge = now - clampedDate\n\t}\n\n\tcurrentAge := max(dateAge, max(resident, e.age))\n\treturn currentAge\n}\n\nfunc appendWarningHeaders(h *fasthttp.ResponseHeader, servedStale, heuristicFreshness bool) { //nolint:revive // flags are intentional to represent Warning variants\n\tif servedStale {\n\t\th.Add(fiber.HeaderWarning, `110 - \"Response is stale\"`)\n\t}\n\tif heuristicFreshness {\n\t\th.Add(fiber.HeaderWarning, `113 - \"Heuristic expiration\"`)\n\t}\n}\n\nfunc remainingFreshness(e *item, now uint64) uint64 {\n\tif e == nil || e.exp == 0 || now >= e.exp {\n\t\treturn 0\n\t}\n\n\treturn e.exp - now\n}\n\nfunc isHeuristicFreshness(e *item, cfg *Config, entryAge uint64) bool {\n\tconst heuristicAgeThresholdSeconds = uint64(24 * time.Hour / time.Second)\n\tif entryAge <= heuristicAgeThresholdSeconds {\n\t\treturn false\n\t}\n\n\tif len(e.expires) > 0 {\n\t\treturn false\n\t}\n\n\tcacheControl := utils.UnsafeString(e.cacheControl)\n\tif parsedCC := parseResponseCacheControl(utils.UnsafeBytes(cacheControl)); parsedCC.maxAgeSet || parsedCC.sMaxAgeSet {\n\t\treturn false\n\t}\n\n\treturn cfg.Expiration > 0\n}\n\nfunc lookupCachedHeader(headers []cachedHeader, name string) ([]byte, bool) {\n\tfor i := range headers {\n\t\tif utils.EqualFold(utils.UnsafeString(headers[i].key), name) {\n\t\t\treturn headers[i].value, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\nfunc parseHTTPDate(dateBytes []byte) (uint64, bool) {\n\tif len(dateBytes) == 0 {\n\t\treturn 0, false\n\t}\n\tparsedDate, err := fasthttp.ParseHTTPDate(dateBytes)\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\n\treturn safeUnixSeconds(parsedDate), true\n}\n\nfunc clampDateSeconds(dateSeconds, fallback uint64) uint64 {\n\tconst maxUnixSeconds = uint64(math.MaxInt64)\n\tif dateSeconds == 0 || dateSeconds > maxUnixSeconds || dateSeconds > fallback {\n\t\treturn fallback\n\t}\n\n\treturn dateSeconds\n}\n\nfunc safeUnixSeconds(t time.Time) uint64 {\n\tsec := t.Unix()\n\tif sec < 0 {\n\t\treturn 0\n\t}\n\n\treturn uint64(sec)\n}\n\nfunc secondsToTime(sec uint64) time.Time {\n\tvar clamped int64\n\tif sec > uint64(math.MaxInt64) {\n\t\tclamped = math.MaxInt64\n\t} else {\n\t\tclamped = int64(sec)\n\t}\n\n\treturn time.Unix(clamped, 0).UTC()\n}\n\nfunc secondsToDuration(sec uint64) time.Duration {\n\tconst maxSeconds = uint64(math.MaxInt64) / uint64(time.Second)\n\tif sec > maxSeconds {\n\t\treturn time.Duration(math.MaxInt64)\n\t}\n\treturn time.Duration(sec) * time.Second\n}\n\nfunc parseVary(vary string) ([]string, bool) {\n\tnames := make([]string, 0, 8)\n\tfor part := range strings.SplitSeq(vary, \",\") {\n\t\tname := utils.TrimSpace(utilsstrings.ToLower(part))\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif name == \"*\" {\n\t\t\treturn nil, true\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\n\tif len(names) == 0 {\n\t\treturn nil, false\n\t}\n\n\tsort.Strings(names)\n\treturn names, false\n}\n\nfunc makeBuildVaryKeyFunc(hexBufPool *sync.Pool) func([]string, *fasthttp.RequestHeader) string {\n\treturn func(names []string, hdr *fasthttp.RequestHeader) string {\n\t\tsum := sha256.New()\n\t\tfor _, name := range names {\n\t\t\t_, _ = sum.Write(utils.UnsafeBytes(name)) //nolint:errcheck // hash.Hash.Write for std hashes never errors\n\t\t\t_, _ = sum.Write([]byte{0})               //nolint:errcheck // hash.Hash.Write for std hashes never errors\n\t\t\t_, _ = sum.Write(hdr.Peek(name))          //nolint:errcheck // hash.Hash.Write for std hashes never errors\n\t\t\t_, _ = sum.Write([]byte{0})               //nolint:errcheck // hash.Hash.Write for std hashes never errors\n\t\t}\n\n\t\tvar hashBytes [sha256.Size]byte\n\t\tsum.Sum(hashBytes[:0])\n\n\t\tv := hexBufPool.Get()\n\t\tbufPtr, ok := v.(*[]byte)\n\t\tif !ok || bufPtr == nil {\n\t\t\tb := make([]byte, hexLen)\n\t\t\tbufPtr = &b\n\t\t}\n\n\t\tbuf := *bufPtr\n\t\t// Defensive in case someone changed Pool.New or Put a different sized buffer.\n\t\tif cap(buf) < hexLen {\n\t\t\tbuf = make([]byte, hexLen)\n\t\t} else {\n\t\t\tbuf = buf[:hexLen]\n\t\t}\n\t\t*bufPtr = buf\n\n\t\thex.Encode(buf, hashBytes[:])\n\t\tresult := \"|vary|\" + string(buf)\n\n\t\thexBufPool.Put(bufPtr)\n\t\treturn result\n\t}\n}\n\nfunc storeVaryManifest(ctx context.Context, manager *manager, manifestKey string, names []string, exp time.Duration) error {\n\tif len(names) == 0 {\n\t\treturn nil\n\t}\n\tdata := strings.Join(names, \",\")\n\treturn manager.setRaw(ctx, manifestKey, utils.UnsafeBytes(data), exp)\n}\n\n//nolint:gocritic // returning explicit values keeps the signature concise while avoiding unnecessary named results\nfunc loadVaryManifest(ctx context.Context, manager *manager, manifestKey string) ([]string, bool, error) {\n\traw, err := manager.getRaw(ctx, manifestKey)\n\tif err != nil {\n\t\tif errors.Is(err, errCacheMiss) {\n\t\t\treturn nil, false, nil\n\t\t}\n\t\treturn nil, false, err\n\t}\n\tmanifest := utils.UnsafeString(raw)\n\tnames, hasStar := parseVary(manifest)\n\tif hasStar {\n\t\treturn nil, false, nil\n\t}\n\treturn names, len(names) > 0, nil\n}\n\nfunc allowsSharedCacheDirectives(cc responseCacheControl) bool {\n\tif cc.hasPrivate {\n\t\treturn false\n\t}\n\tif cc.hasPublic || cc.sMaxAgeSet || cc.mustRevalidate || cc.proxyRevalidate {\n\t\treturn true\n\t}\n\n\t// RFC 9111 §4.2.2 permits Expires as an absolute expiry for cacheable responses, but for\n\t// authenticated requests §3.6 requires an explicit shared-cache directive. Therefore,\n\t// an Expires header alone MUST NOT allow sharing when Authorization is present.\n\treturn false\n}\n\nfunc allowsSharedCache(cc string) bool {\n\treturn allowsSharedCacheDirectives(parseResponseCacheControl(utils.UnsafeBytes(cc)))\n}\n\nfunc makeHashAuthFunc(hexBufPool *sync.Pool) func([]byte) string {\n\treturn func(authHeader []byte) string {\n\t\tsum := sha256.Sum256(authHeader)\n\n\t\tv := hexBufPool.Get()\n\t\tbufPtr, ok := v.(*[]byte)\n\t\tif !ok || bufPtr == nil {\n\t\t\tb := make([]byte, hexLen)\n\t\t\tbufPtr = &b\n\t\t}\n\n\t\tbuf := *bufPtr\n\t\tif cap(buf) < hexLen {\n\t\t\tbuf = make([]byte, hexLen)\n\t\t} else {\n\t\t\tbuf = buf[:hexLen]\n\t\t}\n\t\t*bufPtr = buf\n\n\t\thex.Encode(buf, sum[:])\n\t\tresult := string(buf)\n\n\t\thexBufPool.Put(bufPtr)\n\t\treturn result\n\t}\n}\n"
  },
  {
    "path": "middleware/cache/cache_test.go",
    "content": "// Special thanks to @codemicro for moving this to fiber core\n// Original middleware: github.com/codemicro/fiber-cache\npackage cache\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n\t\"github.com/gofiber/fiber/v3/middleware/etag\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\ntype failingCacheStorage struct {\n\tdata map[string][]byte\n\terrs map[string]error\n\tmu   sync.RWMutex\n}\n\ntype mutatingStorage struct {\n\tdata   map[string][]byte\n\tmutate func(key string, value []byte) []byte\n}\n\nfunc newFailingCacheStorage() *failingCacheStorage {\n\treturn &failingCacheStorage{\n\t\tdata: make(map[string][]byte),\n\t\terrs: make(map[string]error),\n\t}\n}\n\nfunc newMutatingStorage(mutate func(key string, value []byte) []byte) *mutatingStorage {\n\treturn &mutatingStorage{\n\t\tdata:   make(map[string][]byte),\n\t\tmutate: mutate,\n\t}\n}\n\nfunc (s *mutatingStorage) GetWithContext(_ context.Context, key string) ([]byte, error) {\n\treturn s.Get(key)\n}\n\nfunc (s *mutatingStorage) Get(key string) ([]byte, error) {\n\tif value, ok := s.data[key]; ok {\n\t\treturn value, nil\n\t}\n\n\treturn nil, nil\n}\n\nfunc (s *mutatingStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\treturn s.Set(key, val, 0)\n}\n\nfunc (s *mutatingStorage) Set(key string, val []byte, _ time.Duration) error {\n\tif key == \"\" || len(val) == 0 {\n\t\treturn nil\n\t}\n\n\tif s.mutate != nil {\n\t\tval = s.mutate(key, val)\n\t}\n\n\ts.data[key] = val\n\treturn nil\n}\n\nfunc (s *mutatingStorage) DeleteWithContext(_ context.Context, key string) error {\n\treturn s.Delete(key)\n}\n\nfunc (s *mutatingStorage) Delete(key string) error {\n\tdelete(s.data, key)\n\treturn nil\n}\n\nfunc (s *mutatingStorage) ResetWithContext(_ context.Context) error {\n\treturn s.Reset()\n}\n\nfunc (s *mutatingStorage) Reset() error {\n\ts.data = make(map[string][]byte)\n\treturn nil\n}\n\nfunc (s *mutatingStorage) Close() error {\n\ts.data = nil\n\treturn nil\n}\n\nfunc (s *failingCacheStorage) GetWithContext(_ context.Context, key string) ([]byte, error) {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\tif err, ok := s.errs[\"get|\"+key]; ok && err != nil {\n\t\treturn nil, err\n\t}\n\tif val, ok := s.data[key]; ok {\n\t\treturn append([]byte(nil), val...), nil\n\t}\n\treturn nil, nil\n}\n\nfunc (s *failingCacheStorage) Get(key string) ([]byte, error) {\n\treturn s.GetWithContext(context.Background(), key)\n}\n\nfunc (s *failingCacheStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif err, ok := s.errs[\"set|\"+key]; ok && err != nil {\n\t\treturn err\n\t}\n\ts.data[key] = append([]byte(nil), val...)\n\treturn nil\n}\n\nfunc (s *failingCacheStorage) Set(key string, val []byte, exp time.Duration) error {\n\treturn s.SetWithContext(context.Background(), key, val, exp)\n}\n\nfunc (s *failingCacheStorage) DeleteWithContext(_ context.Context, key string) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif err, ok := s.errs[\"del|\"+key]; ok && err != nil {\n\t\treturn err\n\t}\n\tdelete(s.data, key)\n\treturn nil\n}\n\nfunc (s *failingCacheStorage) Delete(key string) error {\n\treturn s.DeleteWithContext(context.Background(), key)\n}\n\nfunc (s *failingCacheStorage) ResetWithContext(context.Context) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.data = make(map[string][]byte)\n\ts.errs = make(map[string]error)\n\treturn nil\n}\n\nfunc (s *failingCacheStorage) Reset() error {\n\treturn s.ResetWithContext(context.Background())\n}\n\nfunc (*failingCacheStorage) Close() error { return nil }\n\ntype contextRecord struct {\n\tkey      string\n\tvalue    string\n\tcanceled bool\n}\n\ntype contextRecorderStorage struct {\n\t*failingCacheStorage\n\tdeletes []contextRecord\n\tgets    []contextRecord\n\tsets    []contextRecord\n}\n\nfunc newContextRecorderStorage() *contextRecorderStorage {\n\treturn &contextRecorderStorage{failingCacheStorage: newFailingCacheStorage()}\n}\n\nfunc contextRecordFrom(ctx context.Context, key string) contextRecord {\n\trecord := contextRecord{\n\t\tkey:      key,\n\t\tcanceled: errors.Is(ctx.Err(), context.Canceled),\n\t}\n\tif value, ok := ctx.Value(markerKey).(string); ok {\n\t\trecord.value = value\n\t}\n\treturn record\n}\n\nfunc (s *contextRecorderStorage) GetWithContext(ctx context.Context, key string) ([]byte, error) {\n\ts.gets = append(s.gets, contextRecordFrom(ctx, key))\n\treturn s.failingCacheStorage.GetWithContext(ctx, key)\n}\n\nfunc (s *contextRecorderStorage) SetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error {\n\ts.sets = append(s.sets, contextRecordFrom(ctx, key))\n\treturn s.failingCacheStorage.SetWithContext(ctx, key, val, exp)\n}\n\nfunc (s *contextRecorderStorage) DeleteWithContext(ctx context.Context, key string) error {\n\ts.deletes = append(s.deletes, contextRecordFrom(ctx, key))\n\treturn s.failingCacheStorage.DeleteWithContext(ctx, key)\n}\n\nfunc (s *contextRecorderStorage) recordedGets() []contextRecord {\n\tout := make([]contextRecord, len(s.gets))\n\tcopy(out, s.gets)\n\treturn out\n}\n\nfunc (s *contextRecorderStorage) recordedSets() []contextRecord {\n\tout := make([]contextRecord, len(s.sets))\n\tcopy(out, s.sets)\n\treturn out\n}\n\nfunc (s *contextRecorderStorage) recordedDeletes() []contextRecord {\n\tout := make([]contextRecord, len(s.deletes))\n\tcopy(out, s.deletes)\n\treturn out\n}\n\nfunc TestCacheStorageGetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCacheStorage()\n\tstorage.errs[\"get|/_GET\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"cache: failed to get key\")\n}\n\nfunc TestCacheStorageSetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCacheStorage()\n\tstorage.errs[\"set|/_GET_body\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"cache: failed to store raw key\")\n}\n\nfunc TestCacheStorageDeleteError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCacheStorage()\n\tstorage.errs[\"del|/_GET\"] = errors.New(\"boom\")\n\n\t// Use an obviously expired timestamp without relying on time-based conversions\n\texpired := &item{exp: 1}\n\traw, err := expired.MarshalMsg(nil)\n\trequire.NoError(t, err)\n\n\tstorage.data[\"/_GET\"] = raw\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"cache: failed to delete expired key\")\n}\n\ntype contextKey string\n\nconst markerKey contextKey = \"marker\"\n\nfunc contextWithMarker(label string) context.Context {\n\treturn context.WithValue(context.Background(), markerKey, label)\n}\n\nfunc canceledContextWithMarker(label string) context.Context {\n\tctx, cancel := context.WithCancel(contextWithMarker(label))\n\tcancel()\n\treturn ctx\n}\n\nfunc TestCacheEvictionPropagatesRequestContextToDelete(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderStorage()\n\tapp := fiber.New()\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tpath := c.Path()\n\t\tif path == \"/first\" {\n\t\t\tc.SetContext(contextWithMarker(\"first\"))\n\t\t}\n\t\tif path == \"/second\" {\n\t\t\tc.SetContext(canceledContextWithMarker(\"evict\"))\n\t\t}\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Minute, MaxBytes: 5}))\n\n\tapp.Get(\"/first\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"aaa\")\n\t})\n\n\tapp.Get(\"/second\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"bbbb\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/first\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/second\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\trecords := storage.recordedDeletes()\n\trequire.Len(t, records, 2)\n\n\tvar keys []string\n\tfor _, rec := range records {\n\t\tkeys = append(keys, rec.key)\n\t\trequire.Equal(t, \"evict\", rec.value)\n\t\trequire.True(t, rec.canceled)\n\t}\n\n\trequire.ElementsMatch(t, []string{\"/first_GET\", \"/first_GET_body\"}, keys)\n}\n\nfunc TestCacheCleanupPropagatesRequestContextToDelete(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderStorage()\n\tstorage.errs[\"set|/_GET\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tc.SetContext(canceledContextWithMarker(\"cleanup\"))\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Minute}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"payload\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"cache: failed to store key\")\n\n\trecords := storage.recordedDeletes()\n\trequire.Len(t, records, 1)\n\trequire.Equal(t, \"/_GET_body\", records[0].key)\n\trequire.Equal(t, \"cleanup\", records[0].value)\n\trequire.True(t, records[0].canceled)\n}\n\nfunc TestCacheStorageOperationsObserveRequestContext(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderStorage()\n\tapp := fiber.New()\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tctxLabel := string(c.Request().Header.Peek(\"X-Context\"))\n\t\tif ctxLabel == \"\" {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tcanceled := string(c.Request().Header.Peek(\"X-Cancel\")) == \"true\"\n\t\tif canceled {\n\t\t\tc.SetContext(canceledContextWithMarker(ctxLabel))\n\t\t} else {\n\t\t\tc.SetContext(contextWithMarker(ctxLabel))\n\t\t}\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Expiration: time.Minute}))\n\n\tapp.Get(\"/cache\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"payload\")\n\t})\n\n\tfirstReq := httptest.NewRequest(fiber.MethodGet, \"/cache\", http.NoBody)\n\tfirstReq.Header.Set(\"X-Context\", \"store\")\n\tfirstReq.Header.Set(\"X-Cancel\", \"true\")\n\n\tresp, err := app.Test(firstReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tsecondReq := httptest.NewRequest(fiber.MethodGet, \"/cache\", http.NoBody)\n\tsecondReq.Header.Set(\"X-Context\", \"fetch\")\n\tsecondReq.Header.Set(\"X-Cancel\", \"true\")\n\n\tresp, err = app.Test(secondReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tsetRecords := storage.recordedSets()\n\trequire.Len(t, setRecords, 2)\n\tfor _, rec := range setRecords {\n\t\trequire.Contains(t, []string{\"/cache_GET\", \"/cache_GET_body\"}, rec.key)\n\t\trequire.Equal(t, \"store\", rec.value)\n\t\trequire.True(t, rec.canceled)\n\t}\n\n\tgetRecords := storage.recordedGets()\n\trequire.NotEmpty(t, getRecords)\n\n\tvar fetchEntry, fetchBody bool\n\tfor _, rec := range getRecords {\n\t\tif rec.value != \"fetch\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif rec.key == \"/cache_GET\" {\n\t\t\trequire.True(t, rec.canceled)\n\t\t\tfetchEntry = true\n\t\t}\n\t\tif rec.key == \"/cache_GET_body\" {\n\t\t\trequire.True(t, rec.canceled)\n\t\t\tfetchBody = true\n\t\t}\n\t}\n\n\trequire.True(t, fetchEntry, \"expected cached entry retrieval to observe request context\")\n\trequire.True(t, fetchBody, \"expected cached body retrieval to observe request context\")\n}\n\nfunc Test_Cache_CacheControl(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"public, max-age=10\", resp.Header.Get(fiber.HeaderCacheControl))\n}\n\nfunc Test_Cache_CacheControl_Disabled(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExpiration:          10 * time.Second,\n\t\tDisableCacheControl: true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderCacheControl))\n}\n\nfunc Test_Cache_Expired(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 2 * time.Second}))\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\t// Sleep until the cache is expired\n\ttime.Sleep(3 * time.Second)\n\n\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCached, err := io.ReadAll(respCached.Body)\n\trequire.NoError(t, err)\n\n\tif bytes.Equal(body, bodyCached) {\n\t\tt.Errorf(\"Cache should have expired: %s, %s\", body, bodyCached)\n\t}\n\n\t// Next response should be also cached\n\trespCachedNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCachedNextRound, err := io.ReadAll(respCachedNextRound.Body)\n\trequire.NoError(t, err)\n\n\tif !bytes.Equal(bodyCachedNextRound, bodyCached) {\n\t\tt.Errorf(\"Cache should not have expired: %s, %s\", bodyCached, bodyCachedNextRound)\n\t}\n}\n\nfunc Test_Cache(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tcachedReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tcachedResp, err := app.Test(cachedReq)\n\trequire.NoError(t, err)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tcachedBody, err := io.ReadAll(cachedResp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, cachedBody, body)\n}\n\n// go test -run Test_Cache_WithNoCacheRequestDirective\nfunc Test_Cache_WithNoCacheRequestDirective(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query(c, \"id\", \"1\"))\n\t})\n\n\t// Request id = 1\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"1\"), body)\n\t// Response cached, entry id = 1\n\n\t// Request id = 2 without Cache-Control: no-cache\n\tcachedReq := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tcachedResp, err := app.Test(cachedReq)\n\trequire.NoError(t, err)\n\tcachedBody, err := io.ReadAll(cachedResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"1\"), cachedBody)\n\t// Response not cached, returns cached response, entry id = 1\n\n\t// Request id = 2 with Cache-Control: no-cache\n\tnoCacheReq := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tnoCacheReq.Header.Set(fiber.HeaderCacheControl, noCache)\n\tnoCacheResp, err := app.Test(noCacheReq)\n\trequire.NoError(t, err)\n\tnoCacheBody, err := io.ReadAll(noCacheResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"2\"), noCacheBody)\n\t// Response cached, returns updated response, entry = 2\n\n\t/* Check Test_Cache_WithETagAndNoCacheRequestDirective */\n\t// Request id = 2 with Cache-Control: no-cache again\n\tnoCacheReq1 := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tnoCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)\n\tnoCacheResp1, err := app.Test(noCacheReq1)\n\trequire.NoError(t, err)\n\tnoCacheBody1, err := io.ReadAll(noCacheResp1.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheResp1.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"2\"), noCacheBody1)\n\t// Response cached, returns updated response, entry = 2\n\n\t// Request id = 3 with Cache-Control: NO-CACHE\n\tnoCacheReqUpper := httptest.NewRequest(fiber.MethodGet, \"/?id=3\", http.NoBody)\n\tnoCacheReqUpper.Header.Set(fiber.HeaderCacheControl, \"NO-CACHE\")\n\tnoCacheRespUpper, err := app.Test(noCacheReqUpper)\n\trequire.NoError(t, err)\n\tnoCacheBodyUpper, err := io.ReadAll(noCacheRespUpper.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheRespUpper.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"3\"), noCacheBodyUpper)\n\t// Response cached, returns updated response, entry = 3\n\n\t// Request id = 4 with Cache-Control: my-no-cache\n\tinvalidReq := httptest.NewRequest(fiber.MethodGet, \"/?id=4\", http.NoBody)\n\tinvalidReq.Header.Set(fiber.HeaderCacheControl, \"my-no-cache\")\n\tinvalidResp, err := app.Test(invalidReq)\n\trequire.NoError(t, err)\n\tinvalidBody, err := io.ReadAll(invalidResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, invalidResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"3\"), invalidBody)\n\t// Response served from cache, existing entry = 3\n\n\t// Request id = 4 again without Cache-Control: no-cache\n\tcachedInvalidReq := httptest.NewRequest(fiber.MethodGet, \"/?id=4\", http.NoBody)\n\tcachedInvalidResp, err := app.Test(cachedInvalidReq)\n\trequire.NoError(t, err)\n\tcachedInvalidBody, err := io.ReadAll(cachedInvalidResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedInvalidResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"3\"), cachedInvalidBody)\n\t// Response cached, returns cached response, entry id = 3\n\n\t// Request id = 1 without Cache-Control: no-cache\n\tcachedReq1 := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tcachedResp1, err := app.Test(cachedReq1)\n\trequire.NoError(t, err)\n\tcachedBody1, err := io.ReadAll(cachedResp1.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedResp1.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"3\"), cachedBody1)\n\t// Response not cached, returns cached response, entry id = 3\n}\n\n// go test -run Test_Cache_WithETagAndNoCacheRequestDirective\nfunc Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tetag.New(),\n\t\tNew(),\n\t)\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query(c, \"id\", \"1\"))\n\t})\n\n\t// Request id = 1\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t// Response cached, entry id = 1\n\n\t// If response status 200\n\tetagToken := resp.Header.Get(\"Etag\")\n\n\t// Request id = 2 with ETag but without Cache-Control: no-cache\n\tcachedReq := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tcachedReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken)\n\tcachedResp, err := app.Test(cachedReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusNotModified, cachedResp.StatusCode)\n\t// Response not cached, returns cached response, entry id = 1, status not modified\n\n\t// Request id = 2 with ETag and Cache-Control: no-cache\n\tnoCacheReq := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tnoCacheReq.Header.Set(fiber.HeaderCacheControl, noCache)\n\tnoCacheReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken)\n\tnoCacheResp, err := app.Test(noCacheReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusOK, noCacheResp.StatusCode)\n\t// Response cached, returns updated response, entry id = 2\n\n\t// If response status 200\n\tetagToken = noCacheResp.Header.Get(\"Etag\")\n\n\t// Request id = 3 with ETag and Cache-Control: NO-CACHE\n\tnoCacheReqUpper := httptest.NewRequest(fiber.MethodGet, \"/?id=3\", http.NoBody)\n\tnoCacheReqUpper.Header.Set(fiber.HeaderCacheControl, \"NO-CACHE\")\n\tnoCacheReqUpper.Header.Set(fiber.HeaderIfNoneMatch, etagToken)\n\tnoCacheRespUpper, err := app.Test(noCacheReqUpper)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheRespUpper.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusOK, noCacheRespUpper.StatusCode)\n\t// Response cached, returns updated response, entry id = 3\n\n\t// Request id = 2 with ETag and Cache-Control: no-cache again\n\tnoCacheReq1 := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tnoCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)\n\tnoCacheReq1.Header.Set(fiber.HeaderIfNoneMatch, etagToken)\n\tnoCacheResp1, err := app.Test(noCacheReq1)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, noCacheResp1.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusNotModified, noCacheResp1.StatusCode)\n\t// Response cached, returns updated response, entry id = 2, status not modified\n\n\t// Request id = 1 without ETag and Cache-Control: no-cache\n\tcachedReq1 := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tcachedResp1, err := app.Test(cachedReq1)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedResp1.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusOK, cachedResp1.StatusCode)\n\t// Response not cached, returns cached response, entry id = 2\n}\n\n// go test -run Test_Cache_WithNoStoreRequestDirective\nfunc Test_Cache_WithNoStoreRequestDirective(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query(c, \"id\", \"1\"))\n\t})\n\n\t// Request id = 2\n\tnoStoreReq := httptest.NewRequest(fiber.MethodGet, \"/?id=2\", http.NoBody)\n\tnoStoreReq.Header.Set(fiber.HeaderCacheControl, noStore)\n\tnoStoreResp, err := app.Test(noStoreReq)\n\trequire.NoError(t, err)\n\tnoStoreBody, err := io.ReadAll(noStoreResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"2\"), noStoreBody)\n\t// Response not cached, returns updated response\n\n\t// Request id = 3 with Cache-Control: NO-STORE\n\tnoStoreReqUpper := httptest.NewRequest(fiber.MethodGet, \"/?id=3\", http.NoBody)\n\tnoStoreReqUpper.Header.Set(fiber.HeaderCacheControl, \"NO-STORE\")\n\tnoStoreRespUpper, err := app.Test(noStoreReqUpper)\n\trequire.NoError(t, err)\n\tnoStoreBodyUpper, err := io.ReadAll(noStoreRespUpper.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"3\"), noStoreBodyUpper)\n\t// Response not cached, returns updated response\n\n\t// Request id = 4 with Cache-Control: my-no-store\n\tinvalidReq := httptest.NewRequest(fiber.MethodGet, \"/?id=4\", http.NoBody)\n\tinvalidReq.Header.Set(fiber.HeaderCacheControl, \"my-no-store\")\n\tinvalidResp, err := app.Test(invalidReq)\n\trequire.NoError(t, err)\n\tinvalidBody, err := io.ReadAll(invalidResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, invalidResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"4\"), invalidBody)\n\t// Response cached, returns updated response, entry = 4\n\n\t// Request id = 4 again without Cache-Control\n\tcachedInvalidReq := httptest.NewRequest(fiber.MethodGet, \"/?id=4\", http.NoBody)\n\tcachedInvalidResp, err := app.Test(cachedInvalidReq)\n\trequire.NoError(t, err)\n\tcachedInvalidBody, err := io.ReadAll(cachedInvalidResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedInvalidResp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, []byte(\"4\"), cachedInvalidBody)\n\t// Response cached previously, served from cache\n}\n\nfunc Test_Cache_WithSeveralRequests(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExpiration: 10 * time.Second,\n\t}))\n\n\tapp.Get(\"/:id\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(c.Params(\"id\"))\n\t})\n\n\tfor range 10 {\n\t\tfor i := range 10 {\n\t\t\tfunc(id int) {\n\t\t\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf(\"/%d\", id), http.NoBody))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tdefer func(body io.ReadCloser) {\n\t\t\t\t\tcloseErr := body.Close()\n\t\t\t\t\trequire.NoError(t, closeErr)\n\t\t\t\t}(rsp.Body)\n\n\t\t\t\tidFromServ, err := io.ReadAll(rsp.Body)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ta, err := strconv.Atoi(string(idFromServ))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Sometimes, the id is not equal to a\n\t\t\t\trequire.Equal(t, id, a)\n\t\t\t}(i)\n\t\t}\n\t}\n}\n\nfunc Test_Cache_Invalid_Expiration(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tcache := New(Config{Expiration: 0 * time.Second})\n\tapp.Use(cache)\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tcachedReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tcachedResp, err := app.Test(cachedReq)\n\trequire.NoError(t, err)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tcachedBody, err := io.ReadAll(cachedResp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, cachedBody, body)\n}\n\nfunc Test_Cache_Get(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t})\n\n\tapp.Get(\"/get\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodPost, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/?cache=12345\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"12345\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/get?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/get?cache=12345\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n}\n\nfunc Test_Cache_Post(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMethods: []string{fiber.MethodPost},\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t})\n\n\tapp.Get(\"/get\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodPost, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/?cache=12345\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/get?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/get?cache=12345\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"12345\", string(body))\n}\n\nfunc Test_Cache_NothingToCache(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Expiration: -(time.Second * 1)}))\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCached, err := io.ReadAll(respCached.Body)\n\trequire.NoError(t, err)\n\n\tif bytes.Equal(body, bodyCached) {\n\t\tt.Errorf(\"Cache should have expired: %s, %s\", body, bodyCached)\n\t}\n}\n\nfunc Test_Cache_CustomNext(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Response().StatusCode() != fiber.StatusOK\n\t\t},\n\t}))\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\terrorCount := 0\n\tapp.Get(\"/error\", func(c fiber.Ctx) error {\n\t\terrorCount++\n\t\treturn c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(errorCount))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCached, err := io.ReadAll(respCached.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Equal(body, bodyCached))\n\trequire.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))\n\n\t_, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/error\", http.NoBody))\n\trequire.NoError(t, err)\n\n\terrRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/error\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Empty(t, errRespCached.Header.Get(fiber.HeaderCacheControl))\n}\n\nfunc Test_CustomKey(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tvar called bool\n\tapp.Use(New(Config{KeyGenerator: func(c fiber.Ctx) string {\n\t\tcalled = true\n\t\treturn utils.CopyString(c.Path())\n\t}}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hi\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t_, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.True(t, called)\n}\n\nfunc Test_CustomExpiration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tvar called bool\n\tvar newCacheTime int\n\tapp.Use(New(Config{ExpirationGenerator: func(c fiber.Ctx, _ *Config) time.Duration {\n\t\tcalled = true\n\t\tvar err error\n\t\tnewCacheTime, err = strconv.Atoi(c.GetRespHeader(\"Cache-Time\", \"600\"))\n\t\trequire.NoError(t, err)\n\t\treturn time.Second * time.Duration(newCacheTime)\n\t}}))\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Response().Header.Add(\"Cache-Time\", \"1\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.True(t, called)\n\trequire.Equal(t, 1, newCacheTime)\n\n\t// Wait until the cache expires (timestamp tick can delay expiry detection slightly).\n\texpireDeadline := time.Now().Add(3 * time.Second)\n\tvar cachedResp *http.Response\n\tfor {\n\t\tcachedResp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\tif cachedResp.Header.Get(\"X-Cache\") != cacheHit {\n\t\t\tbreak\n\t\t}\n\t\trequire.True(t, time.Now().Before(expireDeadline), \"response remained cached beyond expected expiration\")\n\t\ttime.Sleep(50 * time.Millisecond)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tcachedBody, err := io.ReadAll(cachedResp.Body)\n\trequire.NoError(t, err)\n\n\tif bytes.Equal(body, cachedBody) {\n\t\tt.Errorf(\"Cache should have expired: %s, %s\", body, cachedBody)\n\t}\n\n\t// Next response should be cached\n\tcachedRespNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tcachedBodyNextRound, err := io.ReadAll(cachedRespNextRound.Body)\n\trequire.NoError(t, err)\n\n\tif !bytes.Equal(cachedBodyNextRound, cachedBody) {\n\t\tt.Errorf(\"Cache should not have expired: %s, %s\", cachedBodyNextRound, cachedBody)\n\t}\n}\n\nfunc Test_AdditionalE2EResponseHeaders(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tStoreResponseHeaders: true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.Add(\"X-Foobar\", \"foobar\")\n\t\treturn c.SendString(\"hi\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"foobar\", resp.Header.Get(\"X-Foobar\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"foobar\", resp.Header.Get(\"X-Foobar\"))\n}\n\nfunc Test_CacheHeader(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExpiration: 10 * time.Second,\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Response().StatusCode() != fiber.StatusOK\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t})\n\n\tcount := 0\n\tapp.Get(\"/error\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Response().Header.Add(\"Cache-Time\", \"1\")\n\t\treturn c.Status(fiber.StatusInternalServerError).SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/?cache=12345\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\n\terrRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/error\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, errRespCached.Header.Get(\"X-Cache\"))\n}\n\nfunc Test_Cache_WithHead(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tcount := 0\n\thandler := func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Response().Header.Add(\"Cache-Time\", \"1\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t}\n\n\tapp.RouteChain(\"/\").Get(handler).Head(handler)\n\n\treq := httptest.NewRequest(fiber.MethodHead, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tcachedReq := httptest.NewRequest(fiber.MethodHead, \"/\", http.NoBody)\n\tcachedResp, err := app.Test(cachedReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, cachedResp.Header.Get(\"X-Cache\"))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tcachedBody, err := io.ReadAll(cachedResp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, cachedBody, body)\n}\n\nfunc Test_Cache_WithHeadThenGet(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\thandler := func(c fiber.Ctx) error {\n\t\treturn c.SendString(fiber.Query[string](c, \"cache\"))\n\t}\n\tapp.RouteChain(\"/\").Get(handler).Head(handler)\n\n\theadResp, err := app.Test(httptest.NewRequest(fiber.MethodHead, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\theadBody, err := io.ReadAll(headResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, string(headBody))\n\trequire.Equal(t, cacheMiss, headResp.Header.Get(\"X-Cache\"))\n\n\theadResp, err = app.Test(httptest.NewRequest(fiber.MethodHead, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\theadBody, err = io.ReadAll(headResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, string(headBody))\n\trequire.Equal(t, cacheHit, headResp.Header.Get(\"X-Cache\"))\n\n\tgetResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tgetBody, err := io.ReadAll(getResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(getBody))\n\trequire.Equal(t, cacheMiss, getResp.Header.Get(\"X-Cache\"))\n\n\tgetResp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/?cache=123\", http.NoBody))\n\trequire.NoError(t, err)\n\tgetBody, err = io.ReadAll(getResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"123\", string(getBody))\n\trequire.Equal(t, cacheHit, getResp.Header.Get(\"X-Cache\"))\n}\n\nfunc Test_CustomCacheHeader(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tCacheHeader: \"Cache-Status\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"Cache-Status\"))\n}\n\nfunc Test_CacheInvalidation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tCacheInvalidator: func(c fiber.Ctx) bool {\n\t\t\treturn fiber.Query[bool](c, \"invalidate\")\n\t\t},\n\t}))\n\n\tcount := 0\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCached, err := io.ReadAll(respCached.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Equal(body, bodyCached))\n\trequire.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))\n\n\trespInvalidate, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?invalidate=true\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyInvalidate, err := io.ReadAll(respInvalidate.Body)\n\trequire.NoError(t, err)\n\trequire.NotEqual(t, body, bodyInvalidate)\n}\n\nfunc Test_CacheInvalidation_noCacheEntry(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"Cache Invalidator should not be called if no cache entry exist \", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tcacheInvalidatorExecuted := false\n\t\tapp.Use(New(Config{\n\t\t\tCacheInvalidator: func(c fiber.Ctx) bool {\n\t\t\t\tcacheInvalidatorExecuted = true\n\t\t\t\treturn fiber.Query[bool](c, \"invalidate\")\n\t\t\t},\n\t\t\tMaxBytes: 10 * 1024 * 1024,\n\t\t}))\n\t\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?invalidate=true\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, cacheInvalidatorExecuted)\n\t})\n}\n\nfunc Test_CacheInvalidation_removeFromHeap(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"Invalidate and remove from the heap\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tCacheInvalidator: func(c fiber.Ctx) bool {\n\t\t\t\treturn fiber.Query[bool](c, \"invalidate\")\n\t\t\t},\n\t\t\tMaxBytes: 10 * 1024 * 1024,\n\t\t}))\n\n\t\tcount := 0\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tcount++\n\t\t\treturn c.SendString(strconv.Itoa(count))\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\n\t\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\tbodyCached, err := io.ReadAll(respCached.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.True(t, bytes.Equal(body, bodyCached))\n\t\trequire.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))\n\n\t\trespInvalidate, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?invalidate=true\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\tbodyInvalidate, err := io.ReadAll(respInvalidate.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.NotEqual(t, body, bodyInvalidate)\n\t})\n}\n\nfunc Test_CacheStorage_CustomHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tStorage:  memory.New(),\n\t\tMaxBytes: 10 * 1024 * 1024,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.Set(\"Content-Type\", \"text/xml\")\n\t\tc.Response().Header.Set(\"Content-Encoding\", \"utf8\")\n\t\treturn c.Send([]byte(\"<xml><value>Test</value></xml>\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tbodyCached, err := io.ReadAll(respCached.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Equal(body, bodyCached))\n\trequire.NotEmpty(t, respCached.Header.Get(fiber.HeaderCacheControl))\n}\n\n// Because time points are updated once every X milliseconds, entries in tests can often have\n// equal expiration times and thus be in a random order. This closure hands out increasing\n// time intervals to maintain strong ascending order of expiration\nfunc stableAscendingExpiration() func(c1 fiber.Ctx, c2 *Config) time.Duration {\n\ti := 0\n\treturn func(_ fiber.Ctx, _ *Config) time.Duration {\n\t\ti++\n\t\treturn time.Hour * time.Duration(i)\n\t}\n}\n\nfunc Test_Cache_MaxBytesOrder(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tMaxBytes:            2,\n\t\tExpirationGenerator: stableAscendingExpiration(),\n\t}))\n\n\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"1\")\n\t})\n\n\tcases := [][]string{\n\t\t// Insert a, b into cache of size 2 bytes (responses are 1 byte)\n\t\t{\"/a\", cacheMiss},\n\t\t{\"/b\", cacheMiss},\n\t\t{\"/a\", cacheHit},\n\t\t{\"/b\", cacheHit},\n\t\t// Add c -> a evicted\n\t\t{\"/c\", cacheMiss},\n\t\t{\"/b\", cacheHit},\n\t\t// Add a again -> b evicted\n\t\t{\"/a\", cacheMiss},\n\t\t{\"/c\", cacheHit},\n\t\t// Add b -> c evicted\n\t\t{\"/b\", cacheMiss},\n\t\t{\"/c\", cacheMiss},\n\t}\n\n\tfor idx, tcase := range cases {\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tcase[1], rsp.Header.Get(\"X-Cache\"), \"Case %v\", idx)\n\t}\n}\n\nfunc Test_Cache_MaxBytesSizes(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMaxBytes:            7,\n\t\tExpirationGenerator: stableAscendingExpiration(),\n\t}))\n\n\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\tpath := c.RequestCtx().URI().LastPathSegment()\n\t\tsize, err := strconv.Atoi(string(path))\n\t\trequire.NoError(t, err)\n\t\treturn c.Send(make([]byte, size))\n\t})\n\n\tcases := [][]string{\n\t\t{\"/1\", cacheMiss},\n\t\t{\"/2\", cacheMiss},\n\t\t{\"/3\", cacheMiss},\n\t\t{\"/4\", cacheMiss}, // 1+2+3+4 > 7 => 1,2 are evicted now\n\t\t{\"/3\", cacheHit},\n\t\t{\"/1\", cacheMiss},\n\t\t{\"/2\", cacheMiss},\n\t\t{\"/8\", cacheUnreachable}, // too big to cache -> unreachable\n\t}\n\n\tfor idx, tcase := range cases {\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tcase[1], rsp.Header.Get(\"X-Cache\"), \"Case %v\", idx)\n\t}\n}\n\nfunc Test_Cache_UncacheableStatusCodes(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/:statusCode\", func(c fiber.Ctx) error {\n\t\tstatusCode, err := strconv.Atoi(c.Params(\"statusCode\"))\n\t\trequire.NoError(t, err)\n\t\treturn c.Status(statusCode).SendString(\"foo\")\n\t})\n\n\tuncacheableStatusCodes := []int{\n\t\t// Informational responses\n\t\tfiber.StatusContinue,\n\t\tfiber.StatusSwitchingProtocols,\n\t\tfiber.StatusProcessing,\n\t\tfiber.StatusEarlyHints,\n\n\t\t// Successful responses\n\t\tfiber.StatusCreated,\n\t\tfiber.StatusAccepted,\n\t\tfiber.StatusResetContent,\n\t\tfiber.StatusMultiStatus,\n\t\tfiber.StatusAlreadyReported,\n\t\tfiber.StatusIMUsed,\n\n\t\t// Redirection responses\n\t\tfiber.StatusFound,\n\t\tfiber.StatusSeeOther,\n\t\tfiber.StatusNotModified,\n\t\tfiber.StatusUseProxy,\n\t\tfiber.StatusSwitchProxy,\n\t\tfiber.StatusTemporaryRedirect,\n\n\t\t// Client error responses\n\t\tfiber.StatusBadRequest,\n\t\tfiber.StatusUnauthorized,\n\t\tfiber.StatusPaymentRequired,\n\t\tfiber.StatusForbidden,\n\t\tfiber.StatusNotAcceptable,\n\t\tfiber.StatusProxyAuthRequired,\n\t\tfiber.StatusRequestTimeout,\n\t\tfiber.StatusConflict,\n\t\tfiber.StatusLengthRequired,\n\t\tfiber.StatusPreconditionFailed,\n\t\tfiber.StatusRequestEntityTooLarge,\n\t\tfiber.StatusUnsupportedMediaType,\n\t\tfiber.StatusRequestedRangeNotSatisfiable,\n\t\tfiber.StatusExpectationFailed,\n\t\tfiber.StatusMisdirectedRequest,\n\t\tfiber.StatusUnprocessableEntity,\n\t\tfiber.StatusLocked,\n\t\tfiber.StatusFailedDependency,\n\t\tfiber.StatusTooEarly,\n\t\tfiber.StatusUpgradeRequired,\n\t\tfiber.StatusPreconditionRequired,\n\t\tfiber.StatusTooManyRequests,\n\t\tfiber.StatusRequestHeaderFieldsTooLarge,\n\t\tfiber.StatusTeapot,\n\t\tfiber.StatusUnavailableForLegalReasons,\n\n\t\t// Server error responses\n\t\tfiber.StatusInternalServerError,\n\t\tfiber.StatusBadGateway,\n\t\tfiber.StatusServiceUnavailable,\n\t\tfiber.StatusGatewayTimeout,\n\t\tfiber.StatusHTTPVersionNotSupported,\n\t\tfiber.StatusVariantAlsoNegotiates,\n\t\tfiber.StatusInsufficientStorage,\n\t\tfiber.StatusLoopDetected,\n\t\tfiber.StatusNotExtended,\n\t\tfiber.StatusNetworkAuthenticationRequired,\n\t}\n\tfor _, v := range uncacheableStatusCodes {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf(\"/%d\", v), http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\t\trequire.Equal(t, v, resp.StatusCode)\n\t}\n}\n\nfunc TestCacheAgeHeader(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"0\", resp.Header.Get(fiber.HeaderAge))\n\n\ttime.Sleep(4 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tage, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\trequire.NoError(t, err)\n\trequire.Positive(t, age)\n}\n\nfunc TestCacheUpstreamAge(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 3 * time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderAge, \"5\")\n\t\treturn c.SendString(\"hi\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"5\", resp.Header.Get(fiber.HeaderAge))\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, \"5\", resp.Header.Get(fiber.HeaderAge))\n}\n\nfunc Test_CacheRequestMaxAgeRevalidates(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 30 * time.Second,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|req-max-age-zero\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=30\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"max-age=0\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CacheExpiresFutureAllowsCaching(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tStoreResponseHeaders: true,\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderExpires, time.Now().Add(30*time.Second).UTC().Format(time.RFC1123))\n\t\treturn c.SendString(\"expires\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"expires1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"expires1\", string(body))\n}\n\nfunc Test_CacheExpiresPastPreventsCaching(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderExpires, time.Now().Add(-1*time.Minute).UTC().Format(time.RFC1123))\n\t\treturn c.SendString(\"expires\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"expires1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"expires2\", string(body))\n}\n\nfunc Test_CacheAllowsSharedCacheMustRevalidateWithAuthorization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 30 * time.Second,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|must-revalidate-auth\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"must-revalidate, max-age=60\")\n\t\treturn c.SendString(\"auth\" + strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"auth1\", string(body))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"auth1\", string(body))\n}\n\nfunc Test_CacheAllowsSharedCacheProxyRevalidateWithAuthorization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 30 * time.Second,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|proxy-revalidate-auth\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"proxy-revalidate, max-age=60\")\n\t\treturn c.SendString(\"proxy\" + strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxy1\", string(body))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxy1\", string(body))\n}\n\nfunc Test_CacheInvalidExpiresStoredAsStale(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCacheStorage()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 30 * time.Second,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|invalid-expires\"\n\t\t},\n\t\tStorage: storage,\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public\")\n\t\tc.Set(fiber.HeaderExpires, \"invalid-date\")\n\t\treturn c.SendString(\"body\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body1\", string(body))\n\n\texpectedKey := \"/|invalid-expires_GET\"\n\trequire.Contains(t, storage.data, expectedKey)\n\trequire.Contains(t, storage.data, expectedKey+\"_body\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body2\", string(body))\n\trequire.Contains(t, storage.data, expectedKey)\n\trequire.Contains(t, storage.data, expectedKey+\"_body\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body3\", string(body))\n\trequire.Contains(t, storage.data, expectedKey)\n\trequire.Contains(t, storage.data, expectedKey+\"_body\")\n}\n\nfunc Test_CacheSMaxAgeOverridesMaxAgeWhenShorter(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=10, s-maxage=1\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\ttime.Sleep(1700 * time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CacheSMaxAgeOverridesMaxAgeWhenLonger(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=1, s-maxage=2\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tfor time.Now().Nanosecond() >= int(100*time.Millisecond) {\n\t\ttime.Sleep(10 * time.Millisecond)\n\t}\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\ttime.Sleep(1200 * time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\ttime.Sleep(1700 * time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CacheOnlyIfCachedMiss(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"only-if-cached\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusGatewayTimeout, resp.StatusCode)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, 0, count)\n}\n\nfunc Test_CacheOnlyIfCachedStaleNotServed(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=1\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"only-if-cached\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusGatewayTimeout, resp.StatusCode)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, 1, count)\n}\n\nfunc Test_CacheMaxStaleServesStaleResponse(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=2\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\ttime.Sleep(2500 * time.Millisecond)\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"max-stale=5\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equalf(t, cacheHit, resp.Header.Get(\"X-Cache\"), \"dirs=%+v Age=%s count=%d\", parseRequestCacheControlString(\"max-stale=5\"), resp.Header.Get(fiber.HeaderAge), count)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\trequire.Equal(t, 1, count)\n}\n\nfunc Test_CacheMaxStaleRespectsMustRevalidate(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=1, must-revalidate\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"max-stale=30\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n\trequire.Equal(t, 2, count)\n}\n\nfunc Test_CacheMaxStaleRespectsProxyRevalidateSharedAuth(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"s-maxage=1, proxy-revalidate\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer abc\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer abc\")\n\treq.Header.Set(fiber.HeaderCacheControl, \"max-stale=30\")\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n\trequire.Equal(t, 2, count)\n}\n\nfunc Test_CachePreservesCacheControlHeaders(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\texpires := time.Now().Add(10 * time.Second).UTC().Format(http.TimeFormat)\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=5, immutable\")\n\t\tc.Set(fiber.HeaderExpires, expires)\n\t\tc.Set(fiber.HeaderETag, `W/\"abc\"`)\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, \"public, max-age=5, immutable\", resp.Header.Get(fiber.HeaderCacheControl))\n\trequire.Equal(t, expires, resp.Header.Get(fiber.HeaderExpires))\n\trequire.Equal(t, `W/\"abc\"`, resp.Header.Get(fiber.HeaderETag))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, \"public, max-age=5, immutable\", resp.Header.Get(fiber.HeaderCacheControl))\n\trequire.Equal(t, expires, resp.Header.Get(fiber.HeaderExpires))\n\trequire.Equal(t, `W/\"abc\"`, resp.Header.Get(fiber.HeaderETag))\n}\n\nfunc setResponseDate(date time.Time) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.Response().Header.Set(fiber.HeaderDate, date.UTC().Format(http.TimeFormat))\n\t\treturn nil\n\t}\n}\n\nfunc Test_CacheDateAndAgeHandling(t *testing.T) {\n\tt.Parallel()\n\n\ttype testCase struct {\n\t\tname             string\n\t\tcacheControl     string\n\t\tcacheHeader      string\n\t\tdateOffset       time.Duration\n\t\texpiration       time.Duration\n\t\texpectAgeAtLeast int\n\t\texpectCount      int\n\t\toriginAge        int\n\t}\n\n\tcases := []testCase{\n\t\t{\n\t\t\tname:             \"age derived from past date without Age header\",\n\t\t\tdateOffset:       -1 * time.Minute,\n\t\t\tcacheControl:     \"public, max-age=120\",\n\t\t\tcacheHeader:      cacheHit,\n\t\t\texpiration:       5 * time.Minute,\n\t\t\texpectAgeAtLeast: 1,\n\t\t\texpectCount:      1,\n\t\t},\n\t\t{\n\t\t\tname:         \"stale due to past date despite max-age\",\n\t\t\tdateOffset:   -90 * time.Second,\n\t\t\tcacheControl: \"public, max-age=30\",\n\t\t\tcacheHeader:  cacheUnreachable,\n\t\t\texpiration:   5 * time.Minute,\n\t\t\texpectCount:  2,\n\t\t\toriginAge:    90,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{Expiration: tc.expiration}))\n\t\t\tapp.Use(setResponseDate(time.Now().Add(tc.dateOffset).UTC()))\n\n\t\t\tvar count int\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tcount++\n\t\t\t\tif tc.originAge > 0 {\n\t\t\t\t\tc.Response().Header.Set(fiber.HeaderAge, strconv.Itoa(tc.originAge))\n\t\t\t\t}\n\t\t\t\tc.Set(fiber.HeaderCacheControl, tc.cacheControl)\n\t\t\t\treturn c.SendString(strconv.Itoa(count))\n\t\t\t})\n\n\t\t\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.cacheHeader == cacheHit {\n\t\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t}\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.cacheHeader, resp.Header.Get(\"X-Cache\"))\n\t\t\tif tc.cacheHeader == cacheHit {\n\t\t\t\tageVal, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.GreaterOrEqual(t, ageVal, tc.expectAgeAtLeast)\n\t\t\t\trequire.Equal(t, 1, count)\n\t\t\t} else {\n\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Equal(t, strconv.Itoa(tc.expectCount), string(body))\n\t\t\t\trequire.Equal(t, tc.expectCount, count)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_CacheClampsInvalidStoredDate(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newMutatingStorage(func(key string, val []byte) []byte {\n\t\tif strings.HasSuffix(key, \"_body\") {\n\t\t\treturn val\n\t\t}\n\n\t\tvar it item\n\t\tif _, err := it.UnmarshalMsg(val); err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\tit.date = uint64(math.MaxInt64) + 1024\n\t\tupdated, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\treturn updated\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: time.Minute,\n\t\tStorage:    storage,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\tparsedDate, err := http.ParseTime(resp.Header.Get(fiber.HeaderDate))\n\trequire.NoError(t, err)\n\trequire.WithinDuration(t, time.Now(), parsedDate, time.Minute)\n\n\tageVal, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\trequire.NoError(t, err)\n\trequire.Less(t, ageVal, 60)\n\trequire.GreaterOrEqual(t, ageVal, 0)\n}\n\nfunc Test_CacheClampsFutureStoredDate(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newMutatingStorage(func(key string, val []byte) []byte {\n\t\tif strings.HasSuffix(key, \"_body\") {\n\t\t\treturn val\n\t\t}\n\n\t\tvar it item\n\t\tif _, err := it.UnmarshalMsg(val); err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\tfuture := time.Now().Add(2 * time.Second).UTC()\n\t\tsec := future.Unix()\n\t\tif sec < 0 {\n\t\t\tsec = 0\n\t\t}\n\n\t\tit.date = uint64(sec) //nolint:gosec // safe: sec is clamped to non-negative range\n\t\tupdated, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\treturn updated\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: time.Minute,\n\t\tStorage:    storage,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\tparsedDate, err := http.ParseTime(resp.Header.Get(fiber.HeaderDate))\n\trequire.NoError(t, err)\n\trequire.False(t, parsedDate.After(time.Now()))\n\n\tageVal, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\trequire.NoError(t, err)\n\trequire.GreaterOrEqual(t, ageVal, 0)\n}\n\nfunc Test_RequestPragmaNoCacheTriggersMiss(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: time.Minute,\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"body\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body1\", string(body))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderPragma, \"no-cache\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body2\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body2\", string(body))\n}\n\nfunc Test_CacheStaleResponseAddsWarning110(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 2 * time.Second,\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=1\")\n\t\treturn c.SendString(\"body\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"max-stale=5\")\n\n\t// Wait for the cached response to become stale (max-age=1)\n\t// Add extra time to ensure the entry has expired\n\ttime.Sleep(1200 * time.Millisecond)\n\n\tdeadline := time.Now().Add(3 * time.Second)\n\tfor {\n\t\tresp, err = app.Test(req)\n\t\trequire.NoError(t, err)\n\t\tif resp.Header.Get(\"X-Cache\") == cacheHit {\n\t\t\tageVal, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\t\t\trequire.NoError(t, err)\n\t\t\tif ageVal >= 1 {\n\t\t\t\t// Check that Warning header is present before breaking\n\t\t\t\twarnings := resp.Header.Values(fiber.HeaderWarning)\n\t\t\t\tif len(warnings) > 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\trequire.True(t, time.Now().Before(deadline), \"response did not become stale before deadline\")\n\t\ttime.Sleep(50 * time.Millisecond)\n\t}\n\n\twarnings := resp.Header.Values(fiber.HeaderWarning)\n\trequire.NotEmpty(t, warnings, \"Warning header should be present when serving stale response\")\n\tfound := false\n\tfor _, w := range warnings {\n\t\tif strings.Contains(w, \"110\") {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\trequire.True(t, found, \"warning 110 not found in %v\", warnings)\n}\n\nfunc Test_CacheHeuristicFreshnessAddsWarning113(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 2 * time.Second,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"body\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\tfor _, w := range resp.Header.Values(fiber.HeaderWarning) {\n\t\trequire.NotContains(t, w, \"113\", \"warning 113 should not be present for explicitly fresh responses\")\n\t}\n}\n\nfunc Test_CacheHeuristicFreshnessAddsWarning113AfterThreshold(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newMutatingStorage(func(key string, val []byte) []byte {\n\t\tif strings.HasSuffix(key, \"_body\") {\n\t\t\treturn val\n\t\t}\n\n\t\tvar it item\n\t\tif _, err := it.UnmarshalMsg(val); err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\toldDate := time.Now().Add(-25 * time.Hour).UTC()\n\t\tsec := oldDate.Unix()\n\t\tif sec < 0 {\n\t\t\tsec = 0\n\t\t}\n\t\tit.date = uint64(sec) //nolint:gosec // safe: sec is clamped to non-negative range\n\n\t\tfuture := time.Now().Add(48 * time.Hour).UTC()\n\t\texpSec := future.Unix()\n\t\tif expSec < 0 {\n\t\t\texpSec = 0\n\t\t}\n\t\tit.exp = uint64(expSec) //nolint:gosec // safe: expSec is clamped to non-negative range\n\t\tit.ttl = uint64((48 * time.Hour) / time.Second)\n\n\t\tupdated, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\treturn updated\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: 2 * time.Second,\n\t\tStorage:    storage,\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(\"body\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\twarnings := resp.Header.Values(fiber.HeaderWarning)\n\trequire.NotEmpty(t, warnings)\n\tfound := false\n\tfor _, w := range warnings {\n\t\tif strings.Contains(w, \"113\") {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\trequire.True(t, found, \"warning 113 not found in %v\", warnings)\n}\n\nfunc Test_CacheAgeHeaderIsCappedAtMaxDeltaSeconds(t *testing.T) {\n\tt.Parallel()\n\n\tconst veryLargeAge = uint64(math.MaxInt32) + 1000\n\tstorage := newMutatingStorage(func(key string, val []byte) []byte {\n\t\tif strings.HasSuffix(key, \"_body\") {\n\t\t\treturn val\n\t\t}\n\n\t\tvar it item\n\t\tif _, err := it.UnmarshalMsg(val); err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\tit.age = veryLargeAge\n\t\tupdated, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\treturn val\n\t\t}\n\n\t\treturn updated\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration: time.Minute,\n\t\tStorage:    storage,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"body\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\n\tageVal, err := strconv.Atoi(resp.Header.Get(fiber.HeaderAge))\n\trequire.NoError(t, err)\n\trequire.Equal(t, math.MaxInt32, ageVal)\n}\n\nfunc Test_CacheMinFreshForcesRevalidation(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=5\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"min-fresh=10\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equalf(t, cacheMiss, resp.Header.Get(\"X-Cache\"), \"dirs=%+v Age=%s count=%d\", parseRequestCacheControlString(\"min-fresh=10\"), resp.Header.Get(fiber.HeaderAge), count)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CachePermanentRedirectCached(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExpiration:           30 * time.Second,\n\t\tStoreResponseHeaders: true,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|status-308\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=30\")\n\t\tc.Set(fiber.HeaderLocation, \"/dest\")\n\t\treturn c.Status(fiber.StatusPermanentRedirect).SendString(\"redir\" + strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusPermanentRedirect, resp.StatusCode)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"redir1\", string(body))\n\trequire.Equal(t, \"/dest\", resp.Header.Get(fiber.HeaderLocation))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\trequire.Equal(t, fiber.StatusPermanentRedirect, resp.StatusCode)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"redir1\", string(body))\n\trequire.Equal(t, \"/dest\", resp.Header.Get(fiber.HeaderLocation))\n}\n\nfunc Test_CacheNoStoreDirective(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n}\n\nfunc Test_CacheNoCacheDirective(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"no-cache\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CacheNoCacheDirectiveOverridesExistingEntry(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar noCacheMode atomic.Bool\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tif noCacheMode.Load() {\n\t\t\tc.Set(fiber.HeaderCacheControl, \"no-cache\")\n\t\t\treturn c.SendString(\"no-cache\")\n\t\t}\n\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"cacheable\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"cacheable\", string(body))\n\n\tnoCacheMode.Store(true)\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"no-cache\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"no-cache\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"no-cache\", string(body))\n}\n\nfunc Test_CacheRespectsUpstreamAgeForFreshness(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"skipsCachingWhenAgeExhaustsFreshness\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\t\treturn c.Path() + \"|age-exhausted\"\n\t\t\t},\n\t\t}))\n\n\t\tvar count int\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tcount++\n\t\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=2\")\n\t\t\tc.Set(fiber.HeaderAge, \"2\")\n\t\t\treturn c.SendString(strconv.Itoa(count))\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"1\", string(body))\n\n\t\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"2\", string(body))\n\t})\n\n\tt.Run(\"expiresAfterRemainingLifetime\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\t\treturn c.Path() + \"|age-remaining\"\n\t\t\t},\n\t\t}))\n\n\t\tvar count int\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tcount++\n\t\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=2\")\n\t\t\tc.Set(fiber.HeaderAge, \"1\")\n\t\t\treturn c.SendString(strconv.Itoa(count))\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"1\", string(body))\n\n\t\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"1\", string(body))\n\n\t\ttime.Sleep(1500 * time.Millisecond)\n\n\t\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\t\tbody, err = io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"2\", string(body))\n\t})\n}\n\nfunc Test_CacheVarySeparatesVariants(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|vary-separated\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderVary, fiber.HeaderAcceptLanguage)\n\t\treturn c.SendString(c.Get(fiber.HeaderAcceptLanguage) + strconv.Itoa(count))\n\t})\n\n\treqEN := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqEN.Header.Set(fiber.HeaderAcceptLanguage, \"en\")\n\tresp, err := app.Test(reqEN)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"en1\", string(body))\n\n\treqFR := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqFR.Header.Set(fiber.HeaderAcceptLanguage, \"fr\")\n\tresp, err = app.Test(reqFR)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"fr2\", string(body))\n\n\treqENRepeat := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqENRepeat.Header.Set(fiber.HeaderAcceptLanguage, \"en\")\n\tresp, err = app.Test(reqENRepeat)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"en1\", string(body))\n\n\treqFRRepeat := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqFRRepeat.Header.Set(fiber.HeaderAcceptLanguage, \"fr\")\n\tresp, err = app.Test(reqFRRepeat)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"fr2\", string(body))\n}\n\nfunc Test_CacheVaryStarUncacheable(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path() + \"|vary-star\"\n\t\t},\n\t}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderVary, \"*\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CachePrivateDirective(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"private\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CachePrivateDirectiveWithAuthorization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"private\")\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc Test_CachePrivateDirectiveInvalidatesExistingEntry(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar privateMode atomic.Bool\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tif privateMode.Load() {\n\t\t\tc.Set(fiber.HeaderCacheControl, \"private\")\n\t\t\treturn c.SendString(\"private\")\n\t\t}\n\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"public\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"public\", string(body))\n\n\tprivateMode.Store(true)\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderCacheControl, \"no-cache\")\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"private\", string(body))\n\n\tprivateMode.Store(false)\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"public\", string(body))\n}\n\nfunc Test_CacheControlNotOverwritten(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 10 * time.Second, StoreResponseHeaders: true}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"private\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"private\", resp.Header.Get(fiber.HeaderCacheControl))\n}\n\nfunc Test_CacheMaxAgeDirective(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderCacheControl, \"max-age=1\")\n\t\treturn c.SendString(\"1\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1500 * time.Millisecond)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n}\n\nfunc Test_ParseMaxAge(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\theader string\n\t\texpect time.Duration\n\t\tok     bool\n\t}{\n\t\t{\"max-age=60\", 60 * time.Second, true},\n\t\t{\"public, max-age=86400\", 86400 * time.Second, true},\n\t\t{\"no-store\", 0, false},\n\t\t{\"max-age=invalid\", 0, false},\n\t\t{\"public, s-maxage=100, max-age=50\", 50 * time.Second, true},\n\t\t{\"MAX-AGE=20\", 20 * time.Second, true},\n\t\t{\"public , max-age=0\", 0, true},\n\t\t{\"public , max-age\", 0, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.header, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\td, ok := parseMaxAge(tt.header)\n\t\t\tif tt.ok != ok {\n\t\t\t\tt.Fatalf(\"expected ok=%v got %v\", tt.ok, ok)\n\t\t\t}\n\t\t\tif ok && d != tt.expect {\n\t\t\t\tt.Fatalf(\"expected %v got %v\", tt.expect, d)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_AllowsSharedCache(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tdirectives string\n\t\texpect     bool\n\t}{\n\t\t{\"public\", true},\n\t\t{\"private\", false},\n\t\t{\"s-maxage=60\", true},\n\t\t{\"public, max-age=60\", true},\n\t\t{\"public, must-revalidate\", true},\n\t\t{\"max-age=60\", false},\n\t\t{\"no-cache\", false},\n\t\t{\"no-cache, s-maxage=60\", true},\n\t\t{\"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.directives, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := allowsSharedCache(tt.directives)\n\t\t\trequire.Equal(t, tt.expect, got, \"directives: %q\", tt.directives)\n\t\t})\n\t}\n\n\tt.Run(\"private overrules public\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tgot := allowsSharedCache(strings.ToUpper(\"private, public\"))\n\t\trequire.False(t, got)\n\t})\n}\n\nfunc TestCacheSkipsAuthorizationByDefault(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n}\n\nfunc TestCacheBypassesExistingEntryForAuthorization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\tnonAuthReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\n\tresp, err := app.Test(nonAuthReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n\n\tauthReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tauthReq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\tresp, err = app.Test(authReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheUnreachable, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"2\", string(body))\n\n\tresp, err = app.Test(nonAuthReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"1\", string(body))\n}\n\nfunc TestCacheAllowsSharedCacheWithAuthorization(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ok\", string(body))\n\trequire.Equal(t, 1, count)\n}\n\nfunc TestCacheAllowsAuthorizationWithRevalidateDirectives(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tcacheControl  string\n\t\texpires       string\n\t\texpectedBody  string\n\t\texpectedBody2 string\n\t\texpectFirst   string\n\t\texpectSecond  string\n\t}{\n\t\t{\n\t\t\tname:          \"must-revalidate\",\n\t\t\tcacheControl:  \"must-revalidate, max-age=60\",\n\t\t\texpectedBody:  \"ok-1\",\n\t\t\texpectedBody2: \"ok-1\",\n\t\t\texpectFirst:   cacheMiss,\n\t\t\texpectSecond:  cacheHit,\n\t\t},\n\t\t{\n\t\t\tname:          \"proxy-revalidate\",\n\t\t\tcacheControl:  \"proxy-revalidate, max-age=60\",\n\t\t\texpectedBody:  \"ok-1\",\n\t\t\texpectedBody2: \"ok-1\",\n\t\t\texpectFirst:   cacheMiss,\n\t\t\texpectSecond:  cacheHit,\n\t\t},\n\t\t{\n\t\t\tname:          \"expires header\",\n\t\t\tcacheControl:  \"\",\n\t\t\texpires:       time.Now().Add(1 * time.Minute).UTC().Format(http.TimeFormat),\n\t\t\texpectedBody:  \"ok-1\",\n\t\t\texpectedBody2: \"ok-2\",\n\t\t\texpectFirst:   cacheUnreachable,\n\t\t\texpectSecond:  cacheUnreachable,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\n\t\t\tvar count int\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tcount++\n\t\t\t\tc.Set(fiber.HeaderCacheControl, tt.cacheControl)\n\t\t\t\tif tt.expires != \"\" {\n\t\t\t\t\tc.Set(fiber.HeaderExpires, tt.expires)\n\t\t\t\t}\n\t\t\t\treturn c.SendString(fmt.Sprintf(\"ok-%d\", count))\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\t\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer token\")\n\n\t\t\tresp, err := app.Test(req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectFirst, resp.Header.Get(\"X-Cache\"))\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedBody, string(body))\n\n\t\t\tresp, err = app.Test(req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectSecond, resp.Header.Get(\"X-Cache\"))\n\t\t\tbody, err = io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedBody2, string(body))\n\n\t\t\tif tt.expectSecond == cacheHit {\n\t\t\t\trequire.Equal(t, 1, count)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, 2, count)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCacheSeparatesAuthorizationValues(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{Expiration: 10 * time.Second}))\n\n\tvar count int\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(fiber.HeaderCacheControl, \"public, max-age=60\")\n\t\treturn c.SendString(fmt.Sprintf(\"body-%d-%s\", count, c.Get(fiber.HeaderAuthorization)))\n\t})\n\n\tnewRequest := func(token string) *http.Request {\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+token)\n\t\treturn req\n\t}\n\n\tauthTokenA := \"token-a\"\n\tauthTokenB := \"token-b\"\n\n\tresp, err := app.Test(newRequest(authTokenA))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body-1-Bearer \"+authTokenA, string(body))\n\n\tresp, err = app.Test(newRequest(authTokenA))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body-1-Bearer \"+authTokenA, string(body))\n\trequire.Equal(t, 1, count)\n\n\tresp, err = app.Test(newRequest(authTokenB))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body-2-Bearer \"+authTokenB, string(body))\n\trequire.Equal(t, 2, count)\n\n\tresp, err = app.Test(newRequest(authTokenB))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body-2-Bearer \"+authTokenB, string(body))\n\n\tresp, err = app.Test(newRequest(authTokenA))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheHit, resp.Header.Get(\"X-Cache\"))\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"body-1-Bearer \"+authTokenA, string(body))\n\trequire.Equal(t, 2, count)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4\nfunc Benchmark_Cache(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/demo\", func(c fiber.Ctx) error {\n\t\tdata, _ := os.ReadFile(\"../../.github/README.md\") //nolint:errcheck // We're inside a benchmark\n\t\treturn c.Status(fiber.StatusTeapot).Send(data)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/demo\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n\trequire.Greater(b, len(fctx.Response.Body()), 30000)\n}\n\nfunc Benchmark_Cache_Miss(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\tdata, _ := os.ReadFile(\"../../.github/README.md\") //nolint:errcheck // We're inside a benchmark\n\t\treturn c.Status(fiber.StatusOK).Send(data)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tvar n int\n\tfor b.Loop() {\n\t\tn++\n\t\tfctx.Request.SetRequestURI(\"/demo/\" + strconv.Itoa(n))\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusOK, fctx.Response.Header.StatusCode())\n\trequire.Greater(b, len(fctx.Response.Body()), 30000)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Cache_Storage -benchmem -count=4\nfunc Benchmark_Cache_Storage(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStorage: memory.New(),\n\t}))\n\n\tapp.Get(\"/demo\", func(c fiber.Ctx) error {\n\t\tdata, _ := os.ReadFile(\"../../.github/README.md\") //nolint:errcheck // We're inside a benchmark\n\t\treturn c.Status(fiber.StatusTeapot).Send(data)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/demo\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n\trequire.Greater(b, len(fctx.Response.Body()), 30000)\n}\n\nfunc Benchmark_Cache_AdditionalHeaders(b *testing.B) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tStoreResponseHeaders: true,\n\t}))\n\n\tapp.Get(\"/demo\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.Add(\"X-Foobar\", \"foobar\")\n\t\treturn c.SendStatus(418)\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/demo\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n\trequire.Equal(b, []byte(\"foobar\"), fctx.Response.Header.Peek(\"X-Foobar\"))\n}\n\nfunc Benchmark_Cache_MaxSize(b *testing.B) {\n\t// The benchmark is run with three different MaxSize parameters\n\t// 1) 0:        Tracking is disabled = no overhead\n\t// 2) MaxInt32: Enough to store all entries = no removals\n\t// 3) 100:      Small size = constant insertions and removals\n\tcases := []uint{0, math.MaxUint32, 100}\n\tnames := []string{\"Disabled\", \"Unlim\", \"LowBounded\"}\n\tfor i, size := range cases {\n\t\tb.Run(names[i], func(b *testing.B) {\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{MaxBytes: size}))\n\n\t\t\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"1\")\n\t\t\t})\n\n\t\t\th := app.Handler()\n\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\n\t\t\tb.ReportAllocs()\n\n\t\t\tn := 0\n\t\t\tfor b.Loop() {\n\t\t\t\tn++\n\t\t\t\tfctx.Request.SetRequestURI(fmt.Sprintf(\"/%v\", n))\n\t\t\t\th(fctx)\n\t\t\t}\n\n\t\t\trequire.Equal(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode())\n\t\t})\n\t}\n}\n\nfunc Test_Cache_RevalidationWithMaxBytes(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"max-age=0 revalidation removes old entry on storage success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes: 100,\n\t\t}))\n\n\t\trequestCount := 0\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\trequestCount++\n\t\t\tc.Set(fiber.HeaderCacheControl, \"max-age=60\")\n\t\t\treturn c.SendString(fmt.Sprintf(\"response-%d\", requestCount))\n\t\t})\n\n\t\t// First request - cache the response\n\t\treq1 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp1, err := app.Test(req1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp1.Header.Get(\"X-Cache\"))\n\n\t\t// Request with max-age=0 to force revalidation\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq2.Header.Set(fiber.HeaderCacheControl, \"max-age=0\")\n\t\tresp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\tbody2, err := io.ReadAll(resp2.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"response-2\", string(body2))\n\n\t\t// Next request should serve the NEW cached entry\n\t\treq3 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp3, err := app.Test(req3)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp3.Header.Get(\"X-Cache\"))\n\t\tbody3, err := io.ReadAll(resp3.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"response-2\", string(body3), \"New entry should be cached\")\n\t})\n\n\tt.Run(\"min-fresh revalidation with MaxBytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes: 100,\n\t\t}))\n\n\t\trequestCount := 0\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\trequestCount++\n\t\t\tc.Set(fiber.HeaderCacheControl, \"max-age=2\")\n\t\t\treturn c.SendString(fmt.Sprintf(\"response-%d\", requestCount))\n\t\t})\n\n\t\t// First request - cache the response\n\t\treq1 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp1, err := app.Test(req1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp1.Header.Get(\"X-Cache\"))\n\n\t\t// Wait a bit so the entry has aged\n\t\ttime.Sleep(1 * time.Second)\n\n\t\t// Request with min-fresh that exceeds remaining freshness\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq2.Header.Set(fiber.HeaderCacheControl, \"min-fresh=5\")\n\t\tresp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\tbody2, err := io.ReadAll(resp2.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"response-2\", string(body2))\n\n\t\t// Next request should serve the NEW cached entry\n\t\treq3 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp3, err := app.Test(req3)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp3.Header.Get(\"X-Cache\"))\n\t\tbody3, err := io.ReadAll(resp3.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"response-2\", string(body3))\n\t})\n\n\tt.Run(\"revalidation respects MaxBytes eviction\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes:            20, // Only room for 2 responses of 10 bytes each\n\t\t\tExpirationGenerator: stableAscendingExpiration(),\n\t\t}))\n\n\t\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\t\tc.Set(fiber.HeaderCacheControl, \"max-age=60\")\n\t\t\treturn c.SendString(\"1234567890\") // 10 bytes\n\t\t})\n\n\t\t// Cache /a and /b\n\t\treq1 := httptest.NewRequest(fiber.MethodGet, \"/a\", http.NoBody)\n\t\tresp1, err := app.Test(req1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp1.Header.Get(\"X-Cache\"))\n\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/b\", http.NoBody)\n\t\tresp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp2.Header.Get(\"X-Cache\"))\n\n\t\t// Both should be cached\n\t\treq3 := httptest.NewRequest(fiber.MethodGet, \"/a\", http.NoBody)\n\t\tresp3, err := app.Test(req3)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp3.Header.Get(\"X-Cache\"))\n\n\t\treq4 := httptest.NewRequest(fiber.MethodGet, \"/b\", http.NoBody)\n\t\tresp4, err := app.Test(req4)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp4.Header.Get(\"X-Cache\"))\n\n\t\t// Revalidate /a with max-age=0\n\t\treq5 := httptest.NewRequest(fiber.MethodGet, \"/a\", http.NoBody)\n\t\treq5.Header.Set(fiber.HeaderCacheControl, \"max-age=0\")\n\t\t_, err = app.Test(req5)\n\t\trequire.NoError(t, err)\n\n\t\t// /a should be revalidated and cached again\n\t\treq6 := httptest.NewRequest(fiber.MethodGet, \"/a\", http.NoBody)\n\t\tresp6, err := app.Test(req6)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp6.Header.Get(\"X-Cache\"))\n\n\t\t// /b should still be cached (heap accounting should be correct)\n\t\treq7 := httptest.NewRequest(fiber.MethodGet, \"/b\", http.NoBody)\n\t\tresp7, err := app.Test(req7)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp7.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"revalidation with non-cacheable response preserves old entry\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes: 100,\n\t\t}))\n\n\t\trequestCount := 0\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\trequestCount++\n\t\t\tif requestCount == 1 {\n\t\t\t\tc.Set(fiber.HeaderCacheControl, \"max-age=60\")\n\t\t\t\treturn c.SendString(\"cacheable\")\n\t\t\t}\n\t\t\t// Second request returns no-store\n\t\t\tc.Set(fiber.HeaderCacheControl, \"no-store\")\n\t\t\treturn c.SendString(\"not-cacheable\")\n\t\t})\n\n\t\t// First request - cache the response\n\t\treq1 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp1, err := app.Test(req1)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, resp1.Header.Get(\"X-Cache\"))\n\t\tbody1, err := io.ReadAll(resp1.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"cacheable\", string(body1))\n\n\t\t// Request with max-age=0 to force revalidation\n\t\t// The new response will be no-store (not cacheable)\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq2.Header.Set(fiber.HeaderCacheControl, \"max-age=0\")\n\t\tresp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\tbody2, err := io.ReadAll(resp2.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"not-cacheable\", string(body2))\n\n\t\t// Next request should still serve the OLD cached entry\n\t\t// because the new response was not cacheable and old entry should remain tracked\n\t\treq3 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\tresp3, err := app.Test(req3)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, resp3.Header.Get(\"X-Cache\"))\n\t\tbody3, err := io.ReadAll(resp3.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"cacheable\", string(body3), \"Old entry should still be cached\")\n\t})\n}\n\n// Test_parseCacheControlDirectives_QuotedStrings tests RFC 9111 Section 5.2 compliance\n// for quoted-string values in Cache-Control directives\nfunc Test_parseCacheControlDirectives_QuotedStrings(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\texpected map[string]string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"simple quoted value\",\n\t\t\tinput: `community=\"UCI\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"community\": \"UCI\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple directives with quoted values\",\n\t\t\tinput: `max-age=3600, community=\"UCI\", custom=\"value\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"max-age\":   \"3600\",\n\t\t\t\t\"community\": \"UCI\",\n\t\t\t\t\"custom\":    \"value\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quoted value with spaces\",\n\t\t\tinput: `custom=\"value with spaces\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"custom\": \"value with spaces\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quoted value with escaped quote\",\n\t\t\tinput: `custom=\"value with \\\"quotes\\\"\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"custom\": `value with \"quotes\"`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quoted value with escaped backslash\",\n\t\t\tinput: `custom=\"value with \\\\ backslash\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"custom\": `value with \\ backslash`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"mixed quoted and unquoted values\",\n\t\t\tinput: `max-age=3600, community=\"UCI\", no-cache, custom=\"test\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"max-age\":   \"3600\",\n\t\t\t\t\"community\": \"UCI\",\n\t\t\t\t\"no-cache\":  \"\",\n\t\t\t\t\"custom\":    \"test\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"quoted empty value\",\n\t\t\tinput: `custom=\"\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"custom\": \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"spaces around quoted value\",\n\t\t\tinput: `custom = \"value\" , another=\"test\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"custom\":  \"value\",\n\t\t\t\t\"another\": \"test\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"unquoted token value\",\n\t\t\tinput: `max-age=3600`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"max-age\": \"3600\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"complex mixed case\",\n\t\t\tinput: `max-age=3600, s-maxage=7200, community=\"UCI\", no-store, custom=\"value with \\\"escaped\\\" quotes\"`,\n\t\t\texpected: map[string]string{\n\t\t\t\t\"max-age\":   \"3600\",\n\t\t\t\t\"s-maxage\":  \"7200\",\n\t\t\t\t\"community\": \"UCI\",\n\t\t\t\t\"no-store\":  \"\",\n\t\t\t\t\"custom\":    `value with \"escaped\" quotes`,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := make(map[string]string)\n\t\t\tparseCacheControlDirectives([]byte(tt.input), func(key, value []byte) {\n\t\t\t\tresult[string(key)] = string(value)\n\t\t\t})\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\n// Test_unquoteCacheDirective tests the unquoting logic for quoted-string values\nfunc Test_unquoteCacheDirective(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []byte\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname:     \"simple quoted string\",\n\t\t\tinput:    []byte(`\"value\"`),\n\t\t\texpected: []byte(\"value\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"empty quoted string\",\n\t\t\tinput:    []byte(`\"\"`),\n\t\t\texpected: []byte(\"\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"quoted string with spaces\",\n\t\t\tinput:    []byte(`\"value with spaces\"`),\n\t\t\texpected: []byte(\"value with spaces\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"quoted string with escaped quote\",\n\t\t\tinput:    []byte(`\"value with \\\"quote\\\"\"`),\n\t\t\texpected: []byte(`value with \"quote\"`),\n\t\t},\n\t\t{\n\t\t\tname:     \"quoted string with escaped backslash\",\n\t\t\tinput:    []byte(`\"value with \\\\ backslash\"`),\n\t\t\texpected: []byte(`value with \\ backslash`),\n\t\t},\n\t\t{\n\t\t\tname:     \"quoted string with multiple escapes\",\n\t\t\tinput:    []byte(`\"a\\\"b\\\\c\\\"d\"`),\n\t\t\texpected: []byte(`a\"b\\c\"d`),\n\t\t},\n\t\t{\n\t\t\tname:     \"too short input\",\n\t\t\tinput:    []byte(`\"`),\n\t\t\texpected: []byte(`\"`),\n\t\t},\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []byte(``),\n\t\t\texpected: []byte(``),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := unquoteCacheDirective(tt.input)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\n// Test_Cache_MaxBytes_InsufficientSpace tests the \"insufficient space\" error path\n// when an entry is larger than MaxBytes, ensuring such entries are treated as unreachable\nfunc Test_Cache_MaxBytes_InsufficientSpace(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"entry larger than MaxBytes with empty cache\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes:   10, // Very small cache\n\t\t\tExpiration: 1 * time.Hour,\n\t\t}))\n\n\t\tapp.Get(\"/large\", func(c fiber.Ctx) error {\n\t\t\t// Return data larger than MaxBytes\n\t\t\treturn c.Send(make([]byte, 20))\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/large\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\t// Should be unreachable because entry is too large\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"entry larger than MaxBytes after eviction\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes:            15,\n\t\t\tExpirationGenerator: stableAscendingExpiration(),\n\t\t}))\n\n\t\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\t\tpath := c.Path()\n\t\t\tif path == \"/small\" {\n\t\t\t\treturn c.Send(make([]byte, 5))\n\t\t\t}\n\t\t\treturn c.Send(make([]byte, 20))\n\t\t})\n\n\t\t// Cache a small entry first\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/small\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Try to cache a large entry - should return unreachable since it won't fit even after eviction\n\t\trsp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/large\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n}\n\nfunc Test_Cache_MaxBytes_DeletionFailureRestoresTracking(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCacheStorage()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tMaxBytes:   4,\n\t\tExpiration: 1 * time.Hour,\n\t\tStorage:    storage,\n\t}))\n\n\tapp.Get(\"/:name\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"data\")\n\t})\n\n\t// Seed the cache with a single entry\n\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/first\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\tvar storedKeys []string\n\tstorage.mu.Lock()\n\tfor key := range storage.data {\n\t\tstoredKeys = append(storedKeys, key)\n\t\tif strings.Contains(key, \"/first\") {\n\t\t\tstorage.errs[\"del|\"+key] = errors.New(\"delete failed\")\n\t\t}\n\t}\n\tstorage.mu.Unlock()\n\tt.Logf(\"stored keys after first cache: %v\", storedKeys)\n\n\t// Next request triggers eviction; deletion failure should surface an error\n\trsp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/second\", http.NoBody))\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(rsp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, rsp.StatusCode)\n\trequire.Contains(t, string(body), \"failed to delete key\")\n\trequire.NoError(t, rsp.Body.Close())\n\tvar remainingKeys []string\n\tstorage.mu.RLock()\n\tfor key := range storage.data {\n\t\tremainingKeys = append(remainingKeys, key)\n\t}\n\tstorage.mu.RUnlock()\n\tt.Logf(\"stored keys after deletion failure: %v\", remainingKeys)\n\tstorage.mu.Lock()\n\tstorage.errs = make(map[string]error)\n\tstorage.mu.Unlock()\n\n\t// Another request should succeed and be cacheable after restoring heap tracking\n\trsp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/third\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\trequire.NoError(t, rsp.Body.Close())\n}\n\n// Test_Cache_MaxBytes_ConcurrencyAndRaceConditions tests that the race condition fix works correctly\n// under concurrent load, verifying that storedBytes never exceeds MaxBytes even with multiple\n// goroutines making simultaneous requests\nfunc Test_Cache_MaxBytes_ConcurrencyAndRaceConditions(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"concurrent requests with MaxBytes limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tconst maxBytes = uint(1000)\n\t\tconst numGoroutines = 20\n\t\tconst requestsPerGoroutine = 5\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes:   maxBytes,\n\t\t\tExpiration: 10 * time.Second,\n\t\t}))\n\n\t\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\t\t// Return data that will fill up the cache\n\t\t\treturn c.Send(make([]byte, 50))\n\t\t})\n\n\t\t// Launch multiple goroutines making concurrent requests\n\t\tvar wg sync.WaitGroup\n\t\terrChan := make(chan error, numGoroutines*requestsPerGoroutine)\n\n\t\tfor i := 0; i < numGoroutines; i++ {\n\t\t\tid := i\n\t\t\twg.Add(1) //nolint:revive // Standard WaitGroup pattern is appropriate here\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tfor j := 0; j < requestsPerGoroutine; j++ {\n\t\t\t\t\tpath := fmt.Sprintf(\"/test-%d-%d\", id, j)\n\t\t\t\t\treq := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)\n\t\t\t\t\t_, err := app.Test(req)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrChan <- err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(errChan)\n\n\t\t// Check for errors\n\t\tfor err := range errChan {\n\t\t\trequire.NoError(t, err, \"concurrent request failed\")\n\t\t}\n\n\t\t// The test passes if no errors occurred and no race conditions were detected by -race flag\n\t})\n\n\tt.Run(\"concurrent requests near capacity triggers eviction\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tconst maxBytes = uint(200)\n\t\tconst numRequests = 10\n\n\t\tapp.Use(New(Config{\n\t\t\tMaxBytes:   maxBytes,\n\t\t\tExpiration: 10 * time.Second,\n\t\t}))\n\n\t\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\t\t// Each response is about 50 bytes, so we'll exceed capacity\n\t\t\treturn c.Send(make([]byte, 50))\n\t\t})\n\n\t\t// Make concurrent requests that will trigger evictions\n\t\tvar wg sync.WaitGroup\n\t\tfor i := 0; i < numRequests; i++ {\n\t\t\tid := i\n\t\t\twg.Add(1) //nolint:revive // Standard WaitGroup pattern is appropriate here\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tpath := fmt.Sprintf(\"/item-%d\", id)\n\t\t\t\treq := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)\n\t\t\t\t_, err := app.Test(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"request error: %v\", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Test passes if no race conditions or panics occurred\n\t\t// The -race flag will detect any remaining race conditions\n\t})\n}\n\n// Test_Cache_HelperFunctions tests various helper functions for better coverage\nfunc Test_Cache_HelperFunctions(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"parseHTTPDate empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult, ok := parseHTTPDate([]byte{})\n\t\trequire.False(t, ok)\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"parseHTTPDate invalid\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult, ok := parseHTTPDate([]byte(\"invalid\"))\n\t\trequire.False(t, ok)\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"parseHTTPDate valid\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult, ok := parseHTTPDate([]byte(\"Mon, 02 Jan 2006 15:04:05 GMT\"))\n\t\trequire.True(t, ok)\n\t\trequire.Positive(t, result)\n\t})\n\n\tt.Run(\"safeUnixSeconds negative\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := safeUnixSeconds(time.Unix(-1, 0))\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"safeUnixSeconds positive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := safeUnixSeconds(time.Unix(1234567890, 0))\n\t\trequire.Equal(t, uint64(1234567890), result)\n\t})\n\n\tt.Run(\"remainingFreshness nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := remainingFreshness(nil, 100)\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"remainingFreshness zero exp\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\te := &item{exp: 0}\n\t\tresult := remainingFreshness(e, 100)\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"remainingFreshness expired\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\te := &item{exp: 100}\n\t\tresult := remainingFreshness(e, 200)\n\t\trequire.Equal(t, uint64(0), result)\n\t})\n\n\tt.Run(\"remainingFreshness valid\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\te := &item{exp: 200}\n\t\tresult := remainingFreshness(e, 100)\n\t\trequire.Equal(t, uint64(100), result)\n\t})\n\n\tt.Run(\"lookupCachedHeader not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\theaders := []cachedHeader{{key: []byte(\"Content-Type\"), value: []byte(\"text/html\")}}\n\t\tvalue, found := lookupCachedHeader(headers, \"Authorization\")\n\t\trequire.False(t, found)\n\t\trequire.Nil(t, value)\n\t})\n\n\tt.Run(\"lookupCachedHeader case insensitive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\theaders := []cachedHeader{{key: []byte(\"Authorization\"), value: []byte(\"Bearer token\")}}\n\t\tvalue, found := lookupCachedHeader(headers, \"authorization\")\n\t\trequire.True(t, found)\n\t\trequire.Equal(t, []byte(\"Bearer token\"), value)\n\t})\n\n\tt.Run(\"secondsToDuration zero\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := secondsToDuration(0)\n\t\trequire.Equal(t, time.Duration(0), result)\n\t})\n\n\tt.Run(\"secondsToDuration large\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := secondsToDuration(9223372036)\n\t\trequire.Greater(t, result, time.Duration(0))\n\t})\n\n\tt.Run(\"secondsToTime zero\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := secondsToTime(0)\n\t\trequire.Equal(t, time.Unix(0, 0).UTC(), result)\n\t})\n\n\tt.Run(\"secondsToTime value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := secondsToTime(1234567890)\n\t\trequire.Equal(t, time.Unix(1234567890, 0).UTC(), result)\n\t})\n\n\tt.Run(\"isHeuristicFreshness short age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := &Config{Expiration: 1 * time.Hour}\n\t\te := &item{cacheControl: []byte(\"public\")}\n\t\tresult := isHeuristicFreshness(e, cfg, 3600)\n\t\trequire.False(t, result)\n\t})\n\n\tt.Run(\"isHeuristicFreshness with expires\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := &Config{Expiration: 1 * time.Hour}\n\t\te := &item{cacheControl: []byte(\"public\"), expires: []byte(\"Wed, 21 Oct 2015 07:28:00 GMT\")}\n\t\tresult := isHeuristicFreshness(e, cfg, uint64(25*time.Hour/time.Second))\n\t\trequire.False(t, result)\n\t})\n\n\tt.Run(\"isHeuristicFreshness true\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := &Config{Expiration: 1 * time.Hour}\n\t\te := &item{cacheControl: []byte(\"public\")}\n\t\tresult := isHeuristicFreshness(e, cfg, uint64(25*time.Hour/time.Second))\n\t\trequire.True(t, result)\n\t})\n\n\tt.Run(\"cacheBodyFetchError miss\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tmask := func(_ string) string { return \"***\" }\n\t\terr := cacheBodyFetchError(mask, \"key\", errCacheMiss)\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), \"no cached body\")\n\t})\n\n\tt.Run(\"cacheBodyFetchError other\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tmask := func(_ string) string { return \"***\" }\n\t\toriginalErr := errors.New(\"storage error\")\n\t\terr := cacheBodyFetchError(mask, \"key\", originalErr)\n\t\trequire.Equal(t, originalErr, err)\n\t})\n}\n\n// Test_Cache_VaryAndAuth tests vary and auth functionality\nfunc Test_Cache_VaryAndAuth(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"storeVaryManifest failure\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstorage := newFailingCacheStorage()\n\t\tstorage.errs[\"set|manifest\"] = errors.New(\"storage fail\")\n\t\tmanager := &manager{storage: storage}\n\t\terr := storeVaryManifest(context.Background(), manager, \"manifest\", []string{\"Accept\"}, 3600*time.Second)\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"loadVaryManifest not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstorage := newFailingCacheStorage()\n\t\tmanager := &manager{storage: storage}\n\t\tvaryNames, found, err := loadVaryManifest(context.Background(), manager, \"nonexistent\")\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, found)\n\t\trequire.Nil(t, varyNames)\n\t})\n\n\tt.Run(\"vary with multiple headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Vary\", \"Accept, Accept-Encoding\")\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Accept\", \"application/json\")\n\t\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq2.Header.Set(\"Accept\", \"application/json\")\n\t\treq2.Header.Set(\"Accept-Encoding\", \"gzip\")\n\t\trsp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"auth with must-revalidate\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"must-revalidate, max-age=3600\")\n\t\t\treturn c.SendString(\"content\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Authorization\", \"Bearer token1\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\treq2 := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq2.Header.Set(\"Authorization\", \"Bearer token1\")\n\t\trsp2, err := app.Test(req2)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n}\n\n// Test_Cache_DateAndCacheControl tests date parsing and cache control\nfunc Test_Cache_DateAndCacheControl(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"date header parsing\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Date\", \"Mon, 02 Jan 2006 15:04:05 GMT\")\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"invalid date header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Date\", \"invalid\")\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"cache control with quoted values\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", `max-age=3600, ext=\"value, with, commas\"`)\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"cache control with spaces\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600  ,  public  ,  must-revalidate\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n}\n\n// Test_Cache_CacheControlCombinations tests common cache control directive combinations\nfunc Test_Cache_CacheControlCombinations(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"max-age with public\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public, max-age=3600\")\n\t\t\treturn c.SendString(\"public content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"max-age with private\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"private, max-age=3600\")\n\t\t\treturn c.SendString(\"private content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"s-maxage overrides max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public, max-age=60, s-maxage=3600\")\n\t\t\treturn c.SendString(\"content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"no-store prevents caching\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"no-store\")\n\t\t\treturn c.SendString(\"no store content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"no-cache with etag\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"no-cache\")\n\t\t\tc.Response().Header.Set(\"ETag\", `\"123456\"`)\n\t\t\treturn c.SendString(\"no-cache content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"must-revalidate with max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"must-revalidate, max-age=3600\")\n\t\t\treturn c.SendString(\"must revalidate content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"proxy-revalidate with max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public, proxy-revalidate, max-age=3600\")\n\t\t\treturn c.SendString(\"proxy revalidate content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"immutable with max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n\t\t\treturn c.SendString(\"immutable content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"max-age=0 with must-revalidate\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=0, must-revalidate\")\n\t\t\treturn c.SendString(\"always revalidate\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"public with no explicit max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public\")\n\t\t\treturn c.SendString(\"public no max-age\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"multiple cache directives with extensions\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", `public, max-age=3600, custom=\"value\"`)\n\t\t\treturn c.SendString(\"content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"private overrides public\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"public, private, max-age=3600\")\n\t\t\treturn c.SendString(\"conflicting directives\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"stale-while-revalidate with max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=60, stale-while-revalidate=120\")\n\t\t\treturn c.SendString(\"stale while revalidate\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"stale-if-error with max-age\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=60, stale-if-error=3600\")\n\t\t\treturn c.SendString(\"stale if error\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n}\n\n// Test_Cache_RequestResponseDirectives tests caching behavior with various request/response cache-control directives\nfunc Test_Cache_RequestResponseDirectives(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"negative expiration skips caching\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: -1 * time.Second}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.NotEqual(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t\trequire.NotEqual(t, cacheHit, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"request with no-store directive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"no-store\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.NotEqual(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"request with pragma no-cache\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Pragma\", \"no-cache\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"method not in allowed methods list\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration: 1 * time.Hour,\n\t\t\tMethods:    []string{fiber.MethodGet},\n\t\t}))\n\t\tapp.Post(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodPost, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"request with min-fresh directive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=60\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// First request to cache\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Second request with min-fresh that's too high\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"min-fresh=120\")\n\t\trsp, err = app.Test(req)\n\t\trequire.NoError(t, err)\n\t\t// Should be a miss because min-fresh requirement not met\n\t\tcacheStatus := rsp.Header.Get(\"X-Cache\")\n\t\trequire.Contains(t, []string{cacheMiss, cacheUnreachable}, cacheStatus, \"min-fresh requirement should prevent cache hit\")\n\t})\n\n\tt.Run(\"request with max-age=0 directive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// First request to cache\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Second request with max-age=0\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"max-age=0\")\n\t\trsp, err = app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"request with max-stale directive\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Second}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=1\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// First request to cache\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Wait for it to become stale\n\t\ttime.Sleep(2 * time.Second)\n\n\t\t// Request with max-stale to accept stale content\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"max-stale=60\")\n\t\trsp, err = app.Test(req)\n\t\trequire.NoError(t, err)\n\t\t// max-stale should allow serving stale content\n\t\tcacheStatus := rsp.Header.Get(\"X-Cache\")\n\t\t// Should be either a hit (if stale is served) or miss (if revalidated)\n\t\trequire.Contains(t, []string{cacheHit, cacheMiss, \"stale\"}, cacheStatus, \"max-stale should allow stale content or revalidate\")\n\t})\n\n\tt.Run(\"response with expires header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tfutureTime := time.Now().Add(1 * time.Hour).Format(time.RFC1123)\n\t\t\tc.Response().Header.Set(\"Expires\", futureTime)\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"response with age header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\tc.Response().Header.Set(\"Age\", \"30\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"custom key generator\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration: 1 * time.Hour,\n\t\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\t\treturn \"custom-\" + c.Path()\n\t\t\t},\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"response with warning header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Second}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=1\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// Cache the response\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Wait for it to become stale\n\t\ttime.Sleep(2 * time.Second)\n\n\t\t// Request again - should get stale warning or revalidate\n\t\trsp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\t// Check that either cache miss (revalidation) or warning header is present\n\t\tcacheStatus := rsp.Header.Get(\"X-Cache\")\n\t\twarningHeader := rsp.Header.Get(\"Warning\")\n\t\trequire.True(t, cacheStatus == cacheMiss || warningHeader != \"\", \"stale response should either revalidate or have warning header\")\n\t})\n\n\tt.Run(\"external storage with body key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstorage := newFailingCacheStorage()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration: 1 * time.Hour,\n\t\t\tStorage:    storage,\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test content\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Verify body key is stored\n\t\thasBodyKey := false\n\t\tstorage.mu.RLock()\n\t\tfor k := range storage.data {\n\t\t\tif strings.Contains(k, \"_body\") {\n\t\t\t\thasBodyKey = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tstorage.mu.RUnlock()\n\t\trequire.True(t, hasBodyKey)\n\t})\n\n\tt.Run(\"only-if-cached with cache miss\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"only-if-cached\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusGatewayTimeout, rsp.StatusCode)\n\t})\n\n\tt.Run(\"only-if-cached with cache hit\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// First request to cache\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Second request with only-if-cached\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"only-if-cached\")\n\t\trsp2, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"cache control with uppercase directives\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"PUBLIC, MAX-AGE=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n}\n\n// Test_Cache_ConfigurationAndResponseHandling tests cache behavior for specific configuration and response edge cases.\nfunc Test_Cache_ConfigurationAndResponseHandling(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"response with Vary star prevents caching\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Vary\", \"*\")\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"next function prevents caching\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration: 1 * time.Hour,\n\t\t\tNext: func(c fiber.Ctx) bool {\n\t\t\t\treturn c.Path() == \"/skip\"\n\t\t\t},\n\t\t}))\n\t\tapp.Get(\"/skip\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/skip\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"non-cacheable status code\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.Status(fiber.StatusCreated).SendString(\"created\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"body larger than MaxBytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration: 1 * time.Hour,\n\t\t\tMaxBytes:   10,\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\treturn c.Send(make([]byte, 100))\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"authorization without shared cache directives\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Authorization\", \"******\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"disable cache control header generation\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration:          1 * time.Hour,\n\t\t\tDisableCacheControl: true,\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"disable value redaction\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration:            1 * time.Hour,\n\t\t\tDisableValueRedaction: true,\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"response with ETag header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\tc.Response().Header.Set(\"ETag\", `\"abc123\"`)\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t\trequire.Equal(t, `\"abc123\"`, rsp2.Header.Get(\"ETag\"))\n\t})\n\n\tt.Run(\"response with Content-Encoding header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\tc.Response().Header.Set(\"Content-Encoding\", \"gzip\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t\trequire.Equal(t, \"gzip\", rsp2.Header.Get(\"Content-Encoding\"))\n\t})\n\n\tt.Run(\"response with custom headers preserved\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tExpiration:           1 * time.Hour,\n\t\t\tStoreResponseHeaders: true,\n\t\t}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\tc.Response().Header.Set(\"X-Custom-Header\", \"custom-value\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\trsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheHit, rsp2.Header.Get(\"X-Cache\"))\n\t\trequire.Equal(t, \"custom-value\", rsp2.Header.Get(\"X-Custom-Header\"))\n\t})\n\n\tt.Run(\"revalidation scenario with cache miss\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\t// Request with no-cache forces revalidation\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Cache-Control\", \"no-cache\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"delete vary manifest on no-cache response\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\t// First request creates vary manifest\n\t\t\tif c.Query(\"first\") != \"\" {\n\t\t\t\tc.Response().Header.Set(\"Vary\", \"Accept\")\n\t\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\t} else {\n\t\t\t\t// Second request returns no-cache to delete manifest\n\t\t\t\tc.Response().Header.Set(\"Cache-Control\", \"no-cache\")\n\t\t\t}\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test?first=true\", http.NoBody)\n\t\treq.Header.Set(\"Accept\", \"application/json\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Second request without Vary should delete manifest\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheUnreachable, rsp2.Header.Get(\"X-Cache\"))\n\t})\n\n\tt.Run(\"vary manifest deletion on different vary response\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\tvar counter atomic.Int32\n\t\tapp.Use(New(Config{Expiration: 1 * time.Hour}))\n\t\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\t\tif counter.Add(1) == 1 {\n\t\t\t\tc.Response().Header.Set(\"Vary\", \"Accept\")\n\t\t\t}\n\t\t\t// Second response has no Vary header - should delete manifest\n\t\t\tc.Response().Header.Set(\"Cache-Control\", \"max-age=3600\")\n\t\t\treturn c.SendString(\"test\")\n\t\t})\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\t\treq.Header.Set(\"Accept\", \"application/json\")\n\t\trsp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp.Header.Get(\"X-Cache\"))\n\n\t\t// Second request - different vary behavior\n\t\trsp2, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, cacheMiss, rsp2.Header.Get(\"X-Cache\"))\n\t})\n}\n"
  },
  {
    "path": "middleware/cache/config.go",
    "content": "package cache\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Storage is used to store the state of the middleware\n\t//\n\t// Default: an in-memory store for this process only\n\tStorage fiber.Storage\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// CacheInvalidator defines a function to invalidate the cache when returned true\n\t//\n\t// Optional. Default: nil\n\tCacheInvalidator func(fiber.Ctx) bool\n\n\t// Key allows you to generate custom keys, by default c.Path() is used\n\t//\n\t// Default: func(c fiber.Ctx) string {\n\t//   return utils.CopyString(c.Path())\n\t// }\n\tKeyGenerator func(fiber.Ctx) string\n\n\t// ExpirationGenerator allows you to generate a custom expiration per request.\n\t// If nil, the Expiration value is used.\n\t//\n\t// Default: nil\n\tExpirationGenerator func(fiber.Ctx, *Config) time.Duration\n\n\t// CacheHeader header on response header, indicate cache status, with the following possible return value\n\t//\n\t// hit, miss, unreachable\n\t//\n\t// Optional. Default: X-Cache\n\tCacheHeader string\n\n\t// You can specify HTTP methods to cache.\n\t// The middleware just caches the routes of its methods in this slice.\n\t//\n\t// Default: []string{fiber.MethodGet, fiber.MethodHead}\n\tMethods []string\n\n\t// Expiration is the time that a cached response will live\n\t//\n\t// Optional. Default: 5 * time.Minute\n\tExpiration time.Duration\n\n\t// Max number of bytes of response bodies simultaneously stored in cache. When limit is reached,\n\t// entries with the nearest expiration are deleted to make room for new.\n\t// 0 means no limit\n\t//\n\t// Optional. Default: 1 * 1024 * 1024\n\tMaxBytes uint\n\n\t// DisableValueRedaction turns off masking cache keys in logs and error messages when set to true.\n\t//\n\t// Optional. Default: false\n\tDisableValueRedaction bool\n\n\t// DisableCacheControl disables client side caching if set to true\n\t//\n\t// Optional. Default: false\n\tDisableCacheControl bool\n\n\t// StoreResponseHeaders allows you to store additional headers generated by\n\t// next middlewares and handlers.\n\t//\n\t// Default: false\n\tStoreResponseHeaders bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:                  nil,\n\tExpiration:            5 * time.Minute,\n\tCacheHeader:           \"X-Cache\",\n\tDisableCacheControl:   false,\n\tCacheInvalidator:      nil,\n\tDisableValueRedaction: false,\n\tKeyGenerator: func(c fiber.Ctx) string {\n\t\treturn utils.CopyString(c.Path())\n\t},\n\tExpirationGenerator:  nil,\n\tStoreResponseHeaders: false,\n\tStorage:              nil,\n\tMaxBytes:             1 * 1024 * 1024,\n\tMethods:              []string{fiber.MethodGet, fiber.MethodHead},\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif int(cfg.Expiration.Seconds()) == 0 {\n\t\tcfg.Expiration = ConfigDefault.Expiration\n\t}\n\tif cfg.CacheHeader == \"\" {\n\t\tcfg.CacheHeader = ConfigDefault.CacheHeader\n\t}\n\tif cfg.KeyGenerator == nil {\n\t\tcfg.KeyGenerator = ConfigDefault.KeyGenerator\n\t}\n\tif len(cfg.Methods) == 0 {\n\t\tcfg.Methods = ConfigDefault.Methods\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/cache/heap.go",
    "content": "package cache\n\nimport (\n\t\"container/heap\"\n)\n\ntype heapEntry struct {\n\tkey   string\n\texp   uint64\n\tbytes uint\n\tidx   int\n}\n\n// indexedHeap is a regular min-heap that allows finding\n// elements in constant time. It does so by handing out special indices\n// and tracking entry movement.\n//\n// indexedHeap is used for quickly finding entries with the lowest\n// expiration timestamp and deleting arbitrary entries.\ntype indexedHeap struct {\n\t// Slice the heap is built on\n\tentries []heapEntry\n\t// Mapping \"index\" to position in heap slice\n\tindices []int\n\t// Max index handed out\n\tmaxidx int\n}\n\n// Len implements heap.Interface by reporting the number of entries in the heap.\nfunc (h indexedHeap) Len() int {\n\treturn len(h.entries)\n}\n\n// Less implements heap.Interface and orders entries by expiration time.\nfunc (h indexedHeap) Less(i, j int) bool {\n\treturn h.entries[i].exp < h.entries[j].exp\n}\n\n// Swap implements heap.Interface and swaps the entries at the provided indices.\nfunc (h indexedHeap) Swap(i, j int) {\n\th.entries[i], h.entries[j] = h.entries[j], h.entries[i]\n\th.indices[h.entries[i].idx] = i\n\th.indices[h.entries[j].idx] = j\n}\n\n// Push implements heap.Interface and inserts a new entry into the heap.\nfunc (h *indexedHeap) Push(x any) {\n\th.pushInternal(x.(heapEntry)) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface\n}\n\n// Pop implements heap.Interface and removes the last entry from the heap.\nfunc (h *indexedHeap) Pop() any {\n\tn := len(h.entries)\n\th.entries = h.entries[0 : n-1]\n\treturn h.entries[0:n][n-1]\n}\n\nfunc (h *indexedHeap) pushInternal(entry heapEntry) {\n\th.indices[entry.idx] = len(h.entries)\n\th.entries = append(h.entries, entry)\n}\n\n// Returns index to track entry\nfunc (h *indexedHeap) put(key string, exp uint64, bytes uint) int {\n\tidx := 0\n\tif len(h.entries) < h.maxidx {\n\t\t// Steal index from previously removed entry\n\t\t// capacity > size is guaranteed\n\t\tn := len(h.entries)\n\t\tidx = h.entries[:n+1][n].idx\n\t} else {\n\t\tidx = h.maxidx\n\t\th.maxidx++\n\t\th.indices = append(h.indices, idx)\n\t}\n\t// Push manually to avoid allocation\n\th.pushInternal(heapEntry{\n\t\tkey: key, exp: exp, idx: idx, bytes: bytes,\n\t})\n\theap.Fix(h, h.Len()-1)\n\treturn idx\n}\n\nfunc (h *indexedHeap) removeInternal(realIdx int) (key string, size uint) { //nolint:nonamedreturns // gocritic unnamedResult prefers named key and size when removing heap entries\n\tx := heap.Remove(h, realIdx).(heapEntry) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface\n\treturn x.key, x.bytes\n}\n\n// Remove entry by index\nfunc (h *indexedHeap) remove(idx int) (key string, size uint) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned key and size pair\n\treturn h.removeInternal(h.indices[idx])\n}\n\n// Remove entry with lowest expiration time\nfunc (h *indexedHeap) removeFirst() (key string, size uint) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned key and size pair\n\treturn h.removeInternal(0)\n}\n"
  },
  {
    "path": "middleware/cache/manager.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/memory\"\n)\n\n// msgp -file=\"manager.go\" -o=\"manager_msgp.go\" -tests=true -unexported\n// Default slice limits are sized for cache payloads, with tighter field caps below.\n//\n//go:generate msgp -o=manager_msgp.go -tests=true -unexported\n//nolint:revive // msgp requires tags on unexported fields for limit enforcement.\ntype item struct {\n\theaders         []cachedHeader `msg:\",limit=1024\"` // Typical HTTP header count stays well below this.\n\tbody            []byte         // Cache bodies are bounded by storage policy, not msgp limits.\n\tctype           []byte         `msg:\",limit=256\"`  // Content-Type values are short per RFCs.\n\tcencoding       []byte         `msg:\",limit=128\"`  // Content-Encoding is typically a short token.\n\tcacheControl    []byte         `msg:\",limit=2048\"` // Cache-Control directives are bounded.\n\texpires         []byte         `msg:\",limit=128\"`  // Expires is a short HTTP-date string.\n\tetag            []byte         `msg:\",limit=256\"`  // ETags are small tokens/quoted strings.\n\tdate            uint64\n\tstatus          int\n\tage             uint64\n\texp             uint64\n\tttl             uint64\n\tforceRevalidate bool\n\trevalidate      bool\n\tshareable       bool\n\tprivate         bool\n\t// used for finding the item in an indexed heap\n\theapidx int\n}\n\n//nolint:revive // msgp requires tags on unexported fields for limit enforcement.\ntype cachedHeader struct {\n\tkey   []byte `msg:\",limit=512\"`   // Header names are small.\n\tvalue []byte `msg:\",limit=16384\"` // Header values are bounded to reasonable sizes.\n}\n\n//msgp:ignore manager\ntype manager struct {\n\tpool       sync.Pool\n\tmemory     *memory.Storage\n\tstorage    fiber.Storage\n\tredactKeys bool\n}\n\nconst redactedKey = \"[redacted]\"\n\nvar errCacheMiss = errors.New(\"cache: miss\")\n\nfunc newManager(storage fiber.Storage, redactKeys bool) *manager {\n\t// Create new storage handler\n\tmanager := &manager{\n\t\tpool: sync.Pool{\n\t\t\tNew: func() any {\n\t\t\t\treturn new(item)\n\t\t\t},\n\t\t},\n\t\tredactKeys: redactKeys,\n\t}\n\tif storage != nil {\n\t\t// Use provided storage if provided\n\t\tmanager.storage = storage\n\t} else {\n\t\t// Fallback to memory storage\n\t\tmanager.memory = memory.New()\n\t}\n\treturn manager\n}\n\n// acquire returns an *entry from the sync.Pool\nfunc (m *manager) acquire() *item {\n\treturn m.pool.Get().(*item) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n}\n\n// release and reset *entry to sync.Pool\nfunc (m *manager) release(e *item) {\n\t// don't release item if we using in-memory storage\n\tif m.storage == nil {\n\t\treturn\n\t}\n\te.body = nil\n\te.cacheControl = nil\n\te.expires = nil\n\te.etag = nil\n\te.ctype = nil\n\te.cencoding = nil\n\te.date = 0\n\te.status = 0\n\te.age = 0\n\te.exp = 0\n\te.ttl = 0\n\te.forceRevalidate = false\n\te.revalidate = false\n\te.headers = nil\n\te.shareable = false\n\te.private = false\n\te.heapidx = 0\n\tm.pool.Put(e)\n}\n\n// get data from storage or memory\nfunc (m *manager) get(ctx context.Context, key string) (*item, error) {\n\tif m.storage != nil {\n\t\traw, err := m.storage.GetWithContext(ctx, key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cache: failed to get key %q from storage: %w\", m.logKey(key), err)\n\t\t}\n\t\tif raw == nil {\n\t\t\treturn nil, errCacheMiss\n\t\t}\n\n\t\tit := m.acquire()\n\t\tif _, err := it.UnmarshalMsg(raw); err != nil {\n\t\t\tm.release(it)\n\t\t\treturn nil, fmt.Errorf(\"cache: failed to unmarshal key %q: %w\", m.logKey(key), err)\n\t\t}\n\n\t\treturn it, nil\n\t}\n\n\tif value := m.memory.Get(key); value != nil {\n\t\tit, ok := value.(*item)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"cache: unexpected entry type %T for key %q\", value, m.logKey(key))\n\t\t}\n\t\treturn it, nil\n\t}\n\n\treturn nil, errCacheMiss\n}\n\n// get raw data from storage or memory\nfunc (m *manager) getRaw(ctx context.Context, key string) ([]byte, error) {\n\tif m.storage != nil {\n\t\traw, err := m.storage.GetWithContext(ctx, key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cache: failed to get raw key %q from storage: %w\", m.logKey(key), err)\n\t\t}\n\t\tif raw == nil {\n\t\t\treturn nil, errCacheMiss\n\t\t}\n\t\treturn raw, nil\n\t}\n\n\tif value := m.memory.Get(key); value != nil {\n\t\traw, ok := value.([]byte)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"cache: unexpected raw entry type %T for key %q\", value, m.logKey(key))\n\t\t}\n\t\treturn raw, nil\n\t}\n\n\treturn nil, errCacheMiss\n}\n\n// set data to storage or memory\nfunc (m *manager) set(ctx context.Context, key string, it *item, exp time.Duration) error {\n\tif m.storage != nil {\n\t\traw, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\tm.release(it)\n\t\t\treturn fmt.Errorf(\"cache: failed to marshal key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\tif err := m.storage.SetWithContext(ctx, key, raw, exp); err != nil {\n\t\t\tm.release(it)\n\t\t\treturn fmt.Errorf(\"cache: failed to store key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\tm.release(it)\n\t\treturn nil\n\t}\n\n\tm.memory.Set(key, it, exp)\n\treturn nil\n}\n\n// set data to storage or memory\nfunc (m *manager) setRaw(ctx context.Context, key string, raw []byte, exp time.Duration) error {\n\tif m.storage != nil {\n\t\tif err := m.storage.SetWithContext(ctx, key, raw, exp); err != nil {\n\t\t\treturn fmt.Errorf(\"cache: failed to store raw key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tm.memory.Set(key, raw, exp)\n\treturn nil\n}\n\n// delete data from storage or memory\nfunc (m *manager) del(ctx context.Context, key string) error {\n\tif m.storage != nil {\n\t\tif err := m.storage.DeleteWithContext(ctx, key); err != nil {\n\t\t\treturn fmt.Errorf(\"cache: failed to delete key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tm.memory.Delete(key)\n\treturn nil\n}\n\nfunc (m *manager) logKey(key string) string {\n\tif m.redactKeys {\n\t\treturn redactedKey\n\t}\n\treturn key\n}\n"
  },
  {
    "path": "middleware/cache/manager_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage cache\n\nimport (\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *cachedHeader) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"key\":\n\t\t\tz.key, err = dc.ReadBytesLimit(z.key, 512)\n\t\t\tif err == nil && z.key == nil {\n\t\t\t\tz.key = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"key\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"value\":\n\t\t\tz.value, err = dc.ReadBytesLimit(z.value, 16384)\n\t\t\tif err == nil && z.value == nil {\n\t\t\t\tz.value = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"value\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z *cachedHeader) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 2\n\t// write \"key\"\n\terr = en.Append(0x82, 0xa3, 0x6b, 0x65, 0x79)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.key)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"key\")\n\t\treturn\n\t}\n\t// write \"value\"\n\terr = en.Append(0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.value)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"value\")\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z *cachedHeader) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 2\n\t// string \"key\"\n\to = append(o, 0x82, 0xa3, 0x6b, 0x65, 0x79)\n\to = msgp.AppendBytes(o, z.key)\n\t// string \"value\"\n\to = append(o, 0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\to = msgp.AppendBytes(o, z.value)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *cachedHeader) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"key\":\n\t\t\tvar zb0002 uint32\n\t\t\tzb0002, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"key\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0002 > 512 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.key == nil || uint32(cap(z.key)) < zb0002 {\n\t\t\t\tz.key = make([]byte, zb0002)\n\t\t\t} else {\n\t\t\t\tz.key = z.key[:zb0002]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0002 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.key, bts[:zb0002])\n\t\t\tbts = bts[zb0002:]\n\t\tcase \"value\":\n\t\t\tvar zb0003 uint32\n\t\t\tzb0003, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"value\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0003 > 16384 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.value == nil || uint32(cap(z.value)) < zb0003 {\n\t\t\t\tz.value = make([]byte, zb0003)\n\t\t\t} else {\n\t\t\t\tz.value = z.value[:zb0003]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0003 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.value, bts[:zb0003])\n\t\t\tbts = bts[zb0003:]\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z *cachedHeader) Msgsize() (s int) {\n\ts = 1 + 4 + msgp.BytesPrefixSize + len(z.key) + 6 + msgp.BytesPrefixSize + len(z.value)\n\treturn\n}\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *item) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"headers\":\n\t\t\tvar zb0002 uint32\n\t\t\tzb0002, err = dc.ReadArrayHeader()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"headers\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0002 > 1024 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif cap(z.headers) >= int(zb0002) {\n\t\t\t\tz.headers = (z.headers)[:zb0002]\n\t\t\t} else {\n\t\t\t\tz.headers = make([]cachedHeader, zb0002)\n\t\t\t}\n\t\t\tfor za0001 := range z.headers {\n\t\t\t\tvar zb0003 uint32\n\t\t\t\tzb0003, err = dc.ReadMapHeader()\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif zb0003 > 1024 {\n\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor zb0003 > 0 {\n\t\t\t\t\tzb0003--\n\t\t\t\t\tfield, err = dc.ReadMapKeyPtr()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tswitch msgp.UnsafeString(field) {\n\t\t\t\t\tcase \"key\":\n\t\t\t\t\t\tz.headers[za0001].key, err = dc.ReadBytesLimit(z.headers[za0001].key, 512)\n\t\t\t\t\t\tif err == nil && z.headers[za0001].key == nil {\n\t\t\t\t\t\t\tz.headers[za0001].key = []byte{}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"key\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\tcase \"value\":\n\t\t\t\t\t\tz.headers[za0001].value, err = dc.ReadBytesLimit(z.headers[za0001].value, 16384)\n\t\t\t\t\t\tif err == nil && z.headers[za0001].value == nil {\n\t\t\t\t\t\t\tz.headers[za0001].value = []byte{}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"value\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\terr = dc.Skip()\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"body\":\n\t\t\tz.body, err = dc.ReadBytes(z.body)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"body\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"ctype\":\n\t\t\tz.ctype, err = dc.ReadBytesLimit(z.ctype, 256)\n\t\t\tif err == nil && z.ctype == nil {\n\t\t\t\tz.ctype = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"ctype\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"cencoding\":\n\t\t\tz.cencoding, err = dc.ReadBytesLimit(z.cencoding, 128)\n\t\t\tif err == nil && z.cencoding == nil {\n\t\t\t\tz.cencoding = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"cencoding\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"cacheControl\":\n\t\t\tz.cacheControl, err = dc.ReadBytesLimit(z.cacheControl, 2048)\n\t\t\tif err == nil && z.cacheControl == nil {\n\t\t\t\tz.cacheControl = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"cacheControl\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"expires\":\n\t\t\tz.expires, err = dc.ReadBytesLimit(z.expires, 128)\n\t\t\tif err == nil && z.expires == nil {\n\t\t\t\tz.expires = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"expires\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"etag\":\n\t\t\tz.etag, err = dc.ReadBytesLimit(z.etag, 256)\n\t\t\tif err == nil && z.etag == nil {\n\t\t\t\tz.etag = []byte{}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"etag\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"date\":\n\t\t\tz.date, err = dc.ReadUint64()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"date\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"status\":\n\t\t\tz.status, err = dc.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"status\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"age\":\n\t\t\tz.age, err = dc.ReadUint64()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"age\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"exp\":\n\t\t\tz.exp, err = dc.ReadUint64()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"exp\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"ttl\":\n\t\t\tz.ttl, err = dc.ReadUint64()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"ttl\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"forceRevalidate\":\n\t\t\tz.forceRevalidate, err = dc.ReadBool()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"forceRevalidate\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"revalidate\":\n\t\t\tz.revalidate, err = dc.ReadBool()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"revalidate\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"shareable\":\n\t\t\tz.shareable, err = dc.ReadBool()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"shareable\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"private\":\n\t\t\tz.private, err = dc.ReadBool()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"private\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"heapidx\":\n\t\t\tz.heapidx, err = dc.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"heapidx\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z *item) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 17\n\t// write \"headers\"\n\terr = en.Append(0xde, 0x0, 0x11, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteArrayHeader(uint32(len(z.headers)))\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"headers\")\n\t\treturn\n\t}\n\tfor za0001 := range z.headers {\n\t\t// map header, size 2\n\t\t// write \"key\"\n\t\terr = en.Append(0x82, 0xa3, 0x6b, 0x65, 0x79)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = en.WriteBytes(z.headers[za0001].key)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"key\")\n\t\t\treturn\n\t\t}\n\t\t// write \"value\"\n\t\terr = en.Append(0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\terr = en.WriteBytes(z.headers[za0001].value)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"value\")\n\t\t\treturn\n\t\t}\n\t}\n\t// write \"body\"\n\terr = en.Append(0xa4, 0x62, 0x6f, 0x64, 0x79)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.body)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"body\")\n\t\treturn\n\t}\n\t// write \"ctype\"\n\terr = en.Append(0xa5, 0x63, 0x74, 0x79, 0x70, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.ctype)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"ctype\")\n\t\treturn\n\t}\n\t// write \"cencoding\"\n\terr = en.Append(0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.cencoding)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"cencoding\")\n\t\treturn\n\t}\n\t// write \"cacheControl\"\n\terr = en.Append(0xac, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.cacheControl)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"cacheControl\")\n\t\treturn\n\t}\n\t// write \"expires\"\n\terr = en.Append(0xa7, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.expires)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"expires\")\n\t\treturn\n\t}\n\t// write \"etag\"\n\terr = en.Append(0xa4, 0x65, 0x74, 0x61, 0x67)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.etag)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"etag\")\n\t\treturn\n\t}\n\t// write \"date\"\n\terr = en.Append(0xa4, 0x64, 0x61, 0x74, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint64(z.date)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"date\")\n\t\treturn\n\t}\n\t// write \"status\"\n\terr = en.Append(0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteInt(z.status)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"status\")\n\t\treturn\n\t}\n\t// write \"age\"\n\terr = en.Append(0xa3, 0x61, 0x67, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint64(z.age)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"age\")\n\t\treturn\n\t}\n\t// write \"exp\"\n\terr = en.Append(0xa3, 0x65, 0x78, 0x70)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint64(z.exp)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"exp\")\n\t\treturn\n\t}\n\t// write \"ttl\"\n\terr = en.Append(0xa3, 0x74, 0x74, 0x6c)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint64(z.ttl)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"ttl\")\n\t\treturn\n\t}\n\t// write \"forceRevalidate\"\n\terr = en.Append(0xaf, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBool(z.forceRevalidate)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"forceRevalidate\")\n\t\treturn\n\t}\n\t// write \"revalidate\"\n\terr = en.Append(0xaa, 0x72, 0x65, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBool(z.revalidate)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"revalidate\")\n\t\treturn\n\t}\n\t// write \"shareable\"\n\terr = en.Append(0xa9, 0x73, 0x68, 0x61, 0x72, 0x65, 0x61, 0x62, 0x6c, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBool(z.shareable)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"shareable\")\n\t\treturn\n\t}\n\t// write \"private\"\n\terr = en.Append(0xa7, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBool(z.private)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"private\")\n\t\treturn\n\t}\n\t// write \"heapidx\"\n\terr = en.Append(0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteInt(z.heapidx)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"heapidx\")\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z *item) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 17\n\t// string \"headers\"\n\to = append(o, 0xde, 0x0, 0x11, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73)\n\to = msgp.AppendArrayHeader(o, uint32(len(z.headers)))\n\tfor za0001 := range z.headers {\n\t\t// map header, size 2\n\t\t// string \"key\"\n\t\to = append(o, 0x82, 0xa3, 0x6b, 0x65, 0x79)\n\t\to = msgp.AppendBytes(o, z.headers[za0001].key)\n\t\t// string \"value\"\n\t\to = append(o, 0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\t\to = msgp.AppendBytes(o, z.headers[za0001].value)\n\t}\n\t// string \"body\"\n\to = append(o, 0xa4, 0x62, 0x6f, 0x64, 0x79)\n\to = msgp.AppendBytes(o, z.body)\n\t// string \"ctype\"\n\to = append(o, 0xa5, 0x63, 0x74, 0x79, 0x70, 0x65)\n\to = msgp.AppendBytes(o, z.ctype)\n\t// string \"cencoding\"\n\to = append(o, 0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67)\n\to = msgp.AppendBytes(o, z.cencoding)\n\t// string \"cacheControl\"\n\to = append(o, 0xac, 0x63, 0x61, 0x63, 0x68, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c)\n\to = msgp.AppendBytes(o, z.cacheControl)\n\t// string \"expires\"\n\to = append(o, 0xa7, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73)\n\to = msgp.AppendBytes(o, z.expires)\n\t// string \"etag\"\n\to = append(o, 0xa4, 0x65, 0x74, 0x61, 0x67)\n\to = msgp.AppendBytes(o, z.etag)\n\t// string \"date\"\n\to = append(o, 0xa4, 0x64, 0x61, 0x74, 0x65)\n\to = msgp.AppendUint64(o, z.date)\n\t// string \"status\"\n\to = append(o, 0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73)\n\to = msgp.AppendInt(o, z.status)\n\t// string \"age\"\n\to = append(o, 0xa3, 0x61, 0x67, 0x65)\n\to = msgp.AppendUint64(o, z.age)\n\t// string \"exp\"\n\to = append(o, 0xa3, 0x65, 0x78, 0x70)\n\to = msgp.AppendUint64(o, z.exp)\n\t// string \"ttl\"\n\to = append(o, 0xa3, 0x74, 0x74, 0x6c)\n\to = msgp.AppendUint64(o, z.ttl)\n\t// string \"forceRevalidate\"\n\to = append(o, 0xaf, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65)\n\to = msgp.AppendBool(o, z.forceRevalidate)\n\t// string \"revalidate\"\n\to = append(o, 0xaa, 0x72, 0x65, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65)\n\to = msgp.AppendBool(o, z.revalidate)\n\t// string \"shareable\"\n\to = append(o, 0xa9, 0x73, 0x68, 0x61, 0x72, 0x65, 0x61, 0x62, 0x6c, 0x65)\n\to = msgp.AppendBool(o, z.shareable)\n\t// string \"private\"\n\to = append(o, 0xa7, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65)\n\to = msgp.AppendBool(o, z.private)\n\t// string \"heapidx\"\n\to = append(o, 0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78)\n\to = msgp.AppendInt(o, z.heapidx)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"headers\":\n\t\t\tvar zb0002 uint32\n\t\t\tzb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"headers\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0002 > 1024 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif cap(z.headers) >= int(zb0002) {\n\t\t\t\tz.headers = (z.headers)[:zb0002]\n\t\t\t} else {\n\t\t\t\tz.headers = make([]cachedHeader, zb0002)\n\t\t\t}\n\t\t\tfor za0001 := range z.headers {\n\t\t\t\tvar zb0003 uint32\n\t\t\t\tzb0003, bts, err = msgp.ReadMapHeaderBytes(bts)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif zb0003 > 1024 {\n\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor zb0003 > 0 {\n\t\t\t\t\tzb0003--\n\t\t\t\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tswitch msgp.UnsafeString(field) {\n\t\t\t\t\tcase \"key\":\n\t\t\t\t\t\tvar zb0004 uint32\n\t\t\t\t\t\tzb0004, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"key\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif zb0004 > 512 {\n\t\t\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif z.headers[za0001].key == nil || uint32(cap(z.headers[za0001].key)) < zb0004 {\n\t\t\t\t\t\t\tz.headers[za0001].key = make([]byte, zb0004)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tz.headers[za0001].key = z.headers[za0001].key[:zb0004]\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif uint32(len(bts)) < zb0004 {\n\t\t\t\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcopy(z.headers[za0001].key, bts[:zb0004])\n\t\t\t\t\t\tbts = bts[zb0004:]\n\t\t\t\t\tcase \"value\":\n\t\t\t\t\t\tvar zb0005 uint32\n\t\t\t\t\t\tzb0005, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001, \"value\")\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif zb0005 > 16384 {\n\t\t\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif z.headers[za0001].value == nil || uint32(cap(z.headers[za0001].value)) < zb0005 {\n\t\t\t\t\t\t\tz.headers[za0001].value = make([]byte, zb0005)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tz.headers[za0001].value = z.headers[za0001].value[:zb0005]\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif uint32(len(bts)) < zb0005 {\n\t\t\t\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcopy(z.headers[za0001].value, bts[:zb0005])\n\t\t\t\t\t\tbts = bts[zb0005:]\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tbts, err = msgp.Skip(bts)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = msgp.WrapError(err, \"headers\", za0001)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"body\":\n\t\t\tz.body, bts, err = msgp.ReadBytesBytes(bts, z.body)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"body\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"ctype\":\n\t\t\tvar zb0006 uint32\n\t\t\tzb0006, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"ctype\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0006 > 256 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.ctype == nil || uint32(cap(z.ctype)) < zb0006 {\n\t\t\t\tz.ctype = make([]byte, zb0006)\n\t\t\t} else {\n\t\t\t\tz.ctype = z.ctype[:zb0006]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0006 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.ctype, bts[:zb0006])\n\t\t\tbts = bts[zb0006:]\n\t\tcase \"cencoding\":\n\t\t\tvar zb0007 uint32\n\t\t\tzb0007, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"cencoding\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0007 > 128 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.cencoding == nil || uint32(cap(z.cencoding)) < zb0007 {\n\t\t\t\tz.cencoding = make([]byte, zb0007)\n\t\t\t} else {\n\t\t\t\tz.cencoding = z.cencoding[:zb0007]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0007 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.cencoding, bts[:zb0007])\n\t\t\tbts = bts[zb0007:]\n\t\tcase \"cacheControl\":\n\t\t\tvar zb0008 uint32\n\t\t\tzb0008, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"cacheControl\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0008 > 2048 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.cacheControl == nil || uint32(cap(z.cacheControl)) < zb0008 {\n\t\t\t\tz.cacheControl = make([]byte, zb0008)\n\t\t\t} else {\n\t\t\t\tz.cacheControl = z.cacheControl[:zb0008]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0008 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.cacheControl, bts[:zb0008])\n\t\t\tbts = bts[zb0008:]\n\t\tcase \"expires\":\n\t\t\tvar zb0009 uint32\n\t\t\tzb0009, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"expires\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0009 > 128 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.expires == nil || uint32(cap(z.expires)) < zb0009 {\n\t\t\t\tz.expires = make([]byte, zb0009)\n\t\t\t} else {\n\t\t\t\tz.expires = z.expires[:zb0009]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0009 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.expires, bts[:zb0009])\n\t\t\tbts = bts[zb0009:]\n\t\tcase \"etag\":\n\t\t\tvar zb0010 uint32\n\t\t\tzb0010, bts, err = msgp.ReadBytesHeader(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"etag\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0010 > 256 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.etag == nil || uint32(cap(z.etag)) < zb0010 {\n\t\t\t\tz.etag = make([]byte, zb0010)\n\t\t\t} else {\n\t\t\t\tz.etag = z.etag[:zb0010]\n\t\t\t}\n\t\t\tif uint32(len(bts)) < zb0010 {\n\t\t\t\terr = msgp.ErrShortBytes\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcopy(z.etag, bts[:zb0010])\n\t\t\tbts = bts[zb0010:]\n\t\tcase \"date\":\n\t\t\tz.date, bts, err = msgp.ReadUint64Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"date\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"status\":\n\t\t\tz.status, bts, err = msgp.ReadIntBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"status\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"age\":\n\t\t\tz.age, bts, err = msgp.ReadUint64Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"age\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"exp\":\n\t\t\tz.exp, bts, err = msgp.ReadUint64Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"exp\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"ttl\":\n\t\t\tz.ttl, bts, err = msgp.ReadUint64Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"ttl\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"forceRevalidate\":\n\t\t\tz.forceRevalidate, bts, err = msgp.ReadBoolBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"forceRevalidate\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"revalidate\":\n\t\t\tz.revalidate, bts, err = msgp.ReadBoolBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"revalidate\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"shareable\":\n\t\t\tz.shareable, bts, err = msgp.ReadBoolBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"shareable\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"private\":\n\t\t\tz.private, bts, err = msgp.ReadBoolBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"private\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"heapidx\":\n\t\t\tz.heapidx, bts, err = msgp.ReadIntBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"heapidx\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z *item) Msgsize() (s int) {\n\ts = 3 + 8 + msgp.ArrayHeaderSize\n\tfor za0001 := range z.headers {\n\t\ts += 1 + 4 + msgp.BytesPrefixSize + len(z.headers[za0001].key) + 6 + msgp.BytesPrefixSize + len(z.headers[za0001].value)\n\t}\n\ts += 5 + msgp.BytesPrefixSize + len(z.body) + 6 + msgp.BytesPrefixSize + len(z.ctype) + 10 + msgp.BytesPrefixSize + len(z.cencoding) + 13 + msgp.BytesPrefixSize + len(z.cacheControl) + 8 + msgp.BytesPrefixSize + len(z.expires) + 5 + msgp.BytesPrefixSize + len(z.etag) + 5 + msgp.Uint64Size + 7 + msgp.IntSize + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 16 + msgp.BoolSize + 11 + msgp.BoolSize + 10 + msgp.BoolSize + 8 + msgp.BoolSize + 8 + msgp.IntSize\n\treturn\n}\n"
  },
  {
    "path": "middleware/cache/manager_msgp_test.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage cache\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\nfunc TestMarshalUnmarshalcachedHeader(t *testing.T) {\n\tv := cachedHeader{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgcachedHeader(b *testing.B) {\n\tv := cachedHeader{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgcachedHeader(b *testing.B) {\n\tv := cachedHeader{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalcachedHeader(b *testing.B) {\n\tv := cachedHeader{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecodecachedHeader(t *testing.T) {\n\tv := cachedHeader{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecodecachedHeader Msgsize() is inaccurate\")\n\t}\n\n\tvn := cachedHeader{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncodecachedHeader(b *testing.B) {\n\tv := cachedHeader{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecodecachedHeader(b *testing.B) {\n\tv := cachedHeader{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestMarshalUnmarshalitem(t *testing.T) {\n\tv := item{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgitem(b *testing.B) {\n\tv := item{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgitem(b *testing.B) {\n\tv := item{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalitem(b *testing.B) {\n\tv := item{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecodeitem(t *testing.T) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecodeitem Msgsize() is inaccurate\")\n\t}\n\n\tvn := item{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/cache/manager_test.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_manager_get(t *testing.T) {\n\tt.Parallel()\n\tcacheManager := newManager(nil, true)\n\tt.Run(\"Item not found in cache\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tit, err := cacheManager.get(context.Background(), utils.UUIDv4())\n\t\trequire.ErrorIs(t, err, errCacheMiss)\n\t\tassert.Nil(t, it)\n\t})\n\tt.Run(\"Item found in cache\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tid := utils.UUIDv4()\n\t\tcacheItem := cacheManager.acquire()\n\t\tcacheItem.body = []byte(\"test-body\")\n\t\trequire.NoError(t, cacheManager.set(context.Background(), id, cacheItem, 10*time.Second))\n\t\tit, err := cacheManager.get(context.Background(), id)\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, it)\n\t})\n}\n\nfunc Test_manager_logKey(t *testing.T) {\n\tt.Parallel()\n\n\tredactedManager := newManager(nil, true)\n\tassert.Equal(t, redactedKey, redactedManager.logKey(\"secret\"))\n\n\tplainManager := newManager(nil, false)\n\tassert.Equal(t, \"secret\", plainManager.logKey(\"secret\"))\n}\n"
  },
  {
    "path": "middleware/compress/compress.go",
    "content": "package compress\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/etag\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc hasToken(header, token string) bool {\n\tfor part := range strings.SplitSeq(header, \",\") {\n\t\tif utils.EqualFold(utils.TrimSpace(part), token) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc shouldSkip(c fiber.Ctx) bool {\n\tif c.Method() == fiber.MethodHead {\n\t\treturn true\n\t}\n\n\tstatus := c.Response().StatusCode()\n\tif status < 200 ||\n\t\tstatus == fiber.StatusNoContent ||\n\t\tstatus == fiber.StatusResetContent ||\n\t\tstatus == fiber.StatusNotModified ||\n\t\tstatus == fiber.StatusPartialContent ||\n\t\tlen(c.Response().Body()) == 0 ||\n\t\tc.Get(fiber.HeaderRange) != \"\" ||\n\t\thasToken(c.Get(fiber.HeaderCacheControl), \"no-transform\") ||\n\t\thasToken(c.GetRespHeader(fiber.HeaderCacheControl), \"no-transform\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc appendVaryAcceptEncoding(c fiber.Ctx) {\n\tvary := c.GetRespHeader(fiber.HeaderVary)\n\tif vary == \"\" {\n\t\tc.Set(fiber.HeaderVary, fiber.HeaderAcceptEncoding)\n\t\treturn\n\t}\n\tif hasToken(vary, \"*\") || hasToken(vary, fiber.HeaderAcceptEncoding) {\n\t\treturn\n\t}\n\tc.Set(fiber.HeaderVary, vary+\", \"+fiber.HeaderAcceptEncoding)\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Setup request handlers\n\tvar (\n\t\tfctx       = func(_ *fasthttp.RequestCtx) {}\n\t\tcompressor fasthttp.RequestHandler\n\t)\n\n\t// Setup compression algorithm\n\tswitch cfg.Level {\n\tcase LevelDefault:\n\t\t// LevelDefault\n\t\tcompressor = fasthttp.CompressHandlerBrotliLevel(fctx,\n\t\t\tfasthttp.CompressBrotliDefaultCompression,\n\t\t\tfasthttp.CompressDefaultCompression,\n\t\t)\n\tcase LevelBestSpeed:\n\t\t// LevelBestSpeed\n\t\tcompressor = fasthttp.CompressHandlerBrotliLevel(fctx,\n\t\t\tfasthttp.CompressBrotliBestSpeed,\n\t\t\tfasthttp.CompressBestSpeed,\n\t\t)\n\tcase LevelBestCompression:\n\t\t// LevelBestCompression\n\t\tcompressor = fasthttp.CompressHandlerBrotliLevel(fctx,\n\t\t\tfasthttp.CompressBrotliBestCompression,\n\t\t\tfasthttp.CompressBestCompression,\n\t\t)\n\tdefault:\n\t\t// LevelDisabled\n\t\treturn func(c fiber.Ctx) error {\n\t\t\treturn c.Next()\n\t\t}\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Continue stack\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif shouldSkip(c) {\n\t\t\tappendVaryAcceptEncoding(c)\n\t\t\treturn nil\n\t\t}\n\n\t\tif c.GetRespHeader(fiber.HeaderContentEncoding) != \"\" {\n\t\t\tappendVaryAcceptEncoding(c)\n\t\t\treturn nil\n\t\t}\n\n\t\tcompressor(c.RequestCtx())\n\n\t\tif tag := c.GetRespHeader(fiber.HeaderETag); tag != \"\" && !strings.HasPrefix(tag, \"W/\") {\n\t\t\tif c.GetRespHeader(fiber.HeaderContentEncoding) != \"\" {\n\t\t\t\tc.Set(fiber.HeaderETag, string(etag.Generate(c.Response().Body())))\n\t\t\t}\n\t\t}\n\n\t\tappendVaryAcceptEncoding(c)\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "middleware/compress/compress_test.go",
    "content": "package compress\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/etag\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nvar filedata []byte\n\nvar testConfig = fiber.TestConfig{\n\tTimeout:       10 * time.Second,\n\tFailOnTimeout: true,\n}\n\nfunc init() {\n\tdat, err := os.ReadFile(\"../../.github/README.md\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfiledata = dat\n}\n\n// go test -run Test_Compress_Gzip\nfunc Test_Compress_Gzip(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"gzip\", resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t// Validate that the file size has shrunk\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Less(t, len(body), len(filedata))\n}\n\n// go test -run Test_Compress_Different_Level\nfunc Test_Compress_Different_Level(t *testing.T) {\n\tt.Parallel()\n\tlevels := []Level{LevelDefault, LevelBestSpeed, LevelBestCompression}\n\talgorithms := []string{\"gzip\", \"deflate\", \"br\", \"zstd\"}\n\n\tfor _, algo := range algorithms {\n\t\tfor _, level := range levels {\n\t\t\tt.Run(fmt.Sprintf(\"%s_level %d\", algo, level), func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tapp := fiber.New()\n\n\t\t\t\tapp.Use(New(Config{Level: level}))\n\n\t\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\t\t\t\treturn c.Send(filedata)\n\t\t\t\t})\n\n\t\t\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\t\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\n\t\t\t\tresp, err := app.Test(req, testConfig)\n\t\t\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\t\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\t\t\trequire.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t\t\t\t// Validate that the file size has shrunk\n\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.Less(t, len(body), len(filedata))\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc Test_Compress_Deflate(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"deflate\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"deflate\", resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t// Validate that the file size has shrunk\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Less(t, len(body), len(filedata))\n}\n\nfunc Test_Compress_Brotli(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"br\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"br\", resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t// Validate that the file size has shrunk\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Less(t, len(body), len(filedata))\n}\n\nfunc Test_Compress_Zstd(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"zstd\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"zstd\", resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t// Validate that the file size has shrunk\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Less(t, len(body), len(filedata))\n}\n\nfunc Test_Compress_Disabled(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Level: LevelDisabled}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"br\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\n\t// Validate the file size is not shrunk\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Len(t, body, len(filedata))\n}\n\nfunc Test_Compress_Adds_Vary_Header(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Vary_Star(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderVary, \"*\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"*\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Vary_List_Star(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderVary, \"User-Agent, *\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"User-Agent, *\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Vary_Similar_Substring(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderVary, \"Accept-Encoding2\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"Accept-Encoding2, Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_When_Content_Encoding_Set(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentEncoding, \"gzip\")\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello\", string(body))\n\trequire.Equal(t, \"gzip\", resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"\\\"abc\\\"\", resp.Header.Get(fiber.HeaderETag))\n\trequire.Equal(t, \"Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_When_Content_Encoding_Set_Vary_Star(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentEncoding, \"gzip\")\n\t\tc.Set(fiber.HeaderVary, \"*\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"*\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_When_Content_Encoding_Set_Vary_List_Star(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentEncoding, \"gzip\")\n\t\tc.Set(fiber.HeaderVary, \"User-Agent, *\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"User-Agent, *\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_When_Content_Encoding_Set_Vary_Similar_Substring(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentEncoding, \"gzip\")\n\t\tc.Set(fiber.HeaderVary, \"Accept-Encoding2\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"Accept-Encoding2, Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Strong_ETag_Recalculated(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"gzip\", resp.Header.Get(fiber.HeaderContentEncoding))\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\texpected := string(etag.Generate(body))\n\trequire.Equal(t, expected, resp.Header.Get(fiber.HeaderETag))\n}\n\nfunc Test_Compress_Weak_ETag_Unchanged(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\tc.Set(fiber.HeaderETag, \"W/\\\"abc\\\"\")\n\t\treturn c.Send(filedata)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"gzip\", resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"W/\\\"abc\\\"\", resp.Header.Get(fiber.HeaderETag))\n}\n\nfunc Test_Compress_Strong_ETag_Unchanged_When_Not_Compressed(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\treturn c.SendString(\"tiny\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"\\\"abc\\\"\", resp.Header.Get(fiber.HeaderETag))\n\trequire.Equal(t, \"Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_Head(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\thandler := func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\treturn c.Send(filedata)\n\t}\n\tapp.Get(\"/\", handler)\n\tapp.Head(\"/\", handler)\n\n\tgetReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tgetReq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tgetResp, err := app.Test(getReq, testConfig)\n\trequire.NoError(t, err, \"app.Test(getReq)\")\n\tgetBody, err := io.ReadAll(getResp.Body)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, getBody)\n\trequire.Equal(t, \"gzip\", getResp.Header.Get(fiber.HeaderContentEncoding))\n\n\theadReq := httptest.NewRequest(fiber.MethodHead, \"/\", http.NoBody)\n\theadReq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\theadResp, err := app.Test(headReq, testConfig)\n\trequire.NoError(t, err, \"app.Test(headReq)\")\n\theadBody, err := io.ReadAll(headResp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, headBody)\n\n\trequire.Empty(t, headResp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"Accept-Encoding\", headResp.Header.Get(fiber.HeaderVary))\n\trequire.Equal(t, \"\\\"abc\\\"\", headResp.Header.Get(fiber.HeaderETag))\n}\n\nfunc Test_Compress_Skip_Status_NoContent(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\treturn c.SendStatus(fiber.StatusNoContent)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"\\\"abc\\\"\", resp.Header.Get(fiber.HeaderETag))\n}\n\nfunc Test_Compress_Skip_Status_NotModified(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, \"\\\"abc\\\"\")\n\t\tc.Status(fiber.StatusNotModified)\n\t\treturn nil\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusNotModified, resp.StatusCode)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"\\\"abc\\\"\", resp.Header.Get(fiber.HeaderETag))\n}\n\nfunc Test_Compress_Skip_Range(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"Range\", \"bytes=0-1\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_Range_NoAcceptEncoding(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Range\", \"bytes=0-1\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_Range_Vary_Star(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderVary, \"*\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"Range\", \"bytes=0-1\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"*\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_Range_Vary_Similar_Substring(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderVary, \"Accept-Encoding2\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"Range\", \"bytes=0-1\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"Accept-Encoding2, Accept-Encoding\", resp.Header.Get(fiber.HeaderVary))\n}\n\nfunc Test_Compress_Skip_Status_PartialContent(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusPartialContent)\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req, testConfig)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusPartialContent, resp.StatusCode)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n}\n\nfunc Test_Compress_Skip_NoTransform(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname       string\n\t\tsetRequest bool\n\t}{\n\t\t{name: \"request\", setRequest: true},\n\t\t{name: \"response\", setRequest: false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\n\t\t\tapp.Use(New())\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tif !tt.setRequest {\n\t\t\t\t\tc.Set(fiber.HeaderCacheControl, \"no-transform\")\n\t\t\t\t}\n\t\t\t\treturn c.SendString(\"hello\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\t\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\t\t\tif tt.setRequest {\n\t\t\t\treq.Header.Set(fiber.HeaderCacheControl, \"no-transform\")\n\t\t\t}\n\n\t\t\tresp, err := app.Test(req, testConfig)\n\t\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\t\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\t\t})\n\t}\n}\n\nfunc Test_Compress_Next_Error(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn errors.New(\"next error\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 500, resp.StatusCode, \"Status code\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"next error\", string(body))\n}\n\n// go test -run Test_Compress_Next\nfunc Test_Compress_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -bench=Benchmark_Compress\nfunc Benchmark_Compress(b *testing.B) {\n\ttests := []struct {\n\t\tname           string\n\t\tacceptEncoding string\n\t}{\n\t\t{name: \"Gzip\", acceptEncoding: \"gzip\"},\n\t\t{name: \"Deflate\", acceptEncoding: \"deflate\"},\n\t\t{name: \"Brotli\", acceptEncoding: \"br\"},\n\t\t{name: \"Zstd\", acceptEncoding: \"zstd\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(tt.name, func(b *testing.B) {\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New())\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\t\t\treturn c.Send(filedata)\n\t\t\t})\n\n\t\t\th := app.Handler()\n\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\t\tif tt.acceptEncoding != \"\" {\n\t\t\t\tfctx.Request.Header.Set(\"Accept-Encoding\", tt.acceptEncoding)\n\t\t\t}\n\n\t\t\tb.ReportAllocs()\n\n\t\t\tfor b.Loop() {\n\t\t\t\th(fctx)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -bench=Benchmark_Compress_Levels\nfunc Benchmark_Compress_Levels(b *testing.B) {\n\ttests := []struct {\n\t\tname           string\n\t\tacceptEncoding string\n\t}{\n\t\t{name: \"Gzip\", acceptEncoding: \"gzip\"},\n\t\t{name: \"Deflate\", acceptEncoding: \"deflate\"},\n\t\t{name: \"Brotli\", acceptEncoding: \"br\"},\n\t\t{name: \"Zstd\", acceptEncoding: \"zstd\"},\n\t}\n\n\tlevels := []struct {\n\t\tname  string\n\t\tlevel Level\n\t}{\n\t\t{name: \"LevelDisabled\", level: LevelDisabled},\n\t\t{name: \"LevelDefault\", level: LevelDefault},\n\t\t{name: \"LevelBestSpeed\", level: LevelBestSpeed},\n\t\t{name: \"LevelBestCompression\", level: LevelBestCompression},\n\t}\n\n\tfor _, tt := range tests {\n\t\tfor _, lvl := range levels {\n\t\t\tb.Run(tt.name+\"_\"+lvl.name, func(b *testing.B) {\n\t\t\t\tapp := fiber.New()\n\t\t\t\tapp.Use(New(Config{Level: lvl.level}))\n\t\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\t\t\t\treturn c.Send(filedata)\n\t\t\t\t})\n\n\t\t\t\th := app.Handler()\n\t\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\t\t\tif tt.acceptEncoding != \"\" {\n\t\t\t\t\tfctx.Request.Header.Set(\"Accept-Encoding\", tt.acceptEncoding)\n\t\t\t\t}\n\n\t\t\t\tb.ReportAllocs()\n\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\th(fctx)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\n// go test -bench=Benchmark_Compress_Parallel\nfunc Benchmark_Compress_Parallel(b *testing.B) {\n\ttests := []struct {\n\t\tname           string\n\t\tacceptEncoding string\n\t}{\n\t\t{name: \"Gzip\", acceptEncoding: \"gzip\"},\n\t\t{name: \"Deflate\", acceptEncoding: \"deflate\"},\n\t\t{name: \"Brotli\", acceptEncoding: \"br\"},\n\t\t{name: \"Zstd\", acceptEncoding: \"zstd\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(tt.name, func(b *testing.B) {\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New())\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\t\t\treturn c.Send(filedata)\n\t\t\t})\n\n\t\t\th := app.Handler()\n\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\t\t\tif tt.acceptEncoding != \"\" {\n\t\t\t\t\tfctx.Request.Header.Set(\"Accept-Encoding\", tt.acceptEncoding)\n\t\t\t\t}\n\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\th(fctx)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// go test -bench=Benchmark_Compress_Levels_Parallel\nfunc Benchmark_Compress_Levels_Parallel(b *testing.B) {\n\ttests := []struct {\n\t\tname           string\n\t\tacceptEncoding string\n\t}{\n\t\t{name: \"Gzip\", acceptEncoding: \"gzip\"},\n\t\t{name: \"Deflate\", acceptEncoding: \"deflate\"},\n\t\t{name: \"Brotli\", acceptEncoding: \"br\"},\n\t\t{name: \"Zstd\", acceptEncoding: \"zstd\"},\n\t}\n\n\tlevels := []struct {\n\t\tname  string\n\t\tlevel Level\n\t}{\n\t\t{name: \"LevelDisabled\", level: LevelDisabled},\n\t\t{name: \"LevelDefault\", level: LevelDefault},\n\t\t{name: \"LevelBestSpeed\", level: LevelBestSpeed},\n\t\t{name: \"LevelBestCompression\", level: LevelBestCompression},\n\t}\n\n\tfor _, tt := range tests {\n\t\tfor _, lvl := range levels {\n\t\t\tb.Run(tt.name+\"_\"+lvl.name, func(b *testing.B) {\n\t\t\t\tapp := fiber.New()\n\t\t\t\tapp.Use(New(Config{Level: lvl.level}))\n\t\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)\n\t\t\t\t\treturn c.Send(filedata)\n\t\t\t\t})\n\n\t\t\t\th := app.Handler()\n\n\t\t\t\tb.ReportAllocs()\n\t\t\t\tb.ResetTimer()\n\n\t\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\t\tfctx := &fasthttp.RequestCtx{}\n\t\t\t\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\t\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\t\t\t\tif tt.acceptEncoding != \"\" {\n\t\t\t\t\t\tfctx.Request.Header.Set(\"Accept-Encoding\", tt.acceptEncoding)\n\t\t\t\t\t}\n\n\t\t\t\t\tfor pb.Next() {\n\t\t\t\t\t\th(fctx)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/compress/config.go",
    "content": "package compress\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Level sets the compression level used for the response\n\t//\n\t// Optional. Default: LevelDefault\n\t// LevelDisabled:         -1\n\t// LevelDefault:          0\n\t// LevelBestSpeed:        1\n\t// LevelBestCompression:  2\n\tLevel Level\n}\n\n// Level is numeric representation of compression level\ntype Level int\n\n// Represents compression level that will be used in the middleware\nconst (\n\tLevelDisabled        Level = -1\n\tLevelDefault         Level = 0\n\tLevelBestSpeed       Level = 1\n\tLevelBestCompression Level = 2\n)\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:  nil,\n\tLevel: LevelDefault,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Level < LevelDisabled || cfg.Level > LevelBestCompression {\n\t\tcfg.Level = ConfigDefault.Level\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/cors/config.go",
    "content": "package cors\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// AllowOriginsFunc defines a function that will set the 'Access-Control-Allow-Origin'\n\t// response header to the 'origin' request header when returned true. This allows for\n\t// dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins\n\t// will be not have the 'Access-Control-Allow-Credentials' header set to 'true'.\n\t//\n\t// The function receives serialized origins (scheme + host) or the literal \"null\" string.\n\t// According to the CORS specification (https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#origin),\n\t// browsers send \"null\" for privacy-sensitive contexts like sandboxed iframes or file:// URLs.\n\t//\n\t// Origins with userinfo, paths, queries, fragments, or wildcards are rejected and will not\n\t// be passed to this function.\n\t//\n\t// Optional. Default: nil\n\tAllowOriginsFunc func(origin string) bool\n\n\t// AllowOrigin defines a list of origins that may access the resource.\n\t//\n\t// This supports wildcard matching for subdomains by prefixing the domain with a `*.`\n\t// e.g. \"http://.domain.com\". This will allow all level of subdomains of domain.com to access the resource.\n\t//\n\t// If the special wildcard `\"*\"` is present in the list, all origins will be allowed.\n\t//\n\t// Optional. Default value []string{}\n\tAllowOrigins []string\n\n\t// AllowMethods defines a list methods allowed when accessing the resource.\n\t// This is used in response to a preflight request.\n\t//\n\t// Optional. Default value []string{\"GET\", \"POST\", \"HEAD\", \"PUT\", \"DELETE\", \"PATCH\"}\n\tAllowMethods []string\n\n\t// AllowHeaders defines a list of request headers that can be used when\n\t// making the actual request. This is in response to a preflight request.\n\t//\n\t// Optional. Default value []string{}\n\tAllowHeaders []string\n\n\t// ExposeHeaders defines an allowlist of headers that clients are allowed to\n\t// access.\n\t//\n\t// Optional. Default value []string{}.\n\tExposeHeaders []string\n\n\t// MaxAge indicates how long (in seconds) the results of a preflight request\n\t// can be cached.\n\t// If you pass MaxAge 0, Access-Control-Max-Age header will not be added and\n\t// browser will use 5 seconds by default.\n\t// To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header 0.\n\t//\n\t// Optional. Default value 0.\n\tMaxAge int\n\n\t// DisableValueRedaction turns off redaction of configuration values and origins in logs and panics.\n\t//\n\t// Optional. Default: false\n\tDisableValueRedaction bool\n\n\t// AllowCredentials indicates whether or not the response to the request\n\t// can be exposed when the credentials flag is true. When used as part of\n\t// a response to a preflight request, this indicates whether or not the\n\t// actual request can be made using credentials. Note: if true, the\n\t// AllowOrigins setting cannot contain the wildcard \"*\" to prevent\n\t// security vulnerabilities.\n\t//\n\t// Optional. Default value false.\n\tAllowCredentials bool\n\n\t// AllowPrivateNetwork indicates whether the Access-Control-Allow-Private-Network\n\t// response header should be set to true, allowing requests from private networks.\n\t//\n\t// Optional. Default value false.\n\tAllowPrivateNetwork bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:                  nil,\n\tAllowOriginsFunc:      nil,\n\tAllowOrigins:          []string{\"*\"},\n\tDisableValueRedaction: false,\n\tAllowMethods: []string{\n\t\tfiber.MethodGet,\n\t\tfiber.MethodPost,\n\t\tfiber.MethodHead,\n\t\tfiber.MethodPut,\n\t\tfiber.MethodDelete,\n\t\tfiber.MethodPatch,\n\t},\n\tAllowHeaders:        []string{},\n\tAllowCredentials:    false,\n\tExposeHeaders:       []string{},\n\tMaxAge:              0,\n\tAllowPrivateNetwork: false,\n}\n"
  },
  {
    "path": "middleware/cors/cors.go",
    "content": "package cors\n\nimport (\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n)\n\nconst redactedValue = \"[redacted]\"\n\n// isOriginSerializedOrNull checks if the origin is a serialized origin or the literal \"null\".\n// It returns two booleans: (isSerialized, isNull).\nfunc isOriginSerializedOrNull(originHeaderRaw string) (isSerialized, isNull bool) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming serialization and null status results\n\tif originHeaderRaw == \"null\" {\n\t\treturn false, true\n\t}\n\n\toriginIsSerialized, _ := normalizeOrigin(originHeaderRaw)\n\treturn originIsSerialized, false\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := ConfigDefault\n\n\t// Override config if provided\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\n\t\t// Set default values\n\t\tif len(cfg.AllowMethods) == 0 {\n\t\t\tcfg.AllowMethods = ConfigDefault.AllowMethods\n\t\t}\n\t}\n\n\tredactValues := !cfg.DisableValueRedaction\n\n\tmaskValue := func(value string) string {\n\t\tif redactValues {\n\t\t\treturn redactedValue\n\t\t}\n\t\treturn value\n\t}\n\n\t// Warning logs if both AllowOrigins and AllowOriginsFunc are set\n\tif len(cfg.AllowOrigins) > 0 && cfg.AllowOriginsFunc != nil {\n\t\tlog.Warn(\"[CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.\")\n\t}\n\n\t// allowOrigins is a slice of strings that contains the allowed origins\n\t// defined in the 'AllowOrigins' configuration.\n\tallowOrigins := []string{}\n\tallowSubOrigins := []subdomain{}\n\n\t// Validate and normalize static AllowOrigins\n\tallowAllOrigins := len(cfg.AllowOrigins) == 0 && cfg.AllowOriginsFunc == nil\n\tfor _, origin := range cfg.AllowOrigins {\n\t\tif origin == \"*\" {\n\t\t\tallowAllOrigins = true\n\t\t\tbreak\n\t\t}\n\n\t\ttrimmedOrigin := utils.TrimSpace(origin)\n\t\tif before, after, found := strings.Cut(trimmedOrigin, \"://*.\"); found {\n\t\t\twithoutWildcard := before + \"://\" + after\n\t\t\tisValid, normalizedOrigin := normalizeOrigin(withoutWildcard)\n\t\t\tif !isValid {\n\t\t\t\tpanic(\"[CORS] Invalid origin format in configuration: \" + maskValue(trimmedOrigin))\n\t\t\t}\n\t\t\tscheme, host, ok := strings.Cut(normalizedOrigin, \"://\")\n\t\t\tif !ok {\n\t\t\t\tpanic(\"[CORS] Invalid origin format after normalization:\" + maskValue(trimmedOrigin))\n\t\t\t}\n\t\t\tsd := subdomain{prefix: scheme + \"://\", suffix: host}\n\t\t\tallowSubOrigins = append(allowSubOrigins, sd)\n\t\t} else {\n\t\t\tisValid, normalizedOrigin := normalizeOrigin(trimmedOrigin)\n\t\t\tif !isValid {\n\t\t\t\tpanic(\"[CORS] Invalid origin format in configuration: \" + maskValue(trimmedOrigin))\n\t\t\t}\n\t\t\tallowOrigins = append(allowOrigins, normalizedOrigin)\n\t\t}\n\t}\n\n\t// Validate CORS credentials configuration\n\tif cfg.AllowCredentials && allowAllOrigins {\n\t\tpanic(\"[CORS] Configuration error: When 'AllowCredentials' is set to true, 'AllowOrigins' cannot contain a wildcard origin '*'. Please specify allowed origins explicitly or adjust 'AllowCredentials' setting.\")\n\t}\n\n\t// Warn if allowAllOrigins is set to true and AllowOriginsFunc is defined\n\tif allowAllOrigins && cfg.AllowOriginsFunc != nil {\n\t\tlog.Warn(\"[CORS] 'AllowOrigins' is set to allow all origins, 'AllowOriginsFunc' will not be used.\")\n\t}\n\n\t// Convert int to string\n\tmaxAge := strconv.Itoa(cfg.MaxAge)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Get origin header preserving the original case for the response\n\t\toriginHeaderRaw := c.Get(fiber.HeaderOrigin)\n\t\toriginHeader := utilsstrings.ToLower(originHeaderRaw)\n\n\t\t// If the request does not have Origin header, the request is outside the scope of CORS\n\t\tif originHeader == \"\" {\n\t\t\t// See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches\n\t\t\t// Unless all origins are allowed, we include the Vary header to cache the response correctly\n\t\t\tif !allowAllOrigins {\n\t\t\t\tc.Vary(fiber.HeaderOrigin)\n\t\t\t}\n\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// If it's a preflight request and doesn't have Access-Control-Request-Method header, it's outside the scope of CORS\n\t\tif c.Method() == fiber.MethodOptions && c.Get(fiber.HeaderAccessControlRequestMethod) == \"\" {\n\t\t\t// Response to OPTIONS request should not be cached but,\n\t\t\t// some caching can be configured to cache such responses.\n\t\t\t// To Avoid poisoning the cache, we include the Vary header\n\t\t\t// for non-CORS OPTIONS requests:\n\t\t\tc.Vary(fiber.HeaderOrigin)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Set default allowOrigin to empty string\n\t\tallowOrigin := \"\"\n\n\t\t// Check allowed origins\n\t\tif allowAllOrigins {\n\t\t\tallowOrigin = \"*\"\n\t\t} else {\n\t\t\t// Check if the origin is in the list of allowed origins\n\t\t\tif slices.Contains(allowOrigins, originHeader) {\n\t\t\t\tallowOrigin = originHeaderRaw\n\t\t\t}\n\n\t\t\t// Check if the origin is in the list of allowed subdomains\n\t\t\tif allowOrigin == \"\" {\n\t\t\t\tfor _, sOrigin := range allowSubOrigins {\n\t\t\t\t\tif sOrigin.match(originHeader) {\n\t\t\t\t\t\tallowOrigin = originHeaderRaw\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Run AllowOriginsFunc if the logic for\n\t\t// handling the value in 'AllowOrigins' does\n\t\t// not result in allowOrigin being set.\n\t\tif allowOrigin == \"\" && cfg.AllowOriginsFunc != nil && cfg.AllowOriginsFunc(originHeaderRaw) {\n\t\t\toriginIsSerialized, originIsNull := isOriginSerializedOrNull(originHeaderRaw)\n\t\t\tif originIsSerialized || originIsNull {\n\t\t\t\tallowOrigin = originHeaderRaw\n\t\t\t}\n\t\t}\n\n\t\t// Simple request\n\t\t// Omit allowMethods and allowHeaders, only used for pre-flight requests\n\t\tif c.Method() != fiber.MethodOptions {\n\t\t\tif !allowAllOrigins {\n\t\t\t\t// See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches\n\t\t\t\tc.Vary(fiber.HeaderOrigin)\n\t\t\t}\n\t\t\tsetSimpleHeaders(c, allowOrigin, &cfg)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Pre-flight request\n\n\t\t// Response to OPTIONS request should not be cached but,\n\t\t// some caching can be configured to cache such responses.\n\t\t// To Avoid poisoning the cache, we include the Vary header\n\t\t// of preflight responses:\n\t\tc.Vary(fiber.HeaderAccessControlRequestMethod)\n\t\tc.Vary(fiber.HeaderAccessControlRequestHeaders)\n\t\tif cfg.AllowPrivateNetwork && c.Get(fiber.HeaderAccessControlRequestPrivateNetwork) == \"true\" {\n\t\t\tc.Vary(fiber.HeaderAccessControlRequestPrivateNetwork)\n\t\t\tc.Set(fiber.HeaderAccessControlAllowPrivateNetwork, \"true\")\n\t\t}\n\t\tc.Vary(fiber.HeaderOrigin)\n\n\t\tsetPreflightHeaders(c, allowOrigin, maxAge, &cfg)\n\n\t\t// Set Preflight headers\n\t\tif len(cfg.AllowMethods) > 0 {\n\t\t\tc.Set(fiber.HeaderAccessControlAllowMethods, strings.Join(cfg.AllowMethods, \", \"))\n\t\t}\n\t\tif len(cfg.AllowHeaders) > 0 {\n\t\t\tc.Set(fiber.HeaderAccessControlAllowHeaders, strings.Join(cfg.AllowHeaders, \", \"))\n\t\t} else {\n\t\t\th := c.Get(fiber.HeaderAccessControlRequestHeaders)\n\t\t\tif h != \"\" {\n\t\t\t\tc.Set(fiber.HeaderAccessControlAllowHeaders, h)\n\t\t\t}\n\t\t}\n\n\t\t// Send 204 No Content\n\t\treturn c.SendStatus(fiber.StatusNoContent)\n\t}\n}\n\n// Function to set Simple CORS headers\nfunc setSimpleHeaders(c fiber.Ctx, allowOrigin string, cfg *Config) {\n\tif cfg == nil {\n\t\treturn\n\t}\n\n\tif cfg.AllowCredentials {\n\t\t// When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*'\n\t\tif allowOrigin == \"*\" {\n\t\t\tc.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)\n\t\t\tlog.Warn(\"[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.\")\n\t\t} else if allowOrigin != \"\" {\n\t\t\tc.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)\n\t\t\tc.Set(fiber.HeaderAccessControlAllowCredentials, \"true\")\n\t\t}\n\t} else if allowOrigin != \"\" {\n\t\t// For non-credential requests, it's safe to set to '*' or specific origins\n\t\tc.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin)\n\t}\n\n\t// Set Expose-Headers if not empty\n\tif len(cfg.ExposeHeaders) > 0 {\n\t\tc.Set(fiber.HeaderAccessControlExposeHeaders, strings.Join(cfg.ExposeHeaders, \", \"))\n\t}\n}\n\n// Function to set Preflight CORS headers\nfunc setPreflightHeaders(c fiber.Ctx, allowOrigin, maxAge string, cfg *Config) {\n\tsetSimpleHeaders(c, allowOrigin, cfg)\n\n\t// Set MaxAge if set\n\tif cfg != nil && cfg.MaxAge > 0 {\n\t\tc.Set(fiber.HeaderAccessControlMaxAge, maxAge)\n\t} else if cfg != nil && cfg.MaxAge < 0 {\n\t\tc.Set(fiber.HeaderAccessControlMaxAge, \"0\")\n\t}\n}\n"
  },
  {
    "path": "middleware/cors/cors_test.go",
    "content": "package cors\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_CORS_Defaults(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\ttestDefaultOrEmptyConfig(t, app)\n}\n\nfunc Test_CORS_Empty_Config(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{}))\n\n\ttestDefaultOrEmptyConfig(t, app)\n}\n\nfunc Test_CORS_WildcardHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tAllowMethods:  []string{\"*\"},\n\t\tAllowHeaders:  []string{\"*\"},\n\t\tExposeHeaders: []string{\"*\"},\n\t}))\n\n\th := app.Handler()\n\n\t// Test preflight request\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\th(ctx)\n\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)))\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)))\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))\n}\n\nfunc Test_CORS_Negative_MaxAge(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{MaxAge: -1}))\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\tapp.Handler()(ctx)\n\n\trequire.Equal(t, \"0\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge)))\n}\n\nfunc Test_CORS_MaxAge_NotSetOnSimpleRequest(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{MaxAge: 100}))\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\tapp.Handler()(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge)))\n}\n\nfunc Test_CORS_Preserve_Origin_Case(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{AllowOrigins: []string{\"http://example.com\"}}))\n\n\torigin := \"HTTP://EXAMPLE.COM\"\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, origin)\n\tapp.Handler()(ctx)\n\n\trequire.Equal(t, origin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n\nfunc testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) {\n\tt.Helper()\n\n\th := app.Handler()\n\n\t// Test default GET response headers\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\th(ctx)\n\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))\n\n\t// Test default OPTIONS (preflight) response headers\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\th(ctx)\n\n\trequire.Equal(t, \"GET, POST, HEAD, PUT, DELETE, PATCH\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)))\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)))\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge)))\n}\n\nfunc Test_CORS_AllowOrigins_Vary(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(\n\t\tConfig{\n\t\t\tAllowOrigins: []string{\"http://localhost\"},\n\t\t},\n\t))\n\n\th := app.Handler()\n\n\t// Test Vary header non-Cors request\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Contains(t, string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin, \"Vary header should be set\")\n\n\t// Test Vary header Cors request\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\th(ctx)\n\trequire.Contains(t, string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin, \"Vary header should be set\")\n}\n\n// go test -run -v Test_CORS_Wildcard\nfunc Test_CORS_Wildcard(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\t// OPTIONS (preflight) response headers when AllowOrigins is *\n\tapp.Use(New(Config{\n\t\tMaxAge:        3600,\n\t\tExposeHeaders: []string{\"X-Request-ID\"},\n\t\tAllowHeaders:  []string{\"Authentication\"},\n\t}))\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\n\t// Perform request\n\thandler(ctx)\n\n\t// Check result\n\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) // Validates request is not reflecting origin in the response\n\trequire.Contains(t, string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin, \"Vary header should be set\")\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"3600\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge)))\n\trequire.Equal(t, \"Authentication\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)))\n\n\t// Test non OPTIONS (preflight) response headers\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\thandler(ctx)\n\n\trequire.NotContains(t, string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin, \"Vary header should not be set\")\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"X-Request-ID\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))\n}\n\n// go test -run -v Test_CORS_Origin_AllowCredentials\nfunc Test_CORS_Origin_AllowCredentials(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\t// OPTIONS (preflight) response headers when AllowOrigins is *\n\tapp.Use(New(Config{\n\t\tAllowOrigins:     []string{\"http://localhost\"},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           3600,\n\t\tExposeHeaders:    []string{\"X-Request-ID\"},\n\t\tAllowHeaders:     []string{\"Authentication\"},\n\t}))\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\n\t// Perform request\n\thandler(ctx)\n\n\t// Check result\n\trequire.Equal(t, \"http://localhost\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\trequire.Equal(t, \"true\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"3600\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge)))\n\trequire.Equal(t, \"Authentication\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)))\n\n\t// Test non OPTIONS (preflight) response headers\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\thandler(ctx)\n\n\trequire.Equal(t, \"true\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\trequire.Equal(t, \"X-Request-ID\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))\n}\n\n// go test -run -v Test_CORS_Wildcard_AllowCredentials_Panic\n// Test for fiber-ghsa-fmg4-x8pw-hjhg\nfunc Test_CORS_Wildcard_AllowCredentials_Panic(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\n\tdidPanic := false\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tdidPanic = true\n\t\t\t}\n\t\t}()\n\n\t\tapp.Use(New(Config{\n\t\t\tAllowOrigins:     []string{\"*\"},\n\t\t\tAllowCredentials: true,\n\t\t}))\n\t}()\n\n\tif !didPanic {\n\t\tt.Error(\"Expected a panic when AllowOrigins is '*' and AllowCredentials is true\")\n\t}\n}\n\n// Test that a warning is logged when AllowOrigins allows all origins and\n// AllowOriginsFunc is also provided.\nfunc Test_CORS_Warn_AllowAllOrigins_WithFunc(t *testing.T) {\n\tvar buf bytes.Buffer\n\tlog.SetOutput(&buf)\n\tt.Cleanup(func() { log.SetOutput(os.Stderr) })\n\n\tfiber.New().Use(New(Config{\n\t\tAllowOrigins:     []string{\"*\"},\n\t\tAllowOriginsFunc: func(string) bool { return true },\n\t}))\n\n\trequire.Contains(t, buf.String(), \"AllowOriginsFunc' will not be used\")\n}\n\n// go test -run -v Test_CORS_Invalid_Origin_Panic\nfunc Test_CORS_Invalid_Origins_Panic(t *testing.T) {\n\tt.Parallel()\n\n\tinvalidOrigins := []string{\n\t\t\"localhost\",\n\t\t\"http://foo.[a-z]*.example.com\",\n\t\t\"http://*\",\n\t\t\"https://*\",\n\t\t\"http://*.com*\",\n\t\t\"invalid url\",\n\t\t\"*\",\n\t\t\"http://origin.com,invalid url\",\n\t\t// add more invalid origins as needed\n\t}\n\n\tfor _, origin := range invalidOrigins {\n\t\t// New fiber instance\n\t\tapp := fiber.New()\n\n\t\tdidPanic := false\n\t\tfunc() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tdidPanic = true\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tapp.Use(New(Config{\n\t\t\t\tAllowOrigins:     []string{origin},\n\t\t\t\tAllowCredentials: true,\n\t\t\t}))\n\t\t}()\n\n\t\tif !didPanic {\n\t\t\tt.Errorf(\"Expected a panic for invalid origin: %s\", origin)\n\t\t}\n\t}\n}\n\nfunc Test_CORS_DisableValueRedaction(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithValue(t, \"[CORS] Invalid origin format in configuration: [redacted]\", func() {\n\t\tNew(Config{\n\t\t\tAllowOrigins:          []string{\"http://\"},\n\t\t\tDisableValueRedaction: false,\n\t\t})\n\t})\n\n\trequire.PanicsWithValue(t, \"[CORS] Invalid origin format in configuration: http://\", func() {\n\t\tNew(Config{\n\t\t\tAllowOrigins:          []string{\"http://\"},\n\t\t\tDisableValueRedaction: true,\n\t\t})\n\t})\n}\n\n// go test -run -v Test_CORS_Subdomain\nfunc Test_CORS_Subdomain(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\t// OPTIONS (preflight) response headers when AllowOrigins is set to a subdomain\n\tapp.Use(\"/\", New(Config{\n\t\tAllowOrigins: []string{\"  http://*.example.com  \"},\n\t}))\n\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request with disallowed origin\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://google.com\")\n\n\t// Perform request\n\thandler(ctx)\n\n\t// Allow-Origin header should be \"\" because http://google.com does not satisfy http://*.example.com\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with domain only (disallowed)\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\n\thandler(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with malformed subdomain (disallowed)\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://evil.comexample.com\")\n\n\thandler(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with allowed origin\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://test.example.com\")\n\n\thandler(ctx)\n\n\trequire.Equal(t, \"http://test.example.com\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n\nfunc Test_CORS_AllowOriginScheme(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\treqOrigin         string\n\t\tpattern           []string\n\t\tshouldAllowOrigin bool\n\t}{\n\t\t{\n\t\t\tpattern:           []string{\"http://example.com\"},\n\t\t\treqOrigin:         \"http://example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"HTTP://EXAMPLE.COM\"},\n\t\t\treqOrigin:         \"http://example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"https://example.com\"},\n\t\t\treqOrigin:         \"https://example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://example.com\"},\n\t\t\treqOrigin:         \"https://example.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com\"},\n\t\t\treqOrigin:         \"http://aaa.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com\"},\n\t\t\treqOrigin:         \"http://bbb.aaa.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.aaa.example.com\"},\n\t\t\treqOrigin:         \"http://bbb.aaa.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com:8080\"},\n\t\t\treqOrigin:         \"http://aaa.example.com:8080\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com\"},\n\t\t\treqOrigin:         \"http://1.2.aaa.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://example.com\"},\n\t\t\treqOrigin:         \"http://gofiber.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.aaa.example.com\"},\n\t\t\treqOrigin:         \"http://ccc.bbb.example.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com\"},\n\t\t\treqOrigin:         \"http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://example.com\"},\n\t\t\treqOrigin:         \"http://ccc.bbb.example.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"https://--aaa.bbb.com\"},\n\t\t\treqOrigin:         \"https://prod-preview--aaa.bbb.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://*.example.com\"},\n\t\t\treqOrigin:         \"http://ccc.bbb.example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://domain-1.com\", \"http://example.com\"},\n\t\t\treqOrigin:         \"http://example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://domain-1.com\", \"http://example.com\"},\n\t\t\treqOrigin:         \"http://domain-2.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://domain-1.com\", \"http://example.com\"},\n\t\t\treqOrigin:         \"http://example.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://domain-1.com\", \"http://example.com\"},\n\t\t\treqOrigin:         \"http://domain-2.com\",\n\t\t\tshouldAllowOrigin: false,\n\t\t},\n\t\t{\n\t\t\tpattern:           []string{\"http://domain-1.com\", \"http://example.com\"},\n\t\t\treqOrigin:         \"http://domain-1.com\",\n\t\t\tshouldAllowOrigin: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tapp := fiber.New()\n\t\tapp.Use(\"/\", New(Config{AllowOrigins: tt.pattern}))\n\n\t\thandler := app.Handler()\n\n\t\tctx := &fasthttp.RequestCtx{}\n\t\tctx.Request.SetRequestURI(\"/\")\n\t\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\t\tctx.Request.Header.Set(fiber.HeaderOrigin, tt.reqOrigin)\n\n\t\thandler(ctx)\n\n\t\tif tt.shouldAllowOrigin {\n\t\t\trequire.Equal(t, tt.reqOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t} else {\n\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t}\n\t}\n}\n\nfunc Test_CORS_AllowOriginHeader_NoMatch(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\tapp.Use(\"/\", New(Config{\n\t\tAllowOrigins: []string{\"http://example-1.com\", \"https://example-1.com\"},\n\t}))\n\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request with disallowed origin\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://google.com\")\n\n\t// Perform request\n\thandler(ctx)\n\n\tvar headerExists bool\n\tfor key := range ctx.Response.Header.All() {\n\t\tif string(key) == fiber.HeaderAccessControlAllowOrigin {\n\t\t\theaderExists = true\n\t\t}\n\t}\n\trequire.False(t, headerExists, \"Access-Control-Allow-Origin header should not be set\")\n}\n\n// go test -run Test_CORS_Next\nfunc Test_CORS_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_CORS_Headers_BasedOnRequestType\nfunc Test_CORS_Headers_BasedOnRequestType(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tmethods := []string{\n\t\tfiber.MethodGet,\n\t\tfiber.MethodPost,\n\t\tfiber.MethodPut,\n\t\tfiber.MethodDelete,\n\t\tfiber.MethodPatch,\n\t\tfiber.MethodHead,\n\t}\n\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\tt.Run(\"Without origin\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Make request without origin header, and without Access-Control-Request-Method\n\t\tfor _, method := range methods {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(method)\n\t\t\tctx.Request.SetRequestURI(\"https://example.com/\")\n\t\t\thandler(ctx)\n\t\t\trequire.Equal(t, 200, ctx.Response.StatusCode(), \"Status code should be 200\")\n\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), \"Access-Control-Allow-Origin header should not be set\")\n\t\t}\n\t})\n\n\tt.Run(\"Preflight request with origin and Access-Control-Request-Method headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Make preflight request with origin header and with Access-Control-Request-Method\n\t\tfor _, method := range methods {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\t\t\tctx.Request.SetRequestURI(\"https://example.com/\")\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method)\n\t\t\thandler(ctx)\n\t\t\trequire.Equal(t, 204, ctx.Response.StatusCode(), \"Status code should be 204\")\n\t\t\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), \"Access-Control-Allow-Origin header should be set\")\n\t\t\trequire.Equal(t, \"GET, POST, HEAD, PUT, DELETE, PATCH\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), \"Access-Control-Allow-Methods header should be set (preflight request)\")\n\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), \"Access-Control-Allow-Headers header should be set (preflight request)\")\n\t\t}\n\t})\n\n\tt.Run(\"Non-preflight request with origin\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Make non-preflight request with origin header and with Access-Control-Request-Method\n\t\tfor _, method := range methods {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(method)\n\t\t\tctx.Request.SetRequestURI(\"https://example.com/api/action\")\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\t\thandler(ctx)\n\t\t\trequire.Equal(t, 200, ctx.Response.StatusCode(), \"Status code should be 200\")\n\t\t\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), \"Access-Control-Allow-Origin header should be set\")\n\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), \"Access-Control-Allow-Methods header should not be set (non-preflight request)\")\n\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), \"Access-Control-Allow-Headers header should not be set (non-preflight request)\")\n\t\t}\n\t})\n\n\tt.Run(\"Preflight with Access-Control-Request-Headers\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Make preflight request with origin header and with Access-Control-Request-Method\n\t\tfor _, method := range methods {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\t\t\tctx.Request.SetRequestURI(\"https://example.com/\")\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method)\n\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"X-Custom-Header\")\n\t\t\thandler(ctx)\n\t\t\trequire.Equal(t, 204, ctx.Response.StatusCode(), \"Status code should be 204\")\n\t\t\trequire.Equal(t, \"*\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), \"Access-Control-Allow-Origin header should be set\")\n\t\t\trequire.Equal(t, \"GET, POST, HEAD, PUT, DELETE, PATCH\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), \"Access-Control-Allow-Methods header should be set (preflight request)\")\n\t\t\trequire.Equal(t, \"X-Custom-Header\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), \"Access-Control-Allow-Headers header should be set (preflight request)\")\n\t\t}\n\t})\n}\n\nfunc Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\tapp.Use(\"/\", New(Config{\n\t\tAllowOrigins: []string{\"http://example-1.com\"},\n\t\tAllowOriginsFunc: func(origin string) bool {\n\t\t\treturn strings.Contains(origin, \"example-2\")\n\t\t},\n\t}))\n\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request with disallowed origin\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://google.com\")\n\n\t// Perform request\n\thandler(ctx)\n\n\t// Allow-Origin header should be \"\" because http://google.com does not satisfy http://example-1.com or 'strings.Contains(origin, \"example-2\")'\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with allowed origin\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example-1.com\")\n\n\thandler(ctx)\n\n\trequire.Equal(t, \"http://example-1.com\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with allowed origin\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example-2.com\")\n\n\thandler(ctx)\n\n\trequire.Equal(t, \"http://example-2.com\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n\nfunc Test_CORS_AllowOriginsFunc(t *testing.T) {\n\tt.Parallel()\n\t// New fiber instance\n\tapp := fiber.New()\n\tapp.Use(\"/\", New(Config{\n\t\tAllowOriginsFunc: func(origin string) bool {\n\t\t\treturn strings.Contains(origin, \"example-2\")\n\t\t},\n\t}))\n\n\t// Get handler pointer\n\thandler := app.Handler()\n\n\t// Make request with disallowed origin\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://google.com\")\n\n\t// Perform request\n\thandler(ctx)\n\n\t// Allow-Origin header should be empty because http://google.com does not satisfy 'strings.Contains(origin, \"example-2\")'\n\t// and AllowOrigins has not been set\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\n\t// Make request with allowed origin\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example-2.com\")\n\n\thandler(ctx)\n\n\t// Allow-Origin header should be \"http://example-2.com\"\n\trequire.Equal(t, \"http://example-2.com\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n}\n\nfunc Test_CORS_AllowOriginsFuncRejectsNonSerializedOrigins(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tName               string\n\t\tOrigin             string\n\t\tMethod             string\n\t\tSetPreflightMethod bool\n\t\tExpectAllowed      bool\n\t}{\n\t\t{\n\t\t\tName:   \"UserInfoPresent\",\n\t\t\tOrigin: \"http://user:pass@example.com\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:               \"PathPresent\",\n\t\t\tOrigin:             \"http://example.com/path\",\n\t\t\tMethod:             fiber.MethodOptions,\n\t\t\tSetPreflightMethod: true,\n\t\t},\n\t\t{\n\t\t\tName:               \"QueryPresent\",\n\t\t\tOrigin:             \"http://example.com?query=1\",\n\t\t\tMethod:             fiber.MethodOptions,\n\t\t\tSetPreflightMethod: true,\n\t\t},\n\t\t{\n\t\t\tName:   \"FragmentPresent\",\n\t\t\tOrigin: \"http://example.com#section\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:   \"WildcardHost\",\n\t\t\tOrigin: \"http://*.example.com\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:   \"StandaloneWildcard\",\n\t\t\tOrigin: \"*\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:   \"NullOriginUppercase\",\n\t\t\tOrigin: \"NULL\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:   \"NullOriginMixedCase\",\n\t\t\tOrigin: \"Null\",\n\t\t\tMethod: fiber.MethodGet,\n\t\t},\n\t\t{\n\t\t\tName:          \"NullOriginLowercase\",\n\t\t\tOrigin:        \"null\",\n\t\t\tMethod:        fiber.MethodGet,\n\t\t\tExpectAllowed: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(\"/\", New(Config{\n\t\t\t\tAllowOriginsFunc: func(string) bool { return true },\n\t\t\t}))\n\t\t\tapp.All(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t})\n\n\t\t\thandler := app.Handler()\n\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\tctx.Request.Header.SetMethod(tc.Method)\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, tc.Origin)\n\t\t\tif tc.SetPreflightMethod {\n\t\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\t\t\t}\n\n\t\t\thandler(ctx)\n\n\t\t\tif tc.ExpectAllowed {\n\t\t\t\trequire.Equal(t, \"null\", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t} else {\n\t\t\t\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t\t}\n\n\t\t\tif tc.Method == fiber.MethodOptions {\n\t\t\t\trequire.Equal(t, fiber.StatusNoContent, ctx.Response.StatusCode())\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) {\n\ttestCases := []struct {\n\t\tName           string\n\t\tRequestOrigin  string\n\t\tResponseOrigin string\n\t\tConfig         Config\n\t}{\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncUndefined/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"http://aaa.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\", \"http://bbb.com\"},\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://bbb.com\",\n\t\t\tResponseOrigin: \"http://bbb.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/OriginNotAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\", \"http://bbb.com\"},\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://ccc.com\",\n\t\t\tResponseOrigin: \"\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/Whitespace/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins:     []string{\" http://aaa.com \", \" http://bbb.com \"},\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"http://aaa.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncUndefined/OriginNotAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://bbb.com\",\n\t\t\tResponseOrigin: \"\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins: []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"http://aaa.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginNotAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins: []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://bbb.com\",\n\t\t\tResponseOrigin: \"http://bbb.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins: []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"http://aaa.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginNotAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOrigins: []string{\"http://aaa.com\"},\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://bbb.com\",\n\t\t\tResponseOrigin: \"\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsEmpty/AllowOriginsFuncUndefined/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOriginsFunc: nil,\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"*\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsEmpty/AllowOriginsFuncReturnsTrue/OriginAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"http://aaa.com\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsEmpty/AllowOriginsFuncReturnsFalse/OriginNotAllowed\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"http://aaa.com\",\n\t\t\tResponseOrigin: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(\"/\", New(tc.Config))\n\n\t\t\thandler := app.Handler()\n\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin)\n\n\t\t\thandler(ctx)\n\n\t\t\trequire.Equal(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t})\n\t}\n}\n\n// The fix for issue #2422\nfunc Test_CORS_AllowCredentials(t *testing.T) {\n\ttestCases := []struct {\n\t\tName                string\n\t\tRequestOrigin       string\n\t\tResponseOrigin      string\n\t\tResponseCredentials string\n\t\tConfig              Config\n\t}{\n\t\t{\n\t\t\tName: \"AllowOriginsFuncDefined\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin: \"http://aaa.com\",\n\t\t\t// The AllowOriginsFunc config was defined, should use the real origin of the function\n\t\t\tResponseOrigin:      \"http://aaa.com\",\n\t\t\tResponseCredentials: \"true\",\n\t\t},\n\t\t{\n\t\t\tName: \"fiber-ghsa-fmg4-x8pw-hjhg-wildcard-credentials\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tAllowOriginsFunc: func(_ string) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tRequestOrigin:  \"*\",\n\t\t\tResponseOrigin: \"\",\n\t\t\t// Middleware will validate that wildcard won't set credentials to true and reject non-serialized origins\n\t\t\tResponseCredentials: \"\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsFuncNotDefined\",\n\t\t\tConfig: Config{\n\t\t\t\t// Setting this to true will cause the middleware to panic since default AllowOrigins is \"*\"\n\t\t\t\tAllowCredentials: false,\n\t\t\t},\n\t\t\tRequestOrigin: \"http://aaa.com\",\n\t\t\t// None of the AllowOrigins or AllowOriginsFunc config was defined, should use the default origin of \"*\"\n\t\t\t// which will cause the CORS error in the client:\n\t\t\t// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'\n\t\t\t// when the request's credentials mode is 'include'.\n\t\t\tResponseOrigin:      \"*\",\n\t\t\tResponseCredentials: \"\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\"},\n\t\t\t},\n\t\t\tRequestOrigin:       \"http://aaa.com\",\n\t\t\tResponseOrigin:      \"http://aaa.com\",\n\t\t\tResponseCredentials: \"true\",\n\t\t},\n\t\t{\n\t\t\tName: \"AllowOriginsDefined/UnallowedOrigin\",\n\t\t\tConfig: Config{\n\t\t\t\tAllowCredentials: true,\n\t\t\t\tAllowOrigins:     []string{\"http://aaa.com\"},\n\t\t\t},\n\t\t\tRequestOrigin:       \"http://bbb.com\",\n\t\t\tResponseOrigin:      \"\",\n\t\t\tResponseCredentials: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.Name, func(t *testing.T) {\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(\"/\", New(tc.Config))\n\n\t\t\thandler := app.Handler()\n\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\t\t\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\t\t\tctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin)\n\n\t\t\thandler(ctx)\n\n\t\t\trequire.Equal(t, tc.ResponseCredentials, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))\n\t\t\trequire.Equal(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))\n\t\t})\n\t}\n}\n\n// The Enhancement for issue #2804\nfunc Test_CORS_AllowPrivateNetwork(t *testing.T) {\n\tt.Parallel()\n\n\t// Test scenario where AllowPrivateNetwork is enabled\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tAllowPrivateNetwork: true,\n\t}))\n\thandler := app.Handler()\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(\"Access-Control-Request-Private-Network\", \"true\")\n\thandler(ctx)\n\n\t// Verify the Access-Control-Allow-Private-Network header is set to \"true\"\n\trequire.Equal(t, \"true\", string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should be set to 'true' when AllowPrivateNetwork is enabled\")\n\n\t// Non-preflight request should not have Access-Control-Allow-Private-Network header\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(\"Access-Control-Request-Private-Network\", \"true\")\n\thandler(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should be set to 'true' when AllowPrivateNetwork is enabled\")\n\n\t// Non-preflight GET request should not have Access-Control-Allow-Private-Network header\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should be set to 'true' when AllowPrivateNetwork is enabled\")\n\n\t// Non-preflight OPTIONS request should not have Access-Control-Allow-Private-Network header\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(\"Access-Control-Request-Private-Network\", \"true\")\n\thandler(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should be set to 'true' when AllowPrivateNetwork is enabled\")\n\n\t// Reset ctx for next test\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\n\t// Test scenario where AllowPrivateNetwork is disabled (default)\n\tapp = fiber.New()\n\tapp.Use(New())\n\thandler = app.Handler()\n\thandler(ctx)\n\n\t// Verify the Access-Control-Allow-Private-Network header is not present\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should not be present by default\")\n\n\t// Test scenario where AllowPrivateNetwork is disabled but client sends header\n\tapp = fiber.New()\n\tapp.Use(New())\n\thandler = app.Handler()\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(\"Access-Control-Request-Private-Network\", \"true\")\n\thandler(ctx)\n\n\t// Verify the Access-Control-Allow-Private-Network header is not present\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should not be present by default\")\n\n\t// Test scenario where AllowPrivateNetwork is enabled and client does NOT send header\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tAllowPrivateNetwork: true,\n\t}))\n\thandler = app.Handler()\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\thandler(ctx)\n\n\t// Verify the Access-Control-Allow-Private-Network header is not present\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should not be present by default\")\n\n\t// Test scenario where AllowPrivateNetwork is enabled and client sends header with false value\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tAllowPrivateNetwork: true,\n\t}))\n\thandler = app.Handler()\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodOptions)\n\tctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(\"Access-Control-Request-Private-Network\", \"false\")\n\thandler(ctx)\n\n\t// Verify the Access-Control-Allow-Private-Network header is not present\n\trequire.Empty(t, string(ctx.Response.Header.Peek(\"Access-Control-Allow-Private-Network\")), \"The Access-Control-Allow-Private-Network header should not be present by default\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandler -benchmem -count=4\nfunc Benchmark_CORS_NewHandler(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://localhost\", \"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodGet)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandler_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandler_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://localhost\", \"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://localhost\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOrigin -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerSingleOrigin(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodGet)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOrigin_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerSingleOrigin_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcard -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerWildcard(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: false,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodGet)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcard_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerWildcard_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: false,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodGet)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflight -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflight(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://localhost\", \"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Preflight request\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodOptions)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflight_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflight_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://localhost\", \"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodOptions)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOrigin -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflightSingleOrigin(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodOptions)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOrigin_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflightSingleOrigin_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowOrigins:     []string{\"http://example.com\"},\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: true,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodOptions)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcard -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflightWildcard(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: false,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\treq := &fasthttp.Request{}\n\treq.Header.SetMethod(fiber.MethodOptions)\n\treq.SetRequestURI(\"/\")\n\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\tctx.Init(req, nil, nil)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcard_Parallel -benchmem -count=4\nfunc Benchmark_CORS_NewHandlerPreflightWildcard_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\tc := New(Config{\n\t\tAllowMethods:     []string{fiber.MethodGet, fiber.MethodPost, fiber.MethodPut, fiber.MethodDelete},\n\t\tAllowHeaders:     []string{fiber.HeaderOrigin, fiber.HeaderContentType, fiber.HeaderAccept},\n\t\tAllowCredentials: false,\n\t\tMaxAge:           600,\n\t})\n\n\tapp.Use(c)\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\n\t\treq := &fasthttp.Request{}\n\t\treq.Header.SetMethod(fiber.MethodOptions)\n\t\treq.SetRequestURI(\"/\")\n\t\treq.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost)\n\t\treq.Header.Set(fiber.HeaderAccessControlRequestHeaders, \"Origin,Content-Type,Accept\")\n\n\t\tctx.Init(req, nil, nil)\n\n\t\tfor pb.Next() {\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "middleware/cors/utils.go",
    "content": "package cors\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n)\n\n// matchScheme compares the scheme of the domain and pattern\nfunc matchScheme(domain, pattern string) bool {\n\tdScheme, _, dFound := strings.Cut(domain, \":\")\n\tpScheme, _, pFound := strings.Cut(pattern, \":\")\n\treturn dFound && pFound && dScheme == pScheme\n}\n\n// normalizeDomain removes the scheme and port from the input domain\nfunc normalizeDomain(input string) string {\n\t// Remove scheme\n\tif after, found := strings.CutPrefix(input, \"https://\"); found {\n\t\tinput = after\n\t} else if after, found := strings.CutPrefix(input, \"http://\"); found {\n\t\tinput = after\n\t}\n\n\t// Find and remove port, if present\n\tif input != \"\" && input[0] != '[' {\n\t\tif before, _, found := strings.Cut(input, \":\"); found {\n\t\t\tinput = before\n\t\t}\n\t}\n\n\treturn input\n}\n\n// normalizeOrigin checks if the provided origin is in a correct format\n// and normalizes it by removing any path or trailing slash.\n// It returns a boolean indicating whether the origin is valid\n// and the normalized origin.\nfunc normalizeOrigin(origin string) (valid bool, normalized string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming validity and normalized origin results\n\tparsedOrigin, err := url.Parse(origin)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\n\t// Don't allow a wildcard with a protocol\n\t// wildcards cannot be used within any other value. For example, the following header is not valid:\n\t// Access-Control-Allow-Origin: https://*\n\tif strings.IndexByte(parsedOrigin.Host, '*') >= 0 {\n\t\treturn false, \"\"\n\t}\n\n\t// Validate there is a host present. The presence of a path, query, or fragment components\n\t// is checked, but a trailing \"/\" (indicative of the root) is allowed for the path and will be normalized\n\tif parsedOrigin.User != nil ||\n\t\tparsedOrigin.Host == \"\" ||\n\t\t(parsedOrigin.Path != \"\" && parsedOrigin.Path != \"/\") ||\n\t\tparsedOrigin.RawQuery != \"\" ||\n\t\tparsedOrigin.Fragment != \"\" {\n\t\treturn false, \"\"\n\t}\n\n\t// Normalize the origin by constructing it from the scheme and host.\n\t// The path or trailing slash is not included in the normalized origin.\n\treturn true, utilsstrings.ToLower(parsedOrigin.Scheme) + \"://\" + utilsstrings.ToLower(parsedOrigin.Host)\n}\n\ntype subdomain struct {\n\t// The wildcard pattern\n\tprefix string\n\tsuffix string\n}\n\nfunc (s subdomain) match(o string) bool {\n\t// Not a subdomain if not long enough for a dot separator.\n\tif len(o) < len(s.prefix)+len(s.suffix)+1 {\n\t\treturn false\n\t}\n\n\tif !strings.HasPrefix(o, s.prefix) || !strings.HasSuffix(o, s.suffix) {\n\t\treturn false\n\t}\n\n\t// Check for the dot separator and validate that there is at least one\n\t// non-empty label between prefix and suffix. Empty labels like\n\t// \"https://.example.com\" or \"https://..example.com\" should not match.\n\tsuffixStartIndex := len(o) - len(s.suffix)\n\tif suffixStartIndex <= len(s.prefix) {\n\t\treturn false\n\t}\n\tif o[suffixStartIndex-1] != '.' {\n\t\treturn false\n\t}\n\n\t// Extract the subdomain part (without the trailing dot) and ensure it\n\t// doesn't contain empty labels.\n\tsub := o[len(s.prefix) : suffixStartIndex-1]\n\tif sub == \"\" || strings.HasPrefix(sub, \".\") || strings.Contains(sub, \"..\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "middleware/cors/utils_test.go",
    "content": "package cors\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// go test -run -v Test_NormalizeOrigin\nfunc Test_NormalizeOrigin(t *testing.T) {\n\ttestCases := []struct {\n\t\torigin         string\n\t\texpectedOrigin string\n\t\texpectedValid  bool\n\t}{\n\t\t{origin: \"http://example.com\", expectedValid: true, expectedOrigin: \"http://example.com\"},                       // Simple case should work.\n\t\t{origin: \"http://example.com/\", expectedValid: true, expectedOrigin: \"http://example.com\"},                      // Trailing slash should be removed.\n\t\t{origin: \"http://example.com:3000\", expectedValid: true, expectedOrigin: \"http://example.com:3000\"},             // Port should be preserved.\n\t\t{origin: \"http://example.com:3000/\", expectedValid: true, expectedOrigin: \"http://example.com:3000\"},            // Trailing slash should be removed.\n\t\t{origin: \"app://example.com/\", expectedValid: true, expectedOrigin: \"app://example.com\"},                        // App scheme should be accepted.\n\t\t{origin: \"http://\", expectedValid: false, expectedOrigin: \"\"},                                                   // Invalid origin should not be accepted.\n\t\t{origin: \"file:///etc/passwd\", expectedValid: false, expectedOrigin: \"\"},                                        // File scheme should not be accepted.\n\t\t{origin: \"https://*example.com\", expectedValid: false, expectedOrigin: \"\"},                                      // Wildcard domain should not be accepted.\n\t\t{origin: \"http://*.example.com\", expectedValid: false, expectedOrigin: \"\"},                                      // Wildcard subdomain should not be accepted.\n\t\t{origin: \"http://example.com/path\", expectedValid: false, expectedOrigin: \"\"},                                   // Path should not be accepted.\n\t\t{origin: \"http://example.com?query=123\", expectedValid: false, expectedOrigin: \"\"},                              // Query should not be accepted.\n\t\t{origin: \"http://example.com#fragment\", expectedValid: false, expectedOrigin: \"\"},                               // Fragment should not be accepted.\n\t\t{origin: \"http://user:pass@example.com\", expectedValid: false, expectedOrigin: \"\"},                              // Userinfo should not be accepted.\n\t\t{origin: \"http://localhost\", expectedValid: true, expectedOrigin: \"http://localhost\"},                           // Localhost should be accepted.\n\t\t{origin: \"http://127.0.0.1\", expectedValid: true, expectedOrigin: \"http://127.0.0.1\"},                           // IPv4 address should be accepted.\n\t\t{origin: \"http://[::1]\", expectedValid: true, expectedOrigin: \"http://[::1]\"},                                   // IPv6 address should be accepted.\n\t\t{origin: \"http://[::1]:8080\", expectedValid: true, expectedOrigin: \"http://[::1]:8080\"},                         // IPv6 address with port should be accepted.\n\t\t{origin: \"http://[::1]:8080/\", expectedValid: true, expectedOrigin: \"http://[::1]:8080\"},                        // IPv6 address with port and trailing slash should be accepted.\n\t\t{origin: \"http://[::1]:8080/path\", expectedValid: false, expectedOrigin: \"\"},                                    // IPv6 address with port and path should not be accepted.\n\t\t{origin: \"http://[::1]:8080?query=123\", expectedValid: false, expectedOrigin: \"\"},                               // IPv6 address with port and query should not be accepted.\n\t\t{origin: \"http://[::1]:8080#fragment\", expectedValid: false, expectedOrigin: \"\"},                                // IPv6 address with port and fragment should not be accepted.\n\t\t{origin: \"http://[::1]:8080/path?query=123#fragment\", expectedValid: false, expectedOrigin: \"\"},                 // IPv6 address with port, path, query, and fragment should not be accepted.\n\t\t{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.\n\t\t{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.\n\t\t{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.\n\t\t{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.\n\t}\n\n\tfor _, tc := range testCases {\n\t\tvalid, normalizedOrigin := normalizeOrigin(tc.origin)\n\n\t\tif valid != tc.expectedValid {\n\t\t\tt.Errorf(\"Expected origin '%s' to be valid: %v, but got: %v\", tc.origin, tc.expectedValid, valid)\n\t\t}\n\n\t\tif normalizedOrigin != tc.expectedOrigin {\n\t\t\tt.Errorf(\"Expected normalized origin '%s' for origin '%s', but got: '%s'\", tc.expectedOrigin, tc.origin, normalizedOrigin)\n\t\t}\n\t}\n}\n\n// go test -run -v Test_MatchScheme\nfunc Test_MatchScheme(t *testing.T) {\n\ttestCases := []struct {\n\t\tdomain   string\n\t\tpattern  string\n\t\texpected bool\n\t}{\n\t\t{domain: \"http://example.com\", pattern: \"http://example.com\", expected: true},           // Exact match should work.\n\t\t{domain: \"https://example.com\", pattern: \"http://example.com\", expected: false},         // Scheme mismatch should matter.\n\t\t{domain: \"http://example.com\", pattern: \"https://example.com\", expected: false},         // Scheme mismatch should matter.\n\t\t{domain: \"http://example.com\", pattern: \"http://example.org\", expected: true},           // Different domains should not matter.\n\t\t{domain: \"http://example.com\", pattern: \"http://example.com:8080\", expected: true},      // Port should not matter.\n\t\t{domain: \"http://example.com:8080\", pattern: \"http://example.com\", expected: true},      // Port should not matter.\n\t\t{domain: \"http://example.com:8080\", pattern: \"http://example.com:8081\", expected: true}, // Different ports should not matter.\n\t\t{domain: \"http://localhost\", pattern: \"http://localhost\", expected: true},               // Localhost should match.\n\t\t{domain: \"http://127.0.0.1\", pattern: \"http://127.0.0.1\", expected: true},               // IPv4 address should match.\n\t\t{domain: \"http://[::1]\", pattern: \"http://[::1]\", expected: true},                       // IPv6 address should match.\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := matchScheme(tc.domain, tc.pattern)\n\n\t\tif result != tc.expected {\n\t\t\tt.Errorf(\"Expected matchScheme('%s', '%s') to be %v, but got %v\", tc.domain, tc.pattern, tc.expected, result)\n\t\t}\n\t}\n}\n\n// go test -run -v Test_NormalizeDomain\nfunc Test_NormalizeDomain(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput          string\n\t\texpectedOutput string\n\t}{\n\t\t{input: \"http://example.com\", expectedOutput: \"example.com\"},                     // Simple case with http scheme.\n\t\t{input: \"https://example.com\", expectedOutput: \"example.com\"},                    // Simple case with https scheme.\n\t\t{input: \"http://example.com:3000\", expectedOutput: \"example.com\"},                // Case with port.\n\t\t{input: \"https://example.com:3000\", expectedOutput: \"example.com\"},               // Case with port and https scheme.\n\t\t{input: \"http://example.com/path\", expectedOutput: \"example.com/path\"},           // Case with path.\n\t\t{input: \"http://example.com?query=123\", expectedOutput: \"example.com?query=123\"}, // Case with query.\n\t\t{input: \"http://example.com#fragment\", expectedOutput: \"example.com#fragment\"},   // Case with fragment.\n\t\t{input: \"example.com\", expectedOutput: \"example.com\"},                            // Case without scheme.\n\t\t{input: \"example.com:8080\", expectedOutput: \"example.com\"},                       // Case without scheme but with port.\n\t\t{input: \"sub.example.com\", expectedOutput: \"sub.example.com\"},                    // Case with subdomain.\n\t\t{input: \"sub.sub.example.com\", expectedOutput: \"sub.sub.example.com\"},            // Case with nested subdomain.\n\t\t{input: \"http://localhost\", expectedOutput: \"localhost\"},                         // Case with localhost.\n\t\t{input: \"http://127.0.0.1\", expectedOutput: \"127.0.0.1\"},                         // Case with IPv4 address.\n\t\t{input: \"http://[::1]\", expectedOutput: \"[::1]\"},                                 // Case with IPv6 address.\n\t}\n\n\tfor _, tc := range testCases {\n\t\toutput := normalizeDomain(tc.input)\n\n\t\tif output != tc.expectedOutput {\n\t\t\tt.Errorf(\"Expected normalized domain '%s' for input '%s', but got: '%s'\", tc.expectedOutput, tc.input, output)\n\t\t}\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CORS_SubdomainMatch -benchmem -count=4\nfunc Benchmark_CORS_SubdomainMatch(b *testing.B) {\n\ts := subdomain{\n\t\tprefix: \"www\",\n\t\tsuffix: \"example.com\",\n\t}\n\n\to := \"www.example.com\"\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\ts.match(o)\n\t}\n}\n\nfunc Test_CORS_SubdomainMatch(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tsub      subdomain\n\t\torigin   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"match with different scheme\",\n\t\t\tsub:      subdomain{prefix: \"http://api.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with different scheme\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"http://api.service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with valid subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.service.example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with valid nested subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://1.2.api.service.example.com\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t{\n\t\t\tname:     \"no match with invalid prefix\",\n\t\t\tsub:      subdomain{prefix: \"https://abc.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with invalid suffix\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.example.org\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with empty origin\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with malformed subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://evil.comexample.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"partial match not considered a match\",\n\t\t\tsub:      subdomain{prefix: \"https://service.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with empty host label\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with malformed host label\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://..example.com\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.sub.match(tt.origin)\n\t\t\tassert.Equal(t, tt.expected, got, \"subdomain.match()\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf/config.go",
    "content": "package csrf\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/fiber/v3/middleware/session\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Config defines the config for CSRF middleware.\ntype Config struct {\n\t// Storage is used to store the state of the middleware.\n\t//\n\t// Optional. Default: memory.New()\n\t// Ignored if Session is set.\n\tStorage fiber.Storage\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Session is used to store the state of the middleware.\n\t//\n\t// Optional. Default: nil\n\t// If set, the middleware will use the session store instead of the storage.\n\tSession *session.Store\n\n\t// KeyGenerator creates a new CSRF token.\n\t//\n\t// Optional. Default: utils.SecureToken\n\tKeyGenerator func() string\n\n\t// ErrorHandler is executed when an error is returned from fiber.Handler.\n\t//\n\t// Optional. Default: defaultErrorHandler\n\tErrorHandler fiber.ErrorHandler\n\n\t// CookieName is the name of the CSRF cookie.\n\t//\n\t// Optional. Default: \"csrf_\"\n\tCookieName string\n\n\t// CookieDomain is the domain of the CSRF cookie.\n\t//\n\t// Optional. Default: \"\"\n\tCookieDomain string\n\n\t// CookiePath is the path of the CSRF cookie.\n\t//\n\t// Optional. Default: \"\"\n\tCookiePath string\n\n\t// CookieSameSite is the SameSite attribute of the CSRF cookie.\n\t//\n\t// Optional. Default: \"Lax\"\n\tCookieSameSite string\n\n\t// TrustedOrigins is a list of trusted origins for unsafe requests.\n\t// For requests that use the Origin header, the origin must match the\n\t// Host header or one of the TrustedOrigins.\n\t// For secure requests that do not include the Origin header, the Referer\n\t// header must match the Host header or one of the TrustedOrigins.\n\t//\n\t// This supports matching subdomains at any level. This means you can use a value like\n\t// \"https://*.example.com\" to allow any subdomain of example.com to submit requests,\n\t// including multiple subdomain levels such as \"https://sub.sub.example.com\".\n\t//\n\t// Optional. Default: []\n\tTrustedOrigins []string\n\n\t// Extractor returns the CSRF token from the request.\n\t//\n\t// Optional. Default: extractors.FromHeader(\"X-Csrf-Token\")\n\t//\n\t// Available extractors from github.com/gofiber/fiber/v3/extractors:\n\t//   - extractors.FromHeader(\"X-Csrf-Token\"): Most secure, recommended for APIs\n\t//   - extractors.FromForm(\"_csrf\"): Secure, recommended for form submissions\n\t//   - extractors.FromQuery(\"csrf_token\"): Less secure, URLs may be logged\n\t//   - extractors.FromParam(\"csrf\"): Less secure, URLs may be logged\n\t//   - extractors.Chain(...): Advanced chaining of multiple extractors\n\t//\n\t// See the Extractors Guide for complete documentation:\n\t// https://docs.gofiber.io/guide/extractors\n\t//\n\t// WARNING: Never create custom extractors that read from cookies with the same\n\t// CookieName as this defeats CSRF protection entirely.\n\tExtractor extractors.Extractor\n\n\t// IdleTimeout is the duration of time the CSRF token is valid.\n\t//\n\t// Optional. Default: 30 * time.Minute\n\tIdleTimeout time.Duration\n\n\t// DisableValueRedaction turns off masking CSRF tokens and storage keys in logs and errors.\n\t//\n\t// Optional. Default: false\n\tDisableValueRedaction bool\n\n\t// CookieSecure indicates if CSRF cookie is secure.\n\t//\n\t// Optional. Default: false\n\tCookieSecure bool\n\n\t// CookieHTTPOnly indicates if CSRF cookie is HTTP only.\n\t//\n\t// Optional. Default: false\n\tCookieHTTPOnly bool\n\n\t// CookieSessionOnly decides whether cookie should last for only the browser session.\n\t// Ignores Expiration if set to true.\n\t//\n\t// Optional. Default: false\n\tCookieSessionOnly bool\n\n\t// SingleUseToken indicates if the CSRF token should be destroyed\n\t// and a new one generated on each use.\n\t//\n\t// Optional. Default: false\n\tSingleUseToken bool\n}\n\n// HeaderName is the default header name for CSRF tokens.\nconst HeaderName = \"X-Csrf-Token\"\n\n// ConfigDefault is the default config for CSRF middleware.\nvar ConfigDefault = Config{\n\tCookieName:            \"csrf_\",\n\tCookieSameSite:        \"Lax\",\n\tIdleTimeout:           30 * time.Minute,\n\tKeyGenerator:          utils.SecureToken,\n\tErrorHandler:          defaultErrorHandler,\n\tExtractor:             extractors.FromHeader(HeaderName),\n\tDisableValueRedaction: false,\n}\n\n// defaultErrorHandler is the default error handler that processes errors from fiber.Handler.\nfunc defaultErrorHandler(_ fiber.Ctx, _ error) error {\n\treturn fiber.ErrForbidden\n}\n\n// configDefault is a helper function to set default values.\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.IdleTimeout <= 0 {\n\t\tcfg.IdleTimeout = ConfigDefault.IdleTimeout\n\t}\n\tif cfg.CookieName == \"\" {\n\t\tcfg.CookieName = ConfigDefault.CookieName\n\t}\n\tif cfg.CookieSameSite == \"\" {\n\t\tcfg.CookieSameSite = ConfigDefault.CookieSameSite\n\t}\n\tif cfg.KeyGenerator == nil {\n\t\tcfg.KeyGenerator = ConfigDefault.KeyGenerator\n\t}\n\tif cfg.ErrorHandler == nil {\n\t\tcfg.ErrorHandler = ConfigDefault.ErrorHandler\n\t}\n\t// Check if Extractor is zero value (since it's a struct)\n\tif cfg.Extractor.Extract == nil {\n\t\tcfg.Extractor = ConfigDefault.Extractor\n\t}\n\t// Validate extractor security configurations\n\tvalidateExtractorSecurity(&cfg)\n\n\treturn cfg\n}\n\n// validateExtractorSecurity checks for insecure extractor configurations\nfunc validateExtractorSecurity(cfg *Config) {\n\tif cfg == nil {\n\t\treturn\n\t}\n\t// Check primary extractor\n\tif isInsecureCookieExtractor(cfg.Extractor, cfg.CookieName) {\n\t\tpanic(\"CSRF: Extractor reads from the same cookie '\" + cfg.CookieName +\n\t\t\t\"' used for token storage. This completely defeats CSRF protection.\")\n\t}\n\n\t// Check chained extractors\n\tfor i, extractor := range cfg.Extractor.Chain {\n\t\tif isInsecureCookieExtractor(extractor, cfg.CookieName) {\n\t\t\tpanic(fmt.Sprintf(\"CSRF: Chained extractor #%d reads from the same cookie '%s' \"+\n\t\t\t\t\"used for token storage. This completely defeats CSRF protection.\", i+1, cfg.CookieName))\n\t\t}\n\t}\n\n\t// Additional security warnings (non-fatal)\n\tif cfg.Extractor.Source == extractors.SourceQuery || cfg.Extractor.Source == extractors.SourceParam {\n\t\tlog.Warnf(\"[CSRF WARNING] Using %v extractor - URLs may be logged\", cfg.Extractor.Source)\n\t}\n}\n\n// isInsecureCookieExtractor checks if an extractor unsafely reads from the CSRF cookie\nfunc isInsecureCookieExtractor(extractor extractors.Extractor, cookieName string) bool {\n\tif extractor.Source == extractors.SourceCookie {\n\t\t// Exact match - definitely insecure\n\t\tif extractor.Key == cookieName {\n\t\t\treturn true\n\t\t}\n\n\t\t// Case-insensitive match - potentially confusing, warn but don't panic\n\t\tif utils.EqualFold(extractor.Key, cookieName) && extractor.Key != cookieName {\n\t\t\tlog.Warnf(\"[CSRF WARNING] Extractor cookie name '%s' is similar to CSRF cookie '%s' - this may be confusing\",\n\t\t\t\textractor.Key, cookieName)\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "middleware/csrf/config_test.go",
    "content": "package csrf\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Test security validation functions\nfunc Test_CSRF_ExtractorSecurity_Validation(t *testing.T) {\n\tt.Parallel()\n\n\t// Test secure configurations - should not panic\n\tt.Run(\"SecureConfigurations\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tsecureConfigs := []Config{\n\t\t\t{Extractor: extractors.FromHeader(\"X-Csrf-Token\")},\n\t\t\t{Extractor: extractors.FromForm(\"_csrf\")},\n\t\t\t{Extractor: extractors.FromQuery(\"csrf_token\")},\n\t\t\t{Extractor: extractors.FromParam(\"csrf\")},\n\t\t\t{Extractor: extractors.Chain(extractors.FromHeader(\"X-Csrf-Token\"), extractors.FromForm(\"_csrf\"))},\n\t\t}\n\n\t\tfor i, cfg := range secureConfigs {\n\t\t\tt.Run(fmt.Sprintf(\"Config%d\", i), func(t *testing.T) {\n\t\t\t\trequire.NotPanics(t, func() {\n\t\t\t\t\tconfigDefault(cfg)\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t})\n\n\t// Test insecure configurations - should panic\n\tt.Run(\"InsecureCookieExtractor\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// Create a custom extractor that reads from cookie (simulating dangerous behavior)\n\t\tinsecureCookieExtractor := extractors.Extractor{\n\t\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn c.Cookies(\"csrf_\"), nil\n\t\t\t},\n\t\t\tSource: extractors.SourceCookie,\n\t\t\tKey:    \"csrf_\",\n\t\t}\n\n\t\tcfg := Config{\n\t\t\tCookieName: \"csrf_\",\n\t\t\tExtractor:  insecureCookieExtractor,\n\t\t}\n\n\t\trequire.Panics(t, func() {\n\t\t\tconfigDefault(cfg)\n\t\t}, \"Should panic when extractor reads from same cookie\")\n\t})\n\n\t// Test insecure chained extractors\n\tt.Run(\"InsecureChainedExtractor\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tinsecureCookieExtractor := extractors.Extractor{\n\t\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn c.Cookies(\"csrf_\"), nil\n\t\t\t},\n\t\t\tSource: extractors.SourceCookie,\n\t\t\tKey:    \"csrf_\",\n\t\t}\n\n\t\tchainedExtractor := extractors.Chain(\n\t\t\textractors.FromHeader(\"X-Csrf-Token\"),\n\t\t\tinsecureCookieExtractor, // This should trigger panic\n\t\t)\n\n\t\tcfg := Config{\n\t\t\tCookieName: \"csrf_\",\n\t\t\tExtractor:  chainedExtractor,\n\t\t}\n\n\t\trequire.Panics(t, func() {\n\t\t\tconfigDefault(cfg)\n\t\t}, \"Should panic when chained extractor reads from same cookie\")\n\t})\n\n\t// Test different cookie names - should be secure\n\tt.Run(\"DifferentCookieNames\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcookieExtractor := extractors.Extractor{\n\t\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn c.Cookies(\"different_cookie\"), nil\n\t\t\t},\n\t\t\tSource: extractors.SourceCookie,\n\t\t\tKey:    \"different_cookie\",\n\t\t}\n\n\t\tcfg := Config{\n\t\t\tCookieName: \"csrf_\",\n\t\t\tExtractor:  cookieExtractor,\n\t\t}\n\n\t\trequire.NotPanics(t, func() {\n\t\t\tconfigDefault(cfg)\n\t\t}, \"Should not panic when extractor reads from different cookie\")\n\t})\n}\n\n// Test extractor metadata\nfunc Test_CSRF_Extractor_Metadata(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\texpectedKey    string\n\t\textractor      extractors.Extractor\n\t\texpectedSource extractors.Source\n\t}{\n\t\t{\n\t\t\tname:           \"FromHeader\",\n\t\t\textractor:      extractors.FromHeader(\"X-Custom-Token\"),\n\t\t\texpectedSource: extractors.SourceHeader,\n\t\t\texpectedKey:    \"X-Custom-Token\",\n\t\t},\n\t\t{\n\t\t\tname:           \"FromForm\",\n\t\t\textractor:      extractors.FromForm(\"_token\"),\n\t\t\texpectedSource: extractors.SourceForm,\n\t\t\texpectedKey:    \"_token\",\n\t\t},\n\t\t{\n\t\t\tname:           \"FromQuery\",\n\t\t\textractor:      extractors.FromQuery(\"token\"),\n\t\t\texpectedSource: extractors.SourceQuery,\n\t\t\texpectedKey:    \"token\",\n\t\t},\n\t\t{\n\t\t\tname:           \"FromParam\",\n\t\t\textractor:      extractors.FromParam(\"id\"),\n\t\t\texpectedSource: extractors.SourceParam,\n\t\t\texpectedKey:    \"id\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tc.expectedSource, tc.extractor.Source)\n\t\t\trequire.Equal(t, tc.expectedKey, tc.extractor.Key)\n\t\t\trequire.NotNil(t, tc.extractor.Extract)\n\t\t})\n\t}\n}\n\n// Test chain extractor metadata\nfunc Test_CSRF_Chain_Extractor_Metadata(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"EmptyChain\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tchained := extractors.Chain()\n\t\trequire.Equal(t, extractors.SourceCustom, chained.Source)\n\t\trequire.Empty(t, chained.Key)\n\t\trequire.Empty(t, chained.Chain)\n\t})\n\n\tt.Run(\"SingleExtractor\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\theader := extractors.FromHeader(\"X-Token\")\n\t\tchained := extractors.Chain(header)\n\t\trequire.Equal(t, extractors.SourceHeader, chained.Source)\n\t\trequire.Equal(t, \"X-Token\", chained.Key)\n\t\trequire.Len(t, chained.Chain, 1)\n\t})\n\n\tt.Run(\"MultipleExtractors\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\theader := extractors.FromHeader(\"X-Token\")\n\t\tform := extractors.FromForm(\"_csrf\")\n\t\tchained := extractors.Chain(header, form)\n\n\t\t// Should use first extractor's metadata\n\t\trequire.Equal(t, extractors.SourceHeader, chained.Source)\n\t\trequire.Equal(t, \"X-Token\", chained.Key)\n\t\trequire.Len(t, chained.Chain, 2)\n\t\trequire.Equal(t, header.Source, chained.Chain[0].Source)\n\t\trequire.Equal(t, form.Source, chained.Chain[1].Source)\n\t})\n}\n\n// Test custom extractor with new struct pattern\nfunc Test_CSRF_Custom_Extractor_Struct(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Custom extractor using new struct pattern\n\tcustomExtractor := extractors.Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\t// Extract from custom header\n\t\t\ttoken := c.Get(\"X-Custom-CSRF\")\n\t\t\tif token == \"\" {\n\t\t\t\treturn \"\", extractors.ErrNotFound\n\t\t\t}\n\t\t\treturn token, nil\n\t\t},\n\t\tSource: extractors.SourceCustom,\n\t\tKey:    \"X-Custom-CSRF\",\n\t}\n\n\tapp.Use(New(Config{Extractor: customExtractor}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test with custom header\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Custom-CSRF\", token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test without custom header\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\n// Test error types for different extractors\nfunc Test_CSRF_Extractor_Error_Types(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedError error\n\t\tsetupRequest  func(*fasthttp.RequestCtx)\n\t\tname          string\n\t\textractor     extractors.Extractor\n\t}{\n\t\t{\n\t\t\tname:      \"MissingHeader\",\n\t\t\textractor: extractors.FromHeader(\"X-Missing\"),\n\t\t\tsetupRequest: func(_ *fasthttp.RequestCtx) {\n\t\t\t\t// Don't set the header\n\t\t\t},\n\t\t\texpectedError: extractors.ErrNotFound,\n\t\t},\n\t\t{\n\t\t\tname:      \"MissingForm\",\n\t\t\textractor: extractors.FromForm(\"_missing\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx) {\n\t\t\t\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\t\t\t\t// Don't set form data\n\t\t\t},\n\t\t\texpectedError: extractors.ErrNotFound,\n\t\t},\n\t\t{\n\t\t\tname:      \"MissingQuery\",\n\t\t\textractor: extractors.FromQuery(\"missing\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx) {\n\t\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\t\t// Don't set query param\n\t\t\t},\n\t\t\texpectedError: extractors.ErrNotFound,\n\t\t},\n\t\t{\n\t\t\tname:      \"MissingParam\",\n\t\t\textractor: extractors.FromParam(\"missing\"),\n\t\t\tsetupRequest: func(_ *fasthttp.RequestCtx) {\n\t\t\t\t// This would need special route setup to test properly\n\t\t\t\t// For now, we'll test the extractor directly\n\t\t\t},\n\t\t\texpectedError: extractors.ErrNotFound,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tctx := &fasthttp.RequestCtx{}\n\n\t\t\ttc.setupRequest(ctx)\n\n\t\t\tc := app.AcquireCtx(ctx)\n\n\t\t\t_, err := tc.extractor.Extract(c)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.Equal(t, tc.expectedError, err)\n\t\t\tapp.ReleaseCtx(c)\n\t\t})\n\t}\n}\n\n// Test security warning logs (would need to capture log output in real implementation)\nfunc Test_CSRF_Security_Warnings(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that insecure extractors trigger warnings\n\t// Note: In a real implementation, you'd want to capture log output\n\t// For now, we just test that the configuration doesn't panic\n\n\tinsecureConfigs := []Config{\n\t\t{Extractor: extractors.FromQuery(\"csrf_token\")},\n\t\t{Extractor: extractors.FromParam(\"csrf\")},\n\t}\n\n\tfor i, cfg := range insecureConfigs {\n\t\tt.Run(fmt.Sprintf(\"InsecureConfig%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.NotPanics(t, func() {\n\t\t\t\tconfigDefault(cfg)\n\t\t\t})\n\t\t})\n\t}\n}\n\n// Test isInsecureCookieExtractor function directly\nfunc Test_isInsecureCookieExtractor(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tcookieName string\n\t\textractor  extractors.Extractor\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tname: \"SecureHeaderExtractor\",\n\t\t\textractor: extractors.Extractor{\n\t\t\t\tSource: extractors.SourceHeader,\n\t\t\t\tKey:    \"X-Csrf-Token\",\n\t\t\t},\n\t\t\tcookieName: \"csrf_\",\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"InsecureCookieExtractor\",\n\t\t\textractor: extractors.Extractor{\n\t\t\t\tSource: extractors.SourceCookie,\n\t\t\t\tKey:    \"csrf_\",\n\t\t\t},\n\t\t\tcookieName: \"csrf_\",\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tname: \"CookieExtractorDifferentName\",\n\t\t\textractor: extractors.Extractor{\n\t\t\t\tSource: extractors.SourceCookie,\n\t\t\t\tKey:    \"different_cookie\",\n\t\t\t},\n\t\t\tcookieName: \"csrf_\",\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"CustomExtractorSafeName\",\n\t\t\textractor: extractors.Extractor{\n\t\t\t\tSource: extractors.SourceCustom,\n\t\t\t\tKey:    \"safe_key\",\n\t\t\t},\n\t\t\tcookieName: \"csrf_\",\n\t\t\texpected:   false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := isInsecureCookieExtractor(tc.extractor, tc.cookieName)\n\t\t\trequire.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc Test_CSRF_CookieName_CaseInsensitive_Warning(t *testing.T) {\n\tt.Parallel()\n\n\t// Extractor uses \"CSRF_\" (uppercase), config uses \"csrf_\" (lowercase)\n\textractor := extractors.Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\treturn c.Cookies(\"CSRF_\"), nil\n\t\t},\n\t\tSource: extractors.SourceCookie,\n\t\tKey:    \"CSRF_\",\n\t}\n\n\tcfg := Config{\n\t\tCookieName: \"csrf_\",\n\t\tExtractor:  extractor,\n\t}\n\n\t// Should not panic, but should log a warning\n\trequire.NotPanics(t, func() {\n\t\tconfigDefault(cfg)\n\t}, \"Should not panic for case-insensitive cookie name match, but should warn\")\n}\n"
  },
  {
    "path": "middleware/csrf/csrf.go",
    "content": "package csrf\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n)\n\nvar (\n\tErrTokenNotFound    = errors.New(\"csrf: token not found\")\n\tErrTokenInvalid     = errors.New(\"csrf: token invalid\")\n\tErrFetchSiteInvalid = errors.New(\"csrf: sec-fetch-site header invalid\")\n\tErrRefererNotFound  = errors.New(\"csrf: referer header missing\")\n\tErrRefererInvalid   = errors.New(\"csrf: referer header invalid\")\n\tErrRefererNoMatch   = errors.New(\"csrf: referer does not match host or trusted origins\")\n\tErrOriginInvalid    = errors.New(\"csrf: origin header invalid\")\n\tErrOriginNoMatch    = errors.New(\"csrf: origin does not match host or trusted origins\")\n\terrOriginNotFound   = errors.New(\"origin not supplied or is null\") // internal error, will not be returned to the user\n\tdummyValue          = []byte{'+'}                                  // dummyValue is a placeholder value stored in token storage. The actual token validation relies on the key, not this value.\n\n)\n\n// Handler for CSRF middleware\ntype Handler struct {\n\tsessionManager *sessionManager\n\tstorageManager *storageManager\n\tconfig         Config\n}\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\n// The keys for the values in context\nconst (\n\ttokenKey contextKey = iota\n\thandlerKey\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\tredactKeys := !cfg.DisableValueRedaction\n\n\tmaskValue := func(value string) string {\n\t\tif redactKeys {\n\t\t\treturn redactedKey\n\t\t}\n\t\treturn value\n\t}\n\n\t// Create manager to simplify storage operations ( see *_manager.go )\n\tvar sessionManager *sessionManager\n\tvar storageManager *storageManager\n\tif cfg.Session != nil {\n\t\tsessionManager = newSessionManager(cfg.Session)\n\t} else {\n\t\tstorageManager = newStorageManager(cfg.Storage, redactKeys)\n\t}\n\n\t// Pre-parse trusted origins\n\ttrustedOrigins := []string{}\n\ttrustedSubOrigins := []subdomain{}\n\n\tfor _, origin := range cfg.TrustedOrigins {\n\t\ttrimmedOrigin := utils.TrimSpace(origin)\n\t\tif i := strings.Index(trimmedOrigin, \"://*.\"); i != -1 {\n\t\t\twithoutWildcard := trimmedOrigin[:i+len(\"://\")] + trimmedOrigin[i+len(\"://*.\"):]\n\t\t\tisValid, normalizedOrigin := normalizeOrigin(withoutWildcard)\n\t\t\tif !isValid {\n\t\t\t\tpanic(\"[CSRF] Invalid origin format in configuration:\" + maskValue(origin))\n\t\t\t}\n\t\t\tschemeSep := strings.Index(normalizedOrigin, \"://\") + len(\"://\")\n\t\t\tsd := subdomain{prefix: normalizedOrigin[:schemeSep], suffix: normalizedOrigin[schemeSep:]}\n\t\t\ttrustedSubOrigins = append(trustedSubOrigins, sd)\n\t\t} else {\n\t\t\tisValid, normalizedOrigin := normalizeOrigin(trimmedOrigin)\n\t\t\tif !isValid {\n\t\t\t\tpanic(\"[CSRF] Invalid origin format in configuration:\" + maskValue(origin))\n\t\t\t}\n\t\t\ttrustedOrigins = append(trustedOrigins, normalizedOrigin)\n\t\t}\n\t}\n\n\t// Create the handler outside of the returned function\n\thandler := &Handler{\n\t\tconfig:         cfg,\n\t\tsessionManager: sessionManager,\n\t\tstorageManager: storageManager,\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Store the CSRF handler in the context\n\t\tfiber.StoreInContext(c, handlerKey, handler)\n\n\t\tvar token string\n\n\t\t// Action depends on the HTTP method\n\t\tswitch c.Method() {\n\t\tcase fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace:\n\t\t\tcookieToken := c.Cookies(cfg.CookieName)\n\n\t\t\tif cookieToken != \"\" {\n\t\t\t\traw, err := getRawFromStorage(c, cookieToken, &cfg, sessionManager, storageManager)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t\t}\n\n\t\t\t\tif raw != nil {\n\t\t\t\t\ttoken = cookieToken // Token is valid, safe to set it\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\t// Assume that anything not defined as 'safe' by RFC7231 needs protection\n\n\t\t\t// Evaluate Sec-Fetch-Site to reject cross-site requests earlier when available.\n\t\t\tif err := validateSecFetchSite(c); err != nil {\n\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\t// Enforce an origin check for unsafe requests.\n\t\t\terr := originMatchesHost(c, trustedOrigins, trustedSubOrigins)\n\n\t\t\t// If there's no origin, enforce a referer check for HTTPS connections.\n\t\t\tif errors.Is(err, errOriginNotFound) {\n\t\t\t\tif c.Scheme() == schemeHTTPS {\n\t\t\t\t\terr = refererMatchesHost(c, trustedOrigins, trustedSubOrigins)\n\t\t\t\t} else {\n\t\t\t\t\t// If it's not HTTPS, clear the error to allow the request to proceed.\n\t\t\t\t\terr = nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If there's an error (either from origin check or referer check), handle it.\n\t\t\tif err != nil {\n\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\t// Extract token from client request i.e. header, query, param, form\n\t\t\textractedToken, err := cfg.Extractor.Extract(c)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, extractors.ErrNotFound) {\n\t\t\t\t\treturn cfg.ErrorHandler(c, ErrTokenNotFound)\n\t\t\t\t}\n\t\t\t\t// If there's an error during extraction (other than not found), handle it.\n\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\tif extractedToken == \"\" {\n\t\t\t\treturn cfg.ErrorHandler(c, ErrTokenNotFound)\n\t\t\t}\n\n\t\t\t// Double Submit Cookie validation: ensure the extracted token matches the cookie value\n\t\t\t// This prevents CSRF attacks by requiring attackers to know both the cookie AND submit\n\t\t\t// the same token through a different channel (header, form, etc.)\n\t\t\t// WARNING: If using a custom extractor that reads from the same cookie, this provides no protection\n\t\t\tif !compareStrings(extractedToken, c.Cookies(cfg.CookieName)) {\n\t\t\t\treturn cfg.ErrorHandler(c, ErrTokenInvalid)\n\t\t\t}\n\n\t\t\traw, err := getRawFromStorage(c, extractedToken, &cfg, sessionManager, storageManager)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t}\n\n\t\t\tif raw == nil {\n\t\t\t\t// If token is not in storage, expire the cookie\n\t\t\t\texpireCSRFCookie(c, &cfg)\n\t\t\t\t// and return an error\n\t\t\t\treturn cfg.ErrorHandler(c, ErrTokenNotFound)\n\t\t\t}\n\t\t\tif cfg.SingleUseToken {\n\t\t\t\t// If token is single use, delete it from storage\n\t\t\t\tif err := deleteTokenFromStorage(c, extractedToken, &cfg, sessionManager, storageManager); err != nil {\n\t\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttoken = extractedToken // Token is valid, safe to set it\n\t\t\t}\n\t\t}\n\n\t\t// Generate CSRF token if not exist\n\t\tif token == \"\" {\n\t\t\t// And generate a new token\n\t\t\ttoken = cfg.KeyGenerator()\n\t\t}\n\n\t\t// Create or extend the token in the storage\n\t\tif err := createOrExtendTokenInStorage(c, token, &cfg, sessionManager, storageManager); err != nil {\n\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t}\n\n\t\t// Update the CSRF cookie\n\t\tupdateCSRFCookie(c, &cfg, token)\n\n\t\t// Tell the browser that a new header value is generated\n\t\tc.Vary(fiber.HeaderCookie)\n\n\t\t// Store the token in the context\n\t\tfiber.StoreInContext(c, tokenKey, token)\n\n\t\t// Continue stack\n\t\treturn c.Next()\n\t}\n}\n\n// TokenFromContext returns the token found in the context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// It returns an empty string if the token does not exist.\nfunc TokenFromContext(ctx any) string {\n\tif token, ok := fiber.ValueFromContext[string](ctx, tokenKey); ok {\n\t\treturn token\n\t}\n\n\treturn \"\"\n}\n\n// HandlerFromContext returns the Handler found in the context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// It returns nil if the handler does not exist.\nfunc HandlerFromContext(ctx any) *Handler {\n\tif handler, ok := fiber.ValueFromContext[*Handler](ctx, handlerKey); ok {\n\t\treturn handler\n\t}\n\n\treturn nil\n}\n\n// getRawFromStorage returns the raw value from the storage for the given token\n// returns nil if the token does not exist, is expired or is invalid\nfunc getRawFromStorage(c fiber.Ctx, token string, cfg *Config, sessionManager *sessionManager, storageManager *storageManager) ([]byte, error) {\n\tif cfg.Session != nil {\n\t\treturn sessionManager.getRaw(c, token, dummyValue), nil\n\t}\n\traw, err := storageManager.getRaw(c, token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"csrf: failed to fetch token from storage: %w\", err)\n\t}\n\treturn raw, nil\n}\n\n// createOrExtendTokenInStorage creates or extends the token in the storage\nfunc createOrExtendTokenInStorage(c fiber.Ctx, token string, cfg *Config, sessionManager *sessionManager, storageManager *storageManager) error {\n\tif cfg.Session != nil {\n\t\tsessionManager.setRaw(c, token, dummyValue, cfg.IdleTimeout)\n\t\treturn nil\n\t}\n\tif err := storageManager.setRaw(c, token, dummyValue, cfg.IdleTimeout); err != nil {\n\t\treturn fmt.Errorf(\"csrf: failed to store token in storage: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc deleteTokenFromStorage(c fiber.Ctx, token string, cfg *Config, sessionManager *sessionManager, storageManager *storageManager) error {\n\tif cfg.Session != nil {\n\t\tsessionManager.delRaw(c)\n\t\treturn nil\n\t}\n\tif err := storageManager.delRaw(c, token); err != nil {\n\t\treturn fmt.Errorf(\"csrf: failed to delete token from storage: %w\", err)\n\t}\n\treturn nil\n}\n\n// Update CSRF cookie\n// if expireCookie is true, the cookie will expire immediately\nfunc updateCSRFCookie(c fiber.Ctx, cfg *Config, token string) {\n\tsetCSRFCookie(c, cfg, token, cfg.IdleTimeout)\n}\n\nfunc expireCSRFCookie(c fiber.Ctx, cfg *Config) {\n\tsetCSRFCookie(c, cfg, \"\", -time.Hour)\n}\n\nfunc setCSRFCookie(c fiber.Ctx, cfg *Config, token string, expiry time.Duration) {\n\tcookie := &fiber.Cookie{\n\t\tName:        cfg.CookieName,\n\t\tValue:       token,\n\t\tDomain:      cfg.CookieDomain,\n\t\tPath:        cfg.CookiePath,\n\t\tSecure:      cfg.CookieSecure,\n\t\tHTTPOnly:    cfg.CookieHTTPOnly,\n\t\tSameSite:    cfg.CookieSameSite,\n\t\tSessionOnly: cfg.CookieSessionOnly,\n\t\tExpires:     time.Now().Add(expiry),\n\t}\n\n\t// Set the CSRF cookie to the response\n\tc.Cookie(cookie)\n}\n\n// DeleteToken removes the token found in the context from the storage\n// and expires the CSRF cookie\nfunc (handler *Handler) DeleteToken(c fiber.Ctx) error {\n\t// Extract token from the client request cookie\n\tcookieToken := c.Cookies(handler.config.CookieName)\n\tif cookieToken == \"\" {\n\t\treturn handler.config.ErrorHandler(c, ErrTokenNotFound)\n\t}\n\t// Remove the token from storage\n\tif err := deleteTokenFromStorage(c, cookieToken, &handler.config, handler.sessionManager, handler.storageManager); err != nil {\n\t\treturn handler.config.ErrorHandler(c, err)\n\t}\n\t// Expire the cookie\n\texpireCSRFCookie(c, &handler.config)\n\treturn nil\n}\n\nfunc validateSecFetchSite(c fiber.Ctx) error {\n\tsecFetchSite := utils.Trim(c.Get(fiber.HeaderSecFetchSite), ' ')\n\n\tif secFetchSite == \"\" {\n\t\treturn nil\n\t}\n\n\tswitch utilsstrings.ToLower(secFetchSite) {\n\tcase \"same-origin\", \"none\", \"cross-site\", \"same-site\":\n\t\treturn nil\n\tdefault:\n\t\treturn ErrFetchSiteInvalid\n\t}\n}\n\n// originMatchesHost checks that the origin header matches the host header\n// returns an error if the origin header is not present or is invalid\n// returns nil if the origin header is valid\nfunc originMatchesHost(c fiber.Ctx, trustedOrigins []string, trustedSubOrigins []subdomain) error {\n\torigin := utilsstrings.ToLower(c.Get(fiber.HeaderOrigin))\n\tif origin == \"\" || origin == \"null\" { // \"null\" is set by some browsers when the origin is a secure context https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description\n\t\treturn errOriginNotFound\n\t}\n\n\toriginURL, err := url.Parse(origin)\n\tif err != nil {\n\t\treturn ErrOriginInvalid\n\t}\n\n\tif schemeAndHostMatch(originURL.Scheme, originURL.Host, c.Scheme(), c.Host()) {\n\t\treturn nil\n\t}\n\n\tif slices.Contains(trustedOrigins, origin) {\n\t\treturn nil\n\t}\n\n\tfor _, trustedSubOrigin := range trustedSubOrigins {\n\t\tif trustedSubOrigin.match(origin) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn ErrOriginNoMatch\n}\n\n// refererMatchesHost checks that the referer header matches the host header\n// returns an error if the referer header is not present or is invalid\n// returns nil if the referer header is valid\nfunc refererMatchesHost(c fiber.Ctx, trustedOrigins []string, trustedSubOrigins []subdomain) error {\n\treferer := utilsstrings.ToLower(c.Get(fiber.HeaderReferer))\n\tif referer == \"\" {\n\t\treturn ErrRefererNotFound\n\t}\n\n\trefererURL, err := url.Parse(referer)\n\tif err != nil {\n\t\treturn ErrRefererInvalid\n\t}\n\n\tif schemeAndHostMatch(refererURL.Scheme, refererURL.Host, c.Scheme(), c.Host()) {\n\t\treturn nil\n\t}\n\n\treferer = refererURL.String()\n\n\tif slices.Contains(trustedOrigins, referer) {\n\t\treturn nil\n\t}\n\n\tfor _, trustedSubOrigin := range trustedSubOrigins {\n\t\tif trustedSubOrigin.match(referer) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn ErrRefererNoMatch\n}\n"
  },
  {
    "path": "middleware/csrf/csrf_test.go",
    "content": "package csrf\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/fiber/v3/middleware/session\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\ntype failingCSRFStorage struct {\n\tdata map[string][]byte\n\terrs map[string]error\n}\n\nfunc newFailingCSRFStorage() *failingCSRFStorage {\n\treturn &failingCSRFStorage{\n\t\tdata: make(map[string][]byte),\n\t\terrs: make(map[string]error),\n\t}\n}\n\nfunc (s *failingCSRFStorage) GetWithContext(_ context.Context, key string) ([]byte, error) {\n\tif err, ok := s.errs[\"get|\"+key]; ok && err != nil {\n\t\treturn nil, err\n\t}\n\tif val, ok := s.data[key]; ok {\n\t\treturn append([]byte(nil), val...), nil\n\t}\n\treturn nil, nil\n}\n\nvar trustedProxyConfig = fiber.Config{\n\tTrustProxy: true,\n\tTrustProxyConfig: fiber.TrustProxyConfig{\n\t\tProxies: []string{\"0.0.0.0\"},\n\t},\n}\n\nfunc newTrustedApp() *fiber.App {\n\treturn fiber.New(trustedProxyConfig)\n}\n\nfunc newTrustedRequestCtx() *fasthttp.RequestCtx {\n\tctx := &fasthttp.RequestCtx{}\n\tctx.SetRemoteAddr(net.Addr(&net.TCPAddr{IP: net.ParseIP(\"0.0.0.0\")}))\n\n\treturn ctx\n}\n\nfunc (s *failingCSRFStorage) Get(key string) ([]byte, error) {\n\treturn s.GetWithContext(context.Background(), key)\n}\n\nfunc (s *failingCSRFStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\tif err, ok := s.errs[\"set|\"+key]; ok && err != nil {\n\t\treturn err\n\t}\n\ts.data[key] = append([]byte(nil), val...)\n\treturn nil\n}\n\nfunc (s *failingCSRFStorage) Set(key string, val []byte, exp time.Duration) error {\n\treturn s.SetWithContext(context.Background(), key, val, exp)\n}\n\nfunc (s *failingCSRFStorage) DeleteWithContext(_ context.Context, key string) error {\n\tif err, ok := s.errs[\"del|\"+key]; ok && err != nil {\n\t\treturn err\n\t}\n\tdelete(s.data, key)\n\treturn nil\n}\n\nfunc (s *failingCSRFStorage) Delete(key string) error {\n\treturn s.DeleteWithContext(context.Background(), key)\n}\n\nfunc (s *failingCSRFStorage) ResetWithContext(context.Context) error {\n\ts.data = make(map[string][]byte)\n\ts.errs = make(map[string]error)\n\treturn nil\n}\n\nfunc (s *failingCSRFStorage) Reset() error {\n\treturn s.ResetWithContext(context.Background())\n}\n\nfunc (*failingCSRFStorage) Close() error { return nil }\n\nfunc TestCSRFStorageGetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCSRFStorage()\n\tstorage.errs[\"get|token\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStorage: storage,\n\t\tErrorHandler: func(_ fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn fiber.ErrTeapot\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.AddCookie(&http.Cookie{Name: ConfigDefault.CookieName, Value: \"token\"})\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"csrf: failed to fetch token from storage\")\n}\n\nfunc TestCSRFStorageSetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCSRFStorage()\n\tstorage.errs[\"set|token\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStorage: storage,\n\t\tKeyGenerator: func() string {\n\t\t\treturn \"token\"\n\t\t},\n\t\tErrorHandler: func(_ fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn fiber.ErrTeapot\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"csrf: failed to store token in storage\")\n}\n\nfunc TestCSRFStorageDeleteError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingCSRFStorage()\n\tstorage.data[\"token\"] = []byte(\"value\")\n\tstorage.errs[\"del|token\"] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStorage:        storage,\n\t\tSingleUseToken: true,\n\t\tErrorHandler: func(_ fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn fiber.ErrTeapot\n\t\t},\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(HeaderName, \"token\")\n\treq.AddCookie(&http.Cookie{Name: ConfigDefault.CookieName, Value: \"token\"})\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"csrf: failed to delete token from storage\")\n}\n\nfunc Test_CSRF(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\tmethods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace}\n\n\tfor _, method := range methods {\n\t\t// Generate CSRF token\n\t\tctx.Request.Header.SetMethod(method)\n\t\th(ctx)\n\n\t\t// Without CSRF cookie\n\t\tctx.Request.Header.Reset()\n\t\tctx.Request.ResetBody()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\th(ctx)\n\t\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t\t// Invalid CSRF token\n\t\tctx.Request.Header.Reset()\n\t\tctx.Request.ResetBody()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request.Header.Set(HeaderName, \"johndoe\")\n\t\th(ctx)\n\t\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t\t// Valid CSRF token\n\t\tctx.Request.Header.Reset()\n\t\tctx.Request.ResetBody()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(method)\n\t\th(ctx)\n\t\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\t\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t\tctx.Request.Reset()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request.Header.Set(HeaderName, token)\n\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\th(ctx)\n\t\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\t}\n}\n\nfunc Test_CSRF_WithSession(t *testing.T) {\n\tt.Parallel()\n\n\t// session store\n\tstore := session.NewStore(session.Config{\n\t\tExtractor: extractors.FromCookie(\"_session\"),\n\t})\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := &fasthttp.RequestCtx{}\n\tdefer app.ReleaseCtx(app.AcquireCtx(ctx))\n\n\t// get session\n\tsess, err := store.Get(app.AcquireCtx(ctx))\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\t// the session string is no longer be 123\n\tnewSessionIDString := sess.ID()\n\trequire.NoError(t, sess.Save())\n\n\tapp.AcquireCtx(ctx).Request().Header.SetCookie(\"_session\", newSessionIDString)\n\n\t// middleware config\n\tconfig := Config{\n\t\tSession: store,\n\t}\n\n\t// middleware\n\tapp.Use(New(config))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\tmethods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace}\n\n\tfor _, method := range methods {\n\t\t// Generate CSRF token\n\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\t\th(ctx)\n\n\t\t// Without CSRF cookie\n\t\tctx.Request.Reset()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\t\th(ctx)\n\t\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t\t// Empty/invalid CSRF token\n\t\tctx.Request.Reset()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request.Header.Set(HeaderName, \"johndoe\")\n\t\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\t\th(ctx)\n\t\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t\t// Valid CSRF token\n\t\tctx.Request.Reset()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(method)\n\t\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\t\th(ctx)\n\t\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\t\tfor header := range strings.SplitSeq(token, \";\") {\n\t\t\tif strings.Split(utils.TrimSpace(header), \"=\")[0] == ConfigDefault.CookieName {\n\t\t\t\ttoken = strings.Split(header, \"=\")[1]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tctx.Request.Reset()\n\t\tctx.Response.Reset()\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\tctx.Request.Header.Set(HeaderName, token)\n\t\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\th(ctx)\n\t\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\t}\n}\n\n// go test -run Test_CSRF_WithSession_Middleware\nfunc Test_CSRF_WithSession_Middleware(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// session mw\n\tsmh, sstore := session.NewWithStore()\n\n\t// csrf mw\n\tcmh := New(Config{\n\t\tSession: sstore,\n\t})\n\n\tapp.Use(smh)\n\n\tapp.Use(cmh)\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tsess := session.FromContext(c)\n\t\tsess.Set(\"hello\", \"world\")\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tsess := session.FromContext(c)\n\t\tif sess.Get(\"hello\") != \"world\" {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token and session_id\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\n\tcsrfCookie := fasthttp.AcquireCookie()\n\tcsrfCookie.SetKey(ConfigDefault.CookieName)\n\trequire.True(t, ctx.Response.Header.Cookie(csrfCookie))\n\tcsrfToken := string(csrfCookie.Value())\n\trequire.NotEmpty(t, csrfToken)\n\tfasthttp.ReleaseCookie(csrfCookie)\n\n\tsessionCookie := fasthttp.AcquireCookie()\n\tsessionCookie.SetKey(\"session_id\")\n\trequire.True(t, ctx.Response.Header.Cookie(sessionCookie))\n\tsessionID := string(sessionCookie.Value())\n\trequire.NotEmpty(t, sessionID)\n\tfasthttp.ReleaseCookie(sessionCookie)\n\n\t// Use the CSRF token and session_id\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, csrfToken)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, csrfToken)\n\tctx.Request.Header.SetCookie(\"session_id\", sessionID)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n}\n\n// go test -run Test_CSRF_ExpiredToken\nfunc Test_CSRF_ExpiredToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tIdleTimeout: 1 * time.Second,\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Use the CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Wait for the token to expire\n\ttime.Sleep(1250 * time.Millisecond)\n\n\t// Expired CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\n// go test -run Test_CSRF_ExpiredToken_WithSession\nfunc Test_CSRF_ExpiredToken_WithSession(t *testing.T) {\n\tt.Parallel()\n\n\t// session store\n\tstore := session.NewStore(session.Config{\n\t\tExtractor: extractors.FromCookie(\"_session\"),\n\t})\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := &fasthttp.RequestCtx{}\n\tdefer app.ReleaseCtx(app.AcquireCtx(ctx))\n\n\t// get session\n\tsess, err := store.Get(app.AcquireCtx(ctx))\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\t// get session id\n\tnewSessionIDString := sess.ID()\n\trequire.NoError(t, sess.Save())\n\n\tapp.AcquireCtx(ctx).Request().Header.SetCookie(\"_session\", newSessionIDString)\n\n\t// middleware config\n\tconfig := Config{\n\t\tSession:     store,\n\t\tIdleTimeout: 1 * time.Second,\n\t}\n\n\t// middleware\n\tapp.Use(New(config))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\tfor header := range strings.SplitSeq(token, \";\") {\n\t\tif strings.Split(utils.TrimSpace(header), \"=\")[0] == ConfigDefault.CookieName {\n\t\t\ttoken = strings.Split(header, \"=\")[1]\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Use the CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Wait for the token to expire\n\ttime.Sleep(1*time.Second + 100*time.Millisecond)\n\n\t// Expired CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\n// go test -run Test_CSRF_MultiUseToken\nfunc Test_CSRF_MultiUseToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromHeader(\"X-Csrf-Token\"),\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Invalid CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", \"johndoe\")\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\tnewToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\tnewToken = strings.Split(strings.Split(newToken, \";\")[0], \"=\")[1]\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Check if the token is not a dummy value\n\trequire.Equal(t, token, newToken)\n}\n\n// go test -run Test_CSRF_SingleUseToken\nfunc Test_CSRF_SingleUseToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tSingleUseToken: true,\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Use the CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\tnewToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\tnewToken = strings.Split(strings.Split(newToken, \";\")[0], \"=\")[1]\n\tif token == newToken {\n\t\tt.Error(\"new token should not be the same as the old token\")\n\t}\n\n\t// Use the CSRF token again\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\n// go test -run Test_CSRF_Next\nfunc Test_CSRF_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_CSRF_From_Form(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Extractor: extractors.FromForm(\"_csrf\")}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Invalid CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\tctx.Request.SetBodyString(\"_csrf=\" + token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_From_Query(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Extractor: extractors.FromQuery(\"_csrf\")}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Invalid CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/?_csrf=\" + utils.UUIDv4())\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.SetRequestURI(\"/?_csrf=\" + token)\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"OK\", string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_From_Param(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tcsrfGroup := app.Group(\"/:csrf\", New(Config{Extractor: extractors.FromParam(\"csrf\")}))\n\n\tcsrfGroup.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Invalid CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/\" + utils.UUIDv4())\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/\" + utils.UUIDv4())\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.SetRequestURI(\"/\" + token)\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"OK\", string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_From_Custom(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\textractor := extractors.Extractor{\n\t\tExtract: func(c fiber.Ctx) (string, error) {\n\t\t\tbody := string(c.Body())\n\t\t\t// Generate the correct extractor to get the token from the correct location\n\t\t\tselectors := strings.Split(body, \"=\")\n\n\t\t\tif len(selectors) != 2 || selectors[1] == \"\" {\n\t\t\t\treturn \"\", extractors.ErrNotFound\n\t\t\t}\n\t\t\treturn selectors[1], nil\n\t\t},\n\t\tSource: extractors.SourceCustom,\n\t\tKey:    \"_csrf\",\n\t}\n\n\tapp.Use(New(Config{Extractor: extractor}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Invalid CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)\n\tctx.Request.SetBodyString(\"_csrf=\" + token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_Extractor_EmptyString(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\textractor := extractors.Extractor{\n\t\tExtract: func(_ fiber.Ctx) (string, error) {\n\t\t\treturn \"\", nil\n\t\t},\n\t\tSource: extractors.SourceCustom,\n\t\tKey:    \"_csrf\",\n\t}\n\n\terrorHandler := func(c fiber.Ctx, err error) error {\n\t\treturn c.Status(403).SendString(err.Error())\n\t}\n\n\tapp.Use(New(Config{\n\t\tExtractor:    extractor,\n\t\tErrorHandler: errorHandler,\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)\n\tctx.Request.SetBodyString(\"_csrf=\" + token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\trequire.Equal(t, ErrTokenNotFound.Error(), string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_SecFetchSite(t *testing.T) {\n\tt.Parallel()\n\n\terrorHandler := func(c fiber.Ctx, err error) error {\n\t\treturn c.Status(fiber.StatusForbidden).SendString(err.Error())\n\t}\n\n\tapp := newTrustedApp()\n\n\tapp.Use(New(Config{ErrorHandler: errorHandler}))\n\n\tapp.All(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\ttests := []struct {\n\t\tname                   string\n\t\tmethod                 string\n\t\tsecFetchSite           string\n\t\torigin                 string\n\t\texpectedStatus         int16\n\t\thttps                  bool\n\t\texpectFetchSiteInvalid bool\n\t}{\n\t\t{\n\t\t\tname:           \"same-origin allowed\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"same-origin\",\n\t\t\torigin:         \"http://example.com\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"none allowed\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"none\",\n\t\t\torigin:         \"http://example.com\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"cross-site with origin allowed\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\torigin:         \"http://example.com\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"same-site with origin allowed\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"same-site\",\n\t\t\torigin:         \"http://example.com\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"cross-site with mismatched origin blocked\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\torigin:         \"https://attacker.example\",\n\t\t\texpectedStatus: http.StatusForbidden,\n\t\t},\n\t\t{\n\t\t\tname:           \"same-site with null origin blocked\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\tsecFetchSite:   \"same-site\",\n\t\t\torigin:         \"null\",\n\t\t\texpectedStatus: http.StatusForbidden,\n\t\t\thttps:          true,\n\t\t},\n\t\t{\n\t\t\tname:                   \"invalid header blocked\",\n\t\t\tmethod:                 fiber.MethodPost,\n\t\t\tsecFetchSite:           \"weird\",\n\t\t\torigin:                 \"http://example.com\",\n\t\t\texpectedStatus:         http.StatusForbidden,\n\t\t\texpectFetchSiteInvalid: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"no header with no origin\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\torigin:         \"\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"no header with matching origin\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\torigin:         \"http://example.com\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"no header with mismatched origin\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\torigin:         \"https://attacker.example\",\n\t\t\texpectedStatus: http.StatusForbidden,\n\t\t},\n\t\t{\n\t\t\tname:           \"no header with null origin\",\n\t\t\tmethod:         fiber.MethodPost,\n\t\t\torigin:         \"null\",\n\t\t\texpectedStatus: http.StatusForbidden,\n\t\t\thttps:          true,\n\t\t},\n\t\t{\n\t\t\tname:           \"GET allowed\",\n\t\t\tmethod:         fiber.MethodGet,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"HEAD allowed\",\n\t\t\tmethod:         fiber.MethodHead,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"OPTIONS allowed\",\n\t\t\tmethod:         fiber.MethodOptions,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\texpectedStatus: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"PUT with mismatched origin blocked\",\n\t\t\tmethod:         fiber.MethodPut,\n\t\t\tsecFetchSite:   \"cross-site\",\n\t\t\torigin:         \"https://attacker.example\",\n\t\t\texpectedStatus: http.StatusForbidden,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := &fasthttp.RequestCtx{}\n\t\t\tscheme := \"http\"\n\t\t\tif tt.https {\n\t\t\t\tscheme = \"https\"\n\t\t\t}\n\t\t\tc.Request.Header.SetMethod(tt.method)\n\t\t\tc.Request.URI().SetScheme(scheme)\n\t\t\tc.Request.URI().SetHost(\"example.com\")\n\t\t\tc.Request.Header.SetHost(\"example.com\")\n\t\t\tc.Request.Header.SetProtocol(scheme)\n\t\t\tif scheme == \"https\" {\n\t\t\t\tc.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\t\t\t}\n\t\t\tif tt.origin != \"\" {\n\t\t\t\tc.Request.Header.Set(fiber.HeaderOrigin, tt.origin)\n\t\t\t}\n\t\t\tif tt.secFetchSite != \"\" {\n\t\t\t\tc.Request.Header.Set(fiber.HeaderSecFetchSite, tt.secFetchSite)\n\t\t\t}\n\n\t\t\tsafe := tt.method == fiber.MethodGet || tt.method == fiber.MethodHead || tt.method == fiber.MethodOptions || tt.method == fiber.MethodTrace\n\n\t\t\tif !safe {\n\t\t\t\tc.Request.Header.Set(HeaderName, token)\n\t\t\t\tc.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t}\n\n\t\t\th(c)\n\t\t\trequire.Equal(t, int(tt.expectedStatus), c.Response.StatusCode())\n\t\t\tif tt.expectFetchSiteInvalid {\n\t\t\t\trequire.Equal(t, ErrFetchSiteInvalid.Error(), string(c.Response.Body()))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_CSRF_Origin(t *testing.T) {\n\tt.Parallel()\n\tapp := newTrustedApp()\n\n\tapp.Use(New(Config{CookieSecure: true}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test Correct Origin with port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com:8080\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com:8080\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com:8080\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Origin without default HTTP port against host with default port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com:80\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com:80\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Origin with default HTTP port against host without port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com:80\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Correct Origin with wrong port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com:3000\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test Correct Origin with null\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"null\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Correct Origin with ReverseProxy\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"10.0.1.42.com:8080\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"10.0.1.42:8080\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedFor, `192.0.2.43, \"[2001:db8:cafe::17]\"`)\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Origin without default HTTPS port against host with default port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com:443\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.SetHost(\"example.com:443\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Origin with default HTTPS port against host without port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://example.com:443\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Correct Origin with ReverseProxy Missing X-Forwarded-* Headers\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"10.0.1.42:8080\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"10.0.1.42:8080\")\n\tctx.Request.Header.Set(fiber.HeaderXUrlScheme, \"http\") // We need to set this header to make sure c.Protocol() returns http\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test Wrong Origin\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://csrf.example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_TrustedOrigins(t *testing.T) {\n\tt.Parallel()\n\tapp := newTrustedApp()\n\n\tapp.Use(New(Config{\n\t\tCookieSecure: true,\n\t\tTrustedOrigins: []string{\n\t\t\t\"http://safe.example.com\",\n\t\t\t\"https://safe.example.com\",\n\t\t\t\"http://*.domain-1.com\",\n\t\t\t\"https://*.domain-1.com\",\n\t\t},\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test Trusted Origin\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://safe.example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Origin Subdomain\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://safe.domain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Origin deeply nested subdomain\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"a.b.c.domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"a.b.c.domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"https://a.b.c.domain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Origin Invalid\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://evildomain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test Trusted Origin malformed subdomain\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://evil.comdomain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test Trusted Referer\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://safe.example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Referer Wildcard\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://safe.domain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Referer deeply nested subdomain\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"a.b.c.domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"a.b.c.domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://a.b.c.domain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Trusted Referer Invalid\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"api.domain-1.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"api.domain-1.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://evildomain-1.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_TrustedOrigins_InvalidOrigins(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname   string\n\t\torigin string\n\t}{\n\t\t{name: \"No Scheme\", origin: \"localhost\"},\n\t\t{name: \"Wildcard\", origin: \"https://*\"},\n\t\t{name: \"Wildcard domain\", origin: \"https://*example.com\"},\n\t\t{name: \"File Scheme\", origin: \"file://example.com\"},\n\t\t{name: \"FTP Scheme\", origin: \"ftp://example.com\"},\n\t\t{name: \"Port Wildcard\", origin: \"http://example.com:*\"},\n\t\t{name: \"Multiple Wildcards\", origin: \"https://*.*.com\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\torigin := tt.origin\n\t\t\tt.Parallel()\n\t\t\trequire.Panics(t, func() {\n\t\t\t\tapp := fiber.New()\n\t\t\t\tapp.Use(New(Config{\n\t\t\t\t\tCookieSecure:   true,\n\t\t\t\t\tTrustedOrigins: []string{origin},\n\t\t\t\t}))\n\t\t\t}, \"Expected panic\")\n\t\t})\n\t}\n}\n\nfunc Test_CSRF_Referer(t *testing.T) {\n\tt.Parallel()\n\tapp := newTrustedApp()\n\n\tapp.Use(New(Config{CookieSecure: true}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test Correct Referer with port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com:8443\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com:8443\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, ctx.Request.URI().String())\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Referer without default HTTPS port against host with default port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com:443\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com:443\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Referer with default HTTPS port against host without port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com:443\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Correct Referer with ReverseProxy\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"10.0.1.42.com:8443\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"10.0.1.42:8443\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedFor, `192.0.2.43, \"[2001:db8:cafe::17]\"`)\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Referer without default HTTP port against host with default port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com:80\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com:80\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"http://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Referer with default HTTP port against host without port\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"http://example.com:80\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Correct Referer with ReverseProxy Missing X-Forwarded-* Headers\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"10.0.1.42:8443\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"10.0.1.42:8443\")\n\tctx.Request.Header.Set(fiber.HeaderXUrlScheme, \"https\") // We need to set this header to make sure c.Protocol() returns https\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test Correct Referer with path\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com/action/items?gogogo=true\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test Wrong Referer\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://csrf.example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_DeleteToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tconfig := ConfigDefault\n\n\tapp.Use(New(config))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// DeleteToken after token generation and remove the cookie\n\tctx.Request.Header.Reset()\n\tctx.Request.ResetBody()\n\tctx.Response.Reset()\n\tctx.Request.Header.Set(HeaderName, \"\")\n\thandler := HandlerFromContext(app.AcquireCtx(ctx))\n\tif handler != nil {\n\t\tctx.Request.Header.DelAllCookies()\n\t\terr := handler.DeleteToken(app.AcquireCtx(ctx))\n\t\trequire.ErrorIs(t, err, ErrTokenNotFound)\n\t}\n\th(ctx)\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Delete the CSRF token\n\tctx.Request.Header.Reset()\n\tctx.Request.ResetBody()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\thandler = HandlerFromContext(app.AcquireCtx(ctx))\n\tif handler != nil {\n\t\tif err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\th(ctx)\n\n\tctx.Request.Header.Reset()\n\tctx.Request.ResetBody()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_DeleteToken_WithSession(t *testing.T) {\n\tt.Parallel()\n\n\t// session store\n\tstore := session.NewStore(session.Config{\n\t\tExtractor: extractors.FromCookie(\"_session\"),\n\t})\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := &fasthttp.RequestCtx{}\n\n\t// get session\n\tsess, err := store.Get(app.AcquireCtx(ctx))\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\t// the session string is no longer be 123\n\tnewSessionIDString := sess.ID()\n\trequire.NoError(t, sess.Save())\n\n\tapp.AcquireCtx(ctx).Request().Header.SetCookie(\"_session\", newSessionIDString)\n\n\t// middleware config\n\tconfig := Config{\n\t\tSession: store,\n\t}\n\n\t// middleware\n\tapp.Use(New(config))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Delete the CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\thandler := HandlerFromContext(app.AcquireCtx(ctx))\n\tif handler != nil {\n\t\tif err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\th(ctx)\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\tctx.Request.Header.SetCookie(\"_session\", newSessionIDString)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\terrHandler := func(ctx fiber.Ctx, err error) error {\n\t\trequire.Equal(t, ErrTokenInvalid, err)\n\t\treturn ctx.Status(419).Send([]byte(\"invalid CSRF token\"))\n\t}\n\n\tapp.Use(New(Config{ErrorHandler: errHandler}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\n\t// invalid CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, \"johndoe\")\n\th(ctx)\n\trequire.Equal(t, 419, ctx.Response.StatusCode())\n\trequire.Equal(t, \"invalid CSRF token\", string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_ErrorHandler_EmptyToken(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\terrHandler := func(ctx fiber.Ctx, err error) error {\n\t\trequire.Equal(t, ErrTokenNotFound, err)\n\t\treturn ctx.Status(419).Send([]byte(\"empty CSRF token\"))\n\t}\n\n\tapp.Use(New(Config{ErrorHandler: errHandler}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\n\t// empty CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\th(ctx)\n\trequire.Equal(t, 419, ctx.Response.StatusCode())\n\trequire.Equal(t, \"empty CSRF token\", string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_ErrorHandler_MissingReferer(t *testing.T) {\n\tt.Parallel()\n\tapp := newTrustedApp()\n\n\terrHandler := func(ctx fiber.Ctx, err error) error {\n\t\trequire.Equal(t, ErrRefererNotFound, err)\n\t\treturn ctx.Status(419).Send([]byte(\"empty CSRF token\"))\n\t}\n\n\tapp.Use(New(Config{\n\t\tCookieSecure: true,\n\t\tErrorHandler: errHandler,\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.Header.Set(fiber.HeaderXForwardedHost, \"example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 419, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_Cookie_Injection_Exploit(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Inject CSRF token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderCookie, \"csrf_=pwned;\")\n\tctx.Request.SetRequestURI(\"/\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Exploit CSRF token we just injected\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.Set(fiber.HeaderCookie, \"csrf_=pwned;\")\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode(), \"CSRF exploit successful\")\n}\n\n// Test_CSRF_UnsafeHeaderValue ensures that unsafe header values, such as those described in https://github.com/gofiber/fiber/issues/2045, are rejected and the bug remains fixed.\n// go test -race -run Test_CSRF_UnsafeHeaderValue\nfunc Test_CSRF_UnsafeHeaderValue(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar token string\n\tfor _, c := range resp.Cookies() {\n\t\tif c.Name != ConfigDefault.CookieName {\n\t\t\tcontinue\n\t\t}\n\t\ttoken = c.Value\n\t\tbreak\n\t}\n\n\tt.Log(\"token\", token)\n\n\tgetReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tgetReq.Header.Set(HeaderName, token)\n\tresp, err = app.Test(getReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tgetReq = httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody)\n\tgetReq.Header.Set(\"X-Requested-With\", \"XMLHttpRequest\")\n\tgetReq.Header.Set(fiber.HeaderCacheControl, \"no\")\n\tgetReq.Header.Set(HeaderName, token)\n\tgetReq.AddCookie(&http.Cookie{\n\t\tName:  ConfigDefault.CookieName,\n\t\tValue: token,\n\t})\n\n\tresp, err = app.Test(getReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tgetReq.Header.Set(fiber.HeaderAccept, \"*/*\")\n\tgetReq.Header.Del(HeaderName)\n\tresp, err = app.Test(getReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tpostReq := httptest.NewRequest(fiber.MethodPost, \"/\", http.NoBody)\n\tpostReq.Header.Set(\"X-Requested-With\", \"XMLHttpRequest\")\n\tpostReq.Header.Set(HeaderName, token)\n\tpostReq.AddCookie(&http.Cookie{\n\t\tName:  ConfigDefault.CookieName,\n\t\tValue: token,\n\t})\n\tresp, err = app.Test(postReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4\nfunc Benchmark_Middleware_CSRF_Check(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test Correct Referer POST\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"https://example.com\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_GenerateToken -benchmem -count=4\nfunc Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(ctx)\n\t}\n\n\t// Ensure the GET request returns a 418 status code\n\trequire.Equal(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode())\n}\n\nfunc Test_CSRF_InvalidURLHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := newTrustedApp()\n\n\terrHandler := func(ctx fiber.Ctx, err error) error {\n\t\treturn ctx.Status(419).Send([]byte(err.Error()))\n\t}\n\n\tapp.Use(New(Config{ErrorHandler: errHandler}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := newTrustedRequestCtx()\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"http\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// invalid Origin\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.URI().SetScheme(\"http\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"http\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderOrigin, \"http://[::1]:%38%30/Invalid Origin\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 419, ctx.Response.StatusCode())\n\trequire.Equal(t, ErrOriginInvalid.Error(), string(ctx.Response.Body()))\n\n\t// invalid Referer\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderXForwardedProto, \"https\")\n\tctx.Request.URI().SetScheme(\"https\")\n\tctx.Request.URI().SetHost(\"example.com\")\n\tctx.Request.Header.SetProtocol(\"https\")\n\tctx.Request.Header.SetHost(\"example.com\")\n\tctx.Request.Header.Set(fiber.HeaderReferer, \"http://[::1]:%38%30/Invalid Referer\")\n\tctx.Request.Header.Set(HeaderName, token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 419, ctx.Response.StatusCode())\n\trequire.Equal(t, ErrRefererInvalid.Error(), string(ctx.Response.Body()))\n}\n\nfunc Test_CSRF_TokenFromContext(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttoken := TokenFromContext(c)\n\t\trequire.NotEmpty(t, token)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_CSRF_FromContextMethods(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttoken := TokenFromContext(c)\n\t\trequire.NotEmpty(t, token)\n\n\t\thandler := HandlerFromContext(c)\n\t\trequire.NotNil(t, handler)\n\n\t\tcustomCtx, ok := c.(fiber.CustomCtx)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, token, TokenFromContext(customCtx))\n\t\trequire.Equal(t, handler, HandlerFromContext(customCtx))\n\t\trequire.Equal(t, token, TokenFromContext(c.RequestCtx()))\n\t\trequire.Equal(t, token, TokenFromContext(c.Context()))\n\t\trequire.Equal(t, handler, HandlerFromContext(c.RequestCtx()))\n\t\trequire.Equal(t, handler, HandlerFromContext(c.Context()))\n\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_CSRF_FromContextMethods_Invalid(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttoken := TokenFromContext(c)\n\t\trequire.Empty(t, token)\n\n\t\thandler := HandlerFromContext(c)\n\t\trequire.Nil(t, handler)\n\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_deleteTokenFromStorage(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(func() { app.ReleaseCtx(ctx) })\n\n\ttoken := \"token123\"\n\tdummy := []byte(\"dummy\")\n\n\tstore := session.NewStore()\n\tsm := newSessionManager(store)\n\tstm := newStorageManager(nil, true)\n\n\tsm.setRaw(ctx, token, dummy, time.Minute)\n\tcfg := Config{Session: store}\n\trequire.NoError(t, deleteTokenFromStorage(ctx, token, &cfg, sm, stm))\n\traw := sm.getRaw(ctx, token, dummy)\n\trequire.Nil(t, raw)\n\n\tsm2 := newSessionManager(nil)\n\tstm2 := newStorageManager(nil, true)\n\n\trequire.NoError(t, stm2.setRaw(context.Background(), token, dummy, time.Minute))\n\tcfg = Config{}\n\trequire.NoError(t, deleteTokenFromStorage(ctx, token, &cfg, sm2, stm2))\n\traw, err := stm2.getRaw(context.Background(), token)\n\trequire.NoError(t, err)\n\trequire.Nil(t, raw)\n}\n\nfunc Test_storageManager_logKey(t *testing.T) {\n\tt.Parallel()\n\n\tredacted := newStorageManager(nil, true)\n\trequire.Equal(t, redactedKey, redacted.logKey(\"secret\"))\n\n\tplain := newStorageManager(nil, false)\n\trequire.Equal(t, \"secret\", plain.logKey(\"secret\"))\n}\n\nfunc Test_CSRF_Chain_Extractor(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Chain extractor: try header first, fall back to form\n\tchainExtractor := extractors.Chain(\n\t\textractors.FromHeader(\"X-Csrf-Token\"),\n\t\textractors.FromForm(\"_csrf\"),\n\t)\n\n\tapp.Use(New(Config{Extractor: chainExtractor}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test 1: Token in header (first extractor should succeed)\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test 2: Token in form (fallback should succeed)\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\tctx.Request.SetBodyString(\"_csrf=\" + token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test 3: Token in both header and form (header should take precedence)\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\tctx.Request.SetBodyString(\"_csrf=wrong_token\")\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test 4: No token in either location\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n\n\t// Test 5: Wrong token in both locations\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", \"wrong_token\")\n\tctx.Request.SetBodyString(\"_csrf=also_wrong\")\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_Chain_Extractor_Empty(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Empty chain extractor\n\temptyChain := extractors.Chain()\n\n\tapp.Use(New(Config{Extractor: emptyChain}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test with empty chain - should always fail\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_Chain_Extractor_SingleExtractor(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Chain with single extractor (should behave like the single extractor)\n\tsingleChain := extractors.Chain(extractors.FromHeader(\"X-Csrf-Token\"))\n\n\tapp.Use(New(Config{Extractor: singleChain}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test valid token in header\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\t// Test no token\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode())\n}\n\nfunc Test_CSRF_All_Extractors(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tsetupRequest func(ctx *fasthttp.RequestCtx, token string)\n\t\tname         string\n\t\textractor    extractors.Extractor\n\t\texpectStatus int\n\t}{\n\t\t{\n\t\t\tname:      \"FromHeader\",\n\t\t\textractor: extractors.FromHeader(\"X-Csrf-Token\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.Header.Set(\"X-Csrf-Token\", token)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname:      \"FromHeader_Missing\",\n\t\t\textractor: extractors.FromHeader(\"X-Csrf-Token\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 403,\n\t\t},\n\t\t{\n\t\t\tname:      \"FromForm\",\n\t\t\textractor: extractors.FromForm(\"_csrf\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\t\t\t\tctx.Request.SetBodyString(\"_csrf=\" + token)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname:      \"FromForm_Missing\",\n\t\t\textractor: extractors.FromForm(\"_csrf\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 403,\n\t\t},\n\t\t{\n\t\t\tname:      \"FromQuery\",\n\t\t\textractor: extractors.FromQuery(\"csrf_token\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.SetRequestURI(\"/?csrf_token=\" + token)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname:      \"FromQuery_Missing\",\n\t\t\textractor: extractors.FromQuery(\"csrf_token\"),\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 403,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\n\t\t\tapp.Use(New(Config{Extractor: tc.extractor}))\n\t\t\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t})\n\n\t\t\th := app.Handler()\n\t\t\tctx := &fasthttp.RequestCtx{}\n\n\t\t\t// Generate CSRF token\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\th(ctx)\n\t\t\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\t\t\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t\t\t// Test the extractor\n\t\t\tctx.Request.Reset()\n\t\t\tctx.Response.Reset()\n\t\t\ttc.setupRequest(ctx, token)\n\t\t\th(ctx)\n\t\t\trequire.Equal(t, tc.expectStatus, ctx.Response.StatusCode(),\n\t\t\t\t\"Test case %s failed: expected %d, got %d\", tc.name, tc.expectStatus, ctx.Response.StatusCode())\n\t\t})\n\t}\n}\n\nfunc Test_CSRF_Param_Extractor(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tsetupRequest func(ctx *fasthttp.RequestCtx, token string)\n\t\tname         string\n\t\texpectStatus int\n\t}{\n\t\t{\n\t\t\tname: \"FromParam_Valid\",\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.SetRequestURI(\"/\" + token)\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 200,\n\t\t},\n\t\t{\n\t\t\tname: \"FromParam_Invalid\",\n\t\t\tsetupRequest: func(ctx *fasthttp.RequestCtx, token string) {\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.SetRequestURI(\"/wrong_token\")\n\t\t\t\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\t\t\t},\n\t\t\texpectStatus: 403,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\n\t\t\t// Only use param-based routing for param extractor tests\n\t\t\tcsrfGroup := app.Group(\"/:csrf\", New(Config{Extractor: extractors.FromParam(\"csrf\")}))\n\t\t\tcsrfGroup.Post(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t})\n\n\t\t\th := app.Handler()\n\t\t\tctx := &fasthttp.RequestCtx{}\n\n\t\t\t// Generate CSRF token\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.SetRequestURI(\"/\" + utils.UUIDv4())\n\t\t\th(ctx)\n\t\t\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\t\t\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t\t\t// Test the extractor\n\t\t\tctx.Request.Reset()\n\t\t\tctx.Response.Reset()\n\t\t\ttc.setupRequest(ctx, token)\n\t\t\th(ctx)\n\t\t\trequire.Equal(t, tc.expectStatus, ctx.Response.StatusCode(),\n\t\t\t\t\"Test case %s failed: expected %d, got %d\", tc.name, tc.expectStatus, ctx.Response.StatusCode())\n\t\t})\n\t}\n}\n\nfunc Test_CSRF_Param_Extractor_Missing(t *testing.T) {\n\tt.Parallel()\n\n\t// Test the case where no param is provided (should get 403 from CSRF middleware on the catch-all route)\n\tapp := fiber.New()\n\n\t// Add a catch-all route with CSRF middleware for missing param case\n\tapp.Use(New(Config{Extractor: extractors.FromParam(\"csrf\")}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\t// Generate CSRF token\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/\")\n\th(ctx)\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\ttoken = strings.Split(strings.Split(token, \";\")[0], \"=\")[1]\n\n\t// Test missing param (accessing \"/\" instead of \"/:csrf\")\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetCookie(ConfigDefault.CookieName, token)\n\th(ctx)\n\trequire.Equal(t, 403, ctx.Response.StatusCode(), \"Missing param should return 403\")\n}\n\nfunc Test_CSRF_Extractors_ErrorTypes(t *testing.T) {\n\tt.Parallel()\n\n\t// Test all extractor error types\n\ttestCases := []struct {\n\t\texpected  error\n\t\tsetupCtx  func(ctx *fasthttp.RequestCtx) // Add setup function\n\t\tname      string\n\t\textractor extractors.Extractor\n\t}{\n\t\t{\n\t\t\tname:      \"Missing header\",\n\t\t\textractor: extractors.FromHeader(\"X-Missing-Header\"),\n\t\t\texpected:  extractors.ErrNotFound,\n\t\t\tsetupCtx:  func(_ *fasthttp.RequestCtx) {}, // No setup needed for headers\n\t\t},\n\t\t{\n\t\t\tname:      \"Missing query\",\n\t\t\textractor: extractors.FromQuery(\"missing_param\"),\n\t\t\texpected:  extractors.ErrNotFound,\n\t\t\tsetupCtx: func(ctx *fasthttp.RequestCtx) {\n\t\t\t\tctx.Request.SetRequestURI(\"/\") // Set URI for query parsing\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Missing param\",\n\t\t\textractor: extractors.FromParam(\"missing_param\"),\n\t\t\texpected:  extractors.ErrNotFound,\n\t\t\tsetupCtx:  func(_ *fasthttp.RequestCtx) {}, // Params are handled by router\n\t\t},\n\t\t{\n\t\t\tname:      \"Missing form\",\n\t\t\textractor: extractors.FromForm(\"missing_field\"),\n\t\t\texpected:  extractors.ErrNotFound,\n\t\t\tsetupCtx: func(ctx *fasthttp.RequestCtx) {\n\t\t\t\t// Properly initialize request for form parsing\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\tctx.Request.Header.SetContentType(fiber.MIMEApplicationForm)\n\t\t\t\tctx.Request.SetBodyString(\"\") // Empty form body\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\trequestCtx := &fasthttp.RequestCtx{}\n\t\t\ttc.setupCtx(requestCtx) // Set up the context properly\n\n\t\t\tctx := app.AcquireCtx(requestCtx)\n\t\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t\ttoken, err := tc.extractor.Extract(ctx)\n\t\t\trequire.Empty(t, token)\n\t\t\trequire.Equal(t, tc.expected, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf/helpers.go",
    "content": "package csrf\n\nimport (\n\t\"crypto/subtle\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n)\n\nconst (\n\tschemeHTTP  = \"http\"\n\tschemeHTTPS = \"https\"\n)\n\nfunc compareTokens(a, b []byte) bool {\n\treturn subtle.ConstantTimeCompare(a, b) == 1\n}\n\nfunc compareStrings(a, b string) bool {\n\treturn subtle.ConstantTimeCompare(utils.UnsafeBytes(a), utils.UnsafeBytes(b)) == 1\n}\n\nfunc schemeAndHostMatch(schemeA, hostA, schemeB, hostB string) bool {\n\tnormalizedSchemeA := utilsstrings.ToLower(schemeA)\n\tnormalizedSchemeB := utilsstrings.ToLower(schemeB)\n\n\tnormalizedHostA := normalizeSchemeHost(normalizedSchemeA, hostA)\n\tnormalizedHostB := normalizeSchemeHost(normalizedSchemeB, hostB)\n\n\treturn normalizedSchemeA == normalizedSchemeB && normalizedHostA == normalizedHostB\n}\n\nfunc normalizeSchemeHost(scheme, host string) string {\n\thost = utilsstrings.ToLower(host)\n\n\tdefaultPort := \"\"\n\tswitch scheme {\n\tcase schemeHTTP:\n\t\tdefaultPort = \"80\"\n\tcase schemeHTTPS:\n\t\tdefaultPort = \"443\"\n\tdefault:\n\t\treturn host\n\t}\n\n\tparsedHost, err := url.Parse(scheme + \"://\" + host)\n\tif err != nil {\n\t\treturn host\n\t}\n\n\tif port := parsedHost.Port(); port != \"\" {\n\t\treturn host\n\t}\n\n\thostname := parsedHost.Hostname()\n\tif hostname == \"\" {\n\t\treturn host\n\t}\n\n\tif strings.IndexByte(hostname, ':') >= 0 && !strings.HasPrefix(hostname, \"[\") {\n\t\thostname = \"[\" + hostname + \"]\"\n\t}\n\n\treturn hostname + \":\" + defaultPort\n}\n\n// normalizeOrigin checks if the provided origin is in a correct format\n// and normalizes it by removing any path or trailing slash.\n// It returns a boolean indicating whether the origin is valid\n// and the normalized origin.\nfunc normalizeOrigin(origin string) (valid bool, normalized string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming validity and normalized origin results\n\tparsedOrigin, err := url.Parse(origin)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\n\t// Validate the scheme is either http or https\n\tif parsedOrigin.Scheme != schemeHTTP && parsedOrigin.Scheme != schemeHTTPS {\n\t\treturn false, \"\"\n\t}\n\n\t// Don't allow a wildcard with a protocol\n\t// wildcards cannot be used within any other value. For example, the following header is not valid:\n\t// Access-Control-Allow-Origin: https://*\n\tif strings.IndexByte(parsedOrigin.Host, '*') >= 0 {\n\t\treturn false, \"\"\n\t}\n\n\t// Validate there is a host present. The presence of a path, query, or fragment components\n\t// is checked, but a trailing \"/\" (indicative of the root) is allowed for the path and will be normalized\n\tif parsedOrigin.Host == \"\" || (parsedOrigin.Path != \"\" && parsedOrigin.Path != \"/\") || parsedOrigin.RawQuery != \"\" || parsedOrigin.Fragment != \"\" {\n\t\treturn false, \"\"\n\t}\n\n\t// Normalize the origin by constructing it from the scheme and host.\n\t// The path or trailing slash is not included in the normalized origin.\n\treturn true, utilsstrings.ToLower(parsedOrigin.Scheme) + \"://\" + utilsstrings.ToLower(parsedOrigin.Host)\n}\n\ntype subdomain struct {\n\tprefix string\n\tsuffix string\n}\n\nfunc (s subdomain) match(o string) bool {\n\t// Not a subdomain if not long enough for a dot separator.\n\tif len(o) < len(s.prefix)+len(s.suffix)+1 {\n\t\treturn false\n\t}\n\n\tif !strings.HasPrefix(o, s.prefix) || !strings.HasSuffix(o, s.suffix) {\n\t\treturn false\n\t}\n\n\t// Check for the dot separator and validate that there is at least one\n\t// non-empty label between prefix and suffix. Empty labels like\n\t// \"https://.example.com\" or \"https://..example.com\" should not match.\n\tsuffixStartIndex := len(o) - len(s.suffix)\n\tif suffixStartIndex <= len(s.prefix) {\n\t\treturn false\n\t}\n\tif o[suffixStartIndex-1] != '.' {\n\t\treturn false\n\t}\n\n\t// Extract the subdomain part (without the trailing dot) and ensure it\n\t// doesn't contain empty labels.\n\tsub := o[len(s.prefix) : suffixStartIndex-1]\n\tif sub == \"\" || sub[0] == '.' || strings.Contains(sub, \"..\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "middleware/csrf/helpers_test.go",
    "content": "package csrf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// go test -run -v Test_normalizeOrigin\nfunc Test_normalizeOrigin(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\torigin         string\n\t\texpectedOrigin string\n\t\texpectedValid  bool\n\t}{\n\t\t{origin: \"http://example.com\", expectedValid: true, expectedOrigin: \"http://example.com\"},                       // Simple case should work.\n\t\t{origin: \"HTTP://EXAMPLE.COM\", expectedValid: true, expectedOrigin: \"http://example.com\"},                       // Case should be normalized.\n\t\t{origin: \"http://example.com/\", expectedValid: true, expectedOrigin: \"http://example.com\"},                      // Trailing slash should be removed.\n\t\t{origin: \"http://example.com:3000\", expectedValid: true, expectedOrigin: \"http://example.com:3000\"},             // Port should be preserved.\n\t\t{origin: \"http://example.com:3000/\", expectedValid: true, expectedOrigin: \"http://example.com:3000\"},            // Trailing slash should be removed.\n\t\t{origin: \"http://\", expectedValid: false, expectedOrigin: \"\"},                                                   // Invalid origin should not be accepted.\n\t\t{origin: \"file:///etc/passwd\", expectedValid: false, expectedOrigin: \"\"},                                        // File scheme should not be accepted.\n\t\t{origin: \"https://*example.com\", expectedValid: false, expectedOrigin: \"\"},                                      // Wildcard domain should not be accepted.\n\t\t{origin: \"http://*.example.com\", expectedValid: false, expectedOrigin: \"\"},                                      // Wildcard subdomain should not be accepted.\n\t\t{origin: \"http://example.com/path\", expectedValid: false, expectedOrigin: \"\"},                                   // Path should not be accepted.\n\t\t{origin: \"http://example.com?query=123\", expectedValid: false, expectedOrigin: \"\"},                              // Query should not be accepted.\n\t\t{origin: \"http://example.com#fragment\", expectedValid: false, expectedOrigin: \"\"},                               // Fragment should not be accepted.\n\t\t{origin: \"http://localhost\", expectedValid: true, expectedOrigin: \"http://localhost\"},                           // Localhost should be accepted.\n\t\t{origin: \"http://127.0.0.1\", expectedValid: true, expectedOrigin: \"http://127.0.0.1\"},                           // IPv4 address should be accepted.\n\t\t{origin: \"http://[::1]\", expectedValid: true, expectedOrigin: \"http://[::1]\"},                                   // IPv6 address should be accepted.\n\t\t{origin: \"http://[::1]:8080\", expectedValid: true, expectedOrigin: \"http://[::1]:8080\"},                         // IPv6 address with port should be accepted.\n\t\t{origin: \"http://[::1]:8080/\", expectedValid: true, expectedOrigin: \"http://[::1]:8080\"},                        // IPv6 address with port and trailing slash should be accepted.\n\t\t{origin: \"http://[::1]:8080/path\", expectedValid: false, expectedOrigin: \"\"},                                    // IPv6 address with port and path should not be accepted.\n\t\t{origin: \"http://[::1]:8080?query=123\", expectedValid: false, expectedOrigin: \"\"},                               // IPv6 address with port and query should not be accepted.\n\t\t{origin: \"http://[::1]:8080#fragment\", expectedValid: false, expectedOrigin: \"\"},                                // IPv6 address with port and fragment should not be accepted.\n\t\t{origin: \"http://[::1]:8080/path?query=123#fragment\", expectedValid: false, expectedOrigin: \"\"},                 // IPv6 address with port, path, query, and fragment should not be accepted.\n\t\t{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.\n\t\t{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.\n\t\t{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.\n\t\t{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.\n\t}\n\n\tfor _, tc := range testCases {\n\t\tvalid, normalizedOrigin := normalizeOrigin(tc.origin)\n\n\t\tif valid != tc.expectedValid {\n\t\t\tt.Errorf(\"Expected origin '%s' to be valid: %v, but got: %v\", tc.origin, tc.expectedValid, valid)\n\t\t}\n\n\t\tif normalizedOrigin != tc.expectedOrigin {\n\t\t\tt.Errorf(\"Expected normalized origin '%s' for origin '%s', but got: '%s'\", tc.expectedOrigin, tc.origin, normalizedOrigin)\n\t\t}\n\t}\n}\n\nfunc Test_normalizeSchemeHost(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tscheme       string\n\t\thost         string\n\t\texpectedHost string\n\t}{\n\t\t{\n\t\t\tname:         \"http default port added\",\n\t\t\tscheme:       \"http\",\n\t\t\thost:         \"example.com\",\n\t\t\texpectedHost: \"example.com:80\",\n\t\t},\n\t\t{\n\t\t\tname:         \"https default port added\",\n\t\t\tscheme:       \"https\",\n\t\t\thost:         \"example.com\",\n\t\t\texpectedHost: \"example.com:443\",\n\t\t},\n\t\t{\n\t\t\tname:         \"http custom port preserved\",\n\t\t\tscheme:       \"http\",\n\t\t\thost:         \"example.com:8080\",\n\t\t\texpectedHost: \"example.com:8080\",\n\t\t},\n\t\t{\n\t\t\tname:         \"https ipv6 default port added\",\n\t\t\tscheme:       \"https\",\n\t\t\thost:         \"[::1]\",\n\t\t\texpectedHost: \"[::1]:443\",\n\t\t},\n\t\t{\n\t\t\tname:         \"unknown scheme preserved\",\n\t\t\tscheme:       \"ftp\",\n\t\t\thost:         \"example.com\",\n\t\t\texpectedHost: \"example.com\",\n\t\t},\n\t\t{\n\t\t\tname:         \"https ipv6 custom port preserved\",\n\t\t\tscheme:       \"https\",\n\t\t\thost:         \"[::1]:8080\",\n\t\t\texpectedHost: \"[::1]:8080\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expectedHost, normalizeSchemeHost(tc.scheme, tc.host))\n\t\t})\n\t}\n}\n\n// go test -run -v TestSubdomainMatch\nfunc TestSubdomainMatch(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tsub      subdomain\n\t\torigin   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"match with different scheme\",\n\t\t\tsub:      subdomain{prefix: \"http://api.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with different scheme\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"http://api.service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with valid subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.service.example.com\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"match with valid nested subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://1.2.api.service.example.com\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t{\n\t\t\tname:     \"no match with invalid prefix\",\n\t\t\tsub:      subdomain{prefix: \"https://abc.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://service.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with invalid suffix\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.example.org\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with empty origin\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with malformed subdomain\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://evil.comexample.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"partial match not considered a match\",\n\t\t\tsub:      subdomain{prefix: \"https://service.\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://api.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with empty host label\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://.example.com\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"no match with malformed host label\",\n\t\t\tsub:      subdomain{prefix: \"https://\", suffix: \"example.com\"},\n\t\t\torigin:   \"https://..example.com\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := tt.sub.match(tt.origin)\n\t\t\tassert.Equal(t, tt.expected, got, \"subdomain.match()\")\n\t\t})\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_CSRF_SubdomainMatch -benchmem -count=4\nfunc Benchmark_CSRF_SubdomainMatch(b *testing.B) {\n\ts := subdomain{\n\t\tprefix: \"www\",\n\t\tsuffix: \"example.com\",\n\t}\n\n\to := \"www.example.com\"\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\ts.match(o)\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf/session_manager.go",
    "content": "package csrf\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/fiber/v3/middleware/session\"\n)\n\ntype sessionManager struct {\n\tsession *session.Store\n}\n\ntype sessionKeyType int\n\nconst (\n\tsessionKey sessionKeyType = 0\n)\n\nfunc newSessionManager(s *session.Store) *sessionManager {\n\t// Create new storage handler\n\tsessionManager := new(sessionManager)\n\tif s != nil {\n\t\t// Use provided storage if provided\n\t\tsessionManager.session = s\n\n\t\t// Register the sessionKeyType and Token type\n\t\ts.RegisterType(sessionKeyType(0))\n\t\ts.RegisterType(Token{})\n\t}\n\treturn sessionManager\n}\n\n// get token from session\nfunc (m *sessionManager) getRaw(c fiber.Ctx, key string, raw []byte) []byte {\n\tsess := session.FromContext(c)\n\tvar token Token\n\tvar ok bool\n\n\tif sess != nil {\n\t\ttoken, ok = sess.Get(sessionKey).(Token)\n\t} else {\n\t\t// Try to get the session from the store\n\t\tstoreSess, err := m.session.Get(c)\n\t\tif err != nil {\n\t\t\t// Handle error\n\t\t\treturn nil\n\t\t}\n\t\ttoken, ok = storeSess.Get(sessionKey).(Token)\n\t}\n\n\tif ok {\n\t\tif token.Expiration.Before(time.Now()) || key != token.Key || !compareTokens(raw, token.Raw) {\n\t\t\treturn nil\n\t\t}\n\t\treturn token.Raw\n\t}\n\n\treturn nil\n}\n\n// set token in session\nfunc (m *sessionManager) setRaw(c fiber.Ctx, key string, raw []byte, exp time.Duration) {\n\tsess := session.FromContext(c)\n\tif sess != nil {\n\t\t// 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\n\t\tsess.Set(sessionKey, Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)})\n\t} else {\n\t\t// Try to get the session from the store\n\t\tstoreSess, err := m.session.Get(c)\n\t\tif err != nil {\n\t\t\t// Handle error\n\t\t\treturn\n\t\t}\n\t\tstoreSess.Set(sessionKey, Token{Key: key, Raw: raw, Expiration: time.Now().Add(exp)})\n\t\tif err := storeSess.Save(); err != nil {\n\t\t\tlog.Warn(\"csrf: failed to save session: \", err)\n\t\t}\n\t}\n}\n\n// delete token from session\nfunc (m *sessionManager) delRaw(c fiber.Ctx) {\n\tsess := session.FromContext(c)\n\tif sess != nil {\n\t\tsess.Delete(sessionKey)\n\t} else {\n\t\t// Try to get the session from the store\n\t\tstoreSess, err := m.session.Get(c)\n\t\tif err != nil {\n\t\t\t// Handle error\n\t\t\treturn\n\t\t}\n\t\tstoreSess.Delete(sessionKey)\n\t\tif err := storeSess.Save(); err != nil {\n\t\t\tlog.Warn(\"csrf: failed to save session: \", err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf/storage_manager.go",
    "content": "package csrf\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/memory\"\n)\n\n// msgp -file=\"storage_manager.go\" -o=\"storage_manager_msgp.go\" -tests=true -unexported\n//\n//go:generate msgp -o=storage_manager_msgp.go -tests=true -unexported\ntype item struct{}\n\nconst redactedKey = \"[redacted]\"\n\n//msgp:ignore manager\n//msgp:ignore storageManager\ntype storageManager struct {\n\tpool       sync.Pool       `msg:\"-\"` //nolint:revive // Ignore unexported type\n\tmemory     *memory.Storage `msg:\"-\"` //nolint:revive // Ignore unexported type\n\tstorage    fiber.Storage   `msg:\"-\"` //nolint:revive // Ignore unexported type\n\tredactKeys bool\n}\n\nfunc newStorageManager(storage fiber.Storage, redactKeys bool) *storageManager {\n\t// Create new storage handler\n\tstorageManager := &storageManager{\n\t\tpool: sync.Pool{\n\t\t\tNew: func() any {\n\t\t\t\treturn new(item)\n\t\t\t},\n\t\t},\n\t\tredactKeys: redactKeys,\n\t}\n\tif storage != nil {\n\t\t// Use provided storage if provided\n\t\tstorageManager.storage = storage\n\t} else {\n\t\t// Fallback to memory storage\n\t\tstorageManager.memory = memory.New()\n\t}\n\treturn storageManager\n}\n\n// get raw data from storage or memory\nfunc (m *storageManager) getRaw(ctx context.Context, key string) ([]byte, error) {\n\tif m.storage != nil {\n\t\traw, err := m.storage.GetWithContext(ctx, key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"csrf: failed to get value from storage: %w\", err)\n\t\t}\n\t\treturn raw, nil\n\t}\n\n\tif value := m.memory.Get(key); value != nil {\n\t\traw, ok := value.([]byte)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"csrf: unexpected value type %T in storage\", value)\n\t\t}\n\t\treturn raw, nil\n\t}\n\n\treturn nil, nil\n}\n\n// set data to storage or memory\nfunc (m *storageManager) setRaw(ctx context.Context, key string, raw []byte, exp time.Duration) error {\n\tif m.storage != nil {\n\t\tif err := m.storage.SetWithContext(ctx, key, raw, exp); err != nil {\n\t\t\treturn fmt.Errorf(\"csrf: failed to store key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tm.memory.Set(key, raw, exp)\n\treturn nil\n}\n\n// delete data from storage or memory\nfunc (m *storageManager) delRaw(ctx context.Context, key string) error {\n\tif m.storage != nil {\n\t\tif err := m.storage.DeleteWithContext(ctx, key); err != nil {\n\t\t\treturn fmt.Errorf(\"csrf: failed to delete key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tm.memory.Delete(key)\n\treturn nil\n}\n\nfunc (m *storageManager) logKey(key string) string {\n\tif m.redactKeys {\n\t\treturn redactedKey\n\t}\n\treturn key\n}\n"
  },
  {
    "path": "middleware/csrf/storage_manager_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage csrf\n\nimport (\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *item) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z item) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 0\n\t_ = z\n\terr = en.Append(0x80)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z item) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 0\n\t_ = z\n\to = append(o, 0x80)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z item) Msgsize() (s int) {\n\ts = 1\n\treturn\n}\n"
  },
  {
    "path": "middleware/csrf/storage_manager_msgp_test.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage csrf\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\nfunc TestMarshalUnmarshalitem(t *testing.T) {\n\tv := item{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgitem(b *testing.B) {\n\tv := item{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgitem(b *testing.B) {\n\tv := item{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalitem(b *testing.B) {\n\tv := item{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecodeitem(t *testing.T) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecodeitem Msgsize() is inaccurate\")\n\t}\n\n\tvn := item{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/csrf/token.go",
    "content": "package csrf\n\nimport (\n\t\"time\"\n)\n\n// Token represents a CSRF token with expiration metadata.\n// This is used internally for token storage and validation.\ntype Token struct {\n\tExpiration time.Time `json:\"expiration\"`\n\tKey        string    `json:\"key\"`\n\tRaw        []byte    `json:\"raw\"`\n}\n"
  },
  {
    "path": "middleware/earlydata/config.go",
    "content": "package earlydata\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst (\n\tDefaultHeaderName      = \"Early-Data\"\n\tDefaultHeaderTrueValue = \"1\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// IsEarlyData returns whether the request is an early-data request.\n\t//\n\t// Optional. Default: a function which checks if the \"Early-Data\" request header equals \"1\".\n\tIsEarlyData func(c fiber.Ctx) bool\n\n\t// AllowEarlyData returns whether the early-data request should be allowed or rejected.\n\t//\n\t// Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods.\n\tAllowEarlyData func(c fiber.Ctx) bool\n\n\t// Error is returned if an early-data request is rejected.\n\t//\n\t// Optional. Default: fiber.ErrTooEarly.\n\tError error\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tIsEarlyData: func(c fiber.Ctx) bool {\n\t\treturn c.Get(DefaultHeaderName) == DefaultHeaderTrueValue\n\t},\n\n\tAllowEarlyData: func(c fiber.Ctx) bool {\n\t\treturn fiber.IsMethodSafe(c.Method())\n\t},\n\n\tError: fiber.ErrTooEarly,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\n\tif cfg.IsEarlyData == nil {\n\t\tcfg.IsEarlyData = ConfigDefault.IsEarlyData\n\t}\n\n\tif cfg.AllowEarlyData == nil {\n\t\tcfg.AllowEarlyData = ConfigDefault.AllowEarlyData\n\t}\n\n\tif cfg.Error == nil {\n\t\tcfg.Error = ConfigDefault.Error\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/earlydata/earlydata.go",
    "content": "package earlydata\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\nconst (\n\tlocalsKeyAllowed contextKey = 0 // earlydata_allowed\n)\n\n// IsEarly returns true if the request used early data and was accepted by the middleware.\nfunc IsEarly(c fiber.Ctx) bool {\n\treturn c.Locals(localsKeyAllowed) != nil\n}\n\n// New creates a new middleware handler\n// https://datatracker.ietf.org/doc/html/rfc8470#section-5.1\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Continue stack if request is not an early-data request\n\t\tif !cfg.IsEarlyData(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Abort if we can't trust the early-data header\n\t\tif !c.IsProxyTrusted() {\n\t\t\treturn cfg.Error\n\t\t}\n\n\t\t// Continue stack if we allow early-data for this request\n\t\tif cfg.AllowEarlyData(c) {\n\t\t\t_ = c.Locals(localsKeyAllowed, true)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Else return our error\n\t\treturn cfg.Error\n\t}\n}\n"
  },
  {
    "path": "middleware/earlydata/earlydata_test.go",
    "content": "package earlydata\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nconst (\n\theaderName   = \"Early-Data\"\n\theaderValOn  = \"1\"\n\theaderValOff = \"0\"\n)\n\nconst (\n\ttrustedRemoteAddr   = \"0.0.0.0:1234\"\n\tuntrustedRemoteAddr = \"203.0.113.1:1234\"\n)\n\nfunc appWithConfig(t *testing.T, c *fiber.Config) *fiber.App {\n\tt.Helper()\n\n\tvar app *fiber.App\n\tif c == nil {\n\t\tapp = fiber.New()\n\t} else {\n\t\tapp = fiber.New(*c)\n\t}\n\n\tapp.Use(New())\n\n\t// Middleware to test IsEarly func\n\tconst localsKeyTestValid = \"earlydata_testvalid\"\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tisEarly := IsEarly(c)\n\n\t\tswitch h := c.Get(headerName); h {\n\t\tcase \"\", headerValOff:\n\t\t\tif isEarly {\n\t\t\t\treturn errors.New(\"is early-data even though it's not\")\n\t\t\t}\n\n\t\tcase headerValOn:\n\t\t\tswitch {\n\t\t\tcase fiber.IsMethodSafe(c.Method()):\n\t\t\t\tif !isEarly {\n\t\t\t\t\treturn errors.New(\"should be early-data on safe HTTP methods\")\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif isEarly {\n\t\t\t\t\treturn errors.New(\"early-data unsupported on unsafe HTTP methods\")\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"header has unsupported value: %s\", h)\n\t\t}\n\n\t\t_ = c.Locals(localsKeyTestValid, true)\n\n\t\treturn c.Next()\n\t})\n\n\tapp.Add([]string{\n\t\tfiber.MethodGet,\n\t\tfiber.MethodPost,\n\t}, \"/\", func(c fiber.Ctx) error {\n\t\tvalid, ok := c.Locals(localsKeyTestValid).(bool)\n\t\tif !ok {\n\t\t\tpanic(errors.New(\"failed to type-assert to bool\"))\n\t\t}\n\t\tif !valid {\n\t\t\treturn errors.New(\"handler called even though validation failed\")\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn app\n}\n\ntype requestExpectation struct {\n\tmethod string\n\theader string\n\tstatus int\n}\n\nfunc executeExpectations(t *testing.T, app *fiber.App, remoteAddr string, expectations []requestExpectation) {\n\tt.Helper()\n\n\tfor _, expectation := range expectations {\n\t\treq := httptest.NewRequest(expectation.method, \"/\", http.NoBody)\n\t\treq.RemoteAddr = remoteAddr\n\t\tif expectation.header != \"\" {\n\t\t\treq.Header.Set(headerName, expectation.header)\n\t\t}\n\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, expectation.status, resp.StatusCode)\n\t}\n}\n\n// go test -run Test_EarlyData\nfunc Test_EarlyData(t *testing.T) {\n\tt.Parallel()\n\n\tuntrustedExpectations := []requestExpectation{\n\t\t{method: fiber.MethodGet, status: fiber.StatusOK},\n\t\t{method: fiber.MethodGet, header: headerValOff, status: fiber.StatusOK},\n\t\t{method: fiber.MethodGet, header: headerValOn, status: fiber.StatusTooEarly},\n\t\t{method: fiber.MethodPost, status: fiber.StatusOK},\n\t\t{method: fiber.MethodPost, header: headerValOff, status: fiber.StatusOK},\n\t\t{method: fiber.MethodPost, header: headerValOn, status: fiber.StatusTooEarly},\n\t}\n\n\ttrustedExpectations := []requestExpectation{\n\t\t{method: fiber.MethodGet, status: fiber.StatusOK},\n\t\t{method: fiber.MethodGet, header: headerValOff, status: fiber.StatusOK},\n\t\t{method: fiber.MethodGet, header: headerValOn, status: fiber.StatusOK},\n\t\t{method: fiber.MethodPost, status: fiber.StatusOK},\n\t\t{method: fiber.MethodPost, header: headerValOff, status: fiber.StatusOK},\n\t\t{method: fiber.MethodPost, header: headerValOn, status: fiber.StatusTooEarly},\n\t}\n\n\tt.Run(\"empty config\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := appWithConfig(t, nil)\n\t\texecuteExpectations(t, app, untrustedRemoteAddr, untrustedExpectations)\n\t})\n\tt.Run(\"default config\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := appWithConfig(t, &fiber.Config{})\n\t\texecuteExpectations(t, app, untrustedRemoteAddr, untrustedExpectations)\n\t})\n\n\tt.Run(\"config with TrustProxy and untrusted remote\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := appWithConfig(t, &fiber.Config{\n\t\t\tTrustProxy: true,\n\t\t})\n\t\texecuteExpectations(t, app, untrustedRemoteAddr, untrustedExpectations)\n\t})\n\tt.Run(\"config with TrustProxy and trusted TrustProxyConfig.Proxies\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := appWithConfig(t, &fiber.Config{\n\t\t\tTrustProxy: true,\n\t\t\tTrustProxyConfig: fiber.TrustProxyConfig{\n\t\t\t\tProxies: []string{\n\t\t\t\t\t\"0.0.0.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\texecuteExpectations(t, app, trustedRemoteAddr, trustedExpectations)\n\t})\n}\n\n// Test_EarlyDataNext verifies that the middleware skips its logic when Next returns true.\nfunc Test_EarlyDataNext(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(fiber.Ctx) bool { return true },\n\t}))\n\n\tcalled := false\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tcalled = true\n\t\tif IsEarly(c) {\n\t\t\treturn errors.New(\"IsEarly(c) should be false when Next returns true\")\n\t\t}\n\t\treturn nil\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Set(headerName, headerValOn)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.True(t, called)\n}\n\n// Test_configDefault_NoConfig verifies that calling configDefault without\n// providing a configuration returns ConfigDefault as-is.\nfunc Test_configDefault_NoConfig(t *testing.T) {\n\tt.Parallel()\n\tcfg := configDefault()\n\trequire.Equal(t, ConfigDefault.Error, cfg.Error)\n\trequire.Equal(t, reflect.ValueOf(ConfigDefault.IsEarlyData).Pointer(), reflect.ValueOf(cfg.IsEarlyData).Pointer())\n\trequire.Equal(t, reflect.ValueOf(ConfigDefault.AllowEarlyData).Pointer(), reflect.ValueOf(cfg.AllowEarlyData).Pointer())\n}\n\n// Test_configDefault_WithConfig verifies that provided configuration fields are\n// kept while missing fields are populated with defaults.\nfunc Test_configDefault_WithConfig(t *testing.T) {\n\tt.Parallel()\n\texpectedErr := errors.New(\"boom\")\n\tcalled := false\n\tcustom := Config{\n\t\tNext:  func(_ fiber.Ctx) bool { called = true; return false },\n\t\tError: expectedErr,\n\t}\n\n\tcfg := configDefault(custom)\n\n\t// Next should be preserved and not invoked by configDefault.\n\trequire.False(t, called)\n\trequire.Equal(t, reflect.ValueOf(custom.Next).Pointer(), reflect.ValueOf(cfg.Next).Pointer())\n\t// Custom error must be preserved.\n\trequire.Equal(t, expectedErr, cfg.Error)\n\t// Missing fields should be set to defaults.\n\trequire.NotNil(t, cfg.IsEarlyData)\n\trequire.NotNil(t, cfg.AllowEarlyData)\n\n\t// Verify default functions behave as expected.\n\tapp := fiber.New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tc.Request().Header.Set(DefaultHeaderName, DefaultHeaderTrueValue)\n\tc.Request().Header.SetMethod(fiber.MethodGet)\n\trequire.True(t, cfg.IsEarlyData(c))\n\trequire.True(t, cfg.AllowEarlyData(c))\n\tapp.ReleaseCtx(c)\n}\n"
  },
  {
    "path": "middleware/encryptcookie/config.go",
    "content": "package encryptcookie\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Custom function to encrypt cookies.\n\t//\n\t// Optional. Default: EncryptCookie (using AES-GCM)\n\tEncryptor func(name, decryptedString, key string) (string, error)\n\n\t// Custom function to decrypt cookies.\n\t//\n\t// Optional. Default: DecryptCookie (using AES-GCM)\n\tDecryptor func(name, encryptedString, key string) (string, error)\n\n\t// Base64 encoded unique key to encode & decode cookies.\n\t//\n\t// Required. Key length should be 16, 24, or 32 bytes when decoded\n\t// if using the default EncryptCookie and DecryptCookie functions.\n\t// You may use `encryptcookie.GenerateKey(length)` to generate a new key.\n\tKey string\n\n\t// Array of cookie keys that should not be encrypted.\n\t//\n\t// Optional. Default: []\n\tExcept []string\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:      nil,\n\tExcept:    []string{},\n\tKey:       \"\",\n\tEncryptor: EncryptCookie,\n\tDecryptor: DecryptCookie,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Set default config\n\tcfg := ConfigDefault\n\n\t// Override config if provided\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\n\t\t// Set default values\n\t\tif cfg.Next == nil {\n\t\t\tcfg.Next = ConfigDefault.Next\n\t\t}\n\n\t\tif cfg.Except == nil {\n\t\t\tcfg.Except = ConfigDefault.Except\n\t\t}\n\n\t\tif cfg.Encryptor == nil {\n\t\t\tcfg.Encryptor = ConfigDefault.Encryptor\n\t\t}\n\n\t\tif cfg.Decryptor == nil {\n\t\t\tcfg.Decryptor = ConfigDefault.Decryptor\n\t\t}\n\t}\n\n\tif cfg.Key == \"\" {\n\t\tpanic(\"fiber: encrypt cookie middleware requires key\")\n\t}\n\n\tif err := validateKey(cfg.Key); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/encryptcookie/config_test.go",
    "content": "package encryptcookie\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_configDefault_KeyValidation(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"invalid base64\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t_, decErr := base64.StdEncoding.DecodeString(\"invalid\")\n\t\texpectedErr := fmt.Errorf(\"failed to base64-decode key: %w\", decErr).Error()\n\t\trequire.PanicsWithError(t, expectedErr, func() {\n\t\t\tconfigDefault(Config{Key: \"invalid\"})\n\t\t})\n\t})\n\n\tt.Run(\"invalid length\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tkey := make([]byte, 20)\n\t\t_, err := rand.Read(key)\n\t\trequire.NoError(t, err)\n\t\tinvalidKey := base64.StdEncoding.EncodeToString(key)\n\t\trequire.PanicsWithValue(t, ErrInvalidKeyLength, func() {\n\t\t\tconfigDefault(Config{Key: invalidKey})\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/encryptcookie/encryptcookie.go",
    "content": "package encryptcookie\n\nimport (\n\t\"errors\"\n\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Decrypt request cookies\n\t\tcookiesToDelete := make([][]byte, 0, 4)\n\n\t\tfor key, value := range c.Request().Header.Cookies() {\n\t\t\tkeyString := string(key)\n\t\t\tif !isDisabled(keyString, cfg.Except) {\n\t\t\t\tdecryptedValue, err := cfg.Decryptor(keyString, string(value), cfg.Key)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcookiesToDelete = append(cookiesToDelete, key)\n\t\t\t\t} else {\n\t\t\t\t\tc.Request().Header.SetCookie(keyString, decryptedValue)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Delete cookies that failed to decrypt - outside the loop to avoid mutation during iteration\n\t\tfor _, key := range cookiesToDelete {\n\t\t\tc.Request().Header.DelCookieBytes(key)\n\t\t}\n\n\t\t// Continue stack\n\t\terr := c.Next()\n\n\t\t// Encrypt response cookies\n\t\tfor key := range c.Response().Header.Cookies() {\n\t\t\tkeyString := string(key)\n\t\t\tif !isDisabled(keyString, cfg.Except) {\n\t\t\t\tcookieValue := fasthttp.Cookie{}\n\t\t\t\tcookieValue.SetKeyBytes(key)\n\t\t\t\tif c.Response().Header.Cookie(&cookieValue) {\n\t\t\t\t\tencryptedValue, encErr := cfg.Encryptor(keyString, string(cookieValue.Value()), cfg.Key)\n\t\t\t\t\tif encErr != nil {\n\t\t\t\t\t\treturn errors.Join(err, encErr)\n\t\t\t\t\t}\n\n\t\t\t\t\tcookieValue.SetValue(encryptedValue)\n\t\t\t\t\tc.Response().Header.SetCookie(&cookieValue)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "middleware/encryptcookie/encryptcookie_test.go",
    "content": "package encryptcookie\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nfunc Test_Middleware_Panics(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Empty Key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\trequire.Panics(t, func() {\n\t\t\tapp.Use(New(Config{\n\t\t\t\tKey: \"\",\n\t\t\t}))\n\t\t})\n\t})\n\n\tt.Run(\"Invalid Key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trequire.Panics(t, func() {\n\t\t\tGenerateKey(11)\n\t\t})\n\t})\n}\n\nfunc Test_Middleware_InvalidKeys(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tlength int\n\t}{\n\t\t{length: 11},\n\t\t{length: 25},\n\t\t{length: 60},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(strconv.Itoa(tt.length)+\"_length_encrypt\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tkey := make([]byte, tt.length)\n\t\t\t_, err := rand.Read(key)\n\t\t\trequire.NoError(t, err)\n\t\t\tkeyString := base64.StdEncoding.EncodeToString(key)\n\n\t\t\t_, err = EncryptCookie(\"test\", \"SomeThing\", keyString)\n\t\t\trequire.Error(t, err)\n\t\t})\n\n\t\tt.Run(strconv.Itoa(tt.length)+\"_length_decrypt\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tkey := make([]byte, tt.length)\n\t\t\t_, err := rand.Read(key)\n\t\t\trequire.NoError(t, err)\n\t\t\tkeyString := base64.StdEncoding.EncodeToString(key)\n\n\t\t\t_, err = DecryptCookie(\"test\", \"SomeThing\", keyString)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_Middleware_InvalidBase64(t *testing.T) {\n\tt.Parallel()\n\tinvalidBase64 := \"invalid-base64-string-!@#\"\n\n\tt.Run(\"encryptor\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t_, err := EncryptCookie(\"test\", \"SomeText\", invalidBase64)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, err, \"failed to base64-decode key\")\n\t})\n\n\tt.Run(\"decryptor_key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t_, err := DecryptCookie(\"test\", \"SomeText\", invalidBase64)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, err, \"failed to base64-decode key\")\n\t})\n\n\tt.Run(\"decryptor_value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t_, err := DecryptCookie(\"test\", invalidBase64, GenerateKey(32))\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, err, \"failed to base64-decode value\")\n\t})\n}\n\nfunc Test_DecryptCookie_InvalidEncryptedValue(t *testing.T) {\n\tt.Parallel()\n\n\tkey := GenerateKey(32)\n\t// the decoded value is shorter than the GCM nonce size, so decryption should fail immediately\n\tshortValue := base64.StdEncoding.EncodeToString([]byte(\"short\"))\n\n\t_, err := DecryptCookie(\"session\", shortValue, key)\n\trequire.ErrorIs(t, err, ErrInvalidEncryptedValue)\n}\n\nfunc Test_Middleware_EncryptionErrorPropagates(t *testing.T) {\n\tt.Parallel()\n\n\ttestKey := GenerateKey(32)\n\texpected := errors.New(\"encrypt failed\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"encryption error\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tEncryptor: func(name, value, _ string) (string, error) {\n\t\t\tif name == \"test\" {\n\t\t\t\treturn \"\", expected\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"value\",\n\t\t})\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.ErrorIs(t, captured, expected)\n}\n\nfunc Test_Middleware_EncryptionErrorDoesNotMaskNextError(t *testing.T) {\n\tt.Parallel()\n\n\ttestKey := GenerateKey(32)\n\tencryptErr := errors.New(\"encrypt failed\")\n\tdownstreamErr := errors.New(\"downstream failed\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"combined error\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tEncryptor: func(name, value, _ string) (string, error) {\n\t\t\tif name == \"test\" {\n\t\t\t\treturn \"\", encryptErr\n\t\t\t}\n\t\t\treturn value, nil\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"value\",\n\t\t})\n\t\treturn downstreamErr\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\trequire.ErrorIs(t, captured, downstreamErr)\n\trequire.ErrorIs(t, captured, encryptErr)\n}\n\nfunc Test_Middleware_Encrypt_Cookie(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\t// Test empty cookie\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=\", string(ctx.Response.Body()))\n\n\t// Test invalid cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"test\", \"Invalid\")\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=\", string(ctx.Response.Body()))\n\tctx.Request.Header.SetCookie(\"test\", \"ixQURE2XOyZUs0WAOh2ehjWcP7oZb07JvnhWOsmeNUhPsj4+RyI=\")\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=\", string(ctx.Response.Body()))\n\n\t// Test valid cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\tencryptedCookie := fasthttp.Cookie{}\n\tencryptedCookie.SetKey(\"test\")\n\trequire.True(t, ctx.Response.Header.Cookie(&encryptedCookie), \"Get cookie value\")\n\tdecryptedCookieValue, err := DecryptCookie(\"test\", string(encryptedCookie.Value()), testKey)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"SomeThing\", decryptedCookieValue)\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"test\", string(encryptedCookie.Value()))\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=SomeThing\", string(ctx.Response.Body()))\n}\n\nfunc Test_EncryptCookie_Rejects_Swapped_Names(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\n\tencryptedValue, err := EncryptCookie(\"cookieA\", \"ValueA\", testKey)\n\trequire.NoError(t, err)\n\n\tdecryptedValue, err := DecryptCookie(\"cookieA\", encryptedValue, testKey)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"ValueA\", decryptedValue)\n\n\t_, err = DecryptCookie(\"cookieB\", encryptedValue, testKey)\n\trequire.Error(t, err)\n\trequire.ErrorContains(t, err, \"failed to decrypt ciphertext\")\n}\n\nfunc Test_Encrypt_Cookie_Next(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"SomeThing\", resp.Cookies()[0].Value)\n}\n\nfunc Test_Encrypt_Cookie_Except(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tExcept: []string{\n\t\t\t\"test1\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test1\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test2\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\trawCookie := fasthttp.Cookie{}\n\trawCookie.SetKey(\"test1\")\n\trequire.True(t, ctx.Response.Header.Cookie(&rawCookie), \"Get cookie value\")\n\trequire.Equal(t, \"SomeThing\", string(rawCookie.Value()))\n\n\tencryptedCookie := fasthttp.Cookie{}\n\tencryptedCookie.SetKey(\"test2\")\n\trequire.True(t, ctx.Response.Header.Cookie(&encryptedCookie), \"Get cookie value\")\n\tdecryptedCookieValue, err := DecryptCookie(\"test2\", string(encryptedCookie.Value()), testKey)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"SomeThing\", decryptedCookieValue)\n}\n\nfunc Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tEncryptor: func(_, decryptedString, _ string) (string, error) {\n\t\t\treturn base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil\n\t\t},\n\t\tDecryptor: func(_, encryptedString, _ string) (string, error) {\n\t\t\tdecodedBytes, err := base64.StdEncoding.DecodeString(encryptedString)\n\t\t\treturn string(decodedBytes), err\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\n\tencryptedCookie := fasthttp.Cookie{}\n\tencryptedCookie.SetKey(\"test\")\n\trequire.True(t, ctx.Response.Header.Cookie(&encryptedCookie), \"Get cookie value\")\n\tdecodedBytes, err := base64.StdEncoding.DecodeString(string(encryptedCookie.Value()))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"SomeThing\", string(decodedBytes))\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"test\", string(encryptedCookie.Value()))\n\th(ctx)\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=SomeThing\", string(ctx.Response.Body()))\n}\n\nfunc Test_GenerateKey(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tlength int\n\t}{\n\t\t{length: 16},\n\t\t{length: 24},\n\t\t{length: 32},\n\t}\n\n\tdecodeBase64 := func(t *testing.T, s string) []byte {\n\t\tt.Helper()\n\t\tdata, err := base64.StdEncoding.DecodeString(s)\n\t\trequire.NoError(t, err)\n\t\treturn data\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(strconv.Itoa(tt.length)+\"_length\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tkey := GenerateKey(tt.length)\n\t\t\tdecodedKey := decodeBase64(t, key)\n\t\t\trequire.Len(t, decodedKey, tt.length)\n\t\t})\n\t}\n\n\tt.Run(\"Invalid Length\", func(t *testing.T) {\n\t\trequire.Panics(t, func() { GenerateKey(10) })\n\t\trequire.Panics(t, func() { GenerateKey(20) })\n\t})\n}\n\nfunc Benchmark_Middleware_Encrypt_Cookie(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"Empty Cookie\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\th(ctx)\n\t\t}\n\t})\n\n\tb.Run(\"Invalid Cookie\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.Header.SetCookie(\"test\", \"Invalid\")\n\t\t\th(ctx)\n\t\t}\n\t})\n\n\tb.Run(\"Valid Cookie\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Next(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"Encrypt Cookie Next\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Except(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tExcept: []string{\n\t\t\t\"test1\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test1\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test2\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"Encrypt Cookie Except\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Custom_Encryptor(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tEncryptor: func(_, decryptedString, _ string) (string, error) {\n\t\t\treturn base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil\n\t\t},\n\t\tDecryptor: func(_, encryptedString, _ string) (string, error) {\n\t\t\tdecodedBytes, err := base64.StdEncoding.DecodeString(encryptedString)\n\t\t\treturn string(decodedBytes), err\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"Custom Encryptor Post\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\th(ctx)\n\t\t}\n\t})\n\n\tb.Run(\"Custom Encryptor Get\", func(b *testing.B) {\n\t\tctx := &fasthttp.RequestCtx{}\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\th(ctx)\n\t\tencryptedCookie := fasthttp.Cookie{}\n\t\tencryptedCookie.SetKey(\"test\")\n\t\trequire.True(b, ctx.Response.Header.Cookie(&encryptedCookie), \"Get cookie value\")\n\n\t\tfor b.Loop() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.Header.SetCookie(\"test\", string(encryptedCookie.Value()))\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Middleware_Encrypt_Cookie_Parallel(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"Empty Cookie Parallel\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\t\th(ctx)\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"Invalid Cookie Parallel\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\t\tctx.Request.Header.SetCookie(\"test\", \"Invalid\")\n\t\t\t\th(ctx)\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"Valid Cookie Parallel\", func(b *testing.B) {\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\t\t\th(ctx)\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Next_Parallel(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.SetRequestURI(\"/\")\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Except_Parallel(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tExcept: []string{\n\t\t\t\"test1\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test1\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test2\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Encrypt_Cookie_Custom_Encryptor_Parallel(b *testing.B) {\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t\tEncryptor: func(_, decryptedString, _ string) (string, error) {\n\t\t\treturn base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil\n\t\t},\n\t\tDecryptor: func(_, encryptedString, _ string) (string, error) {\n\t\t\tdecodedBytes, err := base64.StdEncoding.DecodeString(encryptedString)\n\t\t\treturn string(decodedBytes), err\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"value=\" + c.Cookies(\"test\"))\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"test\",\n\t\t\tValue: \"SomeThing\",\n\t\t})\n\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tctx := &fasthttp.RequestCtx{}\n\t\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\t\th(ctx)\n\t\tencryptedCookie := fasthttp.Cookie{}\n\t\tencryptedCookie.SetKey(\"test\")\n\t\trequire.True(b, ctx.Response.Header.Cookie(&encryptedCookie), \"Get cookie value\")\n\n\t\tb.ResetTimer()\n\t\tfor pb.Next() {\n\t\t\tctx := &fasthttp.RequestCtx{}\n\t\t\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\t\tctx.Request.Header.SetCookie(\"test\", string(encryptedCookie.Value()))\n\t\t\th(ctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_GenerateKey(b *testing.B) {\n\ttests := []struct {\n\t\tlength int\n\t}{\n\t\t{length: 16},\n\t\t{length: 24},\n\t\t{length: 32},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(strconv.Itoa(tt.length), func(b *testing.B) {\n\t\t\tfor b.Loop() {\n\t\t\t\tGenerateKey(tt.length)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Benchmark_GenerateKey_Parallel(b *testing.B) {\n\ttests := []struct {\n\t\tlength int\n\t}{\n\t\t{length: 16},\n\t\t{length: 24},\n\t\t{length: 32},\n\t}\n\n\tfor _, tt := range tests {\n\t\tb.Run(strconv.Itoa(tt.length), func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\t\t\tb.ResetTimer()\n\t\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t\tfor pb.Next() {\n\t\t\t\t\tGenerateKey(tt.length)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// Test_Middleware_Mixed_Valid_Invalid_Cookies tests that the middleware correctly handles\n// a mix of valid and invalid cookies during iteration\nfunc Test_Middleware_Mixed_Valid_Invalid_Cookies(t *testing.T) {\n\tt.Parallel()\n\ttestKey := GenerateKey(32)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tKey: testKey,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tvalid1 := c.Cookies(\"valid1\")\n\t\tvalid2 := c.Cookies(\"valid2\")\n\t\tinvalid := c.Cookies(\"invalid\")\n\t\treturn c.SendString(\"valid1=\" + valid1 + \",valid2=\" + valid2 + \",invalid=\" + invalid)\n\t})\n\n\th := app.Handler()\n\n\t// First, create some valid encrypted cookies\n\tencryptedValue1, err := EncryptCookie(\"valid1\", \"value1\", testKey)\n\trequire.NoError(t, err)\n\tencryptedValue2, err := EncryptCookie(\"valid2\", \"value2\", testKey)\n\trequire.NoError(t, err)\n\n\t// Test with a mix of valid and invalid cookies\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"valid1\", encryptedValue1)\n\tctx.Request.Header.SetCookie(\"invalid\", \"thisisnotvalid\")\n\tctx.Request.Header.SetCookie(\"valid2\", encryptedValue2)\n\n\th(ctx)\n\n\trequire.Equal(t, 200, ctx.Response.StatusCode())\n\trequire.Equal(t, \"valid1=value1,valid2=value2,invalid=\", string(ctx.Response.Body()))\n\n\t// Verify the invalid cookie was deleted but valid ones remain\n\trequire.NotEmpty(t, ctx.Request.Header.Cookie(\"valid1\"))\n\trequire.Empty(t, ctx.Request.Header.Cookie(\"invalid\"))\n\trequire.NotEmpty(t, ctx.Request.Header.Cookie(\"valid2\"))\n}\n"
  },
  {
    "path": "middleware/encryptcookie/utils.go",
    "content": "package encryptcookie\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n)\n\nvar (\n\tErrInvalidKeyLength      = errors.New(\"encryption key must be 16, 24, or 32 bytes\")\n\tErrInvalidEncryptedValue = errors.New(\"encrypted value is not valid\")\n)\n\n// decodeKey decodes the provided base64-encoded key and validates its length.\n// It returns the decoded key bytes or an error when invalid.\nfunc decodeKey(key string) ([]byte, error) {\n\tkeyDecoded, err := base64.StdEncoding.DecodeString(key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to base64-decode key: %w\", err)\n\t}\n\n\tkeyLen := len(keyDecoded)\n\tif keyLen != 16 && keyLen != 24 && keyLen != 32 {\n\t\treturn nil, ErrInvalidKeyLength\n\t}\n\n\treturn keyDecoded, nil\n}\n\n// validateKey checks if the provided base64-encoded key is of valid length.\nfunc validateKey(key string) error {\n\t_, err := decodeKey(key)\n\treturn err\n}\n\n// EncryptCookie Encrypts a cookie value with specific encryption key\nfunc EncryptCookie(name, value, key string) (string, error) {\n\tkeyDecoded, err := decodeKey(key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tblock, err := aes.NewCipher(keyDecoded)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create AES cipher: %w\", err)\n\t}\n\n\tgcm, err := cipher.NewGCMWithRandomNonce(block)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create GCM mode: %w\", err)\n\t}\n\n\tciphertext := gcm.Seal(nil, nil, []byte(value), []byte(name))\n\treturn base64.StdEncoding.EncodeToString(ciphertext), nil\n}\n\n// DecryptCookie Decrypts a cookie value with specific encryption key\nfunc DecryptCookie(name, value, key string) (string, error) {\n\tkeyDecoded, err := decodeKey(key)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tenc, err := base64.StdEncoding.DecodeString(value)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to base64-decode value: %w\", err)\n\t}\n\n\tblock, err := aes.NewCipher(keyDecoded)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create AES cipher: %w\", err)\n\t}\n\n\tgcm, err := cipher.NewGCMWithRandomNonce(block)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create GCM mode: %w\", err)\n\t}\n\n\tif len(enc) < gcm.NonceSize()+gcm.Overhead() {\n\t\treturn \"\", ErrInvalidEncryptedValue\n\t}\n\n\tplaintext, err := gcm.Open(nil, nil, enc, []byte(name))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decrypt ciphertext: %w\", err)\n\t}\n\n\treturn string(plaintext), nil\n}\n\n// GenerateKey returns a random string of 16, 24, or 32 bytes.\n// The length of the key determines the AES encryption algorithm used:\n// 16 bytes for AES-128, 24 bytes for AES-192, and 32 bytes for AES-256-GCM.\nfunc GenerateKey(length int) string {\n\tif length != 16 && length != 24 && length != 32 {\n\t\tpanic(ErrInvalidKeyLength)\n\t}\n\n\tkey := make([]byte, length)\n\n\tif _, err := rand.Read(key); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(key)\n}\n\n// Check given cookie key is disabled for encryption or not\nfunc isDisabled(key string, except []string) bool {\n\treturn slices.Contains(except, key)\n}\n"
  },
  {
    "path": "middleware/envvar/config.go",
    "content": "package envvar\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// ExportVars specifies the environment variables that should export\n\tExportVars map[string]string\n}\n\n// ConfigDefault is the default config.\nvar ConfigDefault = Config{\n\tExportVars: map[string]string{},\n}\n\nfunc configDefault(config ...Config) Config {\n\tif len(config) == 0 {\n\t\treturn ConfigDefault\n\t}\n\n\tcfg := config[0]\n\n\tif cfg.ExportVars == nil {\n\t\tcfg.ExportVars = ConfigDefault.ExportVars\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/envvar/envvar.go",
    "content": "package envvar\n\nimport (\n\t\"os\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst hAllow = fiber.MethodGet + \", \" + fiber.MethodHead\n\n// EnvVar captures environment variables that are exposed through the\n// middleware response.\ntype EnvVar struct {\n\tVars map[string]string `json:\"vars\"`\n}\n\nfunc (envVar *EnvVar) set(key, val string) {\n\tenvVar.Vars[key] = val\n}\n\n// New creates a handler that returns configured environment variables as a\n// JSON response.\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\treturn func(c fiber.Ctx) error {\n\t\tmethod := c.Method()\n\t\tif method != fiber.MethodGet && method != fiber.MethodHead {\n\t\t\tc.Set(fiber.HeaderAllow, hAllow)\n\t\t\treturn fiber.ErrMethodNotAllowed\n\t\t}\n\n\t\tenvVar := newEnvVar(cfg)\n\t\tvarsByte, err := c.App().Config().JSONEncoder(envVar)\n\t\tif err != nil {\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n\t\t}\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8)\n\t\treturn c.Send(varsByte)\n\t}\n}\n\nfunc newEnvVar(cfg Config) *EnvVar {\n\tvars := &EnvVar{Vars: make(map[string]string)}\n\n\tif len(cfg.ExportVars) == 0 {\n\t\t// do not expose environment variables when no configuration\n\t\t// is supplied to prevent accidental information disclosure\n\t\treturn vars\n\t}\n\n\tfor key, defaultVal := range cfg.ExportVars {\n\t\tvars.set(key, defaultVal)\n\t\tif envVal, exists := os.LookupEnv(key); exists {\n\t\t\tvars.set(key, envVal)\n\t\t}\n\t}\n\n\treturn vars\n}\n"
  },
  {
    "path": "middleware/envvar/envvar_test.go",
    "content": "package envvar\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_EnvVarStructWithExportVars(t *testing.T) {\n\tt.Setenv(\"testKey\", \"testEnvValue\")\n\tt.Setenv(\"anotherEnvKey\", \"anotherEnvVal\")\n\tvars := newEnvVar(Config{\n\t\tExportVars: map[string]string{\"testKey\": \"\", \"testDefaultKey\": \"testDefaultVal\"},\n\t})\n\n\trequire.Equal(t, \"testEnvValue\", vars.Vars[\"testKey\"])\n\trequire.Equal(t, \"testDefaultVal\", vars.Vars[\"testDefaultKey\"])\n\trequire.Empty(t, vars.Vars[\"anotherEnvKey\"])\n}\n\nfunc Test_EnvVarHandler(t *testing.T) {\n\tt.Setenv(\"testKey\", \"testVal\")\n\n\texpectedEnvVarResponse, err := json.Marshal(\n\t\tstruct {\n\t\t\tVars map[string]string `json:\"vars\"`\n\t\t}{\n\t\t\tVars: map[string]string{\"testKey\": \"testVal\"},\n\t\t})\n\trequire.NoError(t, err)\n\n\tapp := fiber.New()\n\tapp.Use(\"/envvars\", New(Config{\n\t\tExportVars: map[string]string{\"testKey\": \"\"},\n\t}))\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"http://localhost/envvars\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, expectedEnvVarResponse, respBody)\n}\n\nfunc Test_EnvVarHandlerNotMatched(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(\"/envvars\", New(Config{\n\t\tExportVars: map[string]string{\"testKey\": \"\"},\n\t}))\n\n\tapp.Get(\"/another-path\", func(ctx fiber.Ctx) error {\n\t\trequire.NoError(t, ctx.SendString(\"OK\"))\n\t\treturn nil\n\t})\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"http://localhost/another-path\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []byte(\"OK\"), respBody)\n}\n\nfunc Test_EnvVarHandlerDefaultConfig(t *testing.T) {\n\tt.Setenv(\"testEnvKey\", \"testEnvVal\")\n\n\tapp := fiber.New()\n\tapp.Use(\"/envvars\", New())\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"http://localhost/envvars\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tvar envVars EnvVar\n\trequire.NoError(t, json.Unmarshal(respBody, &envVars))\n\t_, exists := envVars.Vars[\"testEnvKey\"]\n\trequire.False(t, exists)\n}\n\nfunc Test_EnvVarHandlerMethod(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(\"/envvars\", New())\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, \"http://localhost/envvars\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode)\n\trequire.Equal(t, hAllow, resp.Header.Get(fiber.HeaderAllow))\n}\n\nfunc Test_EnvVarHandlerHead(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(\"/envvars\", New())\n\n\treq := httptest.NewRequest(fiber.MethodHead, \"http://localhost/envvars\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, string(body))\n}\n\nfunc Test_EnvVarHandlerSpecialValue(t *testing.T) {\n\ttestEnvKey := \"testEnvKey\"\n\tfakeBase64 := \"testBase64:TQ==\"\n\tt.Setenv(testEnvKey, fakeBase64)\n\n\tapp := fiber.New()\n\tapp.Use(\"/envvars/export\", New(Config{ExportVars: map[string]string{testEnvKey: \"\"}}))\n\tapp.Use(\"/envvars\", New())\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"http://localhost/envvars\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trespBody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tvar envVars EnvVar\n\trequire.NoError(t, json.Unmarshal(respBody, &envVars))\n\t_, exists := envVars.Vars[testEnvKey]\n\trequire.False(t, exists)\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"http://localhost/envvars/export\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trespBody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tvar envVarsExport EnvVar\n\trequire.NoError(t, json.Unmarshal(respBody, &envVarsExport))\n\tval := envVarsExport.Vars[testEnvKey]\n\trequire.Equal(t, fakeBase64, val)\n}\n"
  },
  {
    "path": "middleware/etag/config.go",
    "content": "package etag\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\t// Weak indicates that a weak validator is used. Weak etags are easy\n\t// to generate, but are far less useful for comparisons. Strong\n\t// validators are ideal for comparisons but can be very difficult\n\t// to generate efficiently. Weak ETag values of two representations\n\t// of the same resources might be semantically equivalent, but not\n\t// byte-for-byte identical. This means weak etags prevent caching\n\t// when byte range requests are used, but strong etags mean range\n\t// requests can still be cached.\n\tWeak bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tWeak: false,\n\tNext: nil,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/etag/etag.go",
    "content": "package etag\n\nimport (\n\t\"bytes\"\n\t\"hash/crc32\"\n\t\"math\"\n\t\"slices\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/bytebufferpool\"\n)\n\nvar (\n\tweakPrefix = []byte(\"W/\")\n\tcrc32q     = crc32.MakeTable(0xD5828281)\n)\n\n// Generate returns a strong ETag for body.\nfunc Generate(body []byte) []byte {\n\tif uint64(len(body)) > math.MaxUint32 {\n\t\treturn nil\n\t}\n\tbb := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(bb)\n\tb := bb.B[:0]\n\tb = append(b, '\"')\n\tb = appendUint(b, uint32(len(body))) // #nosec G115 -- length checked above\n\tb = append(b, '-')\n\tb = appendUint(b, crc32.Checksum(body, crc32q))\n\tb = append(b, '\"')\n\treturn slices.Clone(b)\n}\n\n// GenerateWeak returns a weak ETag for body.\nfunc GenerateWeak(body []byte) []byte {\n\ttag := Generate(body)\n\tif tag == nil {\n\t\treturn nil\n\t}\n\treturn append(weakPrefix, tag...)\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\tnormalizedHeaderETag := []byte(\"Etag\")\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Return err if next handler returns one\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Don't generate ETags for invalid responses\n\t\tif c.Response().StatusCode() != fiber.StatusOK {\n\t\t\treturn nil\n\t\t}\n\t\tbody := c.Response().Body()\n\t\t// Skips ETag if no response body is present\n\t\tif len(body) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\t// Skip ETag if header is already present\n\t\tif c.Response().Header.PeekBytes(normalizedHeaderETag) != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tbodyLength := len(body)\n\t\tif uint64(bodyLength) > math.MaxUint32 {\n\t\t\treturn c.SendStatus(fiber.StatusRequestEntityTooLarge)\n\t\t}\n\n\t\tvar etag []byte\n\t\tif cfg.Weak {\n\t\t\tetag = GenerateWeak(body)\n\t\t} else {\n\t\t\tetag = Generate(body)\n\t\t}\n\n\t\t// Get ETag header from request\n\t\tclientEtag := c.Request().Header.Peek(fiber.HeaderIfNoneMatch)\n\n\t\t// Check if client's ETag is weak\n\t\tif bytes.HasPrefix(clientEtag, weakPrefix) {\n\t\t\t// Check if server's ETag is weak\n\t\t\tif bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) {\n\t\t\t\t// W/1 == 1 || W/1 == W/1\n\t\t\t\tc.RequestCtx().ResetBody()\n\n\t\t\t\treturn c.SendStatus(fiber.StatusNotModified)\n\t\t\t}\n\t\t\t// W/1 != W/2 || W/1 != 2\n\t\t\tc.Response().Header.SetCanonical(normalizedHeaderETag, etag)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif bytes.Contains(clientEtag, etag) {\n\t\t\t// 1 == 1\n\t\t\tc.RequestCtx().ResetBody()\n\n\t\t\treturn c.SendStatus(fiber.StatusNotModified)\n\t\t}\n\t\t// 1 != 2\n\t\tc.Response().Header.SetCanonical(normalizedHeaderETag, etag)\n\n\t\treturn nil\n\t}\n}\n\n// appendUint appends n to dst and returns the extended dst.\nfunc appendUint(dst []byte, n uint32) []byte {\n\tvar b [20]byte\n\tbuf := b[:]\n\ti := len(buf)\n\tvar q uint32\n\tfor n >= 10 {\n\t\ti--\n\t\tq = n / 10\n\t\tbuf[i] = '0' + byte(n-q*10)\n\t\tn = q\n\t}\n\ti--\n\tbuf[i] = '0' + byte(n)\n\n\tdst = append(dst, buf[i:]...)\n\treturn dst\n}\n"
  },
  {
    "path": "middleware/etag/etag_test.go",
    "content": "package etag\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// go test -run Test_ETag_Next\nfunc Test_ETag_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_ETag_SkipError\nfunc Test_ETag_SkipError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn fiber.ErrForbidden\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusForbidden, resp.StatusCode)\n}\n\n// go test -run Test_ETag_NotStatusOK\nfunc Test_ETag_NotStatusOK(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusCreated)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusCreated, resp.StatusCode)\n}\n\n// go test -run Test_ETag_NoBody\nfunc Test_ETag_NoBody(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_ETag_NewEtag\nfunc Test_ETag_NewEtag(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"without HeaderIfNoneMatch\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagNewEtag(t, false, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and not matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagNewEtag(t, true, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagNewEtag(t, true, true)\n\t})\n}\n\nfunc testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine\n\tt.Helper()\n\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tif headerIfNoneMatch {\n\t\tetag := `\"non-match\"`\n\t\tif matched {\n\t\t\tetag = `\"13-1831710635\"`\n\t\t}\n\t\treq.Header.Set(fiber.HeaderIfNoneMatch, etag)\n\t}\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tif !headerIfNoneMatch || !matched {\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t\trequire.Equal(t, `\"13-1831710635\"`, resp.Header.Get(fiber.HeaderETag))\n\t\treturn\n\t}\n\n\tif matched {\n\t\trequire.Equal(t, fiber.StatusNotModified, resp.StatusCode)\n\t\tb, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, b)\n\t}\n}\n\n// go test -run Test_ETag_WeakEtag\nfunc Test_ETag_WeakEtag(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"without HeaderIfNoneMatch\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagWeakEtag(t, false, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and not matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagWeakEtag(t, true, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagWeakEtag(t, true, true)\n\t})\n}\n\nfunc testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine\n\tt.Helper()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Weak: true}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tif headerIfNoneMatch {\n\t\tetag := `W/\"non-match\"`\n\t\tif matched {\n\t\t\tetag = `W/\"13-1831710635\"`\n\t\t}\n\t\treq.Header.Set(fiber.HeaderIfNoneMatch, etag)\n\t}\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tif !headerIfNoneMatch || !matched {\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t\trequire.Equal(t, `W/\"13-1831710635\"`, resp.Header.Get(fiber.HeaderETag))\n\t\treturn\n\t}\n\n\tif matched {\n\t\trequire.Equal(t, fiber.StatusNotModified, resp.StatusCode)\n\t\tb, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, b)\n\t}\n}\n\n// go test -run Test_ETag_CustomEtag\nfunc Test_ETag_CustomEtag(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"without HeaderIfNoneMatch\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagCustomEtag(t, false, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and not matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagCustomEtag(t, true, false)\n\t})\n\tt.Run(\"with HeaderIfNoneMatch and matched\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestETagCustomEtag(t, true, true)\n\t})\n}\n\nfunc testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine\n\tt.Helper()\n\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, `\"custom\"`)\n\t\tif bytes.Equal(c.Request().Header.Peek(fiber.HeaderIfNoneMatch), []byte(`\"custom\"`)) {\n\t\t\treturn c.SendStatus(fiber.StatusNotModified)\n\t\t}\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tif headerIfNoneMatch {\n\t\tetag := `\"non-match\"`\n\t\tif matched {\n\t\t\tetag = `\"custom\"`\n\t\t}\n\t\treq.Header.Set(fiber.HeaderIfNoneMatch, etag)\n\t}\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tif !headerIfNoneMatch || !matched {\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t\trequire.Equal(t, `\"custom\"`, resp.Header.Get(fiber.HeaderETag))\n\t\treturn\n\t}\n\n\tif matched {\n\t\trequire.Equal(t, fiber.StatusNotModified, resp.StatusCode)\n\t\tb, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, b)\n\t}\n}\n\n// go test -run Test_ETag_CustomEtagPut\nfunc Test_ETag_CustomEtagPut(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Put(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderETag, `\"custom\"`)\n\t\tif !bytes.Equal(c.Request().Header.Peek(fiber.HeaderIfMatch), []byte(`\"custom\"`)) {\n\t\t\treturn c.SendStatus(fiber.StatusPreconditionFailed)\n\t\t}\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodPut, \"/\", http.NoBody)\n\treq.Header.Set(fiber.HeaderIfMatch, `\"non-match\"`)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusPreconditionFailed, resp.StatusCode)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Etag -benchmem -count=4\nfunc Benchmark_Etag(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, 200, fctx.Response.Header.StatusCode())\n\trequire.Equal(b, `\"13-1831710635\"`, string(fctx.Response.Header.Peek(fiber.HeaderETag)))\n}\n"
  },
  {
    "path": "middleware/expvar/config.go",
    "content": "package expvar\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n}\n\nvar ConfigDefault = Config{\n\tNext: nil,\n}\n\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/expvar/expvar.go",
    "content": "package expvar\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/fasthttp/expvarhandler\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tpath := c.Path()\n\t\t// We are only interested in /debug/vars routes\n\t\tif len(path) < 11 || !strings.HasPrefix(path, \"/debug/vars\") {\n\t\t\treturn c.Next()\n\t\t}\n\t\tif path == \"/debug/vars\" {\n\t\t\texpvarhandler.ExpvarHandler(c.RequestCtx())\n\t\t\treturn nil\n\t\t}\n\n\t\treturn c.Redirect().To(\"/debug/vars\")\n\t}\n}\n"
  },
  {
    "path": "middleware/expvar/expvar_test.go",
    "content": "package expvar\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_Non_Expvar_Path(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"escaped\", string(b))\n}\n\nfunc Test_Expvar_Index(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/vars\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Contains(b, []byte(\"cmdline\")))\n\trequire.True(t, bytes.Contains(b, []byte(\"memstat\")))\n}\n\nfunc Test_Expvar_Filter(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/vars?r=cmd\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Contains(b, []byte(\"cmdline\")))\n\trequire.False(t, bytes.Contains(b, []byte(\"memstat\")))\n}\n\nfunc Test_Expvar_Other_Path(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/vars/303\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusSeeOther, resp.StatusCode)\n}\n\n// go test -run Test_Expvar_Next\nfunc Test_Expvar_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/vars\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 404, resp.StatusCode)\n}\n"
  },
  {
    "path": "middleware/favicon/config.go",
    "content": "package favicon\n\nimport (\n\t\"io/fs\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// FileSystem is an optional alternate filesystem to search for the favicon in.\n\t// An example of this could be an embedded or network filesystem\n\t//\n\t// Optional. Default: nil\n\tFileSystem fs.FS `json:\"-\"`\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// File holds the path to an actual favicon that will be cached\n\t//\n\t// Optional. Default: \"\"\n\tFile string `json:\"file\"`\n\n\t// URL for favicon handler\n\t//\n\t// Optional. Default: \"/favicon.ico\"\n\tURL string `json:\"url\"`\n\n\t// CacheControl defines how the Cache-Control header in the response should be set\n\t//\n\t// Optional. Default: \"public, max-age=31536000\"\n\tCacheControl string `json:\"cache_control\"`\n\n\t// Raw data of the favicon file\n\t//\n\t// Optional. Default: nil\n\tData []byte `json:\"-\"`\n\n\t// MaxBytes limits the maximum size of the cached favicon asset.\n\t//\n\t// Optional. Default: 1048576\n\tMaxBytes int64 `json:\"max_bytes\"`\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:         nil,\n\tFile:         \"\",\n\tURL:          fPath,\n\tCacheControl: \"public, max-age=31536000\",\n\tMaxBytes:     1024 * 1024,\n}\n\nfunc configDefault(config ...Config) Config {\n\tif len(config) == 0 {\n\t\treturn ConfigDefault\n\t}\n\n\tcfg := config[0]\n\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif cfg.URL == \"\" {\n\t\tcfg.URL = ConfigDefault.URL\n\t}\n\tif cfg.File == \"\" {\n\t\tcfg.File = ConfigDefault.File\n\t}\n\tif cfg.CacheControl == \"\" {\n\t\tcfg.CacheControl = ConfigDefault.CacheControl\n\t}\n\tif cfg.MaxBytes <= 0 {\n\t\tcfg.MaxBytes = ConfigDefault.MaxBytes\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/favicon/favicon.go",
    "content": "package favicon\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst (\n\tfPath  = \"/favicon.ico\"\n\thType  = \"image/x-icon\"\n\thAllow = \"GET, HEAD, OPTIONS\"\n\thZero  = \"0\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\t// Load iconData if provided\n\tvar (\n\t\terr           error\n\t\ticonData      []byte\n\t\ticonLenHeader string\n\t\ticonLen       int\n\t\tf             fs.File\n\t)\n\tif cfg.Data != nil {\n\t\t// use the provided favicon data\n\t\ticonData = cfg.Data\n\t\ticonLenHeader = strconv.Itoa(len(cfg.Data))\n\t\ticonLen = len(cfg.Data)\n\t} else if cfg.File != \"\" {\n\t\t// read from configured filesystem if present\n\t\tif cfg.FileSystem != nil {\n\t\t\tf, err = cfg.FileSystem.Open(cfg.File)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = f.Close() //nolint:errcheck // not needed\n\t\t\t}()\n\t\t\tif iconData, err = readLimited(f, cfg.MaxBytes); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t} else {\n\t\t\tf, err = os.Open(cfg.File)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = f.Close() //nolint:errcheck // not needed\n\t\t\t}()\n\t\t\tif iconData, err = readLimited(f, cfg.MaxBytes); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\n\t\ticonLenHeader = strconv.Itoa(len(iconData))\n\t\ticonLen = len(iconData)\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Only respond to favicon requests\n\t\tif c.Path() != cfg.URL {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Only allow GET, HEAD and OPTIONS requests\n\t\tif c.Method() != fiber.MethodGet && c.Method() != fiber.MethodHead {\n\t\t\tif c.Method() != fiber.MethodOptions {\n\t\t\t\tc.Status(fiber.StatusMethodNotAllowed)\n\t\t\t} else {\n\t\t\t\tc.Status(fiber.StatusOK)\n\t\t\t}\n\t\t\tc.Set(fiber.HeaderAllow, hAllow)\n\t\t\tc.Set(fiber.HeaderContentLength, hZero)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Serve cached favicon\n\t\tif iconLen > 0 {\n\t\t\tc.Set(fiber.HeaderContentLength, iconLenHeader)\n\t\t\tc.Set(fiber.HeaderContentType, hType)\n\t\t\tc.Set(fiber.HeaderCacheControl, cfg.CacheControl)\n\t\t\treturn c.Status(fiber.StatusOK).Send(iconData)\n\t\t}\n\n\t\treturn c.SendStatus(fiber.StatusNoContent)\n\t}\n}\n\nfunc readLimited(reader io.Reader, maxBytes int64) ([]byte, error) {\n\tlimit := maxBytes + 1\n\tdata, err := io.ReadAll(io.LimitReader(reader, limit))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"favicon: read limited: %w\", err)\n\t}\n\tif int64(len(data)) > maxBytes {\n\t\treturn nil, fmt.Errorf(\"favicon: file size exceeds max bytes %d\", maxBytes)\n\t}\n\treturn data, nil\n}\n"
  },
  {
    "path": "middleware/favicon/favicon_test.go",
    "content": "package favicon\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// go test -run Test_Middleware_Favicon\nfunc Test_Middleware_Favicon(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\t// Skip Favicon middleware\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusNoContent, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodOptions, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodPut, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"GET, HEAD, OPTIONS\", resp.Header.Get(fiber.HeaderAllow))\n}\n\n// go test -run Test_Middleware_Favicon_Not_Found\nfunc Test_Middleware_Favicon_Not_Found(t *testing.T) {\n\tt.Parallel()\n\tdefer func() {\n\t\tif err := recover(); err == nil {\n\t\t\tt.Error(\"should catch panic\")\n\t\t\treturn\n\t\t}\n\t}()\n\n\tfiber.New().Use(New(Config{\n\t\tFile: \"non-exist.ico\",\n\t}))\n}\n\n// go test -run Test_Middleware_Favicon_MaxBytes\nfunc Test_Middleware_Favicon_MaxBytes(t *testing.T) {\n\tt.Parallel()\n\tdefer func() {\n\t\tif err := recover(); err == nil {\n\t\t\tt.Error(\"should catch panic\")\n\t\t}\n\t}()\n\n\tdir := t.TempDir()\n\tpath := dir + \"/favicon.ico\"\n\terr := os.WriteFile(path, bytes.Repeat([]byte(\"a\"), 11), 0o600)\n\trequire.NoError(t, err)\n\n\tfiber.New().Use(New(Config{\n\t\tFile:     path,\n\t\tMaxBytes: 10,\n\t}))\n}\n\n// go test -run Test_Middleware_Favicon_MaxBytes_FileSystem\nfunc Test_Middleware_Favicon_MaxBytes_FileSystem(t *testing.T) {\n\tt.Parallel()\n\tdefer func() {\n\t\tif err := recover(); err == nil {\n\t\t\tt.Error(\"should catch panic\")\n\t\t}\n\t}()\n\n\tdir := t.TempDir()\n\tpath := dir + \"/favicon.ico\"\n\terr := os.WriteFile(path, bytes.Repeat([]byte(\"a\"), 11), 0o600)\n\trequire.NoError(t, err)\n\n\tfiber.New().Use(New(Config{\n\t\tFile:       \"favicon.ico\",\n\t\tFileSystem: os.DirFS(dir),\n\t\tMaxBytes:   10,\n\t}))\n}\n\n// go test -run Test_Middleware_Favicon_Found\nfunc Test_Middleware_Favicon_Found(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFile: \"../../.github/testdata/favicon.ico\",\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"image/x-icon\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"public, max-age=31536000\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\n// go test -run Test_Custom_Favicon_Url\nfunc Test_Custom_Favicon_URL(t *testing.T) {\n\tapp := fiber.New()\n\tconst customURL = \"/favicon.svg\"\n\tapp.Use(New(Config{\n\t\tFile: \"../../.github/testdata/favicon.ico\",\n\t\tURL:  customURL,\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, customURL, http.NoBody))\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"image/x-icon\", resp.Header.Get(fiber.HeaderContentType))\n}\n\n// go test -run Test_Custom_Favicon_Data\nfunc Test_Custom_Favicon_Data(t *testing.T) {\n\tdata, err := os.ReadFile(\"../../.github/testdata/favicon.ico\")\n\trequire.NoError(t, err)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tData: data,\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"image/x-icon\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"public, max-age=31536000\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\n// go test -run Test_Middleware_Favicon_FileSystem\nfunc Test_Middleware_Favicon_FileSystem(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFile:       \"favicon.ico\",\n\t\tFileSystem: os.DirFS(\"../../.github/testdata\"),\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"image/x-icon\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"public, max-age=31536000\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\n// go test -run Test_Middleware_Favicon_CacheControl\nfunc Test_Middleware_Favicon_CacheControl(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tCacheControl: \"public, max-age=100\",\n\t\tFile:         \"../../.github/testdata/favicon.ico\",\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/favicon.ico\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, \"image/x-icon\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"public, max-age=100\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\n// go test -v -run=^$ -bench=Benchmark_Middleware_Favicon -benchmem -count=4\nfunc Benchmark_Middleware_Favicon(b *testing.B) {\n\tapp := fiber.New()\n\tapp.Use(New())\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\thandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\thandler(c)\n\t}\n}\n\n// go test -run Test_Favicon_Next\nfunc Test_Favicon_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n"
  },
  {
    "path": "middleware/healthcheck/config.go",
    "content": "package healthcheck\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the configuration options for the healthcheck middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true. If this function returns true\n\t// and no other handlers are defined for the route, Fiber will return a status 404 Not Found, since\n\t// no other handlers were defined to return a different status.\n\t//\n\t// Optional. Default: nil\n\tNext func(fiber.Ctx) bool\n\n\t// Probe is executed to determine the current health state. It can be used for liveness,\n\t// readiness or startup checks. Returning true indicates the application is healthy.\n\t//\n\t// Optional. Default: func(c fiber.Ctx) bool { return true }\n\tProbe func(fiber.Ctx) bool\n}\n\nconst (\n\t// LivenessEndpoint is the conventional path for a liveness check.\n\t// Register the middleware on this path to expose it.\n\tLivenessEndpoint = \"/livez\"\n\n\t// ReadinessEndpoint is the conventional path for a readiness check.\n\t// Register the middleware on this path to expose it.\n\tReadinessEndpoint = \"/readyz\"\n\n\t// StartupEndpoint is the conventional path for a startup check.\n\t// Register the middleware on this path to expose it.\n\tStartupEndpoint = \"/startupz\"\n)\n\nfunc defaultProbe(_ fiber.Ctx) bool { return true }\n\n// ConfigDefault is the default configuration.\nvar ConfigDefault = Config{\n\tNext:  nil,\n\tProbe: defaultProbe,\n}\n\nfunc configDefault(config ...Config) Config {\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\tcfg := config[0]\n\n\tif cfg.Probe == nil {\n\t\tcfg.Probe = ConfigDefault.Probe\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/healthcheck/healthcheck.go",
    "content": "package healthcheck\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New returns a health-check handler that responds based on the provided\n// configuration.\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tif c.Method() != fiber.MethodGet {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tif cfg.Probe(c) {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t}\n\n\t\treturn c.SendStatus(fiber.StatusServiceUnavailable)\n\t}\n}\n"
  },
  {
    "path": "middleware/healthcheck/healthcheck_test.go",
    "content": "package healthcheck\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc shouldGiveStatus(t *testing.T, app *fiber.App, path string, expectedStatus int) {\n\tt.Helper()\n\treq, err := app.Test(httptest.NewRequest(fiber.MethodGet, path, http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectedStatus, req.StatusCode, \"path: \"+path+\" should match \"+strconv.Itoa(expectedStatus))\n}\n\nfunc shouldGiveOK(t *testing.T, app *fiber.App, path string) {\n\tt.Helper()\n\tshouldGiveStatus(t, app, path, fiber.StatusOK)\n}\n\nfunc shouldGiveNotFound(t *testing.T, app *fiber.App, path string) {\n\tt.Helper()\n\tshouldGiveStatus(t, app, path, fiber.StatusNotFound)\n}\n\nfunc Test_HealthCheck_Strict_Routing_Default(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New(fiber.Config{\n\t\tStrictRouting: true,\n\t})\n\n\tapp.Get(LivenessEndpoint, New())\n\tapp.Get(ReadinessEndpoint, New())\n\tapp.Get(StartupEndpoint, New())\n\n\tshouldGiveOK(t, app, \"/readyz\")\n\tshouldGiveOK(t, app, \"/livez\")\n\tshouldGiveOK(t, app, \"/startupz\")\n\tshouldGiveNotFound(t, app, \"/readyz/\")\n\tshouldGiveNotFound(t, app, \"/livez/\")\n\tshouldGiveNotFound(t, app, \"/startupz/\")\n\tshouldGiveNotFound(t, app, \"/notDefined/readyz\")\n\tshouldGiveNotFound(t, app, \"/notDefined/livez\")\n\tshouldGiveNotFound(t, app, \"/notDefined/startupz\")\n}\n\nfunc Test_HealthCheck_Default(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Get(LivenessEndpoint, New())\n\tapp.Get(ReadinessEndpoint, New())\n\tapp.Get(StartupEndpoint, New())\n\n\tshouldGiveOK(t, app, \"/readyz\")\n\tshouldGiveOK(t, app, \"/livez\")\n\tshouldGiveOK(t, app, \"/startupz\")\n\tshouldGiveOK(t, app, \"/readyz/\")\n\tshouldGiveOK(t, app, \"/livez/\")\n\tshouldGiveOK(t, app, \"/startupz/\")\n\tshouldGiveNotFound(t, app, \"/notDefined/readyz\")\n\tshouldGiveNotFound(t, app, \"/notDefined/livez\")\n\tshouldGiveNotFound(t, app, \"/notDefined/startupz\")\n}\n\nfunc Test_HealthCheck_Custom(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tc1 := make(chan struct{}, 1)\n\tapp.Get(\"/live\", New(Config{\n\t\tProbe: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\tapp.Get(\"/ready\", New(Config{\n\t\tProbe: func(_ fiber.Ctx) bool {\n\t\t\tselect {\n\t\t\tcase <-c1:\n\t\t\t\treturn true\n\t\t\tdefault:\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\t}))\n\tapp.Get(StartupEndpoint, New(Config{\n\t\tProbe: func(_ fiber.Ctx) bool {\n\t\t\treturn false\n\t\t},\n\t}))\n\n\t// Setup custom liveness and readiness probes to simulate application health status\n\t// Live should return 200 with GET request\n\tshouldGiveOK(t, app, \"/live\")\n\t// Live should return 404 with POST request\n\treq, err := app.Test(httptest.NewRequest(fiber.MethodPost, \"/live\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusMethodNotAllowed, req.StatusCode)\n\n\t// Ready should return 404 with POST request\n\treq, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/ready\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusMethodNotAllowed, req.StatusCode)\n\n\t// Ready should return 503 with GET request before the channel is closed\n\tshouldGiveStatus(t, app, \"/ready\", fiber.StatusServiceUnavailable)\n\n\tshouldGiveStatus(t, app, \"/startupz\", fiber.StatusServiceUnavailable)\n\n\t// Ready should return 200 with GET request after the channel is closed\n\tc1 <- struct{}{}\n\tshouldGiveOK(t, app, \"/ready\")\n}\n\nfunc Test_HealthCheck_Custom_Nested(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tc1 := make(chan struct{}, 1)\n\tapp.Get(\"/probe/live\", New(Config{\n\t\tProbe: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\tapp.Get(\"/probe/ready\", New(Config{\n\t\tProbe: func(_ fiber.Ctx) bool {\n\t\t\tselect {\n\t\t\tcase <-c1:\n\t\t\t\treturn true\n\t\t\tdefault:\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\t}))\n\n\t// Testing custom health check endpoints with nested paths\n\tshouldGiveOK(t, app, \"/probe/live\")\n\tshouldGiveStatus(t, app, \"/probe/ready\", fiber.StatusServiceUnavailable)\n\tshouldGiveOK(t, app, \"/probe/live/\")\n\tshouldGiveStatus(t, app, \"/probe/ready/\", fiber.StatusServiceUnavailable)\n\tshouldGiveNotFound(t, app, \"/probe/livez\")\n\tshouldGiveNotFound(t, app, \"/probe/readyz\")\n\tshouldGiveNotFound(t, app, \"/probe/livez/\")\n\tshouldGiveNotFound(t, app, \"/probe/readyz/\")\n\tshouldGiveNotFound(t, app, \"/livez\")\n\tshouldGiveNotFound(t, app, \"/readyz\")\n\tshouldGiveNotFound(t, app, \"/readyz/\")\n\tshouldGiveNotFound(t, app, \"/livez/\")\n\n\tc1 <- struct{}{}\n\tshouldGiveOK(t, app, \"/probe/ready\")\n\tc1 <- struct{}{}\n\tshouldGiveOK(t, app, \"/probe/ready/\")\n}\n\nfunc Test_HealthCheck_Next(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tchecker := New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t})\n\n\tapp.Get(LivenessEndpoint, checker)\n\tapp.Get(ReadinessEndpoint, checker)\n\tapp.Get(StartupEndpoint, checker)\n\n\t// This should give not found since there are no other handlers to execute\n\t// so it's like the route isn't defined at all\n\tshouldGiveNotFound(t, app, \"/readyz\")\n\tshouldGiveNotFound(t, app, \"/livez\")\n\tshouldGiveNotFound(t, app, \"/startupz\")\n}\n\nfunc Benchmark_HealthCheck(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Get(LivenessEndpoint, New())\n\tapp.Get(ReadinessEndpoint, New())\n\tapp.Get(StartupEndpoint, New())\n\n\th := app.Handler()\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/livez\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n\n\trequire.Equal(b, fiber.StatusOK, fctx.Response.Header.StatusCode())\n}\n\nfunc Benchmark_HealthCheck_Parallel(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Get(LivenessEndpoint, New())\n\tapp.Get(ReadinessEndpoint, New())\n\tapp.Get(StartupEndpoint, New())\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\tfctx.Request.SetRequestURI(\"/livez\")\n\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "middleware/helmet/config.go",
    "content": "package helmet\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip middleware.\n\t// Optional. Default: nil\n\tNext func(fiber.Ctx) bool\n\n\t// XSSProtection\n\t// Optional. Default value \"0\".\n\tXSSProtection string\n\n\t// ContentTypeNosniff\n\t// Optional. Default value \"nosniff\".\n\tContentTypeNosniff string\n\n\t// XFrameOptions\n\t// Optional. Default value \"SAMEORIGIN\".\n\t// Possible values: \"SAMEORIGIN\", \"DENY\", \"ALLOW-FROM uri\"\n\tXFrameOptions string\n\n\t// ContentSecurityPolicy\n\t// Optional. Default value \"\".\n\tContentSecurityPolicy string\n\n\t// ReferrerPolicy\n\t// Optional. Default value \"no-referrer\".\n\tReferrerPolicy string\n\n\t// Permissions-Policy\n\t// Optional. Default value \"\".\n\tPermissionPolicy string\n\n\t// Cross-Origin-Embedder-Policy\n\t// Optional. Default value \"require-corp\".\n\tCrossOriginEmbedderPolicy string\n\n\t// Cross-Origin-Opener-Policy\n\t// Optional. Default value \"same-origin\".\n\tCrossOriginOpenerPolicy string\n\n\t// Cross-Origin-Resource-Policy\n\t// Optional. Default value \"same-origin\".\n\tCrossOriginResourcePolicy string\n\n\t// Origin-Agent-Cluster\n\t// Optional. Default value \"?1\".\n\tOriginAgentCluster string\n\n\t// X-DNS-Prefetch-Control\n\t// Optional. Default value \"off\".\n\tXDNSPrefetchControl string\n\n\t// X-Download-Options\n\t// Optional. Default value \"noopen\".\n\tXDownloadOptions string\n\n\t// X-Permitted-Cross-Domain-Policies\n\t// Optional. Default value \"none\".\n\tXPermittedCrossDomain string\n\n\t// HSTSMaxAge\n\t// Optional. Default value 0.\n\tHSTSMaxAge int\n\n\t// HSTSExcludeSubdomains\n\t// Optional. Default value false.\n\tHSTSExcludeSubdomains bool\n\n\t// CSPReportOnly\n\t// Optional. Default value false.\n\tCSPReportOnly bool\n\n\t// HSTSPreloadEnabled\n\t// Optional. Default value false.\n\tHSTSPreloadEnabled bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tXSSProtection:             \"0\",\n\tContentTypeNosniff:        \"nosniff\",\n\tXFrameOptions:             \"SAMEORIGIN\",\n\tReferrerPolicy:            \"no-referrer\",\n\tCrossOriginEmbedderPolicy: \"require-corp\",\n\tCrossOriginOpenerPolicy:   \"same-origin\",\n\tCrossOriginResourcePolicy: \"same-origin\",\n\tOriginAgentCluster:        \"?1\",\n\tXDNSPrefetchControl:       \"off\",\n\tXDownloadOptions:          \"noopen\",\n\tXPermittedCrossDomain:     \"none\",\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.XSSProtection == \"\" {\n\t\tcfg.XSSProtection = ConfigDefault.XSSProtection\n\t}\n\n\tif cfg.ContentTypeNosniff == \"\" {\n\t\tcfg.ContentTypeNosniff = ConfigDefault.ContentTypeNosniff\n\t}\n\n\tif cfg.XFrameOptions == \"\" {\n\t\tcfg.XFrameOptions = ConfigDefault.XFrameOptions\n\t}\n\n\tif cfg.ReferrerPolicy == \"\" {\n\t\tcfg.ReferrerPolicy = ConfigDefault.ReferrerPolicy\n\t}\n\n\tif cfg.CrossOriginEmbedderPolicy == \"\" {\n\t\tcfg.CrossOriginEmbedderPolicy = ConfigDefault.CrossOriginEmbedderPolicy\n\t}\n\n\tif cfg.CrossOriginOpenerPolicy == \"\" {\n\t\tcfg.CrossOriginOpenerPolicy = ConfigDefault.CrossOriginOpenerPolicy\n\t}\n\n\tif cfg.CrossOriginResourcePolicy == \"\" {\n\t\tcfg.CrossOriginResourcePolicy = ConfigDefault.CrossOriginResourcePolicy\n\t}\n\n\tif cfg.OriginAgentCluster == \"\" {\n\t\tcfg.OriginAgentCluster = ConfigDefault.OriginAgentCluster\n\t}\n\n\tif cfg.XDNSPrefetchControl == \"\" {\n\t\tcfg.XDNSPrefetchControl = ConfigDefault.XDNSPrefetchControl\n\t}\n\n\tif cfg.XDownloadOptions == \"\" {\n\t\tcfg.XDownloadOptions = ConfigDefault.XDownloadOptions\n\t}\n\n\tif cfg.XPermittedCrossDomain == \"\" {\n\t\tcfg.XPermittedCrossDomain = ConfigDefault.XPermittedCrossDomain\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/helmet/helmet.go",
    "content": "package helmet\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Init config\n\tcfg := configDefault(config...)\n\n\t// Return middleware handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Next request to skip middleware\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Set headers\n\t\tif cfg.XSSProtection != \"\" {\n\t\t\tc.Set(fiber.HeaderXXSSProtection, cfg.XSSProtection)\n\t\t}\n\n\t\tif cfg.ContentTypeNosniff != \"\" {\n\t\t\tc.Set(fiber.HeaderXContentTypeOptions, cfg.ContentTypeNosniff)\n\t\t}\n\n\t\tif cfg.XFrameOptions != \"\" {\n\t\t\tc.Set(fiber.HeaderXFrameOptions, cfg.XFrameOptions)\n\t\t}\n\n\t\tif cfg.CrossOriginEmbedderPolicy != \"\" {\n\t\t\tc.Set(\"Cross-Origin-Embedder-Policy\", cfg.CrossOriginEmbedderPolicy)\n\t\t}\n\n\t\tif cfg.CrossOriginOpenerPolicy != \"\" {\n\t\t\tc.Set(\"Cross-Origin-Opener-Policy\", cfg.CrossOriginOpenerPolicy)\n\t\t}\n\n\t\tif cfg.CrossOriginResourcePolicy != \"\" {\n\t\t\tc.Set(\"Cross-Origin-Resource-Policy\", cfg.CrossOriginResourcePolicy)\n\t\t}\n\n\t\tif cfg.OriginAgentCluster != \"\" {\n\t\t\tc.Set(\"Origin-Agent-Cluster\", cfg.OriginAgentCluster)\n\t\t}\n\n\t\tif cfg.ReferrerPolicy != \"\" {\n\t\t\tc.Set(\"Referrer-Policy\", cfg.ReferrerPolicy)\n\t\t}\n\n\t\tif cfg.XDNSPrefetchControl != \"\" {\n\t\t\tc.Set(\"X-DNS-Prefetch-Control\", cfg.XDNSPrefetchControl)\n\t\t}\n\n\t\tif cfg.XDownloadOptions != \"\" {\n\t\t\tc.Set(\"X-Download-Options\", cfg.XDownloadOptions)\n\t\t}\n\n\t\tif cfg.XPermittedCrossDomain != \"\" {\n\t\t\tc.Set(\"X-Permitted-Cross-Domain-Policies\", cfg.XPermittedCrossDomain)\n\t\t}\n\n\t\t// Handle HSTS headers\n\t\tif c.Protocol() == \"https\" && cfg.HSTSMaxAge != 0 {\n\t\t\tsubdomains := \"\"\n\t\t\tif !cfg.HSTSExcludeSubdomains {\n\t\t\t\tsubdomains = \"; includeSubDomains\"\n\t\t\t}\n\t\t\tif cfg.HSTSPreloadEnabled {\n\t\t\t\tsubdomains += \"; preload\"\n\t\t\t}\n\t\t\tc.Set(fiber.HeaderStrictTransportSecurity, fmt.Sprintf(\"max-age=%d%s\", cfg.HSTSMaxAge, subdomains))\n\t\t}\n\n\t\t// Handle Content-Security-Policy headers\n\t\tif cfg.ContentSecurityPolicy != \"\" {\n\t\t\tif cfg.CSPReportOnly {\n\t\t\t\tc.Set(fiber.HeaderContentSecurityPolicyReportOnly, cfg.ContentSecurityPolicy)\n\t\t\t} else {\n\t\t\t\tc.Set(fiber.HeaderContentSecurityPolicy, cfg.ContentSecurityPolicy)\n\t\t\t}\n\t\t}\n\n\t\t// Handle Permissions-Policy headers\n\t\tif cfg.PermissionPolicy != \"\" {\n\t\t\tc.Set(fiber.HeaderPermissionsPolicy, cfg.PermissionPolicy)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n"
  },
  {
    "path": "middleware/helmet/helmet_test.go",
    "content": "package helmet\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_Default(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"0\", resp.Header.Get(fiber.HeaderXXSSProtection))\n\trequire.Equal(t, \"nosniff\", resp.Header.Get(fiber.HeaderXContentTypeOptions))\n\trequire.Equal(t, \"SAMEORIGIN\", resp.Header.Get(fiber.HeaderXFrameOptions))\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentSecurityPolicy))\n\trequire.Equal(t, \"no-referrer\", resp.Header.Get(fiber.HeaderReferrerPolicy))\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderPermissionsPolicy))\n\trequire.Equal(t, \"require-corp\", resp.Header.Get(\"Cross-Origin-Embedder-Policy\"))\n\trequire.Equal(t, \"same-origin\", resp.Header.Get(\"Cross-Origin-Opener-Policy\"))\n\trequire.Equal(t, \"same-origin\", resp.Header.Get(\"Cross-Origin-Resource-Policy\"))\n\trequire.Equal(t, \"?1\", resp.Header.Get(\"Origin-Agent-Cluster\"))\n\trequire.Equal(t, \"off\", resp.Header.Get(\"X-DNS-Prefetch-Control\"))\n\trequire.Equal(t, \"noopen\", resp.Header.Get(\"X-Download-Options\"))\n\trequire.Equal(t, \"none\", resp.Header.Get(\"X-Permitted-Cross-Domain-Policies\"))\n}\n\nfunc Test_CustomValues_AllHeaders(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\t// Custom values for all headers\n\t\tXSSProtection:             \"0\",\n\t\tContentTypeNosniff:        \"custom-nosniff\",\n\t\tXFrameOptions:             \"DENY\",\n\t\tHSTSExcludeSubdomains:     true,\n\t\tContentSecurityPolicy:     \"default-src 'none'\",\n\t\tCSPReportOnly:             true,\n\t\tHSTSPreloadEnabled:        true,\n\t\tReferrerPolicy:            \"origin\",\n\t\tPermissionPolicy:          \"geolocation=(self)\",\n\t\tCrossOriginEmbedderPolicy: \"custom-value\",\n\t\tCrossOriginOpenerPolicy:   \"custom-value\",\n\t\tCrossOriginResourcePolicy: \"custom-value\",\n\t\tOriginAgentCluster:        \"custom-value\",\n\t\tXDNSPrefetchControl:       \"custom-control\",\n\t\tXDownloadOptions:          \"custom-options\",\n\t\tXPermittedCrossDomain:     \"custom-policies\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\t// Assertions for custom header values\n\trequire.Equal(t, \"0\", resp.Header.Get(fiber.HeaderXXSSProtection))\n\trequire.Equal(t, \"custom-nosniff\", resp.Header.Get(fiber.HeaderXContentTypeOptions))\n\trequire.Equal(t, \"DENY\", resp.Header.Get(fiber.HeaderXFrameOptions))\n\trequire.Equal(t, \"default-src 'none'\", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly))\n\trequire.Equal(t, \"origin\", resp.Header.Get(fiber.HeaderReferrerPolicy))\n\trequire.Equal(t, \"geolocation=(self)\", resp.Header.Get(fiber.HeaderPermissionsPolicy))\n\trequire.Equal(t, \"custom-value\", resp.Header.Get(\"Cross-Origin-Embedder-Policy\"))\n\trequire.Equal(t, \"custom-value\", resp.Header.Get(\"Cross-Origin-Opener-Policy\"))\n\trequire.Equal(t, \"custom-value\", resp.Header.Get(\"Cross-Origin-Resource-Policy\"))\n\trequire.Equal(t, \"custom-value\", resp.Header.Get(\"Origin-Agent-Cluster\"))\n\trequire.Equal(t, \"custom-control\", resp.Header.Get(\"X-DNS-Prefetch-Control\"))\n\trequire.Equal(t, \"custom-options\", resp.Header.Get(\"X-Download-Options\"))\n\trequire.Equal(t, \"custom-policies\", resp.Header.Get(\"X-Permitted-Cross-Domain-Policies\"))\n}\n\nfunc Test_RealWorldValues_AllHeaders(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\t// Real-world values for all headers\n\t\tXSSProtection:             \"0\",\n\t\tContentTypeNosniff:        \"nosniff\",\n\t\tXFrameOptions:             \"SAMEORIGIN\",\n\t\tHSTSExcludeSubdomains:     false,\n\t\tContentSecurityPolicy:     \"default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests\",\n\t\tCSPReportOnly:             false,\n\t\tHSTSPreloadEnabled:        true,\n\t\tReferrerPolicy:            \"no-referrer\",\n\t\tPermissionPolicy:          \"geolocation=(self)\",\n\t\tCrossOriginEmbedderPolicy: \"require-corp\",\n\t\tCrossOriginOpenerPolicy:   \"same-origin\",\n\t\tCrossOriginResourcePolicy: \"same-origin\",\n\t\tOriginAgentCluster:        \"?1\",\n\t\tXDNSPrefetchControl:       \"off\",\n\t\tXDownloadOptions:          \"noopen\",\n\t\tXPermittedCrossDomain:     \"none\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\t// Assertions for real-world header values\n\trequire.Equal(t, \"0\", resp.Header.Get(fiber.HeaderXXSSProtection))\n\trequire.Equal(t, \"nosniff\", resp.Header.Get(fiber.HeaderXContentTypeOptions))\n\trequire.Equal(t, \"SAMEORIGIN\", resp.Header.Get(fiber.HeaderXFrameOptions))\n\trequire.Equal(t, \"default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests\", resp.Header.Get(fiber.HeaderContentSecurityPolicy))\n\trequire.Equal(t, \"no-referrer\", resp.Header.Get(fiber.HeaderReferrerPolicy))\n\trequire.Equal(t, \"geolocation=(self)\", resp.Header.Get(fiber.HeaderPermissionsPolicy))\n\trequire.Equal(t, \"require-corp\", resp.Header.Get(\"Cross-Origin-Embedder-Policy\"))\n\trequire.Equal(t, \"same-origin\", resp.Header.Get(\"Cross-Origin-Opener-Policy\"))\n\trequire.Equal(t, \"same-origin\", resp.Header.Get(\"Cross-Origin-Resource-Policy\"))\n\trequire.Equal(t, \"?1\", resp.Header.Get(\"Origin-Agent-Cluster\"))\n\trequire.Equal(t, \"off\", resp.Header.Get(\"X-DNS-Prefetch-Control\"))\n\trequire.Equal(t, \"noopen\", resp.Header.Get(\"X-Download-Options\"))\n\trequire.Equal(t, \"none\", resp.Header.Get(\"X-Permitted-Cross-Domain-Policies\"))\n}\n\nfunc Test_Next(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(ctx fiber.Ctx) bool {\n\t\t\treturn ctx.Path() == \"/next\"\n\t\t},\n\t\tReferrerPolicy: \"no-referrer\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\tapp.Get(\"/next\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Skipped!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"no-referrer\", resp.Header.Get(fiber.HeaderReferrerPolicy))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/next\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderReferrerPolicy))\n}\n\nfunc Test_ContentSecurityPolicy(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tContentSecurityPolicy: \"default-src 'none'\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"default-src 'none'\", resp.Header.Get(fiber.HeaderContentSecurityPolicy))\n}\n\nfunc Test_ContentSecurityPolicyReportOnly(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tContentSecurityPolicy: \"default-src 'none'\",\n\t\tCSPReportOnly:         true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"default-src 'none'\", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly))\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentSecurityPolicy))\n}\n\nfunc Test_PermissionsPolicy(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tPermissionPolicy: \"microphone=()\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"microphone=()\", resp.Header.Get(fiber.HeaderPermissionsPolicy))\n}\n\nfunc Test_HSTSHeaders(t *testing.T) {\n\thstsAge := 60\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{HSTSMaxAge: hstsAge}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\thandler := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetProtocol(\"https\")\n\n\thandler(ctx)\n\n\trequire.Equal(t, \"max-age=60; includeSubDomains\", string(ctx.Response.Header.Peek(fiber.HeaderStrictTransportSecurity)))\n\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetProtocol(\"http\")\n\n\thandler(ctx)\n\n\trequire.Empty(t, string(ctx.Response.Header.Peek(fiber.HeaderStrictTransportSecurity)))\n}\n\nfunc Test_HSTSExcludeSubdomainsAndPreload(t *testing.T) {\n\thstsAge := 31536000\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tHSTSMaxAge:            hstsAge,\n\t\tHSTSExcludeSubdomains: true,\n\t\tHSTSPreloadEnabled:    true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\thandler := app.Handler()\n\tctx := &fasthttp.RequestCtx{}\n\n\tctx.Request.SetRequestURI(\"/\")\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetProtocol(\"https\")\n\n\thandler(ctx)\n\n\trequire.Equal(t, \"max-age=31536000; preload\", string(ctx.Response.Header.Peek(fiber.HeaderStrictTransportSecurity)))\n}\n"
  },
  {
    "path": "middleware/idempotency/config.go",
    "content": "package idempotency\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n)\n\nvar ErrInvalidIdempotencyKey = errors.New(\"invalid idempotency key\")\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Lock locks an idempotency key.\n\t//\n\t// Optional. Default: an in-memory locker for this process only.\n\tLock Locker\n\n\t// Storage stores response data by idempotency key.\n\t//\n\t// Optional. Default: an in-memory storage for this process only.\n\tStorage fiber.Storage\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: a function which skips the middleware on safe HTTP request method.\n\tNext func(c fiber.Ctx) bool\n\n\t// KeyHeaderValidate defines a function to validate the syntax of the idempotency header.\n\t//\n\t// Optional. Default: a function which ensures the header is 36 characters long (the size of an UUID).\n\tKeyHeaderValidate func(string) error\n\n\t// KeyHeader is the name of the header that contains the idempotency key.\n\t//\n\t// Optional. Default: X-Idempotency-Key\n\tKeyHeader string\n\n\t// KeepResponseHeaders is a list of headers that should be kept from the original response.\n\t//\n\t// Optional. Default: nil (to keep all headers)\n\tKeepResponseHeaders []string\n\n\t// Lifetime is the maximum lifetime of an idempotency key.\n\t//\n\t// Optional. Default: 30 * time.Minute\n\tLifetime time.Duration\n\n\t// DisableValueRedaction turns off masking idempotency keys in logs and errors when set to true.\n\t//\n\t// Optional. Default: false\n\tDisableValueRedaction bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext: func(c fiber.Ctx) bool {\n\t\t// Skip middleware if the request was done using a safe HTTP method\n\t\treturn fiber.IsMethodSafe(c.Method())\n\t},\n\n\tLifetime: 30 * time.Minute,\n\n\tKeyHeader: \"X-Idempotency-Key\",\n\tKeyHeaderValidate: func(k string) error {\n\t\tif l, wl := len(k), 36; l != wl { // UUID length is 36 chars\n\t\t\treturn fmt.Errorf(\"%w: invalid length: %d != %d\", ErrInvalidIdempotencyKey, l, wl)\n\t\t}\n\n\t\treturn nil\n\t},\n\n\tKeepResponseHeaders: nil,\n\n\tLock: nil, // Set in configDefault so we don't allocate data here.\n\n\tStorage:               nil, // Set in configDefault so we don't allocate data here.\n\tDisableValueRedaction: false,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\tcfg := ConfigDefault\n\n\t\tcfg.Lock = NewMemoryLock()\n\t\tcfg.Storage = memory.New(memory.Config{\n\t\t\tGCInterval: cfg.Lifetime / 2, // Half the lifetime interval\n\t\t})\n\n\t\treturn cfg\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\n\tif cfg.Lifetime.Nanoseconds() == 0 {\n\t\tcfg.Lifetime = ConfigDefault.Lifetime\n\t}\n\n\tif cfg.KeyHeader == \"\" {\n\t\tcfg.KeyHeader = ConfigDefault.KeyHeader\n\t}\n\tif cfg.KeyHeaderValidate == nil {\n\t\tcfg.KeyHeaderValidate = ConfigDefault.KeyHeaderValidate\n\t}\n\n\tif cfg.KeepResponseHeaders != nil && len(cfg.KeepResponseHeaders) == 0 {\n\t\tcfg.KeepResponseHeaders = ConfigDefault.KeepResponseHeaders\n\t}\n\n\tif cfg.Lock == nil {\n\t\tcfg.Lock = NewMemoryLock()\n\t}\n\n\tif cfg.Storage == nil {\n\t\tcfg.Storage = memory.New(memory.Config{\n\t\t\tGCInterval: cfg.Lifetime / 2,\n\t\t})\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/idempotency/idempotency.go",
    "content": "package idempotency\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\n// Inspired by https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02\n// and https://github.com/penguin-statistics/backend-next/blob/f2f7d5ba54fc8a58f168d153baa17b2ad4a14e45/internal/pkg/middlewares/idempotency.go\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\nconst (\n\tlocalsKeyIsFromCache contextKey = iota //\n\tlocalsKeyWasPutToCache\n)\n\nconst redactedKey = \"[redacted]\"\n\n// IsFromCache reports whether the middleware served the response from the\n// cache for the current request.\nfunc IsFromCache(c fiber.Ctx) bool {\n\treturn c.Locals(localsKeyIsFromCache) != nil\n}\n\n// WasPutToCache reports whether the middleware stored the response produced by\n// the current request in the cache.\nfunc WasPutToCache(c fiber.Ctx) bool {\n\tval := c.Locals(localsKeyWasPutToCache)\n\tif wasPut, ok := val.(bool); ok {\n\t\treturn wasPut\n\t}\n\treturn val != nil\n}\n\n// New creates idempotency middleware that caches responses keyed by the\n// configured idempotency header.\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\tredactKeys := !cfg.DisableValueRedaction\n\n\tmaskKey := func(key string) string {\n\t\tif redactKeys {\n\t\t\treturn redactedKey\n\t\t}\n\t\treturn key\n\t}\n\n\tkeepResponseHeadersMap := make(map[string]struct{}, len(cfg.KeepResponseHeaders))\n\tfor _, h := range cfg.KeepResponseHeaders {\n\t\t// CopyString is needed because utils.ToLower uses UnsafeString\n\t\t// and map keys must be immutable\n\t\tkeepResponseHeadersMap[utilsstrings.ToLower(h)] = struct{}{}\n\t}\n\n\tmaybeWriteCachedResponse := func(c fiber.Ctx, key string) (bool, error) {\n\t\tif val, err := cfg.Storage.GetWithContext(c, key); err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to read response: %w\", err)\n\t\t} else if val != nil {\n\t\t\tvar res response\n\t\t\tif _, err := res.UnmarshalMsg(val); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t\t\t}\n\n\t\t\t_ = c.Status(res.StatusCode)\n\n\t\t\tfor header, vals := range res.Headers {\n\t\t\t\tfor _, val := range vals {\n\t\t\t\t\tc.RequestCtx().Response.Header.Add(header, val)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(res.Body) != 0 {\n\t\t\t\tif err := c.Send(res.Body); err != nil {\n\t\t\t\t\treturn true, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_ = c.Locals(localsKeyIsFromCache, true)\n\n\t\t\treturn true, nil\n\t\t}\n\n\t\treturn false, nil\n\t}\n\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Don't execute middleware if the idempotency key is empty\n\t\tif c.Get(cfg.KeyHeader) == \"\" {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Validate key\n\t\tkey := utils.CopyString(c.Get(cfg.KeyHeader))\n\t\tif err := cfg.KeyHeaderValidate(key); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// First-pass: if the idempotency key is in the storage, get and return the response\n\t\tif ok, err := maybeWriteCachedResponse(c, key); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write cached response at fastpath: %w\", err)\n\t\t} else if ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := cfg.Lock.Lock(key); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to lock: %w\", err)\n\t\t}\n\t\tdefer func() {\n\t\t\tif err := cfg.Lock.Unlock(key); err != nil {\n\t\t\t\tlog.Errorf(\"[IDEMPOTENCY] failed to unlock key %q: %v\", maskKey(key), err)\n\t\t\t}\n\t\t}()\n\n\t\t// Lock acquired. If the idempotency key now is in the storage, get and return the response\n\t\tif ok, err := maybeWriteCachedResponse(c, key); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write cached response while locked: %w\", err)\n\t\t} else if ok {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Execute the request handler\n\t\tif err := c.Next(); err != nil {\n\t\t\t// If the request handler returned an error, return it and skip idempotency\n\t\t\treturn err\n\t\t}\n\n\t\t// Construct response\n\t\tres := &response{\n\t\t\tStatusCode: c.Response().StatusCode(),\n\t\t\tBody:       c.Response().Body(),\n\t\t}\n\t\t{\n\t\t\theaders := make(map[string][]string)\n\t\t\tif err := c.Bind().RespHeader(headers); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to bind to response headers: %w\", err)\n\t\t\t}\n\n\t\t\tif cfg.KeepResponseHeaders == nil {\n\t\t\t\t// Keep all\n\t\t\t\tres.Headers = headers\n\t\t\t} else {\n\t\t\t\t// Filter\n\t\t\t\tres.Headers = make(map[string][]string)\n\t\t\t\tfor h := range headers {\n\t\t\t\t\tif _, ok := keepResponseHeadersMap[utilsstrings.ToLower(h)]; ok {\n\t\t\t\t\t\tres.Headers[h] = headers[h]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tbodyLimit := c.App().Config().BodyLimit\n\t\tif bodyLimit > 0 && len(res.Body) > bodyLimit {\n\t\t\t_ = c.Locals(localsKeyWasPutToCache, false)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Marshal response\n\t\tbs, err := res.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal response: %w\", err)\n\t\t}\n\n\t\t// Store response\n\t\tif err := cfg.Storage.SetWithContext(c, key, bs, cfg.Lifetime); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to save response: %w\", err)\n\t\t}\n\n\t\t_ = c.Locals(localsKeyWasPutToCache, true)\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "middleware/idempotency/idempotency_test.go",
    "content": "package idempotency\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst validKey = \"00000000-0000-0000-0000-000000000000\"\n\n// go test -run Test_Idempotency\nfunc Test_Idempotency(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tisMethodSafe := fiber.IsMethodSafe(c.Method())\n\t\tisIdempotent := IsFromCache(c) || WasPutToCache(c)\n\t\thasReqHeader := c.Get(\"X-Idempotency-Key\") != \"\"\n\n\t\tif isMethodSafe {\n\t\t\tif isIdempotent {\n\t\t\t\treturn errors.New(\"request with safe HTTP method should not be idempotent\")\n\t\t\t}\n\t\t} else {\n\t\t\t// Unsafe\n\t\t\tif hasReqHeader {\n\t\t\t\tif !isIdempotent {\n\t\t\t\t\treturn errors.New(\"request with unsafe HTTP method should be idempotent if X-Idempotency-Key request header is set\")\n\t\t\t\t}\n\t\t\t} else if isIdempotent {\n\t\t\t\treturn errors.New(\"request with unsafe HTTP method should not be idempotent if X-Idempotency-Key request header is not set\")\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// Needs to be at least a second as the memory storage doesn't support shorter durations.\n\tconst lifetime = 2 * time.Second\n\n\tapp.Use(New(Config{\n\t\tLifetime: lifetime,\n\t}))\n\n\tnextCount := func() func() int {\n\t\tvar count int32\n\t\treturn func() int {\n\t\t\treturn int(atomic.AddInt32(&count, 1))\n\t\t}\n\t}()\n\n\tapp.Add([]string{\n\t\tfiber.MethodGet,\n\t\tfiber.MethodPost,\n\t}, \"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(strconv.Itoa(nextCount()))\n\t})\n\n\tapp.Post(\"/slow\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(3 * lifetime)\n\n\t\treturn c.SendString(strconv.Itoa(nextCount()))\n\t})\n\n\tdoReq := func(method, route, idempotencyKey string) string {\n\t\treq := httptest.NewRequest(method, route, http.NoBody)\n\t\tif idempotencyKey != \"\" {\n\t\t\treq.Header.Set(\"X-Idempotency-Key\", idempotencyKey)\n\t\t}\n\t\tresp, err := app.Test(req, fiber.TestConfig{\n\t\t\tTimeout:       15 * time.Second,\n\t\t\tFailOnTimeout: true,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, string(body))\n\t\treturn string(body)\n\t}\n\n\trequire.Equal(t, \"1\", doReq(fiber.MethodGet, \"/\", \"\"))\n\trequire.Equal(t, \"2\", doReq(fiber.MethodGet, \"/\", \"\"))\n\n\trequire.Equal(t, \"3\", doReq(fiber.MethodPost, \"/\", \"\"))\n\trequire.Equal(t, \"4\", doReq(fiber.MethodPost, \"/\", \"\"))\n\n\trequire.Equal(t, \"5\", doReq(fiber.MethodGet, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\trequire.Equal(t, \"6\", doReq(fiber.MethodGet, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\n\trequire.Equal(t, \"7\", doReq(fiber.MethodPost, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\trequire.Equal(t, \"7\", doReq(fiber.MethodPost, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\trequire.Equal(t, \"8\", doReq(fiber.MethodPost, \"/\", \"\"))\n\trequire.Equal(t, \"9\", doReq(fiber.MethodPost, \"/\", \"11111111-1111-1111-1111-111111111111\"))\n\n\trequire.Equal(t, \"7\", doReq(fiber.MethodPost, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\ttime.Sleep(4 * lifetime)\n\trequire.Equal(t, \"10\", doReq(fiber.MethodPost, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\trequire.Equal(t, \"10\", doReq(fiber.MethodPost, \"/\", \"00000000-0000-0000-0000-000000000000\"))\n\n\t// Test raciness\n\t{\n\t\tvar wg sync.WaitGroup\n\t\tfor range 100 {\n\t\t\twg.Go(func() {\n\t\t\t\tassert.Equal(t, \"11\", doReq(fiber.MethodPost, \"/slow\", \"22222222-2222-2222-2222-222222222222\"))\n\t\t\t})\n\t\t}\n\t\twg.Wait()\n\t\trequire.Equal(t, \"11\", doReq(fiber.MethodPost, \"/slow\", \"22222222-2222-2222-2222-222222222222\"))\n\t}\n\ttime.Sleep(3 * lifetime)\n\trequire.Equal(t, \"12\", doReq(fiber.MethodPost, \"/slow\", \"22222222-2222-2222-2222-222222222222\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Idempotency -benchmem -count=4\nfunc Benchmark_Idempotency(b *testing.B) {\n\tapp := fiber.New()\n\n\t// Needs to be at least a second as the memory storage doesn't support shorter durations.\n\tconst lifetime = 1 * time.Second\n\n\tapp.Use(New(Config{\n\t\tLifetime: lifetime,\n\t}))\n\n\tapp.Post(\"/\", func(_ fiber.Ctx) error {\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.Run(\"hit\", func(b *testing.B) {\n\t\tc := &fasthttp.RequestCtx{}\n\t\tc.Request.Header.SetMethod(fiber.MethodPost)\n\t\tc.Request.SetRequestURI(\"/\")\n\t\tc.Request.Header.Set(\"X-Idempotency-Key\", \"00000000-0000-0000-0000-000000000000\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\th(c)\n\t\t}\n\t})\n\n\tb.Run(\"skip\", func(b *testing.B) {\n\t\tc := &fasthttp.RequestCtx{}\n\t\tc.Request.Header.SetMethod(fiber.MethodPost)\n\t\tc.Request.SetRequestURI(\"/\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\th(c)\n\t\t}\n\t})\n}\n\nfunc Test_configDefault_defaults(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault()\n\trequire.NotNil(t, cfg.Lock)\n\trequire.NotNil(t, cfg.Storage)\n\trequire.Equal(t, ConfigDefault.Lifetime, cfg.Lifetime)\n\trequire.Equal(t, ConfigDefault.KeyHeader, cfg.KeyHeader)\n\trequire.Nil(t, cfg.KeepResponseHeaders)\n\n\tapp := fiber.New()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx := app.AcquireCtx(fctx)\n\trequire.True(t, cfg.Next(ctx))\n\tapp.ReleaseCtx(ctx)\n\n\tfctx = &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx = app.AcquireCtx(fctx)\n\trequire.False(t, cfg.Next(ctx))\n\tapp.ReleaseCtx(ctx)\n\n\trequire.NoError(t, cfg.KeyHeaderValidate(validKey))\n\trequire.Error(t, cfg.KeyHeaderValidate(\"short\"))\n}\n\nfunc Test_configDefault_override(t *testing.T) {\n\tt.Parallel()\n\n\tl := &stubLock{}\n\ts := &stubStorage{}\n\n\tcfg := configDefault(Config{\n\t\tLifetime:            42 * time.Second,\n\t\tKeyHeader:           \"Foo\",\n\t\tKeepResponseHeaders: []string{},\n\t\tLock:                l,\n\t\tStorage:             s,\n\t})\n\n\trequire.Equal(t, 42*time.Second, cfg.Lifetime)\n\trequire.Equal(t, \"Foo\", cfg.KeyHeader)\n\trequire.Nil(t, cfg.KeepResponseHeaders)\n\trequire.Equal(t, l, cfg.Lock)\n\trequire.Equal(t, s, cfg.Storage)\n\trequire.NotNil(t, cfg.Next)\n\trequire.NotNil(t, cfg.KeyHeaderValidate)\n}\n\n// helper to perform request\nfunc do(app *fiber.App, req *http.Request) (resp *http.Response, body string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned response and body payload for clarity\n\tresp, err := app.Test(req, fiber.TestConfig{Timeout: 5 * time.Second})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpayload, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn resp, string(payload)\n}\n\nfunc Test_New_NextSkip(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tvar count int\n\n\tapp.Use(New(Config{Next: func(_ fiber.Ctx) bool { return true }}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(strconv.Itoa(count))\n\t})\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\t_, body1 := do(app, req)\n\n\treq2 := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq2.Header.Set(ConfigDefault.KeyHeader, validKey)\n\t_, body2 := do(app, req2)\n\n\trequire.Equal(t, \"1\", body1)\n\trequire.Equal(t, \"2\", body2)\n}\n\nfunc Test_New_InvalidKey(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\tapp.Post(\"/\", func(_ fiber.Ctx) error { return nil })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, \"bad\")\n\tresp, body := do(app, req)\n\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"invalid length\")\n}\n\nfunc Test_New_StorageGetError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{getErr: errors.New(\"boom\")}\n\tapp.Use(New(Config{Storage: s, Lock: &stubLock{}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"failed to write cached response at fastpath\")\n}\n\nfunc Test_New_UnmarshalError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{data: map[string][]byte{validKey: []byte(\"bad\")}}\n\tapp.Use(New(Config{Storage: s, Lock: &stubLock{}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"failed to write cached response at fastpath\")\n}\n\nfunc Test_New_StoreRetrieve_FilterHeaders(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{}\n\tapp.Use(New(Config{\n\t\tStorage:             s,\n\t\tLock:                &stubLock{},\n\t\tKeepResponseHeaders: []string{\"Foo\"},\n\t}))\n\n\tvar count int\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\tc.Set(\"Foo\", \"foo\")\n\t\tc.Set(\"Bar\", \"bar\")\n\t\treturn c.SendString(fmt.Sprintf(\"resp%d\", count))\n\t})\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, \"resp1\", body)\n\trequire.Equal(t, \"foo\", resp.Header.Get(\"Foo\"))\n\trequire.Equal(t, \"bar\", resp.Header.Get(\"Bar\"))\n\n\treq2 := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq2.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp2, body2 := do(app, req2)\n\trequire.Equal(t, \"resp1\", body2)\n\trequire.Equal(t, \"foo\", resp2.Header.Get(\"Foo\"))\n\trequire.Empty(t, resp2.Header.Get(\"Bar\"))\n\trequire.Equal(t, 1, count)\n\trequire.Equal(t, 1, s.setCount)\n}\n\nfunc Test_New_SkipCache_WhenBodyTooLarge(t *testing.T) {\n\tt.Parallel()\n\tbodyLimit := 8\n\tapp := fiber.New(fiber.Config{BodyLimit: bodyLimit})\n\ts := &stubStorage{}\n\tvar wasPut []bool\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tif err := c.Next(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\twasPut = append(wasPut, WasPutToCache(c))\n\t\treturn nil\n\t})\n\tapp.Use(New(Config{Storage: s, Lock: &stubLock{}}))\n\n\tvar count int\n\toversized := strings.Repeat(\"a\", bodyLimit+1)\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tcount++\n\t\treturn c.SendString(oversized)\n\t})\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp1, body1 := do(app, req)\n\trequire.Equal(t, fiber.StatusOK, resp1.StatusCode)\n\trequire.Equal(t, oversized, body1)\n\n\treq2 := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq2.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp2, body2 := do(app, req2)\n\trequire.Equal(t, fiber.StatusOK, resp2.StatusCode)\n\trequire.Equal(t, oversized, body2)\n\n\trequire.Equal(t, 2, count)\n\trequire.Equal(t, 0, s.setCount)\n\trequire.Len(t, wasPut, 2)\n\trequire.False(t, wasPut[0])\n\trequire.False(t, wasPut[1])\n}\n\nfunc Test_New_HandlerError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{}\n\tapp.Use(New(Config{Storage: s, Lock: &stubLock{}}))\n\tapp.Post(\"/\", func(_ fiber.Ctx) error { return errors.New(\"boom\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Equal(t, \"boom\", body)\n\trequire.Equal(t, 0, s.setCount)\n\n\tresp2, body2 := do(app, req)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp2.StatusCode)\n\trequire.Equal(t, \"boom\", body2)\n\trequire.Equal(t, 0, s.setCount)\n}\n\nfunc Test_New_LockError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tl := &stubLock{lockErr: errors.New(\"fail\")}\n\tapp.Use(New(Config{Lock: l, Storage: &stubStorage{}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"failed to lock\")\n}\n\nfunc Test_New_StorageSetError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{setErr: errors.New(\"nope\")}\n\tapp.Use(New(Config{Storage: s, Lock: &stubLock{}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"failed to save response\")\n}\n\nfunc Test_New_UnlockError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tl := &stubLock{unlockErr: errors.New(\"u\")}\n\tapp.Use(New(Config{Lock: l, Storage: &stubStorage{}}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"ok\", body)\n}\n\nfunc Test_New_SecondPassReadError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\ts := &stubStorage{}\n\tl := &stubLock{afterLock: func() { s.getErr = errors.New(\"g\") }}\n\tapp.Use(New(Config{Lock: l, Storage: s}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error { return c.SendString(\"ok\") })\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", http.NoBody)\n\treq.Header.Set(ConfigDefault.KeyHeader, validKey)\n\tresp, body := do(app, req)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Contains(t, body, \"failed to write cached response while locked\")\n}\n"
  },
  {
    "path": "middleware/idempotency/locker.go",
    "content": "package idempotency\n\nimport (\n\t\"sync\"\n)\n\n// Locker implements a spinlock for a string key.\ntype Locker interface {\n\tLock(key string) error\n\tUnlock(key string) error\n}\n\ntype countedLock struct {\n\tmu     sync.Mutex\n\tlocked int\n}\n\n// MemoryLock coordinates access to idempotency keys using in-memory locks.\ntype MemoryLock struct {\n\tkeys map[string]*countedLock\n\tmu   sync.Mutex\n}\n\n// Lock acquires the lock for the provided key, creating it when necessary.\nfunc (l *MemoryLock) Lock(key string) error {\n\tl.mu.Lock()\n\tlock, ok := l.keys[key]\n\tif !ok {\n\t\tlock = new(countedLock)\n\t\tl.keys[key] = lock\n\t}\n\tlock.locked++\n\tl.mu.Unlock()\n\n\tlock.mu.Lock()\n\n\treturn nil\n}\n\n// Unlock releases the lock associated with the provided key.\nfunc (l *MemoryLock) Unlock(key string) error {\n\tl.mu.Lock()\n\tlock, ok := l.keys[key]\n\tif !ok {\n\t\t// This happens if we try to unlock an unknown key\n\t\tl.mu.Unlock()\n\t\treturn nil\n\t}\n\tl.mu.Unlock()\n\n\tlock.mu.Unlock()\n\n\tl.mu.Lock()\n\tlock.locked--\n\tif lock.locked <= 0 {\n\t\t// This happens if countedLock is used to Lock and Unlock the same number of times\n\t\t// So, we can delete the key to prevent memory leak\n\t\tdelete(l.keys, key)\n\t}\n\tl.mu.Unlock()\n\n\treturn nil\n}\n\n// NewMemoryLock creates a MemoryLock ready for use.\nfunc NewMemoryLock() *MemoryLock {\n\treturn &MemoryLock{\n\t\tkeys: make(map[string]*countedLock),\n\t}\n}\n\nvar _ Locker = (*MemoryLock)(nil)\n"
  },
  {
    "path": "middleware/idempotency/locker_test.go",
    "content": "package idempotency_test\n\nimport (\n\t\"strconv\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3/middleware/idempotency\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_MemoryLock\nfunc Test_MemoryLock(t *testing.T) {\n\tt.Parallel()\n\n\tl := idempotency.NewMemoryLock()\n\n\t// Test that a lock can be acquired\n\t{\n\t\terr := l.Lock(\"a\")\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test that the same lock cannot be acquired again while held\n\t{\n\t\tdone := make(chan struct{})\n\t\tgo func() {\n\t\t\tdefer close(done)\n\n\t\t\terr := l.Lock(\"a\")\n\t\t\tassert.NoError(t, err)\n\t\t}()\n\n\t\tselect {\n\t\tcase <-done:\n\t\t\tt.Fatal(\"lock acquired again\")\n\t\tcase <-time.After(time.Second):\n\t\t\t// Expected: goroutine should still be blocked\n\t\t}\n\t}\n\n\t// Release lock \"a\" to prevent goroutine leak\n\t{\n\t\terr := l.Unlock(\"a\")\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test lock and unlock sequence\n\t{\n\t\terr := l.Lock(\"b\")\n\t\trequire.NoError(t, err)\n\t}\n\t{\n\t\terr := l.Unlock(\"b\")\n\t\trequire.NoError(t, err)\n\t}\n\t{\n\t\terr := l.Lock(\"b\")\n\t\trequire.NoError(t, err)\n\t}\n\t{\n\t\terr := l.Unlock(\"b\")\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test unlocking non-existent lock (should succeed)\n\t{\n\t\terr := l.Unlock(\"c\")\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Test another lock\n\t{\n\t\terr := l.Lock(\"d\")\n\t\trequire.NoError(t, err)\n\t}\n\t{\n\t\terr := l.Unlock(\"d\")\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc Benchmark_MemoryLock(b *testing.B) {\n\tkeys := make([]string, 50_000_000)\n\tfor i := range keys {\n\t\tkeys[i] = strconv.Itoa(i)\n\t}\n\n\tlock := idempotency.NewMemoryLock()\n\n\tfor i := 0; b.Loop(); i++ {\n\t\tkey := keys[i]\n\t\tif err := lock.Lock(key); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tif err := lock.Unlock(key); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_MemoryLock_Parallel(b *testing.B) {\n\t// In order to prevent using repeated keys I pre-allocate keys\n\tkeys := make([]string, 1_000_000)\n\tfor i := range keys {\n\t\tkeys[i] = strconv.Itoa(i)\n\t}\n\n\tb.Run(\"UniqueKeys\", func(b *testing.B) {\n\t\tlock := idempotency.NewMemoryLock()\n\t\tvar keyI atomic.Int32\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(p *testing.PB) {\n\t\t\tfor p.Next() {\n\t\t\t\ti := int(keyI.Add(1)) % len(keys)\n\t\t\t\tkey := keys[i]\n\t\t\t\tif err := lock.Lock(key); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif err := lock.Unlock(key); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"RepeatedKeys\", func(b *testing.B) {\n\t\tlock := idempotency.NewMemoryLock()\n\t\tvar keyI atomic.Int32\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(p *testing.PB) {\n\t\t\tfor p.Next() {\n\t\t\t\t// Division by 3 ensures that index will be repeated exactly 3 times\n\t\t\t\ti := int(keyI.Add(1)) / 3 % len(keys)\n\t\t\t\tkey := keys[i]\n\t\t\t\tif err := lock.Lock(key); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif err := lock.Unlock(key); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/idempotency/response.go",
    "content": "package idempotency\n\n// response is a struct that represents the response of a request.\n// generation tool `go install github.com/tinylib/msgp@latest`\n//\n// Idempotency payloads are stored in backing storage, so keep headers/bodies bounded.\n//\n//go:generate msgp -o=response_msgp.go -tests=true -unexported\ntype response struct {\n\tHeaders map[string][]string `msg:\"hs,limit=1024\"` // HTTP header count norms are well below this.\n\n\tBody       []byte `msg:\"b\"` // Idempotency bodies are bounded by storage policy, not msgp limits.\n\tStatusCode int    `msg:\"sc\"`\n}\n"
  },
  {
    "path": "middleware/idempotency/response_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage idempotency\n\nimport (\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *response) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"hs\":\n\t\t\tvar zb0002 uint32\n\t\t\tzb0002, err = dc.ReadMapHeader()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"Headers\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0002 > 1024 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.Headers == nil {\n\t\t\t\tz.Headers = make(map[string][]string, zb0002)\n\t\t\t} else if len(z.Headers) > 0 {\n\t\t\t\tclear(z.Headers)\n\t\t\t}\n\t\t\tfor zb0002 > 0 {\n\t\t\t\tzb0002--\n\t\t\t\tvar za0001 string\n\t\t\t\tza0001, err = dc.ReadString()\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"Headers\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar za0002 []string\n\t\t\t\tvar zb0003 uint32\n\t\t\t\tzb0003, err = dc.ReadArrayHeader()\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"Headers\", za0001)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif zb0003 > 1024 {\n\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif cap(za0002) >= int(zb0003) {\n\t\t\t\t\tza0002 = (za0002)[:zb0003]\n\t\t\t\t} else {\n\t\t\t\t\tza0002 = make([]string, zb0003)\n\t\t\t\t}\n\t\t\t\tfor za0003 := range za0002 {\n\t\t\t\t\tza0002[za0003], err = dc.ReadString()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = msgp.WrapError(err, \"Headers\", za0001, za0003)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tz.Headers[za0001] = za0002\n\t\t\t}\n\t\tcase \"b\":\n\t\t\tz.Body, err = dc.ReadBytes(z.Body)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"Body\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"sc\":\n\t\t\tz.StatusCode, err = dc.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"StatusCode\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z *response) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 3\n\t// write \"hs\"\n\terr = en.Append(0x83, 0xa2, 0x68, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteMapHeader(uint32(len(z.Headers)))\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"Headers\")\n\t\treturn\n\t}\n\tfor za0001, za0002 := range z.Headers {\n\t\terr = en.WriteString(za0001)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, \"Headers\")\n\t\t\treturn\n\t\t}\n\t\terr = en.WriteArrayHeader(uint32(len(za0002)))\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, \"Headers\", za0001)\n\t\t\treturn\n\t\t}\n\t\tfor za0003 := range za0002 {\n\t\t\terr = en.WriteString(za0002[za0003])\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"Headers\", za0001, za0003)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\t// write \"b\"\n\terr = en.Append(0xa1, 0x62)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBytes(z.Body)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"Body\")\n\t\treturn\n\t}\n\t// write \"sc\"\n\terr = en.Append(0xa2, 0x73, 0x63)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteInt(z.StatusCode)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"StatusCode\")\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z *response) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 3\n\t// string \"hs\"\n\to = append(o, 0x83, 0xa2, 0x68, 0x73)\n\to = msgp.AppendMapHeader(o, uint32(len(z.Headers)))\n\tfor za0001, za0002 := range z.Headers {\n\t\to = msgp.AppendString(o, za0001)\n\t\to = msgp.AppendArrayHeader(o, uint32(len(za0002)))\n\t\tfor za0003 := range za0002 {\n\t\t\to = msgp.AppendString(o, za0002[za0003])\n\t\t}\n\t}\n\t// string \"b\"\n\to = append(o, 0xa1, 0x62)\n\to = msgp.AppendBytes(o, z.Body)\n\t// string \"sc\"\n\to = append(o, 0xa2, 0x73, 0x63)\n\to = msgp.AppendInt(o, z.StatusCode)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *response) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"hs\":\n\t\t\tvar zb0002 uint32\n\t\t\tzb0002, bts, err = msgp.ReadMapHeaderBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"Headers\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif zb0002 > 1024 {\n\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif z.Headers == nil {\n\t\t\t\tz.Headers = make(map[string][]string, zb0002)\n\t\t\t} else if len(z.Headers) > 0 {\n\t\t\t\tclear(z.Headers)\n\t\t\t}\n\t\t\tfor zb0002 > 0 {\n\t\t\t\tvar za0002 []string\n\t\t\t\tzb0002--\n\t\t\t\tvar za0001 string\n\t\t\t\tza0001, bts, err = msgp.ReadStringBytes(bts)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"Headers\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar zb0003 uint32\n\t\t\t\tzb0003, bts, err = msgp.ReadArrayHeaderBytes(bts)\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = msgp.WrapError(err, \"Headers\", za0001)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif zb0003 > 1024 {\n\t\t\t\t\terr = msgp.ErrLimitExceeded\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif cap(za0002) >= int(zb0003) {\n\t\t\t\t\tza0002 = (za0002)[:zb0003]\n\t\t\t\t} else {\n\t\t\t\t\tza0002 = make([]string, zb0003)\n\t\t\t\t}\n\t\t\t\tfor za0003 := range za0002 {\n\t\t\t\t\tza0002[za0003], bts, err = msgp.ReadStringBytes(bts)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = msgp.WrapError(err, \"Headers\", za0001, za0003)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tz.Headers[za0001] = za0002\n\t\t\t}\n\t\tcase \"b\":\n\t\t\tz.Body, bts, err = msgp.ReadBytesBytes(bts, z.Body)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"Body\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"sc\":\n\t\t\tz.StatusCode, bts, err = msgp.ReadIntBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"StatusCode\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z *response) Msgsize() (s int) {\n\ts = 1 + 3 + msgp.MapHeaderSize\n\tif z.Headers != nil {\n\t\tfor za0001, za0002 := range z.Headers {\n\t\t\t_ = za0002\n\t\t\ts += msgp.StringPrefixSize + len(za0001) + msgp.ArrayHeaderSize\n\t\t\tfor za0003 := range za0002 {\n\t\t\t\ts += msgp.StringPrefixSize + len(za0002[za0003])\n\t\t\t}\n\t\t}\n\t}\n\ts += 2 + msgp.BytesPrefixSize + len(z.Body) + 3 + msgp.IntSize\n\treturn\n}\n"
  },
  {
    "path": "middleware/idempotency/response_msgp_test.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage idempotency\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\nfunc TestMarshalUnmarshalresponse(t *testing.T) {\n\tv := response{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgresponse(b *testing.B) {\n\tv := response{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgresponse(b *testing.B) {\n\tv := response{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalresponse(b *testing.B) {\n\tv := response{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecoderesponse(t *testing.T) {\n\tv := response{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecoderesponse Msgsize() is inaccurate\")\n\t}\n\n\tvn := response{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncoderesponse(b *testing.B) {\n\tv := response{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecoderesponse(b *testing.B) {\n\tv := response{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/idempotency/stub_test.go",
    "content": "package idempotency\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// stubLock implements Locker for testing purposes.\ntype stubLock struct {\n\tlockErr   error\n\tunlockErr error\n\tafterLock func()\n}\n\nfunc (s *stubLock) Lock(string) error {\n\tif s.afterLock != nil {\n\t\ts.afterLock()\n\t}\n\treturn s.lockErr\n}\nfunc (s *stubLock) Unlock(string) error { return s.unlockErr }\n\n// stubStorage implements fiber.Storage for testing.\ntype stubStorage struct {\n\tdata     map[string][]byte\n\tgetErr   error\n\tsetErr   error\n\tsetCount int\n}\n\nfunc (s *stubStorage) Get(key string) ([]byte, error) {\n\tif s.getErr != nil {\n\t\treturn nil, s.getErr\n\t}\n\tif s.data == nil {\n\t\treturn nil, nil\n\t}\n\treturn s.data[key], nil\n}\n\nfunc (s *stubStorage) GetWithContext(_ context.Context, key string) ([]byte, error) {\n\t// Call Get method to avoid code duplication\n\treturn s.Get(key)\n}\n\nfunc (s *stubStorage) Set(key string, val []byte, _ time.Duration) error {\n\tif s.setErr != nil {\n\t\treturn s.setErr\n\t}\n\tif s.data == nil {\n\t\ts.data = make(map[string][]byte)\n\t}\n\ts.data[key] = val\n\ts.setCount++\n\treturn nil\n}\n\nfunc (s *stubStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\t// Call Set method to avoid code duplication\n\treturn s.Set(key, val, 0)\n}\n\nfunc (s *stubStorage) Delete(key string) error {\n\tif s.data != nil {\n\t\tdelete(s.data, key)\n\t}\n\treturn nil\n}\n\nfunc (s *stubStorage) DeleteWithContext(_ context.Context, key string) error {\n\t// Call Delete method to avoid code duplication\n\treturn s.Delete(key)\n}\n\nfunc (s *stubStorage) Reset() error {\n\ts.data = make(map[string][]byte)\n\treturn nil\n}\n\nfunc (s *stubStorage) ResetWithContext(_ context.Context) error {\n\t// Call Reset method to avoid code duplication\n\treturn s.Reset()\n}\n\nfunc (*stubStorage) Close() error { return nil }\n"
  },
  {
    "path": "middleware/keyauth/config.go",
    "content": "package keyauth\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n)\n\nconst (\n\tErrorInvalidRequest    = \"invalid_request\"\n\tErrorInvalidToken      = \"invalid_token\"\n\tErrorInsufficientScope = \"insufficient_scope\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// SuccessHandler defines a function which is executed for a valid key.\n\t//\n\t// Optional. Default: c.Next()\n\tSuccessHandler fiber.Handler\n\n\t// ErrorHandler defines a function which is executed for an invalid key.\n\t// It may be used to define a custom error.\n\t//\n\t// Optional. Default: 401 Missing or invalid API Key\n\tErrorHandler fiber.ErrorHandler\n\n\t// Validator is a function to validate the key.\n\t//\n\t// Required.\n\tValidator func(c fiber.Ctx, key string) (bool, error)\n\n\t// Realm defines the protected area for WWW-Authenticate responses.\n\t// This is used to set the `WWW-Authenticate` header when authentication fails.\n\t//\n\t// Optional. Default value \"Restricted\".\n\tRealm string\n\n\t// Challenge defines the full `WWW-Authenticate` header value used when\n\t// the middleware responds with 401 and no Authorization scheme is\n\t// present.\n\t//\n\t// Optional. Default: `ApiKey realm=\"<Realm>\"` when no Authorization scheme\n\t// is configured.\n\tChallenge string\n\n\t// Error is the RFC 6750 `error` parameter appended to Bearer\n\t// `WWW-Authenticate` challenges when validation fails. Allowed values\n\t// are `invalid_request`, `invalid_token`, or `insufficient_scope`.\n\t//\n\t// Optional. Default: \"\".\n\tError string\n\n\t// ErrorDescription is the RFC 6750 `error_description` parameter\n\t// appended to Bearer `WWW-Authenticate` challenges when validation\n\t// fails. This field requires that `Error` is also set.\n\t//\n\t// Optional. Default: \"\".\n\tErrorDescription string\n\n\t// ErrorURI is the RFC 6750 `error_uri` parameter appended to Bearer\n\t// `WWW-Authenticate` challenges when validation fails. This field\n\t// requires that `Error` is also set.\n\t//\n\t// Optional. Default: \"\".\n\tErrorURI string\n\n\t// Scope is the RFC 6750 `scope` parameter appended to Bearer\n\t// challenges when the `error` is `insufficient_scope`. This field\n\t// requires that `Error` is set to `insufficient_scope`.\n\t//\n\t// Optional. Default: \"\".\n\tScope string\n\n\t// Extractor is a function to extract the key from the request.\n\t//\n\t// Optional. Default: extractors.FromAuthHeader(\"Bearer\")\n\tExtractor extractors.Extractor\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tSuccessHandler: func(c fiber.Ctx) error {\n\t\treturn c.Next()\n\t},\n\tErrorHandler: func(c fiber.Ctx, _ error) error {\n\t\treturn c.Status(fiber.StatusUnauthorized).SendString(ErrMissingOrMalformedAPIKey.Error())\n\t},\n\tRealm:     \"Restricted\",\n\tExtractor: extractors.FromAuthHeader(\"Bearer\"),\n}\n\n// configDefault is a helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\tpanic(\"fiber: keyauth middleware requires a validator function\")\n\t}\n\tcfg := config[0]\n\n\t// Require a validator function\n\tif cfg.Validator == nil {\n\t\tpanic(\"fiber: keyauth middleware requires a validator function\")\n\t}\n\n\t// Set default values\n\tif cfg.Extractor.Extract == nil {\n\t\tcfg.Extractor = ConfigDefault.Extractor\n\t}\n\tif cfg.Realm == \"\" {\n\t\tcfg.Realm = ConfigDefault.Realm\n\t}\n\tif cfg.SuccessHandler == nil {\n\t\tcfg.SuccessHandler = ConfigDefault.SuccessHandler\n\t}\n\tif cfg.ErrorHandler == nil {\n\t\tcfg.ErrorHandler = ConfigDefault.ErrorHandler\n\t}\n\n\tif len(getAuthSchemes(cfg.Extractor)) == 0 && cfg.Challenge == \"\" {\n\t\tcfg.Challenge = fmt.Sprintf(\"ApiKey realm=%q\", cfg.Realm)\n\t}\n\n\tif cfg.Error != \"\" {\n\t\tswitch cfg.Error {\n\t\tcase ErrorInvalidRequest, ErrorInvalidToken, ErrorInsufficientScope:\n\t\tdefault:\n\t\t\tpanic(\"fiber: keyauth unsupported error token\")\n\t\t}\n\t}\n\tif cfg.ErrorDescription != \"\" && cfg.Error == \"\" {\n\t\tpanic(\"fiber: keyauth error_description requires error\")\n\t}\n\tif cfg.ErrorURI != \"\" {\n\t\tif cfg.Error == \"\" {\n\t\t\tpanic(\"fiber: keyauth error_uri requires error\")\n\t\t}\n\t\tif u, err := url.Parse(cfg.ErrorURI); err != nil || !u.IsAbs() {\n\t\t\tpanic(\"fiber: keyauth error_uri must be absolute\")\n\t\t}\n\t}\n\tif cfg.Error == ErrorInsufficientScope {\n\t\tif cfg.Scope == \"\" {\n\t\t\tpanic(\"fiber: keyauth insufficient_scope requires scope\")\n\t\t}\n\t\tfor scope := range strings.SplitSeq(cfg.Scope, \" \") {\n\t\t\tif scope == \"\" || !isScopeToken(scope) {\n\t\t\t\tpanic(\"fiber: keyauth scope contains invalid token\")\n\t\t\t}\n\t\t}\n\t} else if cfg.Scope != \"\" {\n\t\tpanic(\"fiber: keyauth scope requires insufficient_scope error\")\n\t}\n\n\treturn cfg\n}\n\nfunc isScopeToken(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tc := s[i]\n\t\tif c < 0x21 || c > 0x7e || c == '\"' || c == '\\\\' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn s != \"\"\n}\n"
  },
  {
    "path": "middleware/keyauth/config_test.go",
    "content": "package keyauth\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Test_KeyAuth_ConfigDefault_NoConfig tests the case where no config is provided.\nfunc Test_KeyAuth_ConfigDefault_NoConfig(t *testing.T) {\n\tt.Parallel()\n\t// The New function will call configDefault with no arguments\n\t// which will panic because ConfigDefault.Validator is nil.\n\tassert.PanicsWithValue(t, \"fiber: keyauth middleware requires a validator function\", func() {\n\t\tNew()\n\t}, \"Calling New() without a validator should panic\")\n}\n\n// Test_KeyAuth_ConfigDefault_PanicWithoutValidator tests that configDefault panics when Validator is nil.\nfunc Test_KeyAuth_ConfigDefault_PanicWithoutValidator(t *testing.T) {\n\tt.Parallel()\n\tassert.PanicsWithValue(t, \"fiber: keyauth middleware requires a validator function\", func() {\n\t\tconfigDefault(Config{})\n\t}, \"configDefault should panic if validator is not provided\")\n}\n\n// Test_KeyAuth_ConfigDefault_WithValidator tests that default values are set when only a validator is provided.\nfunc Test_KeyAuth_ConfigDefault_WithValidator(t *testing.T) {\n\tt.Parallel()\n\tvalidator := func(fiber.Ctx, string) (bool, error) { return true, nil }\n\tcfg := configDefault(Config{\n\t\tValidator: validator,\n\t})\n\n\trequire.NotNil(t, cfg.Validator)\n\tassert.Equal(t, ConfigDefault.Realm, cfg.Realm)\n\trequire.NotNil(t, cfg.SuccessHandler)\n\trequire.NotNil(t, cfg.ErrorHandler)\n\trequire.NotNil(t, cfg.Extractor.Extract)\n}\n\n// Test_KeyAuth_ConfigDefault_CustomConfig tests that custom values are preserved.\nfunc Test_KeyAuth_ConfigDefault_CustomConfig(t *testing.T) {\n\tt.Parallel()\n\tnextFunc := func(_ fiber.Ctx) bool { return true }\n\tsuccessHandler := func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }\n\terrorHandler := func(c fiber.Ctx, _ error) error { return c.SendStatus(fiber.StatusForbidden) }\n\tvalidator := func(_ fiber.Ctx, _ string) (bool, error) { return true, nil }\n\textractor := extractors.FromHeader(\"X-API-Key\")\n\n\tcfg := configDefault(Config{\n\t\tNext:           nextFunc,\n\t\tSuccessHandler: successHandler,\n\t\tErrorHandler:   errorHandler,\n\t\tValidator:      validator,\n\t\tRealm:          \"API\",\n\t\tExtractor:      extractor,\n\t})\n\n\t// Using reflect.ValueOf to compare function pointers\n\tassert.Equal(t, reflect.ValueOf(nextFunc).Pointer(), reflect.ValueOf(cfg.Next).Pointer())\n\tassert.Equal(t, reflect.ValueOf(successHandler).Pointer(), reflect.ValueOf(cfg.SuccessHandler).Pointer())\n\tassert.Equal(t, reflect.ValueOf(errorHandler).Pointer(), reflect.ValueOf(cfg.ErrorHandler).Pointer())\n\tassert.Equal(t, reflect.ValueOf(validator).Pointer(), reflect.ValueOf(cfg.Validator).Pointer())\n\tassert.Equal(t, reflect.ValueOf(extractor.Extract).Pointer(), reflect.ValueOf(cfg.Extractor.Extract).Pointer())\n\n\tassert.Equal(t, \"API\", cfg.Realm)\n}\n"
  },
  {
    "path": "middleware/keyauth/keyauth.go",
    "content": "package keyauth\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\n// The keys for the values in context\nconst (\n\ttokenKey contextKey = iota\n)\n\n// ErrMissingOrMalformedAPIKey is returned when the API key is missing or invalid.\nvar ErrMissingOrMalformedAPIKey = errors.New(\"missing or invalid API Key\")\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Init config\n\tcfg := configDefault(config...)\n\n\t// Determine the auth schemes from the extractor chain.\n\tauthSchemes := getAuthSchemes(cfg.Extractor)\n\n\t// Return middleware handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Filter request to skip middleware\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Extract and verify key\n\t\tkey, err := cfg.Extractor.Extract(c)\n\t\tif errors.Is(err, extractors.ErrNotFound) {\n\t\t\t// Replace shared extractor not found error with a keyauth specific error\n\t\t\terr = ErrMissingOrMalformedAPIKey\n\t\t}\n\t\t// If there was no error extracting the key, validate it\n\t\tif err == nil {\n\t\t\tvar valid bool\n\t\t\tvalid, err = cfg.Validator(c, key)\n\t\t\tif err == nil && valid {\n\t\t\t\tfiber.StoreInContext(c, tokenKey, key)\n\t\t\t\treturn cfg.SuccessHandler(c)\n\t\t\t}\n\t\t}\n\n\t\t// Execute the error handler first\n\t\thandlerErr := cfg.ErrorHandler(c, err)\n\n\t\tstatus := c.Response().StatusCode()\n\t\tif status == fiber.StatusUnauthorized || status == fiber.StatusProxyAuthRequired {\n\t\t\theader := fiber.HeaderWWWAuthenticate\n\t\t\tif status == fiber.StatusProxyAuthRequired {\n\t\t\t\theader = fiber.HeaderProxyAuthenticate\n\t\t\t}\n\t\t\tif len(authSchemes) > 0 {\n\t\t\t\tchallenges := make([]string, 0, len(authSchemes))\n\t\t\t\tfor _, scheme := range authSchemes {\n\t\t\t\t\tvar b strings.Builder\n\t\t\t\t\tfmt.Fprintf(&b, \"%s realm=%q\", scheme, cfg.Realm)\n\t\t\t\t\tif utils.EqualFold(scheme, \"Bearer\") {\n\t\t\t\t\t\tif cfg.Error != \"\" {\n\t\t\t\t\t\t\tfmt.Fprintf(&b, \", error=%q\", cfg.Error)\n\t\t\t\t\t\t\tif cfg.ErrorDescription != \"\" {\n\t\t\t\t\t\t\t\tfmt.Fprintf(&b, \", error_description=%q\", cfg.ErrorDescription)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif cfg.ErrorURI != \"\" {\n\t\t\t\t\t\t\t\tfmt.Fprintf(&b, \", error_uri=%q\", cfg.ErrorURI)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif cfg.Error == ErrorInsufficientScope {\n\t\t\t\t\t\t\t\tfmt.Fprintf(&b, \", scope=%q\", cfg.Scope)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tchallenges = append(challenges, b.String())\n\t\t\t\t}\n\t\t\t\tc.Set(header, strings.Join(challenges, \", \"))\n\t\t\t} else if cfg.Challenge != \"\" {\n\t\t\t\tc.Set(header, cfg.Challenge)\n\t\t\t}\n\t\t}\n\n\t\treturn handlerErr\n\t}\n}\n\n// TokenFromContext returns the bearer token from the request context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// It returns an empty string if the token does not exist.\nfunc TokenFromContext(ctx any) string {\n\tif token, ok := fiber.ValueFromContext[string](ctx, tokenKey); ok {\n\t\treturn token\n\t}\n\n\treturn \"\"\n}\n\n// getAuthSchemes inspects an extractor and its chain to find all auth schemes\n// used by FromAuthHeader. It returns a slice of schemes, or an empty slice if\n// none are found.\nfunc getAuthSchemes(e extractors.Extractor) []string {\n\tvar schemes []string\n\tif e.Source == extractors.SourceAuthHeader && e.AuthScheme != \"\" {\n\t\tschemes = append(schemes, e.AuthScheme)\n\t}\n\tfor _, ex := range e.Chain {\n\t\tschemes = append(schemes, getAuthSchemes(ex)...)\n\t}\n\treturn schemes\n}\n"
  },
  {
    "path": "middleware/keyauth/keyauth_test.go",
    "content": "package keyauth\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n)\n\nconst CorrectKey = \"correct-token_123./~+\"\n\nvar testConfig = fiber.TestConfig{\n\tTimeout: 0,\n}\n\nconst (\n\tparamExtractorName      = \"param\"\n\tformExtractorName       = \"form\"\n\tqueryExtractorName      = \"query\"\n\theaderExtractorName     = \"header\"\n\tauthHeaderExtractorName = \"authHeader\"\n\tcookieExtractorName     = \"cookie\"\n)\n\nfunc Test_AuthSources(t *testing.T) {\n\t// define test cases\n\ttestSources := []string{headerExtractorName, authHeaderExtractorName, cookieExtractorName, queryExtractorName, paramExtractorName, formExtractorName}\n\n\ttests := []struct {\n\t\troute         string\n\t\tauthTokenName string\n\t\tdescription   string\n\t\tAPIKey        string\n\t\texpectedBody  string\n\t\texpectedCode  int\n\t}{\n\t\t{\n\t\t\troute:         \"/\",\n\t\t\tauthTokenName: \"access_token\",\n\t\t\tdescription:   \"auth with correct key\",\n\t\t\tAPIKey:        CorrectKey,\n\t\t\texpectedCode:  200,\n\t\t\texpectedBody:  \"Success!\",\n\t\t},\n\t\t{\n\t\t\troute:         \"/\",\n\t\t\tauthTokenName: \"access_token\",\n\t\t\tdescription:   \"auth with no key\",\n\t\t\tAPIKey:        \"\",\n\t\t\texpectedCode:  401, // 404 in case of param authentication\n\t\t\texpectedBody:  ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\t\t{\n\t\t\troute:         \"/\",\n\t\t\tauthTokenName: \"access_token\",\n\t\t\tdescription:   \"auth with wrong key\",\n\t\t\tAPIKey:        \"WRONGKEY\",\n\t\t\texpectedCode:  401,\n\t\t\texpectedBody:  ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\t}\n\n\tfor _, authSource := range testSources {\n\t\tt.Run(authSource, func(t *testing.T) {\n\t\t\tfor _, test := range tests {\n\t\t\t\tapp := fiber.New(fiber.Config{UnescapePath: true})\n\n\t\t\t\ttestKey := test.APIKey\n\t\t\t\tcorrectKey := CorrectKey\n\n\t\t\t\t// Use a simple key for param and cookie to avoid encoding issues in the test setup\n\t\t\t\tif authSource == paramExtractorName || authSource == cookieExtractorName {\n\t\t\t\t\tif test.APIKey != \"\" && test.APIKey != \"WRONGKEY\" {\n\t\t\t\t\t\ttestKey = \"simple-key\"\n\t\t\t\t\t\tcorrectKey = \"simple-key\"\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tauthMiddleware := New(Config{\n\t\t\t\t\tExtractor: func() extractors.Extractor {\n\t\t\t\t\t\tswitch authSource {\n\t\t\t\t\t\tcase headerExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromHeader(test.authTokenName)\n\t\t\t\t\t\tcase authHeaderExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromAuthHeader(\"Bearer\")\n\t\t\t\t\t\tcase cookieExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromCookie(test.authTokenName)\n\t\t\t\t\t\tcase queryExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromQuery(test.authTokenName)\n\t\t\t\t\t\tcase paramExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromParam(test.authTokenName)\n\t\t\t\t\t\tcase formExtractorName:\n\t\t\t\t\t\t\treturn extractors.FromForm(test.authTokenName)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tpanic(\"unknown source\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}(),\n\t\t\t\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\t\t\t\tif key == correctKey {\n\t\t\t\t\t\t\treturn true, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn false, errors.New(\"invalid key\")\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\thandler := func(c fiber.Ctx) error {\n\t\t\t\t\treturn c.SendString(\"Success!\")\n\t\t\t\t}\n\n\t\t\t\tmethod := fiber.MethodGet\n\t\t\t\tswitch authSource {\n\t\t\t\tcase paramExtractorName:\n\t\t\t\t\tapp.Get(\"/:\"+test.authTokenName, authMiddleware, handler)\n\t\t\t\tcase formExtractorName:\n\t\t\t\t\tmethod = fiber.MethodPost\n\t\t\t\t\tapp.Post(\"/\", authMiddleware, handler)\n\t\t\t\tdefault:\n\t\t\t\t\tapp.Get(\"/\", authMiddleware, handler)\n\t\t\t\t}\n\n\t\t\t\ttargetURL := \"/\"\n\t\t\t\tif authSource == paramExtractorName {\n\t\t\t\t\ttargetURL = \"/\" + url.PathEscape(testKey)\n\t\t\t\t}\n\n\t\t\t\tvar reqBody io.Reader\n\t\t\t\tif authSource == formExtractorName {\n\t\t\t\t\tform := url.Values{}\n\t\t\t\t\tform.Add(test.authTokenName, testKey)\n\t\t\t\t\tbodyStr := form.Encode()\n\t\t\t\t\treqBody = strings.NewReader(bodyStr)\n\t\t\t\t}\n\n\t\t\t\treq, err := http.NewRequestWithContext(context.Background(), method, targetURL, reqBody)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tswitch authSource {\n\t\t\t\tcase headerExtractorName:\n\t\t\t\t\treq.Header.Set(test.authTokenName, testKey)\n\t\t\t\tcase authHeaderExtractorName:\n\t\t\t\t\tif testKey != \"\" {\n\t\t\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+testKey)\n\t\t\t\t\t}\n\t\t\t\tcase cookieExtractorName:\n\t\t\t\t\treq.Header.Set(\"Cookie\", test.authTokenName+\"=\"+testKey)\n\t\t\t\tcase queryExtractorName:\n\t\t\t\t\tq := req.URL.Query()\n\t\t\t\t\tq.Add(test.authTokenName, testKey)\n\t\t\t\t\treq.URL.RawQuery = q.Encode()\n\t\t\t\tcase formExtractorName:\n\t\t\t\t\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\t\t\tdefault:\n\t\t\t\t\t// nothing to do for paramExtractorName\n\t\t\t\t}\n\n\t\t\t\tres, err := app.Test(req, testConfig)\n\t\t\t\trequire.NoError(t, err, test.description)\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terrClose := res.Body.Close()\n\t\t\t\trequire.NoError(t, errClose)\n\n\t\t\t\texpectedCode := test.expectedCode\n\t\t\t\texpectedBody := test.expectedBody\n\t\t\t\tif test.APIKey == \"\" || test.APIKey == \"WRONGKEY\" {\n\t\t\t\t\texpectedBody = ErrMissingOrMalformedAPIKey.Error()\n\t\t\t\t}\n\n\t\t\t\tif authSource == paramExtractorName && testKey == \"\" {\n\t\t\t\t\texpectedCode = 404\n\t\t\t\t\texpectedBody = \"Not Found\"\n\t\t\t\t}\n\t\t\t\trequire.Equal(t, expectedCode, res.StatusCode, test.description)\n\t\t\t\trequire.Equal(t, expectedBody, string(body), test.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMultipleKeyLookup(t *testing.T) {\n\tconst (\n\t\tdesc    = \"auth with correct key\"\n\t\tsuccess = \"Success!\"\n\t\tscheme  = \"Bearer\"\n\t)\n\n\t// set up the fiber endpoint\n\tapp := fiber.New()\n\n\tcustomExtractor := extractors.Chain(\n\t\textractors.FromAuthHeader(\"Bearer\"),\n\t\textractors.FromHeader(\"key\"),\n\t\textractors.FromCookie(\"key\"),\n\t\textractors.FromQuery(\"key\"),\n\t)\n\n\tauthMiddleware := New(Config{\n\t\tExtractor: customExtractor,\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, errors.New(\"invalid key\")\n\t\t},\n\t})\n\tapp.Use(authMiddleware)\n\tapp.Get(\"/foo\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(success)\n\t})\n\n\t// construct the test HTTP request\n\tvar (\n\t\treq *http.Request\n\t\terr error\n\t)\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/foo\", http.NoBody)\n\trequire.NoError(t, err)\n\tq := req.URL.Query()\n\tq.Add(\"key\", CorrectKey)\n\treq.URL.RawQuery = q.Encode()\n\n\tres, err := app.Test(req, testConfig)\n\n\trequire.NoError(t, err)\n\n\t// test the body of the request\n\tbody, err := io.ReadAll(res.Body)\n\trequire.Equal(t, 200, res.StatusCode, desc)\n\t// body\n\trequire.NoError(t, err)\n\trequire.Equal(t, success, string(body), desc)\n\n\terr = res.Body.Close()\n\trequire.NoError(t, err)\n\n\t// construct a second request without proper key\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/foo\", http.NoBody)\n\trequire.NoError(t, err)\n\tres, err = app.Test(req, testConfig)\n\trequire.NoError(t, err)\n\terrBody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(errBody))\n}\n\nfunc Test_MultipleKeyAuth(t *testing.T) {\n\t// set up the fiber endpoint\n\tapp := fiber.New()\n\n\t// set up keyauth for /auth1\n\tapp.Use(New(Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Path() != \"/auth1\"\n\t\t},\n\t\tExtractor: extractors.FromAuthHeader(\"Bearer\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == \"password1\" {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, errors.New(\"invalid key\")\n\t\t},\n\t}))\n\n\t// setup keyauth for /auth2\n\tapp.Use(New(Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Path() != \"/auth2\"\n\t\t},\n\t\tExtractor: extractors.FromAuthHeader(\"Bearer\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == \"password2\" {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, errors.New(\"invalid key\")\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"No auth needed!\")\n\t})\n\n\tapp.Get(\"/auth1\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Successfully authenticated for auth1!\")\n\t})\n\n\tapp.Get(\"/auth2\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Successfully authenticated for auth2!\")\n\t})\n\n\t// define test cases\n\ttests := []struct {\n\t\troute        string\n\t\tdescription  string\n\t\tAPIKey       string\n\t\texpectedBody string\n\t\texpectedCode int\n\t}{\n\t\t// No auth needed for /\n\t\t{\n\t\t\troute:        \"/\",\n\t\t\tdescription:  \"No password needed\",\n\t\t\tAPIKey:       \"\",\n\t\t\texpectedCode: 200,\n\t\t\texpectedBody: \"No auth needed!\",\n\t\t},\n\n\t\t// auth needed for auth1\n\t\t{\n\t\t\troute:        \"/auth1\",\n\t\t\tdescription:  \"Normal Authentication Case\",\n\t\t\tAPIKey:       \"password1\",\n\t\t\texpectedCode: 200,\n\t\t\texpectedBody: \"Successfully authenticated for auth1!\",\n\t\t},\n\t\t{\n\t\t\troute:        \"/auth1\",\n\t\t\tdescription:  \"Wrong API Key\",\n\t\t\tAPIKey:       \"WRONG KEY\",\n\t\t\texpectedCode: 401,\n\t\t\texpectedBody: ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\t\t{\n\t\t\troute:        \"/auth1\",\n\t\t\tdescription:  \"Wrong API Key\",\n\t\t\tAPIKey:       \"\", // NO KEY\n\t\t\texpectedCode: 401,\n\t\t\texpectedBody: ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\n\t\t// Auth 2 has a different password\n\t\t{\n\t\t\troute:        \"/auth2\",\n\t\t\tdescription:  \"Normal Authentication Case for auth2\",\n\t\t\tAPIKey:       \"password2\",\n\t\t\texpectedCode: 200,\n\t\t\texpectedBody: \"Successfully authenticated for auth2!\",\n\t\t},\n\t\t{\n\t\t\troute:        \"/auth2\",\n\t\t\tdescription:  \"Wrong API Key\",\n\t\t\tAPIKey:       \"WRONG KEY\",\n\t\t\texpectedCode: 401,\n\t\t\texpectedBody: ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\t\t{\n\t\t\troute:        \"/auth2\",\n\t\t\tdescription:  \"Wrong API Key\",\n\t\t\tAPIKey:       \"\", // NO KEY\n\t\t\texpectedCode: 401,\n\t\t\texpectedBody: ErrMissingOrMalformedAPIKey.Error(),\n\t\t},\n\t}\n\n\t// run the tests\n\tfor _, test := range tests {\n\t\tvar req *http.Request\n\t\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, test.route, http.NoBody)\n\t\trequire.NoError(t, err)\n\t\tif test.APIKey != \"\" {\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+test.APIKey)\n\t\t}\n\n\t\tres, err := app.Test(req, testConfig)\n\n\t\trequire.NoError(t, err, test.description)\n\n\t\t// test the body of the request\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.Equal(t, test.expectedCode, res.StatusCode, test.description)\n\n\t\t// body\n\t\trequire.NoError(t, err, test.description)\n\t\trequire.Equal(t, test.expectedBody, string(body), test.description)\n\t}\n}\n\nfunc Test_CustomSuccessAndFailureHandlers(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tSuccessHandler: func(c fiber.Ctx) error {\n\t\t\treturn c.Status(fiber.StatusOK).SendString(\"API key is valid and request was handled by custom success handler\")\n\t\t},\n\t\tErrorHandler: func(c fiber.Ctx, _ error) error {\n\t\t\treturn c.Status(fiber.StatusUnauthorized).SendString(\"API key is invalid and request was handled by custom error handler\")\n\t\t},\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\n\t// Define a test handler that should not be called\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\tt.Error(\"Test handler should not be called\")\n\t\treturn nil\n\t})\n\n\t// Create a request without an API key and send it to the app\n\tres, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, \"API key is invalid and request was handled by custom error handler\", string(body))\n\n\t// Create a request with a valid API key in the Authorization header\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+CorrectKey)\n\n\t// Send the request to the app\n\tres, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err = io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"API key is valid and request was handled by custom success handler\", string(body))\n}\n\nfunc Test_CustomNextFunc(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Path() == \"/allowed\"\n\t\t},\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\n\t// Define a test handler\n\tapp.Get(\"/allowed\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"API key is valid and request was allowed by custom filter\")\n\t})\n\tapp.Get(\"/not-allowed\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Should be protected\")\n\t})\n\n\t// Create a request with the \"/allowed\" path and send it to the app\n\treq := httptest.NewRequest(fiber.MethodGet, \"/allowed\", http.NoBody)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"API key is valid and request was allowed by custom filter\", string(body))\n\n\t// Create a request with a different path and send it to the app without correct key\n\treq = httptest.NewRequest(fiber.MethodGet, \"/not-allowed\", http.NoBody)\n\tres, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err = io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n\n\t// Create a request with a different path and send it to the app with correct key\n\treq = httptest.NewRequest(fiber.MethodGet, \"/not-allowed\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+CorrectKey)\n\n\tres, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err = io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"Should be protected\", string(body))\n}\n\nfunc Test_TokenFromContext_None(t *testing.T) {\n\tapp := fiber.New()\n\t// Define a test handler that checks TokenFromContext\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(TokenFromContext(c))\n\t})\n\n\t// Verify a \"\" is sent back if nothing sets the token on the context.\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t// Send\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Empty(t, body)\n}\n\nfunc Test_TokenFromContext(t *testing.T) {\n\tapp := fiber.New()\n\t// Wire up keyauth middleware to set TokenFromContext now\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromAuthHeader(\"Basic\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\t// Define a test handler that checks TokenFromContext\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(TokenFromContext(c))\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Basic \"+CorrectKey)\n\t// Send\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, CorrectKey, string(body))\n}\n\nfunc Test_TokenFromContext_Types(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromAuthHeader(\"Basic\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\trequire.Equal(t, CorrectKey, TokenFromContext(c))\n\t\tcustomCtx, ok := c.(fiber.CustomCtx)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, CorrectKey, TokenFromContext(customCtx))\n\t\trequire.Equal(t, CorrectKey, TokenFromContext(c.RequestCtx()))\n\t\trequire.Equal(t, CorrectKey, TokenFromContext(c.Context()))\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Basic \"+CorrectKey)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_AuthSchemeToken(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromAuthHeader(\"Token\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\n\t// Define a test handler\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"API key is valid\")\n\t})\n\n\t// Create a request with a valid API key in the \"Token\" Authorization header\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Token \"+CorrectKey)\n\n\t// Send the request to the app\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"API key is valid\", string(body))\n}\n\nfunc Test_AuthSchemeBasic(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromAuthHeader(\"Basic\"),\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\n\t// Define a test handler\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"API key is valid\")\n\t})\n\n\t// Create a request without an API key and  Send the request to the app\n\tres, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n\n\t// Create a request with a valid API key in the \"Authorization\" header using the \"Basic\" scheme\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Basic \"+CorrectKey)\n\n\t// Send the request to the app\n\tres, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\t// Read the response body into a string\n\tbody, err = io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\t// Check that the response has the expected status code and body\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"API key is valid\", string(body))\n}\n\nfunc Test_HeaderSchemeCaseInsensitive(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"bearer \"+CorrectKey)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\trequire.Equal(t, \"OK\", string(body))\n}\n\nfunc Test_DefaultErrorHandlerChallenge(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\tres, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, \"Bearer realm=\\\"Restricted\\\"\", res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_DefaultErrorHandlerInvalid(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+CorrectKey)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n\trequire.Equal(t, \"Bearer realm=\\\"Restricted\\\"\", res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_HeaderSchemeMultipleSpaces(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, key string) (bool, error) {\n\t\t\tif key == CorrectKey {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer    \"+CorrectKey)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n}\n\nfunc Test_HeaderSchemeMissingSpace(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{Validator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\treturn false, ErrMissingOrMalformedAPIKey\n\t}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer\"+CorrectKey)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n}\n\nfunc Test_HeaderSchemeNoToken(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{Validator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\treturn false, ErrMissingOrMalformedAPIKey\n\t}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer \")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n}\n\nfunc Test_HeaderSchemeNoSeparator(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{Validator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\treturn false, ErrMissingOrMalformedAPIKey\n\t}}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t// No space between \"Bearer\" and token\n\treq.Header.Add(\"Authorization\", \"BearerTokenWithoutSpace\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n}\n\nfunc Test_HeaderSchemeEmptyTokenAfterTrim(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, ErrMissingOrMalformedAPIKey\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t// Authorization header with scheme followed by only spaces/tabs (no actual token)\n\treq.Header.Add(\"Authorization\", \"Bearer \\t  \\t \")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ErrMissingOrMalformedAPIKey.Error(), string(body))\n}\n\nfunc Test_WWWAuthenticateHeader(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\texpectedHeader     string\n\t\tconfig             Config\n\t\texpectedStatusCode int\n\t}{\n\t\t{\n\t\t\tname: \"default config on failure\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"validation failed\")\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedHeader:     `Bearer realm=\"Restricted\"`,\n\t\t\texpectedStatusCode: fiber.StatusUnauthorized,\n\t\t},\n\t\t{\n\t\t\tname: \"custom realm on failure\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"validation failed\")\n\t\t\t\t},\n\t\t\t\tRealm: \"My Custom Realm\",\n\t\t\t},\n\t\t\texpectedHeader:     `Bearer realm=\"My Custom Realm\"`,\n\t\t\texpectedStatusCode: fiber.StatusUnauthorized,\n\t\t},\n\t\t{\n\t\t\tname: \"default header for non-auth-header extractor\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"validation failed\")\n\t\t\t\t},\n\t\t\t\tExtractor: extractors.FromQuery(\"api_key\"),\n\t\t\t},\n\t\t\texpectedHeader:     `ApiKey realm=\"Restricted\"`,\n\t\t\texpectedStatusCode: fiber.StatusUnauthorized,\n\t\t},\n\t\t{\n\t\t\tname: \"no header on success\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedHeader:     \"\",\n\t\t\texpectedStatusCode: fiber.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname: \"chained extractor with auth header\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"validation failed\")\n\t\t\t\t},\n\t\t\t\tExtractor: extractors.Chain(extractors.FromQuery(\"q\"), extractors.FromAuthHeader(\"MyScheme\")),\n\t\t\t},\n\t\t\texpectedHeader:     `MyScheme realm=\"Restricted\"`,\n\t\t\texpectedStatusCode: fiber.StatusUnauthorized,\n\t\t},\n\t\t{\n\t\t\tname: \"chained extractor without auth header\",\n\t\t\tconfig: Config{\n\t\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"validation failed\")\n\t\t\t\t},\n\t\t\t\tExtractor: extractors.Chain(extractors.FromQuery(\"q\"), extractors.FromCookie(\"c\")),\n\t\t\t},\n\t\t\texpectedHeader:     `ApiKey realm=\"Restricted\"`,\n\t\t\texpectedStatusCode: fiber.StatusUnauthorized,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(tt.config))\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"/\", http.NoBody)\n\t\t\t// Provide a key for the default extractor to find\n\t\t\tif tt.config.Extractor.Extract == nil {\n\t\t\t\treq.Header.Set(fiber.HeaderAuthorization, \"Bearer somekey\")\n\t\t\t}\n\n\t\t\tresp, err := app.Test(req)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tt.expectedStatusCode, resp.StatusCode)\n\t\t\tassert.Equal(t, tt.expectedHeader, resp.Header.Get(fiber.HeaderWWWAuthenticate))\n\t\t})\n\t}\n}\n\nfunc Test_CustomChallenge(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromQuery(\"api_key\"),\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t\tChallenge: `ApiKey realm=\"Restricted\"`,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `ApiKey realm=\"Restricted\"`, res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_BearerErrorFields(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t\tError:            \"invalid_token\",\n\t\tErrorDescription: \"token expired\",\n\t\tErrorURI:         \"https://example.com\",\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer something\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Bearer realm=\"Restricted\", error=\"invalid_token\", error_description=\"token expired\", error_uri=\"https://example.com\"`, res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_BearerErrorURIOnly(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t\tError:    \"invalid_token\",\n\t\tErrorURI: \"https://example.com/docs\",\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer something\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Bearer realm=\"Restricted\", error=\"invalid_token\", error_uri=\"https://example.com/docs\"`, res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_BearerInsufficientScope(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t\tError: ErrorInsufficientScope,\n\t\tScope: \"read\",\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer something\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Bearer realm=\"Restricted\", error=\"insufficient_scope\", scope=\"read\"`, res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_ScopeValidation(t *testing.T) {\n\trequire.PanicsWithValue(t, \"fiber: keyauth scope requires insufficient_scope error\", func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tScope:     \"foo\",\n\t\t})\n\t})\n\n\trequire.PanicsWithValue(t, \"fiber: keyauth insufficient_scope requires scope\", func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tError:     ErrorInsufficientScope,\n\t\t})\n\t})\n\n\trequire.PanicsWithValue(t, \"fiber: keyauth scope contains invalid token\", func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tError:     ErrorInsufficientScope,\n\t\t\tScope:     \"read \\\"write\\\"\",\n\t\t})\n\t})\n\n\trequire.NotPanics(t, func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tError:     ErrorInsufficientScope,\n\t\t\tScope:     \"read write:all\",\n\t\t})\n\t})\n}\n\nfunc Test_WWWAuthenticateOnlyOn401(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) {\n\t\t\treturn false, errors.New(\"invalid\")\n\t\t},\n\t\tErrorHandler: func(c fiber.Ctx, _ error) error {\n\t\t\treturn c.Status(fiber.StatusForbidden).SendString(\"forbidden\")\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer bad\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusForbidden, res.StatusCode)\n\trequire.Empty(t, res.Header.Get(\"WWW-Authenticate\"))\n}\n\nfunc Test_DefaultChallengeForNonAuthExtractor(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.FromQuery(\"api_key\"),\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return false, ErrMissingOrMalformedAPIKey },\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\tres, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `ApiKey realm=\"Restricted\"`, res.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_MultipleWWWAuthenticateChallenges(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tExtractor: extractors.Chain(\n\t\t\textractors.FromAuthHeader(\"Bearer\"),\n\t\t\textractors.FromAuthHeader(\"ApiKey\"),\n\t\t),\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return false, errors.New(\"invalid\") },\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\tres, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Bearer realm=\"Restricted\", ApiKey realm=\"Restricted\"`, res.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_ProxyAuthenticateHeader(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return false, errors.New(\"invalid\") },\n\t\tErrorHandler: func(c fiber.Ctx, _ error) error {\n\t\t\treturn c.Status(fiber.StatusProxyAuthRequired).SendString(\"proxy auth\")\n\t\t},\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error { return c.SendString(\"OK\") })\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(\"Authorization\", \"Bearer bad\")\n\tres, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusProxyAuthRequired, res.StatusCode)\n\trequire.Equal(t, `Bearer realm=\"Restricted\"`, res.Header.Get(fiber.HeaderProxyAuthenticate))\n\trequire.Empty(t, res.Header.Get(fiber.HeaderWWWAuthenticate))\n}\n\nfunc Test_New_InvalidErrorToken(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tError:     \"unsupported\",\n\t\t})\n\t})\n}\n\nfunc Test_New_ErrorDescriptionRequiresError(t *testing.T) {\n\tassert.Panics(t, func() {\n\t\tNew(Config{\n\t\t\tValidator:        func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tErrorDescription: \"desc\",\n\t\t})\n\t})\n}\n\nfunc Test_New_ErrorURIRequiresError(t *testing.T) {\n\tassert.PanicsWithValue(t, \"fiber: keyauth error_uri requires error\", func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tErrorURI:  \"https://example.com/docs\",\n\t\t})\n\t})\n}\n\nfunc Test_New_ErrorURIAbsolute(t *testing.T) {\n\tassert.PanicsWithValue(t, \"fiber: keyauth error_uri must be absolute\", func() {\n\t\tNew(Config{\n\t\t\tValidator: func(_ fiber.Ctx, _ string) (bool, error) { return true, nil },\n\t\t\tError:     \"invalid_token\",\n\t\t\tErrorURI:  \"/docs\",\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/limiter/config.go",
    "content": "package limiter\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst defaultLimiterMax = 5\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Store is used to store the state of the middleware\n\t//\n\t// Default: an in memory store for this process only\n\tStorage fiber.Storage\n\n\t// LimiterMiddleware is the struct that implements a limiter middleware.\n\t//\n\t// Default: a new Fixed Window Rate Limiter\n\tLimiterMiddleware Handler\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// A function to dynamically calculate the max requests supported by the rate limiter middleware\n\t//\n\t// Default: func(c fiber.Ctx) int {\n\t//   return c.Max\n\t// }\n\tMaxFunc func(c fiber.Ctx) int\n\n\t// A function to dynamically calculate the expiration time for rate limiter entries\n\t//\n\t// Default: A function that returns the static `Expiration` value from the config.\n\tExpirationFunc func(c fiber.Ctx) time.Duration\n\n\t// KeyGenerator allows you to generate custom keys, by default c.IP() is used\n\t//\n\t// Default: func(c fiber.Ctx) string {\n\t//   return c.IP()\n\t// }\n\tKeyGenerator func(fiber.Ctx) string\n\n\t// LimitReached is called when a request hits the limit\n\t//\n\t// Default: func(c fiber.Ctx) error {\n\t//   return c.SendStatus(fiber.StatusTooManyRequests)\n\t// }\n\tLimitReached fiber.Handler\n\n\t// Max number of recent connections during `Expiration` seconds before sending a 429 response\n\t//\n\t// Default: 5\n\tMax int\n\n\t// Expiration is the time on how long to keep records of requests in memory\n\t//\n\t// Default: 1 * time.Minute\n\tExpiration time.Duration\n\n\t// When set to true, requests with StatusCode >= 400 won't be counted.\n\t//\n\t// Default: false\n\tSkipFailedRequests bool\n\n\t// When set to true, requests with StatusCode < 400 won't be counted.\n\t//\n\t// Default: false\n\tSkipSuccessfulRequests bool\n\n\t// When set to true, the middleware will not include the rate limit headers (X-RateLimit-* and Retry-After) in the response.\n\t//\n\t// Default: false\n\tDisableHeaders bool\n\n\t// DisableValueRedaction turns off masking limiter keys in logs and error messages when set to true.\n\t//\n\t// Default: false\n\tDisableValueRedaction bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tMax:        defaultLimiterMax,\n\tExpiration: 1 * time.Minute,\n\tMaxFunc: func(_ fiber.Ctx) int {\n\t\treturn defaultLimiterMax\n\t},\n\t// Note: ExpirationFunc is intentionally nil here so that configDefault()\n\t// can create a proper closure that references the configured Expiration value.\n\tKeyGenerator: func(c fiber.Ctx) string {\n\t\treturn c.IP()\n\t},\n\tLimitReached: func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTooManyRequests)\n\t},\n\tSkipFailedRequests:     false,\n\tSkipSuccessfulRequests: false,\n\tDisableHeaders:         false,\n\tDisableValueRedaction:  false,\n\tLimiterMiddleware:      FixedWindow{},\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Use default config if nothing provided\n\tvar cfg Config\n\tif len(config) < 1 {\n\t\tcfg = ConfigDefault\n\t} else {\n\t\tcfg = config[0]\n\t}\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif cfg.Max <= 0 {\n\t\tcfg.Max = ConfigDefault.Max\n\t}\n\tif int(cfg.Expiration.Seconds()) <= 0 {\n\t\tcfg.Expiration = ConfigDefault.Expiration\n\t}\n\tif cfg.KeyGenerator == nil {\n\t\tcfg.KeyGenerator = ConfigDefault.KeyGenerator\n\t}\n\tif cfg.LimitReached == nil {\n\t\tcfg.LimitReached = ConfigDefault.LimitReached\n\t}\n\tif cfg.LimiterMiddleware == nil {\n\t\tcfg.LimiterMiddleware = ConfigDefault.LimiterMiddleware\n\t}\n\tif cfg.MaxFunc == nil {\n\t\tcfg.MaxFunc = func(_ fiber.Ctx) int {\n\t\t\treturn cfg.Max\n\t\t}\n\t}\n\tif cfg.ExpirationFunc == nil {\n\t\tcfg.ExpirationFunc = func(_ fiber.Ctx) time.Duration {\n\t\t\treturn cfg.Expiration\n\t\t}\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/limiter/limiter.go",
    "content": "package limiter\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst (\n\t// X-RateLimit-* headers\n\txRateLimitLimit     = \"X-RateLimit-Limit\"\n\txRateLimitRemaining = \"X-RateLimit-Remaining\"\n\txRateLimitReset     = \"X-RateLimit-Reset\"\n)\n\n// Handler defines a rate-limiting strategy that can produce a middleware\n// handler using the provided configuration.\ntype Handler interface {\n\tNew(config *Config) fiber.Handler\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return the specified middleware handler.\n\treturn cfg.LimiterMiddleware.New(&cfg)\n}\n\n// getEffectiveStatusCode returns the actual status code, considering both the error and response status\nfunc getEffectiveStatusCode(c fiber.Ctx, err error) int {\n\t// If there's an error and it's a *fiber.Error, use its status code\n\tif err != nil {\n\t\tvar fiberErr *fiber.Error\n\t\tif errors.As(err, &fiberErr) {\n\t\t\treturn fiberErr.Code\n\t\t}\n\t}\n\n\t// Otherwise, use the response status code\n\treturn c.Response().StatusCode()\n}\n"
  },
  {
    "path": "middleware/limiter/limiter_fixed.go",
    "content": "package limiter\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// FixedWindow implements a fixed-window rate limiting strategy.\ntype FixedWindow struct{}\n\n// New creates a new fixed window middleware handler\nfunc (FixedWindow) New(cfg *Config) fiber.Handler {\n\tif cfg == nil {\n\t\tdefaultCfg := configDefault()\n\t\tcfg = &defaultCfg\n\t}\n\n\t// Limiter variables\n\tmux := &sync.RWMutex{}\n\n\t// Create manager to simplify storage operations ( see manager.go )\n\tmanager := newManager(cfg.Storage, !cfg.DisableValueRedaction)\n\n\t// Update timestamp every second\n\tutils.StartTimeStampUpdater()\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Generate maxRequests from generator, if no generator was provided the default value returned is 5\n\t\tmaxRequests := cfg.MaxFunc(c)\n\n\t\t// Don't execute middleware if Next returns true or if the max is 0\n\t\tif (cfg.Next != nil && cfg.Next(c)) || maxRequests == 0 {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Generate expiration from generator\n\t\texpirationDuration := cfg.ExpirationFunc(c)\n\t\tif expirationDuration <= 0 {\n\t\t\texpirationDuration = ConfigDefault.Expiration\n\t\t}\n\t\texpiration := uint64(expirationDuration.Seconds())\n\n\t\t// Get key from request\n\t\tkey := cfg.KeyGenerator(c)\n\n\t\t// Lock entry\n\t\tmux.Lock()\n\n\t\treqCtx := c.Context()\n\n\t\t// Get entry from pool and release when finished\n\t\te, err := manager.get(reqCtx, key)\n\t\tif err != nil {\n\t\t\tmux.Unlock()\n\t\t\treturn err\n\t\t}\n\n\t\t// Get timestamp\n\t\tts := uint64(utils.Timestamp())\n\n\t\t// Set expiration if entry does not exist\n\t\tif e.exp == 0 {\n\t\t\te.exp = ts + expiration\n\t\t} else if ts >= e.exp {\n\t\t\t// Check if entry is expired\n\t\t\te.currHits = 0\n\t\t\te.exp = ts + expiration\n\t\t}\n\n\t\t// Increment hits\n\t\te.currHits++\n\n\t\t// Calculate when it resets in seconds\n\t\tresetInSec := e.exp - ts\n\n\t\t// Set how many hits we have left\n\t\tremaining := maxRequests - e.currHits\n\n\t\t// Update storage\n\t\tif setErr := manager.set(reqCtx, key, e, expirationDuration); setErr != nil {\n\t\t\tmux.Unlock()\n\t\t\treturn fmt.Errorf(\"limiter: failed to persist state: %w\", setErr)\n\t\t}\n\n\t\t// Unlock entry\n\t\tmux.Unlock()\n\n\t\t// Check if hits exceed the max\n\t\tif remaining < 0 {\n\t\t\t// Return response with Retry-After header\n\t\t\t// https://tools.ietf.org/html/rfc6584\n\t\t\tif !cfg.DisableHeaders {\n\t\t\t\tc.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10))\n\t\t\t}\n\n\t\t\t// Call LimitReached handler\n\t\t\treturn cfg.LimitReached(c)\n\t\t}\n\n\t\t// Continue stack for reaching c.Response().StatusCode()\n\t\t// Store err for returning\n\t\terr = c.Next()\n\n\t\t// Get the effective status code from either the error or response\n\t\tstatusCode := getEffectiveStatusCode(c, err)\n\n\t\t// Check for SkipFailedRequests and SkipSuccessfulRequests\n\t\tif (cfg.SkipSuccessfulRequests && statusCode < fiber.StatusBadRequest) ||\n\t\t\t(cfg.SkipFailedRequests && statusCode >= fiber.StatusBadRequest) {\n\t\t\t// Lock entry\n\t\t\tmux.Lock()\n\t\t\tentry, getErr := manager.get(reqCtx, key)\n\t\t\tif getErr != nil {\n\t\t\t\tmux.Unlock()\n\t\t\t\treturn getErr\n\t\t\t}\n\t\t\te = entry\n\t\t\te.currHits--\n\t\t\tremaining++\n\t\t\tif setErr := manager.set(reqCtx, key, e, expirationDuration); setErr != nil {\n\t\t\t\tmux.Unlock()\n\t\t\t\treturn fmt.Errorf(\"limiter: failed to persist state: %w\", setErr)\n\t\t\t}\n\t\t\t// Unlock entry\n\t\t\tmux.Unlock()\n\t\t}\n\n\t\t// We can continue, update RateLimit headers\n\t\tif !cfg.DisableHeaders {\n\t\t\tc.Set(xRateLimitLimit, strconv.Itoa(maxRequests))\n\t\t\tc.Set(xRateLimitRemaining, strconv.Itoa(remaining))\n\t\t\tc.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10))\n\t\t}\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "middleware/limiter/limiter_sliding.go",
    "content": "package limiter\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// SlidingWindow implements the sliding-window rate limiting strategy.\ntype SlidingWindow struct{}\n\n// New creates a new sliding window middleware handler\nfunc (SlidingWindow) New(cfg *Config) fiber.Handler {\n\tif cfg == nil {\n\t\tdefaultCfg := configDefault()\n\t\tcfg = &defaultCfg\n\t}\n\n\t// Limiter variables\n\tmux := &sync.RWMutex{}\n\n\t// Create manager to simplify storage operations ( see manager.go )\n\tmanager := newManager(cfg.Storage, !cfg.DisableValueRedaction)\n\n\t// Update timestamp every second\n\tutils.StartTimeStampUpdater()\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Generate maxRequests from generator, if no generator was provided the default value returned is 5\n\t\tmaxRequests := cfg.MaxFunc(c)\n\n\t\t// Don't execute middleware if Next returns true or if the max is 0\n\t\tif (cfg.Next != nil && cfg.Next(c)) || maxRequests == 0 {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Generate expiration from generator\n\t\texpirationDuration := cfg.ExpirationFunc(c)\n\t\tif expirationDuration <= 0 {\n\t\t\texpirationDuration = ConfigDefault.Expiration\n\t\t}\n\t\texpiration := uint64(expirationDuration.Seconds())\n\n\t\t// Get key from request\n\t\tkey := cfg.KeyGenerator(c)\n\n\t\t// Lock entry\n\t\tmux.Lock()\n\n\t\treqCtx := c.Context()\n\n\t\t// Get entry from pool and release when finished\n\t\te, err := manager.get(reqCtx, key)\n\t\tif err != nil {\n\t\t\tmux.Unlock()\n\t\t\treturn err\n\t\t}\n\n\t\t// Get timestamp\n\t\tts := uint64(utils.Timestamp())\n\n\t\t// Rotate window\n\t\tresetInSec := rotateWindow(e, ts, expiration)\n\t\twindowExpiresAt := e.exp\n\n\t\t// Increment hits\n\t\te.currHits++\n\n\t\t// weight = time until current window reset / total window length\n\t\tweight := float64(resetInSec) / float64(expiration)\n\n\t\t// rate = request count in previous window - weight + request count in current window\n\t\trate := int(math.Ceil(float64(e.prevHits)*weight)) + e.currHits\n\n\t\t// Calculate how many hits can be made based on the current rate\n\t\tremaining := maxRequests - rate\n\n\t\t// Update storage. Garbage collect when the next window ends.\n\t\t// |--------------------------|--------------------------|\n\t\t//               ^            ^               ^          ^\n\t\t//              ts         e.exp   End sample window   End next window\n\t\t//               <------------>\n\t\t// \t\t\t\t   Reset In Sec\n\t\t// resetInSec = e.exp - ts - time until end of current window.\n\t\t// duration + expiration = end of next window.\n\t\t// Because we don't want to garbage collect in the middle of a window\n\t\t// we add the expiration to the duration.\n\t\t// Otherwise, after the end of \"sample window\", attackers could launch\n\t\t// a new request with the full window length.\n\t\tif setErr := manager.set(reqCtx, key, e, ttlDuration(resetInSec, expiration)); setErr != nil {\n\t\t\tmux.Unlock()\n\t\t\treturn fmt.Errorf(\"limiter: failed to persist state: %w\", setErr)\n\t\t}\n\n\t\t// Unlock entry\n\t\tmux.Unlock()\n\n\t\t// Check if hits exceed the allowed maximum for this request\n\t\tif remaining < 0 {\n\t\t\t// Return response with Retry-After header\n\t\t\t// https://tools.ietf.org/html/rfc6584\n\t\t\tif !cfg.DisableHeaders {\n\t\t\t\tc.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10))\n\t\t\t}\n\n\t\t\t// Call LimitReached handler\n\t\t\treturn cfg.LimitReached(c)\n\t\t}\n\n\t\t// Continue stack for reaching c.Response().StatusCode()\n\t\t// Store err for returning\n\t\terr = c.Next()\n\n\t\t// Get the effective status code from either the error or response\n\t\tstatusCode := getEffectiveStatusCode(c, err)\n\n\t\tskipHit := (cfg.SkipSuccessfulRequests && statusCode < fiber.StatusBadRequest) ||\n\t\t\t(cfg.SkipFailedRequests && statusCode >= fiber.StatusBadRequest)\n\n\t\tif skipHit || !cfg.DisableHeaders {\n\t\t\t// Lock entry\n\t\t\tmux.Lock()\n\t\t\tentry, getErr := manager.get(reqCtx, key)\n\t\t\tif getErr != nil {\n\t\t\t\tmux.Unlock()\n\t\t\t\treturn getErr\n\t\t\t}\n\t\t\te = entry\n\n\t\t\tts = uint64(utils.Timestamp())\n\t\t\tresetInSec = rotateWindow(e, ts, expiration)\n\t\t\tweight = float64(resetInSec) / float64(expiration)\n\n\t\t\tif skipHit {\n\t\t\t\tif counter := bucketForOriginalHit(e, windowExpiresAt, ts, expiration); counter != nil && *counter > 0 {\n\t\t\t\t\t*counter--\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trate = int(math.Ceil(float64(e.prevHits)*weight)) + e.currHits\n\t\t\tremaining = maxRequests - rate\n\t\t\tif setErr := manager.set(reqCtx, key, e, ttlDuration(resetInSec, expiration)); setErr != nil {\n\t\t\t\tmux.Unlock()\n\t\t\t\treturn fmt.Errorf(\"limiter: failed to persist state: %w\", setErr)\n\t\t\t}\n\t\t\t// Unlock entry\n\t\t\tmux.Unlock()\n\n\t\t\t// We can continue, update RateLimit headers\n\t\t\tif !cfg.DisableHeaders {\n\t\t\t\tc.Set(xRateLimitLimit, strconv.Itoa(maxRequests))\n\t\t\t\tc.Set(xRateLimitRemaining, strconv.Itoa(remaining))\n\t\t\t\tc.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10))\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n}\n\nfunc rotateWindow(e *item, ts, expiration uint64) uint64 {\n\t// Set expiration if entry does not exist\n\tif e.exp == 0 {\n\t\te.exp = ts + expiration\n\t} else if ts >= e.exp {\n\t\t// The entry has expired, handle the expiration.\n\t\t// Reset the current hits to 0.\n\t\telapsed := ts - e.exp\n\t\tif elapsed >= expiration {\n\t\t\te.prevHits = 0\n\t\t\te.currHits = 0\n\t\t\te.exp = ts + expiration\n\t\t} else {\n\t\t\te.prevHits = e.currHits\n\t\t\te.currHits = 0\n\n\t\t\te.exp = ts + expiration - elapsed\n\t\t}\n\t}\n\n\t// Calculate when it resets in seconds\n\treturn e.exp - ts\n}\n\nfunc bucketForOriginalHit(e *item, requestExpiration, ts, expiration uint64) *int {\n\tif ts < requestExpiration {\n\t\treturn &e.currHits\n\t}\n\n\tif ts-requestExpiration < expiration {\n\t\treturn &e.prevHits\n\t}\n\n\treturn nil\n}\n\nfunc ttlDuration(resetInSec, expiration uint64) time.Duration {\n\tresetDuration, ok := secondsToDuration(resetInSec)\n\tif !ok {\n\t\treturn time.Duration(math.MaxInt64)\n\t}\n\n\texpirationDuration, ok := secondsToDuration(expiration)\n\tif !ok {\n\t\treturn time.Duration(math.MaxInt64)\n\t}\n\n\tif resetDuration > time.Duration(math.MaxInt64)-expirationDuration {\n\t\treturn time.Duration(math.MaxInt64)\n\t}\n\n\treturn resetDuration + expirationDuration\n}\n\nfunc secondsToDuration(seconds uint64) (time.Duration, bool) {\n\tconst maxSeconds = math.MaxInt64 / int64(time.Second)\n\n\tif seconds > uint64(maxSeconds) {\n\t\treturn time.Duration(math.MaxInt64), false\n\t}\n\n\treturn time.Duration(seconds) * time.Second, true\n}\n"
  },
  {
    "path": "middleware/limiter/limiter_test.go",
    "content": "package limiter\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n)\n\ntype failingLimiterStorage struct {\n\tdata map[string][]byte\n\terrs map[string]error\n}\n\nconst testLimiterClientKey = \"client-key\"\n\nfunc newFailingLimiterStorage() *failingLimiterStorage {\n\treturn &failingLimiterStorage{\n\t\tdata: make(map[string][]byte),\n\t\terrs: make(map[string]error),\n\t}\n}\n\n// countingFailStorage fails set operations after a specified number of successful calls\ntype countingFailStorage struct {\n\t*failingLimiterStorage\n\tsetFailErr error\n\tsetCount   int\n\tfailAfterN int\n}\n\nfunc newCountingFailStorage(failAfterN int, err error) *countingFailStorage {\n\treturn &countingFailStorage{\n\t\tfailingLimiterStorage: newFailingLimiterStorage(),\n\t\tfailAfterN:            failAfterN,\n\t\tsetFailErr:            err,\n\t}\n}\n\nfunc (s *countingFailStorage) SetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error {\n\ts.setCount++\n\tif s.setCount > s.failAfterN {\n\t\treturn s.setFailErr\n\t}\n\treturn s.failingLimiterStorage.SetWithContext(ctx, key, val, exp)\n}\n\ntype contextRecord struct {\n\tkey      string\n\tvalue    string\n\tcanceled bool\n}\n\ntype contextRecorderLimiterStorage struct {\n\t*failingLimiterStorage\n\tgets []contextRecord\n\tsets []contextRecord\n}\n\nfunc sleepForRetryAfter(t *testing.T, resp *http.Response) {\n\tt.Helper()\n\n\tretryAfter := resp.Header.Get(fiber.HeaderRetryAfter)\n\tif retryAfter == \"\" {\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\treturn\n\t}\n\n\tseconds, err := strconv.Atoi(retryAfter)\n\trequire.NoError(t, err)\n\n\tdelay := time.Duration(seconds) * time.Second\n\t// Sliding window needs roughly 2x the reported delay for the previous window to expire.\n\tif doubled := 2 * delay; doubled > delay {\n\t\tdelay = doubled\n\t}\n\tif minDelay := 4 * time.Second; delay < minDelay {\n\t\tdelay = minDelay\n\t}\n\n\ttime.Sleep(delay + 500*time.Millisecond)\n}\n\nfunc newContextRecorderLimiterStorage() *contextRecorderLimiterStorage {\n\treturn &contextRecorderLimiterStorage{failingLimiterStorage: newFailingLimiterStorage()}\n}\n\nfunc contextRecordFrom(ctx context.Context, key string) contextRecord {\n\trecord := contextRecord{\n\t\tkey:      key,\n\t\tcanceled: errors.Is(ctx.Err(), context.Canceled),\n\t}\n\tif value, ok := ctx.Value(markerKey).(string); ok {\n\t\trecord.value = value\n\t}\n\treturn record\n}\n\nfunc (s *contextRecorderLimiterStorage) GetWithContext(ctx context.Context, key string) ([]byte, error) {\n\ts.gets = append(s.gets, contextRecordFrom(ctx, key))\n\treturn s.failingLimiterStorage.GetWithContext(ctx, key)\n}\n\nfunc (s *contextRecorderLimiterStorage) SetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error {\n\ts.sets = append(s.sets, contextRecordFrom(ctx, key))\n\treturn s.failingLimiterStorage.SetWithContext(ctx, key, val, exp)\n}\n\nfunc (s *contextRecorderLimiterStorage) recordedGets() []contextRecord {\n\tout := make([]contextRecord, len(s.gets))\n\tcopy(out, s.gets)\n\treturn out\n}\n\nfunc (s *contextRecorderLimiterStorage) recordedSets() []contextRecord {\n\tout := make([]contextRecord, len(s.sets))\n\tcopy(out, s.sets)\n\treturn out\n}\n\nfunc (s *failingLimiterStorage) GetWithContext(_ context.Context, key string) ([]byte, error) {\n\tif err, ok := s.errs[\"get|\"+key]; ok && err != nil {\n\t\treturn nil, err\n\t}\n\tif val, ok := s.data[key]; ok {\n\t\treturn append([]byte(nil), val...), nil\n\t}\n\treturn nil, nil\n}\n\nfunc (s *failingLimiterStorage) Get(key string) ([]byte, error) {\n\treturn s.GetWithContext(context.Background(), key)\n}\n\nfunc (s *failingLimiterStorage) SetWithContext(_ context.Context, key string, val []byte, _ time.Duration) error {\n\tif err, ok := s.errs[\"set|\"+key]; ok && err != nil {\n\t\treturn err\n\t}\n\ts.data[key] = append([]byte(nil), val...)\n\treturn nil\n}\n\nfunc (s *failingLimiterStorage) Set(key string, val []byte, exp time.Duration) error {\n\treturn s.SetWithContext(context.Background(), key, val, exp)\n}\n\nfunc (*failingLimiterStorage) DeleteWithContext(context.Context, string) error { return nil }\n\nfunc (*failingLimiterStorage) Delete(string) error { return nil }\n\nfunc (*failingLimiterStorage) ResetWithContext(context.Context) error { return nil }\n\nfunc (*failingLimiterStorage) Reset() error { return nil }\n\nfunc (*failingLimiterStorage) Close() error { return nil }\n\ntype contextKey string\n\nconst markerKey contextKey = \"marker\"\n\nfunc contextWithMarker(label string) context.Context {\n\treturn context.WithValue(context.Background(), markerKey, label)\n}\n\nfunc canceledContextWithMarker(label string) context.Context {\n\tctx, cancel := context.WithCancel(contextWithMarker(label))\n\tcancel()\n\treturn ctx\n}\n\nfunc TestLimiterDefaultConfigNoPanic(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New())\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\trequire.NotPanics(t, func() {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t})\n}\n\nfunc TestLimiterFixedStorageGetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingLimiterStorage()\n\tstorage.errs[\"get|\"+testLimiterClientKey] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Max: 1, Expiration: time.Second, KeyGenerator: func(fiber.Ctx) string { return testLimiterClientKey }}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"limiter: failed to get key\")\n\trequire.ErrorContains(t, captured, \"[redacted]\")\n}\n\nfunc TestLimiterFixedStorageSetError(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingLimiterStorage()\n\tstorage.errs[\"set|\"+testLimiterClientKey] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{Storage: storage, Max: 1, Expiration: time.Second, KeyGenerator: func(fiber.Ctx) string { return testLimiterClientKey }}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"limiter: failed to persist state\")\n\trequire.ErrorContains(t, captured, \"limiter: failed to store key\")\n\trequire.ErrorContains(t, captured, \"[redacted]\")\n}\n\nfunc TestLimiterFixedPropagatesRequestContextToStorage(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderLimiterStorage()\n\n\tapp := fiber.New()\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tpath := c.Path()\n\t\tif path == \"/normal\" {\n\t\t\tc.SetContext(contextWithMarker(\"fixed-normal\"))\n\t\t}\n\t\tif path == \"/rollback\" {\n\t\t\tc.SetContext(canceledContextWithMarker(\"fixed-rollback\"))\n\t\t}\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(New(Config{\n\t\tStorage:                storage,\n\t\tMax:                    1,\n\t\tExpiration:             time.Minute,\n\t\tSkipSuccessfulRequests: true,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path()\n\t\t},\n\t\tLimiterMiddleware: FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:mode\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tfor _, path := range []string{\"/normal\", \"/rollback\"} {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, path, http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t}\n\n\tgets := storage.recordedGets()\n\trequire.Len(t, gets, 4)\n\n\tsets := storage.recordedSets()\n\trequire.Len(t, sets, 4)\n\n\tverifyRecords := func(t *testing.T, records []contextRecord, key, wantValue string, wantCanceled bool) {\n\t\tt.Helper()\n\t\tvar matched []contextRecord\n\t\tfor _, rec := range records {\n\t\t\tif rec.key == key {\n\t\t\t\tmatched = append(matched, rec)\n\t\t\t}\n\t\t}\n\t\trequire.Len(t, matched, 2)\n\t\tfor _, rec := range matched {\n\t\t\trequire.Equal(t, wantValue, rec.value)\n\t\t\trequire.Equal(t, wantCanceled, rec.canceled)\n\t\t}\n\t}\n\n\tverifyRecords(t, gets, \"/normal\", \"fixed-normal\", false)\n\tverifyRecords(t, gets, \"/rollback\", \"fixed-rollback\", true)\n\tverifyRecords(t, sets, \"/normal\", \"fixed-normal\", false)\n\tverifyRecords(t, sets, \"/rollback\", \"fixed-rollback\", true)\n}\n\nfunc TestLimiterFixedStorageGetErrorDisableRedaction(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingLimiterStorage()\n\tstorage.errs[\"get|\"+testLimiterClientKey] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{DisableValueRedaction: true, Storage: storage, Max: 1, Expiration: time.Second, KeyGenerator: func(fiber.Ctx) string { return testLimiterClientKey }}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, testLimiterClientKey)\n\trequire.NotContains(t, captured.Error(), \"[redacted]\")\n}\n\nfunc TestLimiterFixedStorageSetErrorDisableRedaction(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newFailingLimiterStorage()\n\tstorage.errs[\"set|\"+testLimiterClientKey] = errors.New(\"boom\")\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{DisableValueRedaction: true, Storage: storage, Max: 1, Expiration: time.Second, KeyGenerator: func(fiber.Ctx) string { return testLimiterClientKey }}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, testLimiterClientKey)\n\trequire.NotContains(t, captured.Error(), \"[redacted]\")\n}\n\nfunc TestLimiterFixedStorageSetErrorOnSkipSuccessfulRequests(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newCountingFailStorage(1, errors.New(\"second set failed\"))\n\n\tvar captured error\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcaptured = err\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(\"storage failure\")\n\t\t},\n\t})\n\n\tapp.Use(New(Config{\n\t\tStorage:                storage,\n\t\tMax:                    10,\n\t\tExpiration:             time.Second,\n\t\tSkipSuccessfulRequests: true,\n\t\tKeyGenerator:           func(fiber.Ctx) string { return testLimiterClientKey },\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Error(t, captured)\n\trequire.ErrorContains(t, captured, \"limiter: failed to persist state\")\n}\n\nfunc TestLimiterSlidingPropagatesRequestContextToStorage(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderLimiterStorage()\n\n\tapp := fiber.New()\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tpath := c.Path()\n\t\tif path == \"/normal\" {\n\t\t\tc.SetContext(contextWithMarker(\"sliding-normal\"))\n\t\t}\n\t\tif path == \"/rollback\" {\n\t\t\tc.SetContext(canceledContextWithMarker(\"sliding-rollback\"))\n\t\t}\n\t\treturn c.Next()\n\t})\n\n\tapp.Use(New(Config{\n\t\tStorage:                storage,\n\t\tMax:                    1,\n\t\tExpiration:             time.Minute,\n\t\tSkipSuccessfulRequests: true,\n\t\tKeyGenerator: func(c fiber.Ctx) string {\n\t\t\treturn c.Path()\n\t\t},\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:mode\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tfor _, path := range []string{\"/normal\", \"/rollback\"} {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, path, http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t}\n\n\tgets := storage.recordedGets()\n\trequire.Len(t, gets, 4)\n\n\tsets := storage.recordedSets()\n\trequire.Len(t, sets, 4)\n\n\tverifyRecords := func(t *testing.T, records []contextRecord, key, wantValue string, wantCanceled bool) {\n\t\tt.Helper()\n\t\tvar matched []contextRecord\n\t\tfor _, rec := range records {\n\t\t\tif rec.key == key {\n\t\t\t\tmatched = append(matched, rec)\n\t\t\t}\n\t\t}\n\t\trequire.Len(t, matched, 2)\n\t\tfor _, rec := range matched {\n\t\t\trequire.Equal(t, wantValue, rec.value)\n\t\t\trequire.Equal(t, wantCanceled, rec.canceled)\n\t\t}\n\t}\n\n\tverifyRecords(t, gets, \"/normal\", \"sliding-normal\", false)\n\tverifyRecords(t, gets, \"/rollback\", \"sliding-rollback\", true)\n\tverifyRecords(t, sets, \"/normal\", \"sliding-normal\", false)\n\tverifyRecords(t, sets, \"/rollback\", \"sliding-rollback\", true)\n}\n\nfunc TestLimiterSlidingSkipsPostUpdateWhenHeadersDisabled(t *testing.T) {\n\tt.Parallel()\n\n\tstorage := newContextRecorderLimiterStorage()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpiration:        time.Second,\n\t\tStorage:           storage,\n\t\tDisableHeaders:    true,\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\trequire.Len(t, storage.recordedGets(), 1)\n\trequire.Len(t, storage.recordedSets(), 1)\n}\n\n// go test -run Test_Limiter_With_Max_Func_With_Zero -race -v\nfunc Test_Limiter_With_Max_Func_With_Zero_And_Limiter_Sliding(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMaxFunc:                func(_ fiber.Ctx) int { return 0 },\n\t\tExpiration:             2 * time.Second,\n\t\tSkipFailedRequests:     false,\n\t\tSkipSuccessfulRequests: false,\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\ttime.Sleep(4*time.Second + 500*time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\nfunc Test_Limiter_Sliding_MaxFuncOverridesStaticMax(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tstaticMax := 5\n\tdynamicMax := 2\n\n\tapp.Use(New(Config{\n\t\tMax:               staticMax,\n\t\tMaxFunc:           func(fiber.Ctx) int { return dynamicMax },\n\t\tExpiration:        2 * time.Second,\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, strconv.Itoa(dynamicMax), resp.Header.Get(\"X-RateLimit-Limit\"))\n\trequire.Equal(t, strconv.Itoa(dynamicMax-1), resp.Header.Get(\"X-RateLimit-Remaining\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, strconv.Itoa(dynamicMax), resp.Header.Get(\"X-RateLimit-Limit\"))\n\trequire.Equal(t, strconv.Itoa(dynamicMax-2), resp.Header.Get(\"X-RateLimit-Remaining\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_With_Max_Func_With_Zero -race -v\nfunc Test_Limiter_With_Max_Func_With_Zero(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMaxFunc: func(_ fiber.Ctx) int {\n\t\t\treturn 0\n\t\t},\n\t\tExpiration: 2 * time.Second,\n\t\tStorage:    memory.New(),\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i <= 4; i++ {\n\t\twg.Go(func() {\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"Hello tester!\", string(body))\n\t\t})\n\t}\n\n\twg.Wait()\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_With_Max_Func -race -v\nfunc Test_Limiter_With_Max_Func(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tmaxRequests := 10\n\n\tapp.Use(New(Config{\n\t\tMaxFunc: func(_ fiber.Ctx) int {\n\t\t\treturn maxRequests\n\t\t},\n\t\tExpiration: 2 * time.Second,\n\t\tStorage:    memory.New(),\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i <= maxRequests-1; i++ {\n\t\twg.Go(func() {\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"Hello tester!\", string(body))\n\t\t})\n\t}\n\n\twg.Wait()\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_ExpirationFuncOverridesStaticExpiration -race -v\nfunc Test_Limiter_Fixed_ExpirationFuncOverridesStaticExpiration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               2,\n\t\tExpiration:        10 * time.Second,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return 2 * time.Second },\n\t\tLimiterMiddleware: FixedWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_ExpirationFuncOverridesStaticExpiration -race -v\nfunc Test_Limiter_Sliding_ExpirationFuncOverridesStaticExpiration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               2,\n\t\tExpiration:        10 * time.Second,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return 2 * time.Second },\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n\n\t// Sliding window needs ~2x expiration to fully reset (considers previous window)\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_ExpirationFunc_FallbackOnZeroDuration -race -v\nfunc Test_Limiter_Fixed_ExpirationFunc_FallbackOnZeroDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return 0 },\n\t\tLimiterMiddleware: FixedWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_ExpirationFunc_FallbackOnNegativeDuration -race -v\nfunc Test_Limiter_Fixed_ExpirationFunc_FallbackOnNegativeDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return -1 * time.Second },\n\t\tLimiterMiddleware: FixedWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_ExpirationFunc_FallbackOnZeroDuration -race -v\nfunc Test_Limiter_Sliding_ExpirationFunc_FallbackOnZeroDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return 0 },\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_ExpirationFunc_FallbackOnNegativeDuration -race -v\nfunc Test_Limiter_Sliding_ExpirationFunc_FallbackOnNegativeDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpirationFunc:    func(_ fiber.Ctx) time.Duration { return -1 * time.Second },\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Concurrency_Store -race -v\nfunc Test_Limiter_Concurrency_Store(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:        50,\n\t\tExpiration: 2 * time.Second,\n\t\tStorage:    memory.New(),\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i <= 49; i++ {\n\t\twg.Go(func() {\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"Hello tester!\", string(body))\n\t\t})\n\t}\n\n\twg.Wait()\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Concurrency -race -v\nfunc Test_Limiter_Concurrency(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:        50,\n\t\tExpiration: 2 * time.Second,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tvar wg sync.WaitGroup\n\n\tfor i := 0; i <= 49; i++ {\n\t\twg.Go(func() {\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"Hello tester!\", string(body))\n\t\t})\n\t}\n\n\twg.Wait()\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_Window_No_Skip_Choices -v\nfunc Test_Limiter_Fixed_Window_No_Skip_Choices(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    2,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipFailedRequests:     false,\n\t\tSkipSuccessfulRequests: false,\n\t\tLimiterMiddleware:      FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices -v\nfunc Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    2,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipFailedRequests:     false,\n\t\tSkipSuccessfulRequests: false,\n\t\tStorage:                memory.New(),\n\t\tLimiterMiddleware:      FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_No_Skip_Choices -v\nfunc Test_Limiter_Sliding_Window_No_Skip_Choices(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    2,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipFailedRequests:     false,\n\t\tSkipSuccessfulRequests: false,\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices -v\nfunc Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    2,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipFailedRequests:     false,\n\t\tSkipSuccessfulRequests: false,\n\t\tStorage:                memory.New(),\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\nfunc Test_Limiter_Sliding_Window_RecalculatesAfterHandlerDelay(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               2,\n\t\tExpiration:        time.Second,\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(600 * time.Millisecond)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tfor i := 0; i < 2; i++ {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t}\n\n\ttime.Sleep(time.Second + 100*time.Millisecond)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"2\", resp.Header.Get(xRateLimitLimit))\n\trequire.Equal(t, \"1\", resp.Header.Get(xRateLimitRemaining))\n\trequire.NotEmpty(t, resp.Header.Get(xRateLimitReset))\n}\n\nfunc Test_Limiter_Sliding_Window_ExpiresStalePrevHits(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:               1,\n\t\tExpiration:        time.Second,\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\ttime.Sleep(2500 * time.Millisecond)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"0\", resp.Header.Get(xRateLimitRemaining))\n}\n\nfunc Test_Limiter_Sliding_Window_SkipFailedRequests_DecrementsPreviousWindow(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                2,\n\t\tExpiration:         200 * time.Millisecond,\n\t\tSkipFailedRequests: true,\n\t\tLimiterMiddleware:  SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:mode\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"mode\") == \"fail\" {\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\ttype respErr struct {\n\t\tresp *http.Response\n\t\terr  error\n\t}\n\tfailCh := make(chan respErr, 1)\n\n\tgo func() {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\t\tfailCh <- respErr{resp: resp, err: err}\n\t}()\n\n\ttime.Sleep(220 * time.Millisecond)\n\n\tsuccessResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/ok\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, successResp.StatusCode)\n\n\tresult := <-failCh\n\trequire.NoError(t, result.err)\n\trequire.Equal(t, fiber.StatusInternalServerError, result.resp.StatusCode)\n\trequire.Equal(t, \"2\", result.resp.Header.Get(xRateLimitLimit))\n\trequire.Equal(t, \"1\", result.resp.Header.Get(xRateLimitRemaining))\n\tassert.NotEmpty(t, result.resp.Header.Get(xRateLimitReset))\n}\n\n// go test -run Test_Limiter_Fixed_Window_Skip_Failed_Requests -v\nfunc Test_Limiter_Fixed_Window_Skip_Failed_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                1,\n\t\tExpiration:         2 * time.Second,\n\t\tSkipFailedRequests: true,\n\t\tLimiterMiddleware:  FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests -v\nfunc Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                1,\n\t\tExpiration:         2 * time.Second,\n\t\tStorage:            memory.New(),\n\t\tSkipFailedRequests: true,\n\t\tLimiterMiddleware:  FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_Skip_Failed_Requests -v\nfunc Test_Limiter_Sliding_Window_Skip_Failed_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                1,\n\t\tExpiration:         2 * time.Second,\n\t\tSkipFailedRequests: true,\n\t\tLimiterMiddleware:  SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests -v\nfunc Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                1,\n\t\tExpiration:         2 * time.Second,\n\t\tStorage:            memory.New(),\n\t\tSkipFailedRequests: true,\n\t\tLimiterMiddleware:  SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_Window_Skip_Successful_Requests -v\nfunc Test_Limiter_Fixed_Window_Skip_Successful_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipSuccessfulRequests: true,\n\t\tLimiterMiddleware:      FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests -v\nfunc Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             2 * time.Second,\n\t\tStorage:                memory.New(),\n\t\tSkipSuccessfulRequests: true,\n\t\tLimiterMiddleware:      FixedWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\ttime.Sleep(3 * time.Second)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_Skip_Successful_Requests -v\nfunc Test_Limiter_Sliding_Window_Skip_Successful_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             2 * time.Second,\n\t\tSkipSuccessfulRequests: true,\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n}\n\n// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests -v\nfunc Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             2 * time.Second,\n\t\tStorage:                memory.New(),\n\t\tSkipSuccessfulRequests: true,\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/:status\", func(c fiber.Ctx) error {\n\t\tif c.Params(\"status\") == \"fail\" {\n\t\t\treturn c.SendStatus(400)\n\t\t}\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/success\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 429, resp.StatusCode)\n\n\tsleepForRetryAfter(t, resp)\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/fail\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 400, resp.StatusCode)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Limiter_Custom_Store -benchmem -count=4\nfunc Benchmark_Limiter_Custom_Store(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:        100,\n\t\tExpiration: 60 * time.Second,\n\t\tStorage:    memory.New(),\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n}\n\n// Test to reproduce the bug where fiber.NewErrorf responses are not counted as failed requests\nfunc Test_Limiter_Bug_NewErrorf_SkipSuccessfulRequests_SlidingWindow(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             60 * time.Second,\n\t\tLimiterMiddleware:      SlidingWindow{},\n\t\tSkipSuccessfulRequests: true,\n\t\tSkipFailedRequests:     false,\n\t\tDisableHeaders:         true,\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn fiber.NewErrorf(fiber.StatusInternalServerError, \"Error\")\n\t})\n\n\t// First request should succeed (and be counted because it's a failed request)\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\t// Second request should be rate limited because the first failed request was counted\n\t// But currently this is not happening due to the bug\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\t// This should be 429 (rate limited) but currently returns 500 due to the bug\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode, \"Second request should be rate limited\")\n}\n\n// Test to reproduce the bug where fiber.NewErrorf responses are not counted as failed requests (FixedWindow)\nfunc Test_Limiter_Bug_NewErrorf_SkipSuccessfulRequests_FixedWindow(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:                    1,\n\t\tExpiration:             60 * time.Second,\n\t\tLimiterMiddleware:      FixedWindow{},\n\t\tSkipSuccessfulRequests: true,\n\t\tSkipFailedRequests:     false,\n\t\tDisableHeaders:         true,\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn fiber.NewErrorf(fiber.StatusInternalServerError, \"Error\")\n\t})\n\n\t// First request should succeed (and be counted because it's a failed request)\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\t// Second request should be rate limited because the first failed request was counted\n\t// But currently this is not happening due to the bug\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\t// This should be 429 (rate limited) but currently returns 500 due to the bug\n\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode, \"Second request should be rate limited\")\n}\n\n// go test -run Test_Limiter_Next\nfunc Test_Limiter_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Limiter_Headers(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:        50,\n\t\tExpiration: 2 * time.Second,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tapp.Handler()(fctx)\n\n\trequire.Equal(t, \"50\", string(fctx.Response.Header.Peek(\"X-RateLimit-Limit\")))\n\tif v := string(fctx.Response.Header.Peek(\"X-RateLimit-Remaining\")); v == \"\" {\n\t\tt.Error(\"The X-RateLimit-Remaining header is not set correctly - value is an empty string.\")\n\t}\n\tif v := string(fctx.Response.Header.Peek(\"X-RateLimit-Reset\")); (v != \"1\") && (v != \"2\") {\n\t\tt.Error(\"The X-RateLimit-Reset header is not set correctly - value is out of bounds.\")\n\t}\n}\n\nfunc Test_Limiter_Disable_Headers(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:            1,\n\t\tExpiration:     2 * time.Second,\n\t\tDisableHeaders: true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\t// first request should pass\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tapp.Handler()(fctx)\n\n\trequire.Equal(t, fiber.StatusOK, fctx.Response.StatusCode())\n\trequire.Equal(t, \"Hello tester!\", string(fctx.Response.Body()))\n\trequire.Empty(t, string(fctx.Response.Header.Peek(\"X-RateLimit-Limit\")))\n\trequire.Empty(t, string(fctx.Response.Header.Peek(\"X-RateLimit-Remaining\")))\n\trequire.Empty(t, string(fctx.Response.Header.Peek(\"X-RateLimit-Reset\")))\n\n\t// second request should hit the limit and return 429 without headers\n\tfctx2 := &fasthttp.RequestCtx{}\n\tfctx2.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx2.Request.SetRequestURI(\"/\")\n\n\tapp.Handler()(fctx2)\n\n\trequire.Equal(t, fiber.StatusTooManyRequests, fctx2.Response.StatusCode())\n\trequire.Empty(t, string(fctx2.Response.Header.Peek(fiber.HeaderRetryAfter)))\n\trequire.Empty(t, string(fctx2.Response.Header.Peek(\"X-RateLimit-Limit\")))\n\trequire.Empty(t, string(fctx2.Response.Header.Peek(\"X-RateLimit-Remaining\")))\n\trequire.Empty(t, string(fctx2.Response.Header.Peek(\"X-RateLimit-Reset\")))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Limiter -benchmem -count=4\nfunc Benchmark_Limiter(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tMax:        100,\n\t\tExpiration: 60 * time.Second,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n}\n\n// go test -run Test_Sliding_Window -race -v\nfunc Test_Sliding_Window(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tMax:               10,\n\t\tExpiration:        1 * time.Second,\n\t\tStorage:           memory.New(),\n\t\tLimiterMiddleware: SlidingWindow{},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello tester!\")\n\t})\n\n\tsingleRequest := func(shouldFail bool) {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\tif shouldFail {\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, 429, resp.StatusCode)\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t\t}\n\t}\n\n\tfor range 5 {\n\t\tsingleRequest(false)\n\t}\n\n\ttime.Sleep(3 * time.Second)\n\n\tfor range 5 {\n\t\tsingleRequest(false)\n\t}\n\n\ttime.Sleep(3 * time.Second)\n\n\tfor range 5 {\n\t\tsingleRequest(false)\n\t}\n\n\ttime.Sleep(3 * time.Second)\n\n\tfor range 10 {\n\t\tsingleRequest(false)\n\t}\n\n\t// requests should fail now\n\tfor range 5 {\n\t\tsingleRequest(true)\n\t}\n}\n"
  },
  {
    "path": "middleware/limiter/manager.go",
    "content": "package limiter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/memory\"\n)\n\n// msgp -file=\"manager.go\" -o=\"manager_msgp.go\" -tests=false -unexported\n//\n//go:generate msgp -o=manager_msgp.go -tests=false -unexported\ntype item struct {\n\tcurrHits int\n\tprevHits int\n\texp      uint64\n}\n\n//msgp:ignore manager\ntype manager struct {\n\tpool       sync.Pool\n\tmemory     *memory.Storage\n\tstorage    fiber.Storage\n\tredactKeys bool\n}\n\nconst redactedKey = \"[redacted]\"\n\nfunc newManager(storage fiber.Storage, redactKeys bool) *manager {\n\t// Create new storage handler\n\tmanager := &manager{\n\t\tpool: sync.Pool{\n\t\t\tNew: func() any {\n\t\t\t\treturn new(item)\n\t\t\t},\n\t\t},\n\t\tredactKeys: redactKeys,\n\t}\n\tif storage != nil {\n\t\t// Use provided storage if provided\n\t\tmanager.storage = storage\n\t} else {\n\t\t// Fallback too memory storage\n\t\tmanager.memory = memory.New()\n\t}\n\treturn manager\n}\n\n// acquire returns an *entry from the sync.Pool\nfunc (m *manager) acquire() *item {\n\treturn m.pool.Get().(*item) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n}\n\n// release and reset *entry to sync.Pool\nfunc (m *manager) release(e *item) {\n\te.prevHits = 0\n\te.currHits = 0\n\te.exp = 0\n\tm.pool.Put(e)\n}\n\n// get data from storage or memory\nfunc (m *manager) get(ctx context.Context, key string) (*item, error) {\n\tif m.storage != nil {\n\t\traw, err := m.storage.GetWithContext(ctx, key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"limiter: failed to get key %q from storage: %w\", m.logKey(key), err)\n\t\t}\n\t\tif raw != nil {\n\t\t\tit := m.acquire()\n\t\t\tif _, err := it.UnmarshalMsg(raw); err != nil {\n\t\t\t\tm.release(it)\n\t\t\t\treturn nil, fmt.Errorf(\"limiter: failed to unmarshal key %q: %w\", m.logKey(key), err)\n\t\t\t}\n\t\t\treturn it, nil\n\t\t}\n\t\treturn m.acquire(), nil\n\t}\n\n\tvalue := m.memory.Get(key)\n\tif value == nil {\n\t\treturn m.acquire(), nil\n\t}\n\n\tit, ok := value.(*item)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"limiter: unexpected entry type %T for key %q\", value, m.logKey(key))\n\t}\n\n\treturn it, nil\n}\n\n// set data to storage or memory\nfunc (m *manager) set(ctx context.Context, key string, it *item, exp time.Duration) error {\n\tif m.storage != nil {\n\t\traw, err := it.MarshalMsg(nil)\n\t\tif err != nil {\n\t\t\tm.release(it)\n\t\t\treturn fmt.Errorf(\"limiter: failed to marshal key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\tif err := m.storage.SetWithContext(ctx, key, raw, exp); err != nil {\n\t\t\tm.release(it)\n\t\t\treturn fmt.Errorf(\"limiter: failed to store key %q: %w\", m.logKey(key), err)\n\t\t}\n\t\tm.release(it)\n\t\treturn nil\n\t}\n\n\tm.memory.Set(key, it, exp)\n\treturn nil\n}\n\nfunc (m *manager) logKey(key string) string {\n\tif m.redactKeys {\n\t\treturn redactedKey\n\t}\n\treturn key\n}\n"
  },
  {
    "path": "middleware/limiter/manager_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage limiter\n\nimport (\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *item) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"currHits\":\n\t\t\tz.currHits, err = dc.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"currHits\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"prevHits\":\n\t\t\tz.prevHits, err = dc.ReadInt()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"prevHits\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"exp\":\n\t\t\tz.exp, err = dc.ReadUint64()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"exp\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z item) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 3\n\t// write \"currHits\"\n\terr = en.Append(0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteInt(z.currHits)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"currHits\")\n\t\treturn\n\t}\n\t// write \"prevHits\"\n\terr = en.Append(0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteInt(z.prevHits)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"prevHits\")\n\t\treturn\n\t}\n\t// write \"exp\"\n\terr = en.Append(0xa3, 0x65, 0x78, 0x70)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint64(z.exp)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"exp\")\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z item) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 3\n\t// string \"currHits\"\n\to = append(o, 0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73)\n\to = msgp.AppendInt(o, z.currHits)\n\t// string \"prevHits\"\n\to = append(o, 0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73)\n\to = msgp.AppendInt(o, z.prevHits)\n\t// string \"exp\"\n\to = append(o, 0xa3, 0x65, 0x78, 0x70)\n\to = msgp.AppendUint64(o, z.exp)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"currHits\":\n\t\t\tz.currHits, bts, err = msgp.ReadIntBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"currHits\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"prevHits\":\n\t\t\tz.prevHits, bts, err = msgp.ReadIntBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"prevHits\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"exp\":\n\t\t\tz.exp, bts, err = msgp.ReadUint64Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"exp\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z item) Msgsize() (s int) {\n\ts = 1 + 9 + msgp.IntSize + 9 + msgp.IntSize + 4 + msgp.Uint64Size\n\treturn\n}\n"
  },
  {
    "path": "middleware/limiter/manager_msgp_test.go",
    "content": "package limiter\n\n// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\nfunc TestMarshalUnmarshalitem(t *testing.T) {\n\tv := item{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgitem(b *testing.B) {\n\tv := item{}\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgitem(b *testing.B) {\n\tv := item{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalitem(b *testing.B) {\n\tv := item{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tfor b.Loop() {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecodeitem(t *testing.T) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecodeitem Msgsize() is inaccurate\")\n\t}\n\n\tvn := item{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecodeitem(b *testing.B) {\n\tv := item{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tfor b.Loop() {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/logger/config.go",
    "content": "package logger\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Stream is a writer where logs are written\n\t//\n\t// Default: os.Stdout\n\tStream io.Writer\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Skip is a function to determine if logging is skipped or written to Stream.\n\t//\n\t// Optional. Default: nil\n\tSkip func(c fiber.Ctx) bool\n\n\t// Done is a function that is called after the log string for a request is written to Output,\n\t// and pass the log string as parameter.\n\t//\n\t// Optional. Default: nil\n\tDone func(c fiber.Ctx, logString []byte)\n\n\t// tagFunctions defines the custom tag action\n\t//\n\t// Optional. Default: map[string]LogFunc\n\tCustomTags map[string]LogFunc\n\n\t// You can define specific things before returning the handler: colors, template, etc.\n\t//\n\t// Optional. Default: beforeHandlerFunc\n\tBeforeHandlerFunc func(*Config)\n\n\t// You can use custom loggers with Fiber by using this field.\n\t// This field is really useful if you're using Zerolog, Zap, Logrus, apex/log etc.\n\t// If you don't define anything for this field, it'll use default logger of Fiber.\n\t//\n\t// Optional. Default: defaultLogger\n\tLoggerFunc func(c fiber.Ctx, data *Data, cfg *Config) error\n\n\ttimeZoneLocation *time.Location\n\n\t// Format defines the logging format for the middleware.\n\t//\n\t// You can customize the log output by defining a format string with placeholders\n\t// such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc.\n\t// The full list of available placeholders can be found in 'tags.go' or at\n\t// 'https://docs.gofiber.io/api/middleware/logger/#constants'.\n\t//\n\t// Fiber provides predefined logging formats that can be used directly:\n\t//\n\t//   - DefaultFormat    → Uses the default log format: \"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\"\n\t//   - CommonFormat     → Uses the Apache Common Log Format (CLF): \"${ip} - - [${time}] \\\"${method} ${url} ${protocol}\\\" ${status} ${bytesSent}\\n\"\n\t//   - CombinedFormat   → Uses the Apache Combined Log Format: \"${ip} - - [${time}] \\\"${method} ${url} ${protocol}\\\" ${status} ${bytesSent} \\\"${referer}\\\" \\\"${ua}\\\"\\n\"\n\t//   - JSONFormat      → Uses the JSON log format: \"{\\\"time\\\":\\\"${time}\\\",\\\"ip\\\":\\\"${ip}\\\",\\\"method\\\":\\\"${method}\\\",\\\"url\\\":\\\"${url}\\\",\\\"status\\\":${status},\\\"bytesSent\\\":${bytesSent}}\\n\"\n\t//   - ECSFormat        → Uses the Elastic Common Schema (ECS) log format: {\\\"@timestamp\\\":\\\"${time}\\\",\\\"ecs\\\":{\\\"version\\\":\\\"1.6.0\\\"},\\\"client\\\":{\\\"ip\\\":\\\"${ip}\\\"},\\\"http\\\":{\\\"request\\\":{\\\"method\\\":\\\"${method}\\\",\\\"url\\\":\\\"${url}\\\",\\\"protocol\\\":\\\"${protocol}\\\"},\\\"response\\\":{\\\"status_code\\\":${status},\\\"body\\\":{\\\"bytes\\\":${bytesSent}}}},\\\"log\\\":{\\\"level\\\":\\\"INFO\\\",\\\"logger\\\":\\\"fiber\\\"},\\\"message\\\":\\\"${method} ${url} responded with ${status}\\\"}\"\n\t// If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored.\n\t// If no format is specified, the default format is used:\n\t// \"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\"\n\tFormat string\n\n\t// TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html\n\t//\n\t// Optional. Default: 15:04:05\n\tTimeFormat string\n\n\t// TimeZone can be specified, such as \"UTC\" and \"America/New_York\" and \"Asia/Chongqing\", etc\n\t//\n\t// Optional. Default: \"Local\"\n\tTimeZone string\n\n\t// TimeInterval is the delay before the timestamp is updated\n\t//\n\t// Optional. Default: 500 * time.Millisecond\n\tTimeInterval time.Duration\n\n\t// DisableColors defines if the logs output should be colorized\n\t//\n\t// Default: false\n\tDisableColors bool\n\n\t// ForceColors forces the colors to be enabled even if the output is not a terminal\n\t//\n\t// Default: false\n\tForceColors bool\n\n\tenableColors  bool\n\tenableLatency bool\n}\n\nconst (\n\tstartTag       = \"${\"\n\tendTag         = \"}\"\n\tparamSeparator = \":\"\n)\n\n// Buffer abstracts the buffer operations used when rendering log entries.\ntype Buffer interface {\n\tLen() int\n\tReadFrom(r io.Reader) (int64, error)\n\tWriteTo(w io.Writer) (int64, error)\n\tBytes() []byte\n\tWrite(p []byte) (int, error)\n\tWriteByte(c byte) error\n\tWriteString(s string) (int, error)\n\tSet(p []byte)\n\tSetString(s string)\n\tString() string\n}\n\n// LogFunc formats logging output using the provided buffer and request data.\ntype LogFunc func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error)\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:              nil,\n\tSkip:              nil,\n\tDone:              nil,\n\tFormat:            DefaultFormat,\n\tTimeFormat:        \"15:04:05\",\n\tTimeZone:          \"Local\",\n\tTimeInterval:      500 * time.Millisecond,\n\tStream:            os.Stdout,\n\tBeforeHandlerFunc: beforeHandlerFunc,\n\tLoggerFunc:        defaultLoggerInstance,\n\tenableColors:      true,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif cfg.Skip == nil {\n\t\tcfg.Skip = ConfigDefault.Skip\n\t}\n\tif cfg.Done == nil {\n\t\tcfg.Done = ConfigDefault.Done\n\t}\n\tif cfg.Format == \"\" {\n\t\tcfg.Format = ConfigDefault.Format\n\t}\n\tif cfg.TimeZone == \"\" {\n\t\tcfg.TimeZone = ConfigDefault.TimeZone\n\t}\n\tif cfg.TimeFormat == \"\" {\n\t\tcfg.TimeFormat = ConfigDefault.TimeFormat\n\t}\n\tif int(cfg.TimeInterval) <= 0 {\n\t\tcfg.TimeInterval = ConfigDefault.TimeInterval\n\t}\n\tif cfg.Stream == nil {\n\t\tcfg.Stream = ConfigDefault.Stream\n\t}\n\n\tif cfg.BeforeHandlerFunc == nil {\n\t\tcfg.BeforeHandlerFunc = ConfigDefault.BeforeHandlerFunc\n\t}\n\n\tif cfg.LoggerFunc == nil {\n\t\tcfg.LoggerFunc = ConfigDefault.LoggerFunc\n\t}\n\n\t// Enable colors if no custom format or output is given\n\tif (!cfg.DisableColors && cfg.Stream == ConfigDefault.Stream) || cfg.ForceColors {\n\t\tcfg.enableColors = true\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/logger/data.go",
    "content": "package logger\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// Data is a struct to define some variables to use in custom logger function.\ntype Data struct {\n\tStart         time.Time\n\tStop          time.Time\n\tChainErr      error\n\tTimestamp     atomic.Value\n\tPid           string\n\tErrPaddingStr string\n\tTemplateChain [][]byte\n\tLogFuncChain  []LogFunc\n}\n"
  },
  {
    "path": "middleware/logger/default_logger.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/mattn/go-colorable\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/valyala/bytebufferpool\"\n)\n\n// default logger for fiber\nfunc defaultLoggerInstance(c fiber.Ctx, data *Data, cfg *Config) error {\n\tif cfg == nil {\n\t\tcfg = &Config{\n\t\t\tStream:       os.Stdout,\n\t\t\tFormat:       DefaultFormat,\n\t\t\tenableColors: true,\n\t\t}\n\t}\n\t// Check if Skip is defined and call it.\n\t// Now, if Skip(c) == true, we SKIP logging:\n\tif cfg.Skip != nil && cfg.Skip(c) {\n\t\treturn nil // Skip logging if Skip returns true\n\t}\n\n\t// Alias colors\n\tcolors := c.App().Config().ColorScheme\n\n\t// Get new buffer\n\tbuf := bytebufferpool.Get()\n\n\t// Default output when no custom Format or io.Writer is given\n\tif cfg.Format == DefaultFormat {\n\t\t// Format error if exist\n\t\tformatErr := \"\"\n\t\tif cfg.enableColors {\n\t\t\tif data.ChainErr != nil {\n\t\t\t\tformatErr = colors.Red + \" | \" + data.ChainErr.Error() + colors.Reset\n\t\t\t}\n\t\t\tfmt.Fprintf(buf,\n\t\t\t\t\"%s |%s %3d %s| %13v | %15s |%s %-7s %s| %-\"+data.ErrPaddingStr+\"s %s\\n\",\n\t\t\t\tdata.Timestamp.Load().(string), //nolint:forcetypeassert,errcheck // Timestamp is always a string\n\t\t\t\tstatusColor(c.Response().StatusCode(), &colors), c.Response().StatusCode(), colors.Reset,\n\t\t\t\tdata.Stop.Sub(data.Start),\n\t\t\t\tc.IP(),\n\t\t\t\tmethodColor(c.Method(), &colors), c.Method(), colors.Reset,\n\t\t\t\tc.Path(),\n\t\t\t\tformatErr,\n\t\t\t)\n\t\t} else {\n\t\t\tif data.ChainErr != nil {\n\t\t\t\tformatErr = \" | \" + data.ChainErr.Error()\n\t\t\t}\n\n\t\t\t// Helper function to append fixed-width string with padding\n\t\t\tfixedWidth := func(s string, width int, rightAlign bool) {\n\t\t\t\tif rightAlign {\n\t\t\t\t\tfor i := len(s); i < width; i++ {\n\t\t\t\t\t\tbuf.WriteByte(' ')\n\t\t\t\t\t}\n\t\t\t\t\tbuf.WriteString(s)\n\t\t\t\t} else {\n\t\t\t\t\tbuf.WriteString(s)\n\t\t\t\t\tfor i := len(s); i < width; i++ {\n\t\t\t\t\t\tbuf.WriteByte(' ')\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Timestamp\n\t\t\tbuf.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert,errcheck // Timestamp is always a string\n\t\t\tbuf.WriteString(\" | \")\n\n\t\t\t// Status Code with 3 fixed width, right aligned\n\t\t\tfixedWidth(strconv.Itoa(c.Response().StatusCode()), 3, true)\n\t\t\tbuf.WriteString(\" | \")\n\n\t\t\t// Duration with 13 fixed width, right aligned\n\t\t\tfixedWidth(data.Stop.Sub(data.Start).String(), 13, true)\n\t\t\tbuf.WriteString(\" | \")\n\n\t\t\t// Client IP with 15 fixed width, right aligned\n\t\t\tfixedWidth(c.IP(), 15, true)\n\t\t\tbuf.WriteString(\" | \")\n\n\t\t\t// HTTP Method with 7 fixed width, left aligned\n\t\t\tfixedWidth(c.Method(), 7, false)\n\t\t\tbuf.WriteString(\" | \")\n\n\t\t\t// Path with dynamic padding for error message, left aligned\n\t\t\terrPadding, _ := strconv.Atoi(data.ErrPaddingStr) //nolint:errcheck // It is fine to ignore the error\n\t\t\tfixedWidth(c.Path(), errPadding, false)\n\n\t\t\t// Error message\n\t\t\tbuf.WriteString(\" \")\n\t\t\tbuf.WriteString(formatErr)\n\t\t\tbuf.WriteString(\"\\n\")\n\t\t}\n\n\t\t// Write buffer to output\n\t\twriteLog(cfg.Stream, buf.Bytes())\n\n\t\tif cfg.Done != nil {\n\t\t\tcfg.Done(c, buf.Bytes())\n\t\t}\n\n\t\t// Put buffer back to pool\n\t\tbytebufferpool.Put(buf)\n\n\t\t// End chain\n\t\treturn nil\n\t}\n\n\tvar err error\n\t// Loop over template parts execute dynamic parts and add fixed parts to the buffer\n\tfor i, logFunc := range data.LogFuncChain {\n\t\tswitch {\n\t\tcase logFunc == nil:\n\t\t\tbuf.Write(data.TemplateChain[i])\n\t\tcase data.TemplateChain[i] == nil:\n\t\t\t_, err = logFunc(buf, c, data, \"\")\n\t\tdefault:\n\t\t\t_, err = logFunc(buf, c, data, utils.UnsafeString(data.TemplateChain[i]))\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Also write errors to the buffer\n\tif err != nil {\n\t\tbuf.WriteString(err.Error())\n\t}\n\n\twriteLog(cfg.Stream, buf.Bytes())\n\n\tif cfg.Done != nil {\n\t\tcfg.Done(c, buf.Bytes())\n\t}\n\n\t// Put buffer back to pool\n\tbytebufferpool.Put(buf)\n\n\treturn nil\n}\n\n// run something before returning the handler\nfunc beforeHandlerFunc(cfg *Config) {\n\tif cfg == nil {\n\t\treturn\n\t}\n\n\t// If colors are enabled, check terminal compatibility\n\tif cfg.enableColors && cfg.Stream == os.Stdout {\n\t\tcfg.Stream = colorable.NewColorableStdout()\n\t\tif !cfg.ForceColors && (os.Getenv(\"TERM\") == \"dumb\" || os.Getenv(\"NO_COLOR\") == \"1\" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))) {\n\t\t\tcfg.Stream = colorable.NewNonColorable(os.Stdout)\n\t\t}\n\t}\n}\n\nfunc appendInt(output Buffer, v int) (int, error) {\n\told := output.Len()\n\toutput.Set(strconv.AppendInt(output.Bytes(), int64(v), 10))\n\treturn output.Len() - old, nil\n}\n\n// writeLog writes a msg to w, printing a warning to stderr if the log fails.\nfunc writeLog(w io.Writer, msg []byte) {\n\tif _, err := w.Write(msg); err != nil {\n\t\t// Write error to output\n\t\tif _, writeErr := w.Write([]byte(err.Error())); writeErr != nil {\n\t\t\t// There is something wrong with the given io.Writer\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Failed to write to log, %v\\n\", writeErr)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "middleware/logger/errors.go",
    "content": "package logger\n\nimport (\n\t\"errors\"\n)\n\n// ErrTemplateParameterMissing indicates that a template parameter was referenced but not provided.\nvar ErrTemplateParameterMissing = errors.New(\"logger: template parameter missing\")\n"
  },
  {
    "path": "middleware/logger/format.go",
    "content": "package logger\n\nconst (\n\t// Fiber's default logger\n\tDefaultFormat = \"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\\n\"\n\t// Apache Common Log Format (CLF)\n\tCommonFormat = \"${ip} - - [${time}] \\\"${method} ${url} ${protocol}\\\" ${status} ${bytesSent}\\n\"\n\t// Apache Combined Log Format\n\tCombinedFormat = \"${ip} - - [${time}] \\\"${method} ${url} ${protocol}\\\" ${status} ${bytesSent} \\\"${referer}\\\" \\\"${ua}\\\"\\n\"\n\t// JSON log formats\n\tJSONFormat = \"{\\\"time\\\":\\\"${time}\\\",\\\"ip\\\":\\\"${ip}\\\",\\\"method\\\":\\\"${method}\\\",\\\"url\\\":\\\"${url}\\\",\\\"status\\\":${status},\\\"bytesSent\\\":${bytesSent}}\\n\"\n\t// Elastic Common Schema (ECS) Log Format\n\tECSFormat = \"{\\\"@timestamp\\\":\\\"${time}\\\",\\\"ecs\\\":{\\\"version\\\":\\\"1.6.0\\\"},\\\"client\\\":{\\\"ip\\\":\\\"${ip}\\\"},\\\"http\\\":{\\\"request\\\":{\\\"method\\\":\\\"${method}\\\",\\\"url\\\":\\\"${url}\\\",\\\"protocol\\\":\\\"${protocol}\\\"},\\\"response\\\":{\\\"status_code\\\":${status},\\\"body\\\":{\\\"bytes\\\":${bytesSent}}}},\\\"log\\\":{\\\"level\\\":\\\"INFO\\\",\\\"logger\\\":\\\"fiber\\\"},\\\"message\\\":\\\"${method} ${url} responded with ${status}\\\"}\\n\"\n)\n"
  },
  {
    "path": "middleware/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Get timezone location\n\ttz, err := time.LoadLocation(cfg.TimeZone)\n\tif err != nil || tz == nil {\n\t\tcfg.timeZoneLocation = time.Local\n\t} else {\n\t\tcfg.timeZoneLocation = tz\n\t}\n\n\t// Check if format contains latency\n\tcfg.enableLatency = strings.Contains(cfg.Format, \"${\"+TagLatency+\"}\")\n\n\tvar timestamp atomic.Value\n\t// Create correct timeformat\n\ttimestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))\n\n\t// Update date/time every 500 milliseconds in a separate go routine\n\tif strings.Contains(cfg.Format, \"${\"+TagTime+\"}\") {\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\ttime.Sleep(cfg.TimeInterval)\n\t\t\t\ttimestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))\n\t\t\t}\n\t\t}()\n\t}\n\t// Set PID once\n\tpid := strconv.Itoa(os.Getpid())\n\n\t// Set variables\n\tvar (\n\t\tonce       sync.Once\n\t\terrHandler fiber.ErrorHandler\n\n\t\tdataPool = sync.Pool{New: func() any { return new(Data) }}\n\t)\n\n\t// Err padding\n\terrPadding := 15\n\terrPaddingStr := strconv.Itoa(errPadding)\n\n\t// Before handling func\n\tcfg.BeforeHandlerFunc(&cfg)\n\n\t// Logger data\n\t// instead of analyzing the template inside(handler) each time, this is done once before\n\t// and we create several slices of the same length with the functions to be executed and fixed parts.\n\ttemplateChain, logFunChain, err := buildLogFuncChain(&cfg, createTagMap(&cfg))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Set error handler once\n\t\tonce.Do(func() {\n\t\t\t// get longest possible path\n\t\t\tstack := c.App().Stack()\n\t\t\tfor m := range stack {\n\t\t\t\tfor r := range stack[m] {\n\t\t\t\t\tif len(stack[m][r].Path) > errPadding {\n\t\t\t\t\t\terrPadding = len(stack[m][r].Path)\n\t\t\t\t\t\terrPaddingStr = strconv.Itoa(errPadding)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// override error handler\n\t\t\terrHandler = c.App().ErrorHandler\n\t\t})\n\n\t\t// Logger data\n\t\tdata := dataPool.Get().(*Data) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n\t\t// no need for a reset, as long as we always override everything\n\t\tdata.Pid = pid\n\t\tdata.ErrPaddingStr = errPaddingStr\n\t\tdata.Timestamp = timestamp\n\t\tdata.TemplateChain = templateChain\n\t\tdata.LogFuncChain = logFunChain\n\t\t// put data back in the pool\n\t\tdefer dataPool.Put(data)\n\n\t\t// Set latency start time\n\t\tif cfg.enableLatency {\n\t\t\tdata.Start = time.Now()\n\t\t}\n\n\t\t// Handle request, store err for logging\n\t\tchainErr := c.Next()\n\n\t\tdata.ChainErr = chainErr\n\t\t// Manually call error handler\n\t\tif chainErr != nil {\n\t\t\tif err := errHandler(c, chainErr); err != nil {\n\t\t\t\t_ = c.SendStatus(fiber.StatusInternalServerError) //nolint:errcheck // TODO: Explain why we ignore the error here\n\t\t\t}\n\t\t}\n\n\t\t// Set latency stop time\n\t\tif cfg.enableLatency {\n\t\t\tdata.Stop = time.Now()\n\t\t}\n\n\t\t// Logger instance & update some logger data fields\n\t\treturn cfg.LoggerFunc(c, data, &cfg)\n\t}\n}\n"
  },
  {
    "path": "middleware/logger/logger_test.go",
    "content": "//nolint:depguard // Because we test logging :D\npackage logger\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\tfiberlog \"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/fiber/v3/middleware/requestid\"\n)\n\nconst (\n\tpathFooBar = \"/?foo=bar\"\n\thttpProto  = \"HTTP/1.1\"\n)\n\nfunc benchmarkSetup(b *testing.B, app *fiber.App, uri string) {\n\tb.Helper()\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\tfctx.Request.SetRequestURI(uri)\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n}\n\nfunc benchmarkSetupParallel(b *testing.B, app *fiber.App, path string) {\n\tb.Helper()\n\n\thandler := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.Header.SetMethod(fiber.MethodGet)\n\t\tfctx.Request.SetRequestURI(path)\n\n\t\tfor pb.Next() {\n\t\t\thandler(fctx)\n\t\t}\n\t})\n}\n\n// go test -run Test_Logger\nfunc Test_Logger(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${error}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Get(\"/\", func(_ fiber.Ctx) error {\n\t\treturn errors.New(\"some random error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Equal(t, \"some random error\", buf.String())\n}\n\n// go test -run Test_Logger_locals\nfunc Test_Logger_locals(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${locals:demo}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Locals(\"demo\", \"johndoe\")\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Get(\"/int\", func(c fiber.Ctx) error {\n\t\tc.Locals(\"demo\", 55)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Get(\"/empty\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"johndoe\", buf.String())\n\n\tbuf.Reset()\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/int\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"55\", buf.String())\n\n\tbuf.Reset()\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/empty\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Empty(t, buf.String())\n}\n\n// go test -run Test_Logger_Next\nfunc Test_Logger_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_Logger_Done\nfunc Test_Logger_Done(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytes.NewBuffer(nil)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tDone: func(c fiber.Ctx, logString []byte) {\n\t\t\tif c.Response().StatusCode() == fiber.StatusOK {\n\t\t\t\t_, err := buf.Write(logString)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t},\n\t})).Get(\"/logging\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/logging\", http.NoBody))\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Positive(t, buf.Len(), 0)\n}\n\n// Test_Logger_Filter tests the Filter functionality of the logger middleware.\n// It verifies that logs are written or skipped based on the filter condition.\nfunc Test_Logger_Filter(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Test Not Found\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tlogOutput := bytes.Buffer{}\n\n\t\t// Return true to skip logging for all requests != 404\n\t\tapp.Use(New(Config{\n\t\t\tSkip: func(c fiber.Ctx) bool {\n\t\t\t\treturn c.Response().StatusCode() != fiber.StatusNotFound\n\t\t\t},\n\t\t\tStream: &logOutput,\n\t\t}))\n\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/nonexistent\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\t\t// Expect logs for the 404 request\n\t\trequire.Contains(t, logOutput.String(), \"404\")\n\t})\n\n\tt.Run(\"Test OK\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tlogOutput := bytes.Buffer{}\n\n\t\t// Return true to skip logging for all requests == 200\n\t\tapp.Use(New(Config{\n\t\t\tSkip: func(c fiber.Ctx) bool {\n\t\t\t\treturn c.Response().StatusCode() == fiber.StatusOK\n\t\t\t},\n\t\t\tStream: &logOutput,\n\t\t}))\n\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t// We skip logging for status == 200, so \"200\" should not appear\n\t\trequire.NotContains(t, logOutput.String(), \"200\")\n\t})\n\n\tt.Run(\"Always Skip\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tlogOutput := bytes.Buffer{}\n\n\t\t// Filter always returns true => skip all logs\n\t\tapp.Use(New(Config{\n\t\t\tSkip: func(_ fiber.Ctx) bool {\n\t\t\t\treturn true // always skip\n\t\t\t},\n\t\t\tStream: &logOutput,\n\t\t}))\n\n\t\tapp.Get(\"/something\", func(c fiber.Ctx) error {\n\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"I'm a teapot\")\n\t\t})\n\n\t\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/something\", http.NoBody))\n\t\trequire.NoError(t, err)\n\n\t\t// Expect NO logs\n\t\trequire.Empty(t, logOutput.String())\n\t})\n\n\tt.Run(\"Never Skip\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tlogOutput := bytes.Buffer{}\n\n\t\t// Filter always returns false => never skip logs\n\t\tapp.Use(New(Config{\n\t\t\tSkip: func(_ fiber.Ctx) bool {\n\t\t\t\treturn false // never skip\n\t\t\t},\n\t\t\tStream: &logOutput,\n\t\t}))\n\n\t\tapp.Get(\"/always\", func(c fiber.Ctx) error {\n\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"Teapot again\")\n\t\t})\n\n\t\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/always\", http.NoBody))\n\t\trequire.NoError(t, err)\n\n\t\t// Expect some logging - check any substring\n\t\trequire.Contains(t, logOutput.String(), strconv.Itoa(fiber.StatusTeapot))\n\t})\n\n\tt.Run(\"Skip /healthz\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\n\t\tlogOutput := bytes.Buffer{}\n\n\t\t// Filter returns true (skip logs) if the request path is /healthz\n\t\tapp.Use(New(Config{\n\t\t\tSkip: func(c fiber.Ctx) bool {\n\t\t\t\treturn c.Path() == \"/healthz\"\n\t\t\t},\n\t\t\tStream: &logOutput,\n\t\t}))\n\n\t\t// Normal route\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello World!\")\n\t\t})\n\t\t// Health route\n\t\tapp.Get(\"/healthz\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\t// Request to \"/\" -> should be logged\n\t\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, logOutput.String(), \"200\")\n\n\t\t// Reset output buffer\n\t\tlogOutput.Reset()\n\n\t\t// Request to \"/healthz\" -> should be skipped\n\t\t_, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/healthz\", http.NoBody))\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, logOutput.String())\n\t})\n}\n\n// go test -run Test_Logger_ErrorTimeZone\nfunc Test_Logger_ErrorTimeZone(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tTimeZone: \"invalid\",\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_Logger_Fiber_Logger\nfunc Test_Logger_LoggerToWriter(t *testing.T) {\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tt.Cleanup(func() {\n\t\tbytebufferpool.Put(buf)\n\t})\n\n\tlogger := fiberlog.DefaultLogger[*log.Logger]()\n\tstdlogger := logger.Logger()\n\tstdlogger.SetFlags(0)\n\tlogger.SetOutput(buf)\n\n\ttestCases := []struct {\n\t\tlevelStr string\n\t\tlevel    fiberlog.Level\n\t}{\n\t\t{\n\t\t\tlevel:    fiberlog.LevelTrace,\n\t\t\tlevelStr: \"Trace\",\n\t\t},\n\t\t{\n\t\t\tlevel:    fiberlog.LevelDebug,\n\t\t\tlevelStr: \"Debug\",\n\t\t},\n\t\t{\n\t\t\tlevel:    fiberlog.LevelInfo,\n\t\t\tlevelStr: \"Info\",\n\t\t},\n\t\t{\n\t\t\tlevel:    fiberlog.LevelWarn,\n\t\t\tlevelStr: \"Warn\",\n\t\t},\n\t\t{\n\t\t\tlevel:    fiberlog.LevelError,\n\t\t\tlevelStr: \"Error\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tlevel := strconv.Itoa(int(tc.level))\n\t\tt.Run(level, func(t *testing.T) {\n\t\t\tbuf.Reset()\n\n\t\t\tapp.Use(\"/\"+level, New(Config{\n\t\t\t\tFormat: \"${error}\",\n\t\t\t\tStream: LoggerToWriter(logger, tc.\n\t\t\t\t\tlevel),\n\t\t\t}))\n\n\t\t\tapp.Get(\"/\"+level, func(_ fiber.Ctx) error {\n\t\t\t\treturn errors.New(\"some random error\")\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\"+level, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\t\t\trequire.Equal(t, \"[\"+tc.levelStr+\"] some random error\\n\", buf.String())\n\t\t})\n\n\t\trequire.Panics(t, func() {\n\t\t\tLoggerToWriter(logger, fiberlog.LevelPanic)\n\t\t})\n\n\t\trequire.Panics(t, func() {\n\t\t\tLoggerToWriter(logger, fiberlog.LevelFatal)\n\t\t})\n\n\t\trequire.Panics(t, func() {\n\t\t\tLoggerToWriter[any](nil, fiberlog.LevelFatal)\n\t\t})\n\t}\n}\n\ntype fakeErrorOutput int\n\nfunc (o *fakeErrorOutput) Write([]byte) (int, error) {\n\t*o++\n\treturn 0, errors.New(\"fake output\")\n}\n\n// go test -run Test_Logger_ErrorOutput_WithoutColor\nfunc Test_Logger_ErrorOutput_WithoutColor(t *testing.T) {\n\tt.Parallel()\n\to := new(fakeErrorOutput)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStream:        o,\n\t\tDisableColors: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\trequire.EqualValues(t, 2, *o)\n}\n\n// go test -run Test_Logger_ErrorOutput\nfunc Test_Logger_ErrorOutput(t *testing.T) {\n\tt.Parallel()\n\to := new(fakeErrorOutput)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStream: o,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\trequire.EqualValues(t, 2, *o)\n}\n\n// go test -run Test_Logger_All\nfunc Test_Logger_All(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}\",\n\t\tStream: buf,\n\t}))\n\n\t// Alias colors\n\tcolors := app.Config().ColorScheme\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, pathFooBar, http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := fmt.Sprintf(\"%dHost=example.comhttpHTTP/1.10.0.0.0example.com/?foo=bar/%s%s%s%s%s%s%s%s%sNot Found\", os.Getpid(), colors.Black, colors.Red, colors.Green, colors.Yellow, colors.Blue, colors.Magenta, colors.Cyan, colors.White, colors.Reset)\n\trequire.Equal(t, expected, buf.String())\n}\n\nfunc Test_Logger_CLF_Format(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tFormat: CommonFormat,\n\t\tStream: buf,\n\t}))\n\n\tmethod := fiber.MethodGet\n\tstatus := fiber.StatusNotFound\n\tbytesSent := 0\n\n\tresp, err := app.Test(httptest.NewRequest(method, pathFooBar, http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, status, resp.StatusCode)\n\n\tpattern := fmt.Sprintf(`0\\.0\\.0\\.0 - - \\[\\d{2}:\\d{2}:\\d{2}\\] \"%s %s %s\" %d %d`, method, regexp.QuoteMeta(pathFooBar), httpProto, status, bytesSent)\n\trequire.Regexp(t, pattern, buf.String())\n}\n\nfunc Test_Logger_Combined_CLF_Format(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tFormat: CombinedFormat,\n\t\tStream: buf,\n\t}))\n\n\tmethod := fiber.MethodGet\n\tstatus := fiber.StatusNotFound\n\tbytesSent := 0\n\treferer := \"http://example.com\"\n\tua := \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36\"\n\n\treq := httptest.NewRequest(method, pathFooBar, http.NoBody)\n\treq.Header.Set(\"Referer\", referer)\n\treq.Header.Set(\"User-Agent\", ua)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, status, resp.StatusCode)\n\n\tpattern := fmt.Sprintf(`0\\.0\\.0\\.0 - - \\[\\d{2}:\\d{2}:\\d{2}\\] \"%s %s %s\" %d %d \"%s\" \"%s\"`, method, regexp.QuoteMeta(pathFooBar), httpProto, status, bytesSent, regexp.QuoteMeta(referer), regexp.QuoteMeta(ua)) //nolint:gocritic // double quoting for regex and string is not needed\n\trequire.Regexp(t, pattern, buf.String())\n}\n\nfunc Test_Logger_Json_Format(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tFormat: JSONFormat,\n\t\tStream: buf,\n\t}))\n\n\tmethod := fiber.MethodGet\n\tstatus := fiber.StatusNotFound\n\tip := \"0.0.0.0\"\n\tbytesSent := 0\n\n\treq := httptest.NewRequest(method, pathFooBar, http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, status, resp.StatusCode)\n\n\tpattern := fmt.Sprintf(`\\{\"time\":\"\\d{2}:\\d{2}:\\d{2}\",\"ip\":\"%s\",\"method\":%q,\"url\":\"%s\",\"status\":%d,\"bytesSent\":%d\\}`, regexp.QuoteMeta(ip), method, regexp.QuoteMeta(pathFooBar), status, bytesSent) //nolint:gocritic // double quoting for regex and string is not needed\n\trequire.Regexp(t, pattern, buf.String())\n}\n\nfunc Test_Logger_ECS_Format(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tFormat: ECSFormat,\n\t\tStream: buf,\n\t}))\n\n\tmethod := fiber.MethodGet\n\tstatus := fiber.StatusNotFound\n\tip := \"0.0.0.0\"\n\tbytesSent := 0\n\tmsg := fmt.Sprintf(\"%s %s responded with %d\", method, pathFooBar, status)\n\n\treq := httptest.NewRequest(method, pathFooBar, http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, status, resp.StatusCode)\n\n\tpattern := fmt.Sprintf(`\\{\"@timestamp\":\"\\d{2}:\\d{2}:\\d{2}\",\"ecs\":\\{\"version\":\"1.6.0\"\\},\"client\":\\{\"ip\":\"%s\"\\},\"http\":\\{\"request\":\\{\"method\":%q,\"url\":\"%s\",\"protocol\":%q\\},\"response\":\\{\"status_code\":%d,\"body\":\\{\"bytes\":%d\\}\\}\\},\"log\":\\{\"level\":\"INFO\",\"logger\":\"fiber\"\\},\"message\":\"%s\"\\}`, regexp.QuoteMeta(ip), method, regexp.QuoteMeta(pathFooBar), httpProto, status, bytesSent, regexp.QuoteMeta(msg)) //nolint:gocritic // double quoting for regex and string is not needed\n\trequire.Regexp(t, pattern, buf.String())\n}\n\nfunc getLatencyTimeUnits() []struct {\n\tunit string\n\tdiv  time.Duration\n} {\n\t// windows does not support µs sleep precision\n\t// https://github.com/golang/go/issues/29485\n\tif runtime.GOOS == \"windows\" {\n\t\treturn []struct {\n\t\t\tunit string\n\t\t\tdiv  time.Duration\n\t\t}{\n\t\t\t{unit: \"ms\", div: time.Millisecond},\n\t\t\t{unit: \"s\", div: time.Second},\n\t\t}\n\t}\n\treturn []struct {\n\t\tunit string\n\t\tdiv  time.Duration\n\t}{\n\t\t{unit: \"µs\", div: time.Microsecond},\n\t\t{unit: \"ms\", div: time.Millisecond},\n\t\t{unit: \"s\", div: time.Second},\n\t}\n}\n\n// go test -run Test_Logger_WithLatency\nfunc Test_Logger_WithLatency(t *testing.T) {\n\tbuff := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buff)\n\tapp := fiber.New()\n\n\tlogger := New(Config{\n\t\tStream: buff,\n\t\tFormat: \"${latency}\",\n\t})\n\tapp.Use(logger)\n\n\t// Define a list of time units to test\n\ttimeUnits := getLatencyTimeUnits()\n\n\t// Initialize a new time unit\n\tsleepDuration := 1 * time.Nanosecond\n\n\t// Define a test route that sleeps\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(sleepDuration)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\t// Loop through each time unit and assert that the log output contains the expected latency value\n\tfor _, tu := range timeUnits {\n\t\t// Update the sleep duration for the next iteration\n\t\tsleepDuration = 1 * tu.div\n\n\t\t// Create a new HTTP request to the test route\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\t\tTimeout:       3 * time.Second,\n\t\t\tFailOnTimeout: true,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t// Assert that the log output contains the expected latency value in the current time unit\n\t\trequire.True(t, bytes.HasSuffix(buff.Bytes(), []byte(tu.unit)), \"Expected latency to be in %s, got %s\", tu.unit, buff.String())\n\n\t\t// Reset the buffer\n\t\tbuff.Reset()\n\t}\n}\n\n// go test -run Test_Logger_WithLatency_DefaultFormat\nfunc Test_Logger_WithLatency_DefaultFormat(t *testing.T) {\n\tbuff := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buff)\n\tapp := fiber.New()\n\n\tlogger := New(Config{\n\t\tStream: buff,\n\t})\n\tapp.Use(logger)\n\n\t// Define a list of time units to test\n\ttimeUnits := getLatencyTimeUnits()\n\n\t// Initialize a new time unit\n\tsleepDuration := 1 * time.Nanosecond\n\n\t// Define a test route that sleeps\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(sleepDuration)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\t// Loop through each time unit and assert that the log output contains the expected latency value\n\tfor _, tu := range timeUnits {\n\t\t// Update the sleep duration for the next iteration\n\t\tsleepDuration = 1 * tu.div\n\n\t\t// Create a new HTTP request to the test route\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\t\tTimeout:       2 * time.Second,\n\t\t\tFailOnTimeout: true,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t// Assert that the log output contains the expected latency value in the current time unit\n\t\t// parse out the latency value from the log output\n\t\tlatency := bytes.Split(buff.Bytes(), []byte(\" | \"))[2]\n\t\t// Assert that the latency value is in the current time unit\n\t\trequire.True(t, bytes.HasSuffix(latency, []byte(tu.unit)), \"Expected latency to be in %s, got %s\", tu.unit, latency)\n\n\t\t// Reset the buffer\n\t\tbuff.Reset()\n\t}\n}\n\n// go test -run Test_Query_Params\nfunc Test_Query_Params(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${queryParams}\",\n\t\tStream: buf,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?foo=bar&baz=moz\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := \"foo=bar&baz=moz\"\n\trequire.Equal(t, expected, buf.String())\n}\n\n// go test -run Test_Response_Body\nfunc Test_Response_Body(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${resBody}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Sample response body\")\n\t})\n\n\tapp.Post(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.Send([]byte(\"Post in test\"))\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\n\texpectedGetResponse := \"Sample response body\"\n\trequire.Equal(t, expectedGetResponse, buf.String())\n\n\tbuf.Reset() // Reset buffer to test POST\n\t_, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/test\", http.NoBody))\n\n\texpectedPostResponse := \"Post in test\"\n\trequire.NoError(t, err)\n\trequire.Equal(t, expectedPostResponse, buf.String())\n}\n\n// go test -run Test_Request_Body\nfunc Test_Request_Body(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.SetContentLength(5)\n\t\treturn c.SendString(\"World\")\n\t})\n\n\t// Create a POST request with a body\n\tbody := []byte(\"Hello\")\n\treq := httptest.NewRequest(fiber.MethodPost, \"/\", bytes.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\n\t_, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"5 5 200\", buf.String())\n}\n\n// go test -run Test_Logger_AppendUint\nfunc Test_Logger_AppendUint(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tapp.Get(\"/content\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.SetContentLength(5)\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"-2 0 200\", buf.String())\n\n\tbuf.Reset()\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/content\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"-2 5 200\", buf.String())\n}\n\n// go test -run Test_Logger_Data_Race -race\nfunc Test_Logger_Data_Race(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Use(New(ConfigDefault))\n\tapp.Use(New(Config{\n\t\tFormat: \"${time} | ${pid} | ${locals:requestid} | ${status} | ${latency} | ${method} | ${path}\\n\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tvar (\n\t\tresp1, resp2 *http.Response\n\t\terr1, err2   error\n\t)\n\twg := &sync.WaitGroup{}\n\twg.Go(func() {\n\t\tresp1, err1 = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\t})\n\tresp2, err2 = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\twg.Wait()\n\n\trequire.NoError(t, err1)\n\trequire.Equal(t, fiber.StatusOK, resp1.StatusCode)\n\trequire.NoError(t, err2)\n\trequire.Equal(t, fiber.StatusOK, resp2.StatusCode)\n}\n\n// go test -run Test_Response_Header\nfunc Test_Response_Header(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(requestid.New(requestid.Config{\n\t\tNext:      nil,\n\t\tHeader:    fiber.HeaderXRequestID,\n\t\tGenerator: func() string { return \"Hello fiber!\" },\n\t}))\n\tapp.Use(New(Config{\n\t\tFormat: \"${respHeader:X-Request-ID}\",\n\t\tStream: buf,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello fiber!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"Hello fiber!\", buf.String())\n}\n\n// go test -run Test_Req_Header\nfunc Test_Req_Header(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${reqHeader:test}\",\n\t\tStream: buf,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello fiber!\")\n\t})\n\theaderReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\theaderReq.Header.Add(\"test\", \"Hello fiber!\")\n\n\tresp, err := app.Test(headerReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"Hello fiber!\", buf.String())\n}\n\n// go test -run Test_ReqHeader_Header\nfunc Test_ReqHeader_Header(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${reqHeader:test}\",\n\t\tStream: buf,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello fiber!\")\n\t})\n\treqHeaderReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqHeaderReq.Header.Add(\"test\", \"Hello fiber!\")\n\n\tresp, err := app.Test(reqHeaderReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"Hello fiber!\", buf.String())\n}\n\n// go test -run Test_CustomTags\nfunc Test_CustomTags(t *testing.T) {\n\tt.Parallel()\n\tcustomTag := \"it is a custom tag\"\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${custom_tag}\",\n\t\tCustomTags: map[string]LogFunc{\n\t\t\t\"custom_tag\": func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\t\treturn output.WriteString(customTag)\n\t\t\t},\n\t\t},\n\t\tStream: buf,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello fiber!\")\n\t})\n\treqHeaderReq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treqHeaderReq.Header.Add(\"test\", \"Hello fiber!\")\n\n\tresp, err := app.Test(reqHeaderReq)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, customTag, buf.String())\n}\n\n// go test -run Test_Logger_ByteSent_Streaming\nfunc Test_Logger_ByteSent_Streaming(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp.Use(New(Config{\n\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\tStream: buf,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"Connection\", \"keep-alive\")\n\t\tc.Set(\"Transfer-Encoding\", \"chunked\")\n\t\tc.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {\n\t\t\tvar i int\n\t\t\tfor {\n\t\t\t\ti++\n\t\t\t\tmsg := fmt.Sprintf(\"%d - the time is %v\", i, time.Now())\n\t\t\t\tfmt.Fprintf(w, \"data: Message: %s\\n\\n\", msg)\n\t\t\t\terr := w.Flush()\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif i == 10 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t// -2 means identity, -1 means chunked, 200 status\n\trequire.Equal(t, \"-2 -1 200\", buf.String())\n}\n\ntype fakeOutput int\n\nfunc (o *fakeOutput) Write(b []byte) (int, error) {\n\t*o++\n\treturn len(b), nil\n}\n\n// go test -run Test_Logger_EnableColors\nfunc Test_Logger_EnableColors(t *testing.T) {\n\tt.Parallel()\n\to := new(fakeOutput)\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStream: o,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\trequire.EqualValues(t, 1, *o)\n}\n\n// go test -run Test_Logger_ForceColors\nfunc Test_Logger_ForceColors(t *testing.T) {\n\tt.Parallel()\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tFormat:        \"${ip}${status}${method}${path}${error}\\n\",\n\t\tStream:        buf,\n\t\tDisableColors: true,\n\t\tForceColors:   true,\n\t}))\n\n\t// Alias colors\n\tcolors := app.Config().ColorScheme\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := fmt.Sprintf(\"0.0.0.0%s404%s%sGET%s/%sNot Found%s\\n\", colors.Yellow, colors.Reset, colors.Cyan, colors.Reset, colors.Red, colors.Reset)\n\trequire.Equal(t, expected, buf.String())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Logger$ -benchmem -count=4\nfunc Benchmark_Logger(b *testing.B) {\n\tb.Run(\"NoMiddleware\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithBytesAndStatus\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"test\", \"test\")\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormat\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatDisableColors\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream:        io.Discard,\n\t\t\tDisableColors: true,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatForceColors\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream:      io.Discard,\n\t\t\tForceColors: true,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatWithFiberLog\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tlogger := fiberlog.DefaultLogger[*log.Logger]()\n\t\tlogger.SetOutput(io.Discard)\n\t\tapp.Use(New(Config{\n\t\t\tStream: LoggerToWriter(logger, fiberlog.LevelDebug),\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithTagParameter\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"test\", \"test\")\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithLocals\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${locals:demo}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Locals(\"demo\", \"johndoe\")\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithLocalsInt\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${locals:demo}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/int\", func(c fiber.Ctx) error {\n\t\t\tc.Locals(\"demo\", 55)\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/int\")\n\t})\n\n\tb.Run(\"WithCustomDone\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tDone: func(c fiber.Ctx, logString []byte) {\n\t\t\t\tif c.Response().StatusCode() == fiber.StatusOK {\n\t\t\t\t\tio.Discard.Write(logString) //nolint:errcheck // ignore error\n\t\t\t\t}\n\t\t\t},\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/logging\", func(ctx fiber.Ctx) error {\n\t\t\treturn ctx.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/logging\")\n\t})\n\n\tb.Run(\"WithAllTags\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"Streaming\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"Connection\", \"keep-alive\")\n\t\t\tc.Set(\"Transfer-Encoding\", \"chunked\")\n\t\t\tc.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {\n\t\t\t\tvar i int\n\t\t\t\tfor {\n\t\t\t\t\ti++\n\t\t\t\t\tmsg := fmt.Sprintf(\"%d - the time is %v\", i, time.Now())\n\t\t\t\t\tfmt.Fprintf(w, \"data: Message: %s\\n\\n\", msg)\n\t\t\t\t\terr := w.Flush()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tif i == 10 {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn nil\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithBody\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${resBody}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Sample response body\")\n\t\t})\n\t\tbenchmarkSetup(bb, app, \"/\")\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Logger_Parallel$ -benchmem -count=4\nfunc Benchmark_Logger_Parallel(b *testing.B) {\n\tb.Run(\"NoMiddleware\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithBytesAndStatus\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"test\", \"test\")\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormat\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatWithFiberLog\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tlogger := fiberlog.DefaultLogger[*log.Logger]()\n\t\tlogger.SetOutput(io.Discard)\n\t\tapp.Use(New(Config{\n\t\t\tStream: LoggerToWriter(logger, fiberlog.LevelDebug),\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatDisableColors\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream:        io.Discard,\n\t\t\tDisableColors: true,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"DefaultFormatForceColors\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tStream:      io.Discard,\n\t\t\tForceColors: true,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithTagParameter\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"test\", \"test\")\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithLocals\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${locals:demo}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Locals(\"demo\", \"johndoe\")\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithLocalsInt\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${locals:demo}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/int\", func(c fiber.Ctx) error {\n\t\t\tc.Locals(\"demo\", 55)\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/int\")\n\t})\n\n\tb.Run(\"WithCustomDone\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tDone: func(c fiber.Ctx, logString []byte) {\n\t\t\t\tif c.Response().StatusCode() == fiber.StatusOK {\n\t\t\t\t\tio.Discard.Write(logString) //nolint:errcheck // ignore error\n\t\t\t\t}\n\t\t\t},\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/logging\", func(ctx fiber.Ctx) error {\n\t\t\treturn ctx.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/logging\")\n\t})\n\n\tb.Run(\"WithAllTags\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Hello, World!\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"Streaming\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${bytesReceived} ${bytesSent} ${status}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\tc.Set(\"Connection\", \"keep-alive\")\n\t\t\tc.Set(\"Transfer-Encoding\", \"chunked\")\n\t\t\tc.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {\n\t\t\t\tvar i int\n\t\t\t\tfor {\n\t\t\t\t\ti++\n\t\t\t\t\tmsg := fmt.Sprintf(\"%d - the time is %v\", i, time.Now())\n\t\t\t\t\tfmt.Fprintf(w, \"data: Message: %s\\n\\n\", msg)\n\t\t\t\t\terr := w.Flush()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tif i == 10 {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn nil\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n\n\tb.Run(\"WithBody\", func(bb *testing.B) {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(Config{\n\t\t\tFormat: \"${resBody}\",\n\t\t\tStream: io.Discard,\n\t\t}))\n\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Sample response body\")\n\t\t})\n\t\tbenchmarkSetupParallel(bb, app, \"/\")\n\t})\n}\n"
  },
  {
    "path": "middleware/logger/tags.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Logger variables\nconst (\n\tTagPid               = \"pid\"\n\tTagTime              = \"time\"\n\tTagReferer           = \"referer\"\n\tTagProtocol          = \"protocol\"\n\tTagScheme            = \"scheme\"\n\tTagPort              = \"port\"\n\tTagIP                = \"ip\"\n\tTagIPs               = \"ips\"\n\tTagHost              = \"host\"\n\tTagMethod            = \"method\"\n\tTagPath              = \"path\"\n\tTagURL               = \"url\"\n\tTagUA                = \"ua\"\n\tTagLatency           = \"latency\"\n\tTagStatus            = \"status\"\n\tTagResBody           = \"resBody\"\n\tTagReqHeaders        = \"reqHeaders\"\n\tTagQueryStringParams = \"queryParams\"\n\tTagBody              = \"body\"\n\tTagBytesSent         = \"bytesSent\"\n\tTagBytesReceived     = \"bytesReceived\"\n\tTagRoute             = \"route\"\n\tTagError             = \"error\"\n\tTagReqHeader         = \"reqHeader:\"\n\tTagRespHeader        = \"respHeader:\"\n\tTagLocals            = \"locals:\"\n\tTagQuery             = \"query:\"\n\tTagForm              = \"form:\"\n\tTagCookie            = \"cookie:\"\n\tTagBlack             = \"black\"\n\tTagRed               = \"red\"\n\tTagGreen             = \"green\"\n\tTagYellow            = \"yellow\"\n\tTagBlue              = \"blue\"\n\tTagMagenta           = \"magenta\"\n\tTagCyan              = \"cyan\"\n\tTagWhite             = \"white\"\n\tTagReset             = \"reset\"\n)\n\n// createTagMap function merged the default with the custom tags\nfunc createTagMap(cfg *Config) map[string]LogFunc {\n\t// Set default tags\n\ttagFunctions := map[string]LogFunc{\n\t\tTagReferer: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Get(fiber.HeaderReferer))\n\t\t},\n\t\tTagProtocol: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Protocol())\n\t\t},\n\t\tTagScheme: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Scheme())\n\t\t},\n\t\tTagPort: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Port())\n\t\t},\n\t\tTagIP: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.IP())\n\t\t},\n\t\tTagIPs: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Get(fiber.HeaderXForwardedFor))\n\t\t},\n\t\tTagHost: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Hostname())\n\t\t},\n\t\tTagPath: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Path())\n\t\t},\n\t\tTagURL: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.OriginalURL())\n\t\t},\n\t\tTagUA: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Get(fiber.HeaderUserAgent))\n\t\t},\n\t\tTagBody: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.Write(c.Body())\n\t\t},\n\t\tTagBytesReceived: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn appendInt(output, c.Request().Header.ContentLength())\n\t\t},\n\t\tTagBytesSent: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn appendInt(output, c.Response().Header.ContentLength())\n\t\t},\n\t\tTagRoute: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Route().Path)\n\t\t},\n\t\tTagResBody: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.Write(c.Response().Body())\n\t\t},\n\t\tTagReqHeaders: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\tout := make(map[string][]string)\n\t\t\tif err := c.Bind().Header(&out); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\n\t\t\treqHeaders := make([]string, 0, len(out))\n\t\t\tfor k, v := range out {\n\t\t\t\treqHeaders = append(reqHeaders, k+\"=\"+strings.Join(v, \",\"))\n\t\t\t}\n\t\t\treturn output.WriteString(strings.Join(reqHeaders, \"&\"))\n\t\t},\n\t\tTagQueryStringParams: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.Request().URI().QueryArgs().String())\n\t\t},\n\n\t\tTagBlack: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Black)\n\t\t},\n\t\tTagRed: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Red)\n\t\t},\n\t\tTagGreen: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Green)\n\t\t},\n\t\tTagYellow: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Yellow)\n\t\t},\n\t\tTagBlue: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Blue)\n\t\t},\n\t\tTagMagenta: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Magenta)\n\t\t},\n\t\tTagCyan: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Cyan)\n\t\t},\n\t\tTagWhite: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.White)\n\t\t},\n\t\tTagReset: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(c.App().Config().ColorScheme.Reset)\n\t\t},\n\t\tTagError: func(output Buffer, c fiber.Ctx, data *Data, _ string) (int, error) {\n\t\t\tif data.ChainErr != nil {\n\t\t\t\tif cfg.enableColors {\n\t\t\t\t\tcolors := c.App().Config().ColorScheme\n\t\t\t\t\treturn fmt.Fprintf(output, \"%s%s%s\", colors.Red, data.ChainErr.Error(), colors.Reset)\n\t\t\t\t}\n\t\t\t\treturn output.WriteString(data.ChainErr.Error())\n\t\t\t}\n\t\t\treturn output.WriteString(\"-\")\n\t\t},\n\t\tTagReqHeader: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\treturn output.WriteString(c.Get(extraParam))\n\t\t},\n\t\tTagRespHeader: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\treturn output.WriteString(c.GetRespHeader(extraParam))\n\t\t},\n\t\tTagQuery: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\treturn output.WriteString(fiber.Query[string](c, extraParam))\n\t\t},\n\t\tTagForm: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\treturn output.WriteString(c.FormValue(extraParam))\n\t\t},\n\t\tTagCookie: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\treturn output.WriteString(c.Cookies(extraParam))\n\t\t},\n\t\tTagLocals: func(output Buffer, c fiber.Ctx, _ *Data, extraParam string) (int, error) {\n\t\t\tswitch v := c.Locals(extraParam).(type) {\n\t\t\tcase []byte:\n\t\t\t\treturn output.Write(v)\n\t\t\tcase string:\n\t\t\t\treturn output.WriteString(v)\n\t\t\tcase nil:\n\t\t\t\treturn 0, nil\n\t\t\tdefault:\n\t\t\t\treturn fmt.Fprintf(output, \"%v\", v)\n\t\t\t}\n\t\t},\n\t\tTagStatus: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\tif cfg.enableColors {\n\t\t\t\tcolors := c.App().Config().ColorScheme\n\t\t\t\treturn fmt.Fprintf(output, \"%s%3d%s\", statusColor(c.Response().StatusCode(), &colors), c.Response().StatusCode(), colors.Reset)\n\t\t\t}\n\t\t\treturn appendInt(output, c.Response().StatusCode())\n\t\t},\n\t\tTagMethod: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) {\n\t\t\tif cfg.enableColors {\n\t\t\t\tcolors := c.App().Config().ColorScheme\n\t\t\t\treturn fmt.Fprintf(output, \"%s%s%s\", methodColor(c.Method(), &colors), c.Method(), colors.Reset)\n\t\t\t}\n\t\t\treturn output.WriteString(c.Method())\n\t\t},\n\t\tTagPid: func(output Buffer, _ fiber.Ctx, data *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(data.Pid)\n\t\t},\n\t\tTagLatency: func(output Buffer, _ fiber.Ctx, data *Data, _ string) (int, error) {\n\t\t\tlatency := data.Stop.Sub(data.Start)\n\t\t\treturn fmt.Fprintf(output, \"%13v\", latency)\n\t\t},\n\t\tTagTime: func(output Buffer, _ fiber.Ctx, data *Data, _ string) (int, error) {\n\t\t\treturn output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert,errcheck // We always store a string in here\n\t\t},\n\t}\n\t// merge with custom tags from user\n\tmaps.Copy(tagFunctions, cfg.CustomTags)\n\n\treturn tagFunctions\n}\n"
  },
  {
    "path": "middleware/logger/template_chain.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// buildLogFuncChain analyzes the template and creates slices with the functions for execution and\n// slices with the fixed parts of the template and the parameters\n//\n// fixParts contains the fixed parts of the template or parameters if a function is stored in the funcChain at this position\n// funcChain contains for the parts which exist the functions for the dynamic parts\n// funcChain and fixParts always have the same length and contain nil for the parts where no data is required in the chain,\n// if a function exists for the part, a parameter for it can also exist in the fixParts slice\nfunc buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) ([][]byte, []LogFunc, error) {\n\t// process flow is copied from the fasttemplate flow https://github.com/valyala/fasttemplate/blob/2a2d1afadadf9715bfa19683cdaeac8347e5d9f9/template.go#L23-L62\n\ttemplateB := utils.UnsafeBytes(cfg.Format)\n\tstartTagB := utils.UnsafeBytes(startTag)\n\tendTagB := utils.UnsafeBytes(endTag)\n\tparamSeparatorB := utils.UnsafeBytes(paramSeparator)\n\n\tvar fixParts [][]byte\n\tvar funcChain []LogFunc\n\n\tfor {\n\t\tbefore, after, found := bytes.Cut(templateB, startTagB)\n\t\tif !found {\n\t\t\t// no starting tag found in the existing template part\n\t\t\tbreak\n\t\t}\n\t\t// add fixed part\n\t\tfuncChain = append(funcChain, nil)\n\t\tfixParts = append(fixParts, before)\n\n\t\ttemplateB = after\n\t\tbefore, after, found = bytes.Cut(templateB, endTagB)\n\t\tif !found {\n\t\t\t// cannot find end tag - just write it to the output.\n\t\t\tfuncChain = append(funcChain, nil)\n\t\t\tfixParts = append(fixParts, startTagB)\n\t\t\tbreak\n\t\t}\n\t\t// ## function block ##\n\t\t// first check for tags with parameters\n\t\ttag, param, foundParam := bytes.Cut(before, paramSeparatorB)\n\t\tif foundParam {\n\t\t\tlogFunc, ok := tagFunctions[utils.UnsafeString(tag)+paramSeparator]\n\t\t\tif !ok {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"%w: %q\", ErrTemplateParameterMissing, utils.UnsafeString(before))\n\t\t\t}\n\t\t\tfuncChain = append(funcChain, logFunc)\n\t\t\t// add param to the fixParts\n\t\t\tfixParts = append(fixParts, param)\n\t\t} else if logFunc, ok := tagFunctions[utils.UnsafeString(before)]; ok {\n\t\t\t// add functions without parameter\n\t\t\tfuncChain = append(funcChain, logFunc)\n\t\t\tfixParts = append(fixParts, nil)\n\t\t}\n\t\t// ## function block end ##\n\n\t\t// reduce the template string\n\t\ttemplateB = after\n\t}\n\t// set the rest\n\tfuncChain = append(funcChain, nil)\n\tfixParts = append(fixParts, templateB)\n\n\treturn fixParts, funcChain, nil\n}\n"
  },
  {
    "path": "middleware/logger/utils.go",
    "content": "package logger\n\nimport (\n\t\"io\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\tfiberlog \"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\nfunc methodColor(method string, colors *fiber.Colors) string {\n\tif colors == nil {\n\t\treturn \"\"\n\t}\n\tswitch method {\n\tcase fiber.MethodGet:\n\t\treturn colors.Cyan\n\tcase fiber.MethodPost:\n\t\treturn colors.Green\n\tcase fiber.MethodPut:\n\t\treturn colors.Yellow\n\tcase fiber.MethodDelete:\n\t\treturn colors.Red\n\tcase fiber.MethodPatch:\n\t\treturn colors.White\n\tcase fiber.MethodHead:\n\t\treturn colors.Magenta\n\tcase fiber.MethodOptions:\n\t\treturn colors.Blue\n\tdefault:\n\t\treturn colors.Reset\n\t}\n}\n\nfunc statusColor(code int, colors *fiber.Colors) string {\n\tif colors == nil {\n\t\treturn \"\"\n\t}\n\tswitch {\n\tcase code >= fiber.StatusOK && code < fiber.StatusMultipleChoices:\n\t\treturn colors.Green\n\tcase code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest:\n\t\treturn colors.Blue\n\tcase code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError:\n\t\treturn colors.Yellow\n\tdefault:\n\t\treturn colors.Red\n\t}\n}\n\ntype customLoggerWriter[T any] struct {\n\tloggerInstance fiberlog.AllLogger[T]\n\tlevel          fiberlog.Level\n}\n\n// Write implements io.Writer and forwards the payload to the configured logger.\nfunc (cl *customLoggerWriter[T]) Write(p []byte) (int, error) {\n\tswitch cl.level {\n\tcase fiberlog.LevelTrace:\n\t\tcl.loggerInstance.Trace(utils.UnsafeString(p))\n\tcase fiberlog.LevelDebug:\n\t\tcl.loggerInstance.Debug(utils.UnsafeString(p))\n\tcase fiberlog.LevelInfo:\n\t\tcl.loggerInstance.Info(utils.UnsafeString(p))\n\tcase fiberlog.LevelWarn:\n\t\tcl.loggerInstance.Warn(utils.UnsafeString(p))\n\tcase fiberlog.LevelError:\n\t\tcl.loggerInstance.Error(utils.UnsafeString(p))\n\tdefault:\n\t\treturn 0, nil\n\t}\n\n\treturn len(p), nil\n}\n\n// LoggerToWriter is a helper function that returns an io.Writer that writes to a custom logger.\n// You can integrate 3rd party loggers such as zerolog, logrus, etc. to logger middleware using this function.\n//\n// Valid levels: fiberlog.LevelInfo, fiberlog.LevelTrace, fiberlog.LevelWarn, fiberlog.LevelDebug, fiberlog.LevelError\nfunc LoggerToWriter[T any](logger fiberlog.AllLogger[T], level fiberlog.Level) io.Writer {\n\t// Check if customLogger is nil\n\tif logger == nil {\n\t\tfiberlog.Panic(\"LoggerToWriter: customLogger must not be nil\")\n\t}\n\n\t// Check if level is valid\n\tif level == fiberlog.LevelFatal || level == fiberlog.LevelPanic {\n\t\tfiberlog.Panic(\"LoggerToWriter: invalid level\")\n\t}\n\n\treturn &customLoggerWriter[T]{\n\t\tlevel:          level,\n\t\tloggerInstance: logger,\n\t}\n}\n"
  },
  {
    "path": "middleware/paginate/config.go",
    "content": "package paginate\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for the pagination middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\tNext func(c fiber.Ctx) bool\n\n\t// PageKey is the query string key for page number.\n\t//\n\t// Optional. Default: \"page\"\n\tPageKey string\n\n\t// LimitKey is the query string key for limit.\n\t//\n\t// Optional. Default: \"limit\"\n\tLimitKey string\n\n\t// SortKey is the query string key for sort.\n\t//\n\t// Optional. Default: \"\"\n\tSortKey string\n\n\t// DefaultSort is the default sort field.\n\t//\n\t// Optional. Default: \"id\"\n\tDefaultSort string\n\n\t// CursorKey is the query string key for cursor-based pagination.\n\t//\n\t// Optional. Default: \"cursor\"\n\tCursorKey string\n\n\t// OffsetKey is the query string key for offset.\n\t//\n\t// Optional. Default: \"offset\"\n\tOffsetKey string\n\n\t// CursorParam is an optional alias for the cursor query key.\n\t//\n\t// Optional. Default: \"\"\n\tCursorParam string\n\n\t// AllowedSorts is the list of allowed sort fields.\n\t//\n\t// Optional. Default: nil\n\tAllowedSorts []string\n\n\t// DefaultPage is the default page number.\n\t//\n\t// Optional. Default: 1\n\tDefaultPage int\n\n\t// DefaultLimit is the default items per page.\n\t//\n\t// Optional. Default: 10\n\tDefaultLimit int\n\n\t// MaxLimit is the maximum items per page.\n\t//\n\t// Optional. Default: 100\n\tMaxLimit int\n}\n\n// ConfigDefault is the default config.\nvar ConfigDefault = Config{\n\tNext:         nil,\n\tPageKey:      \"page\",\n\tDefaultPage:  1,\n\tLimitKey:     \"limit\",\n\tDefaultLimit: 10,\n\tMaxLimit:     DefaultMaxLimit,\n\tDefaultSort:  \"id\",\n\tOffsetKey:    \"offset\",\n\tCursorKey:    \"cursor\",\n}\n\nfunc configDefault(config ...Config) Config {\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\tcfg := config[0]\n\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif cfg.PageKey == \"\" {\n\t\tcfg.PageKey = ConfigDefault.PageKey\n\t}\n\tif cfg.DefaultLimit < 1 {\n\t\tcfg.DefaultLimit = ConfigDefault.DefaultLimit\n\t}\n\tif cfg.LimitKey == \"\" {\n\t\tcfg.LimitKey = ConfigDefault.LimitKey\n\t}\n\tif cfg.DefaultPage < 1 {\n\t\tcfg.DefaultPage = ConfigDefault.DefaultPage\n\t}\n\tif cfg.CursorKey == \"\" {\n\t\tcfg.CursorKey = ConfigDefault.CursorKey\n\t}\n\tif cfg.DefaultSort == \"\" {\n\t\tcfg.DefaultSort = ConfigDefault.DefaultSort\n\t}\n\tif cfg.OffsetKey == \"\" {\n\t\tcfg.OffsetKey = ConfigDefault.OffsetKey\n\t}\n\tif cfg.MaxLimit < 1 {\n\t\tcfg.MaxLimit = ConfigDefault.MaxLimit\n\t}\n\tif cfg.DefaultLimit > cfg.MaxLimit {\n\t\tcfg.DefaultLimit = cfg.MaxLimit\n\t}\n\tif len(cfg.AllowedSorts) > 0 && !slices.Contains(cfg.AllowedSorts, cfg.DefaultSort) {\n\t\tcfg.DefaultSort = cfg.AllowedSorts[0]\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/paginate/page_info.go",
    "content": "package paginate\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// ErrCursorEncode is returned when cursor values cannot be encoded.\nvar ErrCursorEncode = errors.New(\"paginate: failed to encode cursor values\")\n\n// SortOrder represents sort order.\ntype SortOrder string\n\nconst (\n\tASC  SortOrder = \"asc\"\n\tDESC SortOrder = \"desc\"\n)\n\n// SortField represents a sort field with direction.\ntype SortField struct {\n\tField string    `json:\"field\"`\n\tOrder SortOrder `json:\"order\"`\n}\n\n// SortOrderFromString returns a SortOrder from a string (case-insensitive).\nfunc SortOrderFromString(s string) SortOrder {\n\tif utils.EqualFold(s, \"desc\") {\n\t\treturn DESC\n\t}\n\treturn ASC\n}\n\n// PageInfo contains pagination information.\ntype PageInfo struct {\n\tcursorData    map[string]any\n\tjsonMarshal   utils.JSONMarshal\n\tjsonUnmarshal utils.JSONUnmarshal\n\tCursor        string      `json:\"cursor,omitempty\"`\n\tNextCursor    string      `json:\"next_cursor,omitempty\"`\n\tSort          []SortField `json:\"sort\"`\n\tPage          int         `json:\"page\"`\n\tLimit         int         `json:\"limit\"`\n\tOffset        int         `json:\"offset\"`\n\tHasMore       bool        `json:\"has_more,omitempty\"`\n}\n\n// NewPageInfo creates a new PageInfo.\nfunc NewPageInfo(page, limit, offset int, sort []SortField) *PageInfo {\n\treturn &PageInfo{\n\t\tPage:   page,\n\t\tLimit:  limit,\n\t\tOffset: offset,\n\t\tSort:   sort,\n\t}\n}\n\n// Start returns the start index based on page/limit or offset.\nfunc (p *PageInfo) Start() int {\n\tif p.Offset > 0 {\n\t\treturn p.Offset\n\t}\n\tif p.Page < 1 {\n\t\treturn 0\n\t}\n\treturn (p.Page - 1) * p.Limit\n}\n\n// SortBy adds a sort field. Chainable.\nfunc (p *PageInfo) SortBy(field string, order SortOrder) *PageInfo {\n\tp.Sort = append(p.Sort, SortField{Field: field, Order: order})\n\treturn p\n}\n\n// NextPageURLWithKeys returns the URL for the next page using custom query keys.\nfunc (p *PageInfo) NextPageURLWithKeys(baseURL, pageKey, limitKey string) string {\n\treturn buildPaginationURL(baseURL, pageKey, utils.FormatInt(int64(p.Page+1)), limitKey, utils.FormatInt(int64(p.Limit)))\n}\n\n// NextPageURL returns the URL for the next page.\nfunc (p *PageInfo) NextPageURL(baseURL string) string {\n\treturn p.NextPageURLWithKeys(baseURL, \"page\", \"limit\")\n}\n\n// PreviousPageURLWithKeys returns the URL for the previous page using custom query keys.\n// Returns empty string if on page 1.\nfunc (p *PageInfo) PreviousPageURLWithKeys(baseURL, pageKey, limitKey string) string {\n\tif p.Page > 1 {\n\t\treturn buildPaginationURL(baseURL, pageKey, utils.FormatInt(int64(p.Page-1)), limitKey, utils.FormatInt(int64(p.Limit)))\n\t}\n\treturn \"\"\n}\n\n// PreviousPageURL returns the URL for the previous page.\n// Returns empty string if on page 1.\nfunc (p *PageInfo) PreviousPageURL(baseURL string) string {\n\treturn p.PreviousPageURLWithKeys(baseURL, \"page\", \"limit\")\n}\n\n// NextCursorURLWithKeys returns the URL for the next cursor page using custom query keys.\n// Returns empty string if HasMore is false.\nfunc (p *PageInfo) NextCursorURLWithKeys(baseURL, cursorKey, limitKey string) string {\n\tif !p.HasMore {\n\t\treturn \"\"\n\t}\n\treturn buildPaginationURL(baseURL, cursorKey, p.NextCursor, limitKey, utils.FormatInt(int64(p.Limit)))\n}\n\n// NextCursorURL returns the URL for the next cursor page.\n// Returns empty string if HasMore is false.\nfunc (p *PageInfo) NextCursorURL(baseURL string) string {\n\treturn p.NextCursorURLWithKeys(baseURL, \"cursor\", \"limit\")\n}\n\n// buildPaginationURL parses baseURL and sets/replaces two query parameters,\n// preserving any existing query string values.\nfunc buildPaginationURL(baseURL, pageParam, pageValue, limitParam, limitValue string) string {\n\tu, err := url.Parse(baseURL)\n\tif err != nil {\n\t\treturn baseURL\n\t}\n\tq := u.Query()\n\tq.Set(pageParam, pageValue)\n\tq.Set(limitParam, limitValue)\n\tu.RawQuery = q.Encode()\n\treturn u.String()\n}\n\n// CursorValues returns the decoded cursor key-value map.\n// If the cursor was parsed by the middleware, the pre-parsed data is returned.\n// Otherwise it decodes the opaque cursor string.\n// Returns nil if cursor is empty or invalid.\nfunc (p *PageInfo) CursorValues() map[string]any {\n\tif p.cursorData != nil {\n\t\treturn p.cursorData\n\t}\n\n\tif p.Cursor == \"\" {\n\t\treturn nil\n\t}\n\n\tif len(p.Cursor) > maxCursorLen {\n\t\treturn nil\n\t}\n\n\tdata, err := base64.RawURLEncoding.DecodeString(p.Cursor)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tvar values map[string]any\n\tunmarshal := p.jsonUnmarshal\n\tif unmarshal == nil {\n\t\tunmarshal = json.Unmarshal\n\t}\n\tif err := unmarshal(data, &values); err != nil {\n\t\treturn nil\n\t}\n\n\tp.cursorData = values\n\n\treturn values\n}\n\n// SetNextCursor encodes a key-value map into an opaque cursor token\n// and sets both NextCursor and HasMore on the PageInfo.\nfunc (p *PageInfo) SetNextCursor(values map[string]any) error {\n\tmarshal := p.jsonMarshal\n\tif marshal == nil {\n\t\tmarshal = json.Marshal\n\t}\n\tdata, err := marshal(values)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%w: %w\", ErrCursorEncode, err)\n\t}\n\n\tencoded := base64.RawURLEncoding.EncodeToString(data)\n\tif len(encoded) > maxCursorLen {\n\t\treturn fmt.Errorf(\"%w: cursor token exceeds maximum length (%d)\", ErrCursorEncode, maxCursorLen)\n\t}\n\n\tp.NextCursor = encoded\n\tp.HasMore = true\n\n\treturn nil\n}\n"
  },
  {
    "path": "middleware/paginate/paginate.go",
    "content": "package paginate\n\nimport (\n\t\"encoding/base64\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\nconst (\n\tpageInfoKey contextKey = iota\n)\n\n// DefaultMaxLimit is the default maximum limit allowed.\nconst DefaultMaxLimit = 100\n\n// maxCursorLen is the maximum allowed cursor string length.\nconst maxCursorLen = 2048\n\n// New creates a new pagination middleware handler.\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\treturn func(c fiber.Ctx) error {\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tappCfg := c.App().Config()\n\n\t\tlimit := fiber.Query(c, cfg.LimitKey, cfg.DefaultLimit)\n\t\tif limit < 1 {\n\t\t\tlimit = cfg.DefaultLimit\n\t\t}\n\t\tif limit > cfg.MaxLimit {\n\t\t\tlimit = cfg.MaxLimit\n\t\t}\n\n\t\tsorts := parseSortQuery(c.Query(cfg.SortKey), cfg.AllowedSorts, cfg.DefaultSort)\n\n\t\tcursorRaw := c.Query(cfg.CursorKey)\n\t\tif cursorRaw == \"\" && cfg.CursorParam != \"\" {\n\t\t\tcursorRaw = c.Query(cfg.CursorParam)\n\t\t}\n\n\t\tif cursorRaw != \"\" {\n\t\t\tif len(cursorRaw) > maxCursorLen {\n\t\t\t\treturn fiber.NewError(fiber.StatusBadRequest, \"cursor too long\")\n\t\t\t}\n\n\t\t\tdata, err := base64.RawURLEncoding.DecodeString(cursorRaw)\n\t\t\tif err != nil {\n\t\t\t\treturn fiber.NewError(fiber.StatusBadRequest, \"invalid cursor\")\n\t\t\t}\n\t\t\tvar obj map[string]any\n\t\t\tif err := appCfg.JSONDecoder(data, &obj); err != nil {\n\t\t\t\treturn fiber.NewError(fiber.StatusBadRequest, \"invalid cursor\")\n\t\t\t}\n\n\t\t\tpageInfo := &PageInfo{\n\t\t\t\tLimit:         limit,\n\t\t\t\tSort:          sorts,\n\t\t\t\tCursor:        cursorRaw,\n\t\t\t\tcursorData:    obj,\n\t\t\t\tjsonMarshal:   appCfg.JSONEncoder,\n\t\t\t\tjsonUnmarshal: appCfg.JSONDecoder,\n\t\t\t}\n\t\t\tfiber.StoreInContext(c, pageInfoKey, pageInfo)\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tpage := max(fiber.Query(c, cfg.PageKey, cfg.DefaultPage), 1)\n\t\toffset := max(fiber.Query(c, cfg.OffsetKey, 0), 0)\n\n\t\tpageInfo := NewPageInfo(page, limit, offset, sorts)\n\t\tpageInfo.jsonMarshal = appCfg.JSONEncoder\n\t\tpageInfo.jsonUnmarshal = appCfg.JSONDecoder\n\t\tfiber.StoreInContext(c, pageInfoKey, pageInfo)\n\t\treturn c.Next()\n\t}\n}\n\n// FromContext returns the PageInfo from the request context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// Returns nil and false if no PageInfo is stored.\nfunc FromContext(ctx any) (*PageInfo, bool) {\n\treturn fiber.ValueFromContext[*PageInfo](ctx, pageInfoKey)\n}\n\nfunc parseSortQuery(query string, allowedSorts []string, defaultSort string) []SortField {\n\tif query == \"\" {\n\t\treturn []SortField{{Field: defaultSort, Order: ASC}}\n\t}\n\n\tfields := strings.Split(query, \",\")\n\tsortFields := make([]SortField, 0, len(fields))\n\n\tfor _, field := range fields {\n\t\tfield = utils.TrimSpace(field)\n\t\tif field == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\torder := ASC\n\t\tif strings.HasPrefix(field, \"-\") {\n\t\t\torder = DESC\n\t\t\tfield = utils.TrimSpace(field[1:])\n\t\t}\n\t\tif field == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif len(allowedSorts) == 0 || slices.Contains(allowedSorts, field) {\n\t\t\tsortFields = append(sortFields, SortField{Field: field, Order: order})\n\t\t}\n\t}\n\n\tif len(sortFields) == 0 {\n\t\treturn []SortField{{Field: defaultSort, Order: ASC}}\n\t}\n\n\treturn sortFields\n}\n"
  },
  {
    "path": "middleware/paginate/paginate_test.go",
    "content": "package paginate\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype paginateResponse struct {\n\tNextPageURL     string      `json:\"next_page_url\"`\n\tPreviousPageURL string      `json:\"prev_page_url\"`\n\tSort            []SortField `json:\"sort\"`\n\tPage            int         `json:\"page\"`\n\tLimit           int         `json:\"limit\"`\n\tOffset          int         `json:\"offset\"`\n\tStart           int         `json:\"start\"`\n}\n\ntype cursorResponse struct {\n\tCursor     string      `json:\"cursor\"`\n\tNextCursor string      `json:\"next_cursor\"`\n\tSort       []SortField `json:\"sort\"`\n\tLimit      int         `json:\"limit\"`\n\tHasMore    bool        `json:\"has_more\"`\n}\n\n// --- Config tests ---\n\nfunc Test_ConfigDefault(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault()\n\trequire.Equal(t, \"page\", cfg.PageKey)\n\trequire.Equal(t, 1, cfg.DefaultPage)\n\trequire.Equal(t, \"limit\", cfg.LimitKey)\n\trequire.Equal(t, 10, cfg.DefaultLimit)\n\trequire.Equal(t, DefaultMaxLimit, cfg.MaxLimit)\n}\n\nfunc Test_ConfigOverride(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{\n\t\tPageKey:      \"p\",\n\t\tLimitKey:     \"l\",\n\t\tDefaultPage:  5,\n\t\tDefaultLimit: 50,\n\t})\n\trequire.Equal(t, \"p\", cfg.PageKey)\n\trequire.Equal(t, \"l\", cfg.LimitKey)\n\trequire.Equal(t, 5, cfg.DefaultPage)\n\trequire.Equal(t, 50, cfg.DefaultLimit)\n}\n\nfunc Test_ConfigDefaultCursorKey(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault()\n\trequire.Equal(t, \"cursor\", cfg.CursorKey)\n}\n\nfunc Test_ConfigOverrideCursorKey(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{\n\t\tCursorKey:   \"after\",\n\t\tCursorParam: \"starting_after\",\n\t})\n\trequire.Equal(t, \"after\", cfg.CursorKey)\n\trequire.Equal(t, \"starting_after\", cfg.CursorParam)\n}\n\nfunc Test_ConfigNegativeDefaults(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{\n\t\tDefaultPage:  -1,\n\t\tDefaultLimit: -1,\n\t})\n\trequire.Equal(t, 1, cfg.DefaultPage)\n\trequire.Equal(t, 10, cfg.DefaultLimit)\n}\n\n// --- PageInfo tests ---\n\nfunc Test_SortOrderFromString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tinput    string\n\t\texpected SortOrder\n\t}{\n\t\t{\"asc\", ASC},\n\t\t{\"desc\", DESC},\n\t\t{\"DESC\", DESC},\n\t\t{\"Desc\", DESC},\n\t\t{\"invalid\", ASC},\n\t\t{\"\", ASC},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.expected, SortOrderFromString(tt.input))\n\t\t})\n\t}\n}\n\nfunc Test_PageInfoStart(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tpageInfo PageInfo\n\t\texpected int\n\t}{\n\t\t{\"Page 1, limit 10\", PageInfo{Page: 1, Limit: 10}, 0},\n\t\t{\"Page 2, limit 10\", PageInfo{Page: 2, Limit: 10}, 10},\n\t\t{\"Page 3, limit 20\", PageInfo{Page: 3, Limit: 20}, 40},\n\t\t{\"With offset\", PageInfo{Page: 2, Limit: 10, Offset: 25}, 25},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.expected, tt.pageInfo.Start())\n\t\t})\n\t}\n}\n\nfunc Test_PageInfoSortBy(t *testing.T) {\n\tt.Parallel()\n\n\tp := NewPageInfo(1, 10, 0, nil)\n\tp.SortBy(\"name\", ASC).SortBy(\"date\", DESC)\n\n\trequire.Len(t, p.Sort, 2)\n\trequire.Equal(t, \"name\", p.Sort[0].Field)\n\trequire.Equal(t, ASC, p.Sort[0].Order)\n\trequire.Equal(t, \"date\", p.Sort[1].Field)\n\trequire.Equal(t, DESC, p.Sort[1].Order)\n}\n\nfunc Test_PageInfoNextPageURL(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tbaseURL  string\n\t\texpected string\n\t\tpageInfo PageInfo\n\t}{\n\t\t{\n\t\t\tname:     \"Middle page\",\n\t\t\tbaseURL:  \"https://example.com/users\",\n\t\t\texpected: \"https://example.com/users?limit=10&page=3\",\n\t\t\tpageInfo: PageInfo{Page: 2, Limit: 10},\n\t\t},\n\t\t{\n\t\t\tname:     \"First page\",\n\t\t\tbaseURL:  \"https://example.com/users\",\n\t\t\texpected: \"https://example.com/users?limit=20&page=2\",\n\t\t\tpageInfo: PageInfo{Page: 1, Limit: 20},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.expected, tt.pageInfo.NextPageURL(tt.baseURL))\n\t\t})\n\t}\n}\n\nfunc Test_PageInfoPreviousPageURL(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tbaseURL  string\n\t\texpected string\n\t\tpageInfo PageInfo\n\t}{\n\t\t{\n\t\t\tname:     \"Middle page\",\n\t\t\tbaseURL:  \"https://example.com/users\",\n\t\t\texpected: \"https://example.com/users?limit=10&page=1\",\n\t\t\tpageInfo: PageInfo{Page: 2, Limit: 10},\n\t\t},\n\t\t{\n\t\t\tname:     \"First page returns empty\",\n\t\t\tbaseURL:  \"https://example.com/users\",\n\t\t\texpected: \"\",\n\t\t\tpageInfo: PageInfo{Page: 1, Limit: 20},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.expected, tt.pageInfo.PreviousPageURL(tt.baseURL))\n\t\t})\n\t}\n}\n\nfunc Test_PageInfoStartCursorMode(t *testing.T) {\n\tt.Parallel()\n\n\t// In cursor mode, Page is 0 (not set). Start() should return 0, not negative.\n\tp := &PageInfo{Page: 0, Limit: 20}\n\trequire.Equal(t, 0, p.Start())\n}\n\nfunc Test_PageInfoNextPageURLWithExistingQueryParams(t *testing.T) {\n\tt.Parallel()\n\n\tp := PageInfo{Page: 2, Limit: 10}\n\tresult := p.NextPageURL(\"https://example.com/users?filter=active\")\n\trequire.Contains(t, result, \"filter=active\")\n\trequire.Contains(t, result, \"page=3\")\n\trequire.Contains(t, result, \"limit=10\")\n}\n\nfunc Test_PageInfoPreviousPageURLWithExistingQueryParams(t *testing.T) {\n\tt.Parallel()\n\n\tp := PageInfo{Page: 3, Limit: 10}\n\tresult := p.PreviousPageURL(\"https://example.com/users?filter=active\")\n\trequire.Contains(t, result, \"filter=active\")\n\trequire.Contains(t, result, \"page=2\")\n\trequire.Contains(t, result, \"limit=10\")\n}\n\nfunc Test_PageInfoCursorFields(t *testing.T) {\n\tt.Parallel()\n\n\tp := &PageInfo{\n\t\tCursor:     \"abc123\",\n\t\tHasMore:    true,\n\t\tNextCursor: \"def456\",\n\t}\n\n\trequire.Equal(t, \"abc123\", p.Cursor)\n\trequire.True(t, p.HasMore)\n\trequire.Equal(t, \"def456\", p.NextCursor)\n}\n\nfunc Test_CursorValuesRoundTrip(t *testing.T) {\n\tt.Parallel()\n\n\toriginal := map[string]any{\n\t\t\"id\":         float64(42),\n\t\t\"created_at\": \"2026-01-01T00:00:00Z\",\n\t}\n\n\tp := &PageInfo{}\n\trequire.NoError(t, p.SetNextCursor(original))\n\n\trequire.True(t, p.HasMore)\n\trequire.NotEmpty(t, p.NextCursor)\n\n\tp2 := &PageInfo{Cursor: p.NextCursor}\n\tdecoded := p2.CursorValues()\n\n\trequire.NotNil(t, decoded)\n\trequire.InEpsilon(t, float64(42), decoded[\"id\"], 0)\n\trequire.Equal(t, \"2026-01-01T00:00:00Z\", decoded[\"created_at\"])\n}\n\nfunc Test_CursorValuesEmptyCursor(t *testing.T) {\n\tt.Parallel()\n\n\tp := &PageInfo{Cursor: \"\"}\n\trequire.Nil(t, p.CursorValues())\n}\n\nfunc Test_CursorValuesInvalidBase64(t *testing.T) {\n\tt.Parallel()\n\n\tp := &PageInfo{Cursor: \"not-valid-base64!!!\"}\n\trequire.Nil(t, p.CursorValues())\n}\n\nfunc Test_CursorValuesInvalidJSON(t *testing.T) {\n\tt.Parallel()\n\n\tp := &PageInfo{Cursor: \"bm90LWpzb24\"}\n\trequire.Nil(t, p.CursorValues())\n}\n\nfunc Test_NextCursorURL(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with HasMore\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &PageInfo{Limit: 20}\n\t\trequire.NoError(t, p.SetNextCursor(map[string]any{\"id\": float64(42)}))\n\n\t\turl := p.NextCursorURL(\"https://example.com/users\")\n\t\texpected := fmt.Sprintf(\"https://example.com/users?cursor=%s&limit=20\", p.NextCursor)\n\t\trequire.Equal(t, expected, url)\n\t})\n\n\tt.Run(\"without HasMore\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &PageInfo{Limit: 20}\n\t\trequire.Empty(t, p.NextCursorURL(\"https://example.com/users\"))\n\t})\n}\n\nfunc Test_SetNextCursorSetsFields(t *testing.T) {\n\tt.Parallel()\n\n\tp := &PageInfo{Limit: 10}\n\trequire.NoError(t, p.SetNextCursor(map[string]any{\"id\": float64(1)}))\n\trequire.True(t, p.HasMore)\n\trequire.NotEmpty(t, p.NextCursor)\n}\n\n// --- Middleware handler tests ---\n\nfunc Test_PaginateWithQueries(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tDefaultSort: \"id\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:            pageInfo.Page,\n\t\t\tLimit:           pageInfo.Limit,\n\t\t\tOffset:          pageInfo.Offset,\n\t\t\tStart:           pageInfo.Start(),\n\t\t\tSort:            pageInfo.Sort,\n\t\t\tNextPageURL:     pageInfo.NextPageURL(c.BaseURL()),\n\t\t\tPreviousPageURL: pageInfo.PreviousPageURL(c.BaseURL()),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?page=2&limit=20\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 2, body.Page)\n\trequire.Equal(t, 20, body.Limit)\n\trequire.Equal(t, 0, body.Offset)\n\trequire.Equal(t, 20, body.Start)\n\trequire.Equal(t, \"http://example.com?limit=20&page=3\", body.NextPageURL)\n\trequire.Equal(t, \"http://example.com?limit=20&page=1\", body.PreviousPageURL)\n\trequire.Equal(t, []SortField{{Field: \"id\", Order: ASC}}, body.Sort)\n}\n\nfunc Test_PaginateWithOffset(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:   pageInfo.Page,\n\t\t\tLimit:  pageInfo.Limit,\n\t\t\tOffset: pageInfo.Offset,\n\t\t\tStart:  pageInfo.Start(),\n\t\t\tSort:   pageInfo.Sort,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?offset=20&limit=20\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 1, body.Page)\n\trequire.Equal(t, 20, body.Limit)\n\trequire.Equal(t, 20, body.Offset)\n\trequire.Equal(t, 20, body.Start)\n}\n\nfunc Test_PaginateCheckDefaultsWhenNoQueries(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:   pageInfo.Page,\n\t\t\tLimit:  pageInfo.Limit,\n\t\t\tOffset: pageInfo.Offset,\n\t\t\tStart:  pageInfo.Start(),\n\t\t\tSort:   pageInfo.Sort,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 1, body.Page)\n\trequire.Equal(t, 10, body.Limit)\n\trequire.Equal(t, 0, body.Offset)\n\trequire.Equal(t, 0, body.Start)\n\trequire.Equal(t, []SortField{{Field: \"id\", Order: ASC}}, body.Sort)\n}\n\nfunc Test_PaginateCheckDefaultsWhenNoPage(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?limit=20\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 1, body.Page)\n\trequire.Equal(t, 20, body.Limit)\n\trequire.Equal(t, 0, body.Start)\n}\n\nfunc Test_PaginateCheckDefaultsWhenNoLimit(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?page=2\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 2, body.Page)\n\trequire.Equal(t, 10, body.Limit)\n\trequire.Equal(t, 10, body.Start)\n}\n\nfunc Test_PaginateConfigDefaultPageDefaultLimit(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDefaultPage:  100,\n\t\tDefaultLimit: DefaultMaxLimit,\n\t\tDefaultSort:  \"name\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t\tSort:  pageInfo.Sort,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 100, body.Page)\n\trequire.Equal(t, DefaultMaxLimit, body.Limit)\n\trequire.Equal(t, 9900, body.Start)\n\trequire.Equal(t, []SortField{{Field: \"name\", Order: ASC}}, body.Sort)\n}\n\nfunc Test_PaginateConfigPageKeyLimitKey(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPageKey:     \"site\",\n\t\tLimitKey:    \"size\",\n\t\tDefaultSort: \"id\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t\tSort:  pageInfo.Sort,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/?site=2&size=5\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 2, body.Page)\n\trequire.Equal(t, 5, body.Limit)\n\trequire.Equal(t, 5, body.Start)\n}\n\nfunc Test_PaginateNegativeDefaultPageDefaultLimitValues(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDefaultPage:  -1,\n\t\tDefaultLimit: -1,\n\t\tDefaultSort:  \"id\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar body paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\n\trequire.Equal(t, 1, body.Page)\n\trequire.Equal(t, 10, body.Limit)\n\trequire.Equal(t, 0, body.Start)\n}\n\nfunc Test_PaginateFromContextWithoutNew(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t_, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(nil)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PaginateNextSkip(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t_, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(nil)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PaginateEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\turl           string\n\t\texpectedPage  int\n\t\texpectedLimit int\n\t}{\n\t\t{\"Negative page\", \"/?page=-1\", 1, 10},\n\t\t{\"Page zero\", \"/?page=0\", 1, 10},\n\t\t{\"Negative limit\", \"/?limit=-10\", 1, 10},\n\t\t{\"Limit zero\", \"/?limit=0\", 1, 10},\n\t\t{\"Limit exceeds max\", \"/?limit=200\", 1, DefaultMaxLimit},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{\n\t\t\t\tDefaultSort:  \"id\",\n\t\t\t\tDefaultLimit: 10,\n\t\t\t}))\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tpageInfo, ok := FromContext(c)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fiber.ErrBadRequest\n\t\t\t\t}\n\t\t\t\treturn c.JSON(pageInfo)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, tc.url, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t\t\trequire.Equal(t, 200, resp.StatusCode)\n\n\t\t\tvar result PageInfo\n\t\t\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\t\t\trequire.Equal(t, tc.expectedPage, result.Page)\n\t\t\trequire.Equal(t, tc.expectedLimit, result.Limit)\n\t\t})\n\t}\n}\n\nfunc Test_PaginateWithMultipleSorting(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\turl          string\n\t\texpectedSort []SortField\n\t}{\n\t\t{\"Default Sort\", \"/\", []SortField{{Field: \"id\", Order: ASC}}},\n\t\t{\"Single Sort\", \"/?sort=name\", []SortField{{Field: \"name\", Order: ASC}}},\n\t\t{\"Multiple Sort\", \"/?sort=name,-date\", []SortField{{Field: \"name\", Order: ASC}, {Field: \"date\", Order: DESC}}},\n\t\t{\"Invalid Sort\", \"/?sort=invalid\", []SortField{{Field: \"id\", Order: ASC}}},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{\n\t\t\t\tSortKey:      \"sort\",\n\t\t\t\tDefaultSort:  \"id\",\n\t\t\t\tAllowedSorts: []string{\"id\", \"name\", \"date\"},\n\t\t\t}))\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tpageInfo, ok := FromContext(c)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fiber.ErrBadRequest\n\t\t\t\t}\n\t\t\t\treturn c.JSON(paginateResponse{\n\t\t\t\t\tSort: pageInfo.Sort,\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tc.url, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\t\t\tvar result paginateResponse\n\t\t\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\t\t\trequire.Equal(t, tc.expectedSort, result.Sort)\n\t\t})\n\t}\n}\n\nfunc Test_ParseSortQuery(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tquery        string\n\t\tallowedSorts []string\n\t\tdefaultSort  string\n\t\texpected     []SortField\n\t}{\n\t\t{\n\t\t\t\"Empty query\",\n\t\t\t\"\",\n\t\t\t[]string{\"id\", \"name\", \"date\"},\n\t\t\t\"id\",\n\t\t\t[]SortField{{Field: \"id\", Order: ASC}},\n\t\t},\n\t\t{\n\t\t\t\"Single allowed field\",\n\t\t\t\"name\",\n\t\t\t[]string{\"id\", \"name\", \"date\"},\n\t\t\t\"id\",\n\t\t\t[]SortField{{Field: \"name\", Order: ASC}},\n\t\t},\n\t\t{\n\t\t\t\"Multiple fields with mixed order\",\n\t\t\t\"name,-date,id\",\n\t\t\t[]string{\"id\", \"name\", \"date\"},\n\t\t\t\"id\",\n\t\t\t[]SortField{\n\t\t\t\t{Field: \"name\", Order: ASC},\n\t\t\t\t{Field: \"date\", Order: DESC},\n\t\t\t\t{Field: \"id\", Order: ASC},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"Disallowed field\",\n\t\t\t\"email,name\",\n\t\t\t[]string{\"id\", \"name\", \"date\"},\n\t\t\t\"id\",\n\t\t\t[]SortField{{Field: \"name\", Order: ASC}},\n\t\t},\n\t\t{\n\t\t\t\"All disallowed fields\",\n\t\t\t\"email,phone\",\n\t\t\t[]string{\"id\", \"name\", \"date\"},\n\t\t\t\"id\",\n\t\t\t[]SortField{{Field: \"id\", Order: ASC}},\n\t\t},\n\t\t{\n\t\t\t\"Nil AllowedSorts allows all fields\",\n\t\t\t\"email,-phone\",\n\t\t\tnil,\n\t\t\t\"id\",\n\t\t\t[]SortField{\n\t\t\t\t{Field: \"email\", Order: ASC},\n\t\t\t\t{Field: \"phone\", Order: DESC},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"Bare dash is skipped\",\n\t\t\t\"-\",\n\t\t\tnil,\n\t\t\t\"id\",\n\t\t\t[]SortField{{Field: \"id\", Order: ASC}},\n\t\t},\n\t\t{\n\t\t\t\"Dash in comma list is skipped\",\n\t\t\t\"name,-,email\",\n\t\t\tnil,\n\t\t\t\"id\",\n\t\t\t[]SortField{\n\t\t\t\t{Field: \"name\", Order: ASC},\n\t\t\t\t{Field: \"email\", Order: ASC},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := parseSortQuery(tt.query, tt.allowedSorts, tt.defaultSort)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\n// --- Cursor tests ---\n\nfunc Test_PaginateWithCursor(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDefaultSort: \"id\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(cursorResponse{\n\t\t\tCursor: pageInfo.Cursor,\n\t\t\tLimit:  pageInfo.Limit,\n\t\t\tSort:   pageInfo.Sort,\n\t\t})\n\t})\n\n\tcursorJSON := `{\"id\":42}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?cursor=\"+cursor+\"&limit=20\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tvar result cursorResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, cursor, result.Cursor)\n\trequire.Equal(t, 20, result.Limit)\n}\n\nfunc Test_PaginateCursorPriorityOverPage(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(pageInfo)\n\t})\n\n\tcursorJSON := `{\"id\":42}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?cursor=\"+cursor+\"&page=5&limit=10\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar result PageInfo\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, cursor, result.Cursor)\n\trequire.Equal(t, 0, result.Page)\n}\n\nfunc Test_PaginateEmptyCursorIsFirstPage(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(pageInfo)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?cursor=&limit=10\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tvar result PageInfo\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Empty(t, result.Cursor)\n}\n\nfunc Test_PaginateInvalidCursorReturns400(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname   string\n\t\tcursor string\n\t}{\n\t\t{\"Invalid base64\", \"not-valid!!!\"},\n\t\t{\"Valid base64 but invalid JSON\", base64.RawURLEncoding.EncodeToString([]byte(\"not-json\"))},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New())\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tpageInfo, _ := FromContext(c)\n\t\t\t\treturn c.JSON(pageInfo)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?cursor=\"+tc.cursor, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t\t\trequire.Equal(t, 400, resp.StatusCode)\n\t\t})\n\t}\n}\n\nfunc Test_PaginateCursorWithSort(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSortKey:      \"sort\",\n\t\tDefaultSort:  \"id\",\n\t\tAllowedSorts: []string{\"id\", \"name\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(cursorResponse{\n\t\t\tCursor: pageInfo.Cursor,\n\t\t\tSort:   pageInfo.Sort,\n\t\t})\n\t})\n\n\tcursorJSON := `{\"id\":42}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?cursor=\"+cursor+\"&sort=name,-id\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar result cursorResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, []SortField{{Field: \"name\", Order: ASC}, {Field: \"id\", Order: DESC}}, result.Sort)\n}\n\nfunc Test_PaginateCursorWithCustomKey(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tCursorKey: \"after\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(cursorResponse{\n\t\t\tCursor: pageInfo.Cursor,\n\t\t\tLimit:  pageInfo.Limit,\n\t\t})\n\t})\n\n\tcursorJSON := `{\"id\":1}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?after=\"+cursor, http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tvar result cursorResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, cursor, result.Cursor)\n}\n\nfunc Test_PaginateCursorWithParamAlias(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tCursorParam: \"starting_after\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(cursorResponse{\n\t\t\tCursor: pageInfo.Cursor,\n\t\t})\n\t})\n\n\tcursorJSON := `{\"id\":1}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?starting_after=\"+cursor, http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tvar result cursorResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, cursor, result.Cursor)\n}\n\nfunc Test_PaginateNoCursorFallsBackToPageMode(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDefaultSort: \"id\",\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, ok := FromContext(c)\n\t\tif !ok {\n\t\t\treturn fiber.ErrBadRequest\n\t\t}\n\t\treturn c.JSON(paginateResponse{\n\t\t\tPage:  pageInfo.Page,\n\t\t\tLimit: pageInfo.Limit,\n\t\t\tStart: pageInfo.Start(),\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/?page=3&limit=15\", http.NoBody))\n\trequire.NoError(t, err)\n\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\n\tvar result paginateResponse\n\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\trequire.Equal(t, 3, result.Page)\n\trequire.Equal(t, 15, result.Limit)\n\trequire.Equal(t, 30, result.Start)\n}\n\nfunc Test_NextPageURLWithKeys(t *testing.T) {\n\tt.Parallel()\n\n\tp := PageInfo{Page: 2, Limit: 10}\n\tresult := p.NextPageURLWithKeys(\"https://example.com/users\", \"p\", \"per_page\")\n\trequire.Equal(t, \"https://example.com/users?p=3&per_page=10\", result)\n}\n\nfunc Test_PreviousPageURLWithKeys(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"has previous\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := PageInfo{Page: 3, Limit: 15}\n\t\tresult := p.PreviousPageURLWithKeys(\"https://example.com/items\", \"p\", \"size\")\n\t\trequire.Equal(t, \"https://example.com/items?p=2&size=15\", result)\n\t})\n\n\tt.Run(\"first page returns empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := PageInfo{Page: 1, Limit: 15}\n\t\tresult := p.PreviousPageURLWithKeys(\"https://example.com/items\", \"p\", \"size\")\n\t\trequire.Empty(t, result)\n\t})\n}\n\nfunc Test_NextCursorURLWithKeys(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"has more\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &PageInfo{Limit: 20}\n\t\trequire.NoError(t, p.SetNextCursor(map[string]any{\"id\": float64(42)}))\n\n\t\tresult := p.NextCursorURLWithKeys(\"https://example.com/users\", \"after\", \"per_page\")\n\t\texpected := fmt.Sprintf(\"https://example.com/users?after=%s&per_page=20\", p.NextCursor)\n\t\trequire.Equal(t, expected, result)\n\t})\n\n\tt.Run(\"no more\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp := &PageInfo{Limit: 20}\n\t\tresult := p.NextCursorURLWithKeys(\"https://example.com/users\", \"after\", \"per_page\")\n\t\trequire.Empty(t, result)\n\t})\n}\n\nfunc Test_PaginateCustomMaxLimit(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\turl           string\n\t\texpectedLimit int\n\t}{\n\t\t{\"Limit within custom max\", \"/?limit=40\", 40},\n\t\t{\"Limit exceeds custom max\", \"/?limit=200\", 50},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New()\n\t\t\tapp.Use(New(Config{\n\t\t\t\tDefaultSort:  \"id\",\n\t\t\t\tDefaultLimit: 10,\n\t\t\t\tMaxLimit:     50,\n\t\t\t}))\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tpageInfo, ok := FromContext(c)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fiber.ErrBadRequest\n\t\t\t\t}\n\t\t\t\treturn c.JSON(pageInfo)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, tc.url, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer resp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t\t\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\n\t\t\tvar result PageInfo\n\t\t\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&result))\n\t\t\trequire.Equal(t, tc.expectedLimit, result.Limit)\n\t\t})\n\t}\n}\n\nfunc Test_ConfigDefaultMaxLimitNormalization(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := configDefault(Config{MaxLimit: 0})\n\trequire.Equal(t, DefaultMaxLimit, cfg.MaxLimit)\n\n\tcfg2 := configDefault(Config{MaxLimit: 50})\n\trequire.Equal(t, 50, cfg2.MaxLimit)\n}\n\n// --- Benchmarks ---\n\nfunc Benchmark_PaginateMiddleware(b *testing.B) {\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, _ := FromContext(c)\n\t\treturn c.JSON(pageInfo)\n\t})\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/?page=2&limit=20&sort=name,-date\", http.NoBody)\n\t\tresp, err := app.Test(req, fiber.TestConfig{Timeout: 0})\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tresp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t}\n}\n\nfunc Benchmark_PaginateMiddlewareWithCustomConfig(b *testing.B) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPageKey:      \"p\",\n\t\tLimitKey:     \"l\",\n\t\tSortKey:      \"s\",\n\t\tDefaultPage:  1,\n\t\tDefaultLimit: 30,\n\t\tDefaultSort:  \"id\",\n\t\tAllowedSorts: []string{\"id\", \"name\", \"date\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, _ := FromContext(c)\n\t\treturn c.JSON(pageInfo)\n\t})\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/?p=3&l=25&s=name,-id\", http.NoBody)\n\t\tresp, err := app.Test(req, fiber.TestConfig{Timeout: 0})\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tresp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t}\n}\n\nfunc Benchmark_PaginateCursorMiddleware(b *testing.B) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSortKey:      \"sort\",\n\t\tDefaultSort:  \"id\",\n\t\tAllowedSorts: []string{\"id\", \"name\", \"date\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tpageInfo, _ := FromContext(c)\n\t\treturn c.JSON(pageInfo)\n\t})\n\n\tcursorJSON := `{\"id\":42,\"created_at\":\"2026-01-01T00:00:00Z\"}`\n\tcursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))\n\n\tb.ResetTimer()\n\n\tfor i := 0; i < b.N; i++ {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/?cursor=\"+cursor+\"&limit=20&sort=name,-id\", http.NoBody)\n\t\tresp, err := app.Test(req, fiber.TestConfig{Timeout: 0})\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tresp.Body.Close() //nolint:errcheck // close error not relevant in tests\n\t}\n}\n"
  },
  {
    "path": "middleware/pprof/config.go",
    "content": "package pprof\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Prefix defines a URL prefix added before \"/debug/pprof\".\n\t// Note that it should start with (but not end with) a slash.\n\t// Example: \"/federated-fiber\"\n\t//\n\t// Optional. Default: \"\"\n\tPrefix string\n}\n\nvar ConfigDefault = Config{\n\tNext:   nil,\n\tPrefix: \"\",\n}\n\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\tif cfg.Prefix == \"\" {\n\t\tcfg.Prefix = ConfigDefault.Prefix\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/pprof/pprof.go",
    "content": "package pprof\n\nimport (\n\t\"net/http/pprof\"\n\t\"strings\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp/fasthttpadaptor\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Set pprof adaptors\n\tvar (\n\t\tpprofIndex        = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Index)\n\t\tpprofCmdline      = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Cmdline)\n\t\tpprofProfile      = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Profile)\n\t\tpprofSymbol       = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Symbol)\n\t\tpprofTrace        = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Trace)\n\t\tpprofAllocs       = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"allocs\").ServeHTTP)\n\t\tpprofBlock        = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"block\").ServeHTTP)\n\t\tpprofGoroutine    = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"goroutine\").ServeHTTP)\n\t\tpprofHeap         = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"heap\").ServeHTTP)\n\t\tpprofMutex        = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"mutex\").ServeHTTP)\n\t\tpprofThreadcreate = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler(\"threadcreate\").ServeHTTP)\n\t)\n\n\t// Construct actual prefix\n\tprefix := cfg.Prefix + \"/debug/pprof\"\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tpath := c.Path()\n\t\t// We are only interested in /debug/pprof routes\n\t\tpath, found := strings.CutPrefix(path, prefix)\n\t\tif !found {\n\t\t\treturn c.Next()\n\t\t}\n\t\t// Switch on trimmed path against constant strings\n\t\tswitch path {\n\t\tcase \"/\":\n\t\t\tpprofIndex(c.RequestCtx())\n\t\tcase \"/cmdline\":\n\t\t\tpprofCmdline(c.RequestCtx())\n\t\tcase \"/profile\":\n\t\t\tpprofProfile(c.RequestCtx())\n\t\tcase \"/symbol\":\n\t\t\tpprofSymbol(c.RequestCtx())\n\t\tcase \"/trace\":\n\t\t\tpprofTrace(c.RequestCtx())\n\t\tcase \"/allocs\":\n\t\t\tpprofAllocs(c.RequestCtx())\n\t\tcase \"/block\":\n\t\t\tpprofBlock(c.RequestCtx())\n\t\tcase \"/goroutine\":\n\t\t\tpprofGoroutine(c.RequestCtx())\n\t\tcase \"/heap\":\n\t\t\tpprofHeap(c.RequestCtx())\n\t\tcase \"/mutex\":\n\t\t\tpprofMutex(c.RequestCtx())\n\t\tcase \"/threadcreate\":\n\t\t\tpprofThreadcreate(c.RequestCtx())\n\t\tdefault:\n\t\t\t// pprof index only works with trailing slash\n\t\t\tif strings.HasSuffix(path, \"/\") {\n\t\t\t\tpath = utils.TrimRight(path, '/')\n\t\t\t} else {\n\t\t\t\tpath = prefix + \"/\"\n\t\t\t}\n\n\t\t\treturn c.Redirect().To(path)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "middleware/pprof/pprof_test.go",
    "content": "package pprof\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testConfig = fiber.TestConfig{\n\tTimeout:       5 * time.Second,\n\tFailOnTimeout: true,\n}\n\nfunc Test_Non_Pprof_Path(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"escaped\", string(b))\n}\n\nfunc Test_Non_Pprof_Path_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Prefix: \"/federated-fiber\"}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"escaped\", string(b))\n}\n\nfunc Test_Pprof_Index(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/pprof/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.True(t, bytes.Contains(b, []byte(\"<title>/debug/pprof/</title>\")))\n}\n\nfunc Test_Pprof_Index_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Prefix: \"/federated-fiber\"}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/federated-fiber/debug/pprof/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(b), \"<title>/debug/pprof/</title>\")\n}\n\nfunc Test_Pprof_Subs(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tsubs := []string{\n\t\t\"cmdline\", \"profile\", \"symbol\", \"trace\", \"allocs\", \"block\",\n\t\t\"goroutine\", \"heap\", \"mutex\", \"threadcreate\",\n\t}\n\n\tfor _, sub := range subs {\n\t\tt.Run(sub, func(t *testing.T) {\n\t\t\ttarget := \"/debug/pprof/\" + sub\n\t\t\tif sub == \"profile\" {\n\t\t\t\ttarget += \"?seconds=1\"\n\t\t\t}\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, http.NoBody), testConfig)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, 200, resp.StatusCode)\n\t\t})\n\t}\n}\n\nfunc Test_Pprof_Subs_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Prefix: \"/federated-fiber\"}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tsubs := []string{\n\t\t\"cmdline\", \"profile\", \"symbol\", \"trace\", \"allocs\", \"block\",\n\t\t\"goroutine\", \"heap\", \"mutex\", \"threadcreate\",\n\t}\n\n\tfor _, sub := range subs {\n\t\tt.Run(sub, func(t *testing.T) {\n\t\t\ttarget := \"/federated-fiber/debug/pprof/\" + sub\n\t\t\tif sub == \"profile\" {\n\t\t\t\ttarget += \"?seconds=1\"\n\t\t\t}\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, http.NoBody), testConfig)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, 200, resp.StatusCode)\n\t\t})\n\t}\n}\n\nfunc Test_Pprof_Other(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/pprof/303\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusSeeOther, resp.StatusCode)\n}\n\nfunc Test_Pprof_Other_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{Prefix: \"/federated-fiber\"}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/federated-fiber/debug/pprof/303\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusSeeOther, resp.StatusCode)\n}\n\n// go test -run Test_Pprof_Next\nfunc Test_Pprof_Next(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/debug/pprof/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 404, resp.StatusCode)\n}\n\n// go test -run Test_Pprof_Next_WithPrefix\nfunc Test_Pprof_Next_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tPrefix: \"/federated-fiber\",\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/federated-fiber/debug/pprof/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 404, resp.StatusCode)\n}\n"
  },
  {
    "path": "middleware/proxy/config.go",
    "content": "package proxy\n\nimport (\n\t\"crypto/tls\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// ModifyRequest allows you to alter the request\n\t//\n\t// Optional. Default: nil\n\tModifyRequest fiber.Handler\n\n\t// ModifyResponse allows you to alter the response\n\t//\n\t// Optional. Default: nil\n\tModifyResponse fiber.Handler\n\n\t// tls config for the http client.\n\tTLSConfig *tls.Config\n\n\t// Client is custom client when client config is complex.\n\t// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize, TLSConfig\n\t// and DialDualStack will not be used if the client are set.\n\tClient *fasthttp.LBClient\n\n\t// Servers defines a list of <scheme>://<host> HTTP servers,\n\t//\n\t// which are used in a round-robin manner.\n\t// i.e.: \"https://foobar.com, http://www.foobar.com\"\n\t//\n\t// Required\n\tServers []string\n\n\t// Timeout is the request timeout used when calling the proxy client\n\t//\n\t// Optional. Default: 1 second\n\tTimeout time.Duration\n\n\t// Per-connection buffer size for requests' reading.\n\t// This also limits the maximum header size.\n\t// Increase this buffer if your clients send multi-KB RequestURIs\n\t// and/or multi-KB headers (for example, BIG cookies).\n\tReadBufferSize int\n\n\t// Per-connection buffer size for responses' writing.\n\tWriteBufferSize int\n\n\t// KeepConnectionHeader keeps the \"Connection\" header when set to true.\n\t//\n\t// Optional. Default: false\n\tKeepConnectionHeader bool\n\n\t// Attempt to connect to both ipv4 and ipv6 host addresses if set to true.\n\t//\n\t// By default client connects only to ipv4 addresses, since unfortunately ipv6\n\t// remains broken in many networks worldwide :)\n\t//\n\t// Optional. Default: false\n\tDialDualStack bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:                 nil,\n\tModifyRequest:        nil,\n\tModifyResponse:       nil,\n\tTimeout:              fasthttp.DefaultLBClientTimeout,\n\tKeepConnectionHeader: false,\n}\n\n// configDefault function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Timeout <= 0 {\n\t\tcfg.Timeout = ConfigDefault.Timeout\n\t}\n\n\t// Set default values\n\tif len(cfg.Servers) == 0 && cfg.Client == nil {\n\t\tpanic(\"Servers cannot be empty\")\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/proxy/proxy.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Balancer creates a load balancer among multiple upstream servers\nfunc Balancer(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Load balanced client\n\tlbc := &fasthttp.LBClient{}\n\t// Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TLSConfig\n\t// will not be used if the client are set.\n\tif cfg.Client == nil {\n\t\t// Set timeout\n\t\tlbc.Timeout = cfg.Timeout\n\t\t// Scheme must be provided, falls back to http\n\t\tfor _, server := range cfg.Servers {\n\t\t\tif !strings.HasPrefix(server, \"http\") {\n\t\t\t\tserver = \"http://\" + server\n\t\t\t}\n\n\t\t\tu, err := url.Parse(server)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tclient := &fasthttp.HostClient{\n\t\t\t\tNoDefaultUserAgentHeader: true,\n\t\t\t\tDisablePathNormalizing:   true,\n\t\t\t\tAddr:                     u.Host,\n\n\t\t\t\tReadBufferSize:  cfg.ReadBufferSize,\n\t\t\t\tWriteBufferSize: cfg.WriteBufferSize,\n\n\t\t\t\tTLSConfig: cfg.TLSConfig,\n\n\t\t\t\tDialDualStack: cfg.DialDualStack,\n\t\t\t}\n\n\t\t\tlbc.Clients = append(lbc.Clients, client)\n\t\t}\n\t} else {\n\t\t// Set custom client\n\t\tlbc = cfg.Client\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Set request and response\n\t\treq := c.Request()\n\t\tres := c.Response()\n\n\t\tif !cfg.KeepConnectionHeader {\n\t\t\t// Don't proxy \"Connection\" header\n\t\t\treq.Header.Del(fiber.HeaderConnection)\n\t\t}\n\n\t\t// Modify request\n\t\tif cfg.ModifyRequest != nil {\n\t\t\tif err := cfg.ModifyRequest(c); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif c.App().Config().Immutable {\n\t\t\treq.SetRequestURIBytes(req.RequestURI())\n\t\t} else {\n\t\t\treq.SetRequestURI(utils.UnsafeString(req.RequestURI()))\n\t\t}\n\n\t\t// Forward request\n\t\tif err := lbc.Do(req, res); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !cfg.KeepConnectionHeader {\n\t\t\t// Don't proxy \"Connection\" header\n\t\t\tres.Header.Del(fiber.HeaderConnection)\n\t\t}\n\n\t\t// Modify response\n\t\tif cfg.ModifyResponse != nil {\n\t\t\tif err := cfg.ModifyResponse(c); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// Return nil to end proxying if no error\n\t\treturn nil\n\t}\n}\n\nvar client = &fasthttp.Client{\n\tNoDefaultUserAgentHeader: true,\n\tDisablePathNormalizing:   true,\n}\n\nvar (\n\terrNilProxyClientOverride = errors.New(\"proxy: nil client override passed to Do/Forward\")\n\terrNilGlobalProxyClient   = errors.New(\"proxy: global client is nil, set a non-nil client with proxy.WithClient\")\n)\n\nvar lock sync.RWMutex\n\n// WithClient sets the global proxy client.\n// This function should be called before Do and Forward.\nfunc WithClient(cli *fasthttp.Client) {\n\tif cli == nil {\n\t\tpanic(\"proxy: WithClient requires a non-nil *fasthttp.Client\")\n\t}\n\n\tlock.Lock()\n\tdefer lock.Unlock()\n\tclient = cli\n}\n\n// Forward performs the given http request and fills the given http response.\n// This method will return a fiber.Handler\nfunc Forward(addr string, clients ...*fasthttp.Client) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\treturn Do(c, addr, clients...)\n\t}\n}\n\n// Do performs the given http request and fills the given http response.\n// This method can be used within a fiber.Handler\nfunc Do(c fiber.Ctx, addr string, clients ...*fasthttp.Client) error {\n\treturn doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error {\n\t\treturn cli.Do(req, resp)\n\t}, clients...)\n}\n\n// DoRedirects performs the given http request and fills the given http response, following up to maxRedirectsCount redirects.\n// When the redirect count exceeds maxRedirectsCount, ErrTooManyRedirects is returned.\n// This method can be used within a fiber.Handler\nfunc DoRedirects(c fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error {\n\treturn doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error {\n\t\treturn cli.DoRedirects(req, resp, maxRedirectsCount)\n\t}, clients...)\n}\n\n// DoDeadline performs the given request and waits for response until the given deadline.\n// This method can be used within a fiber.Handler\nfunc DoDeadline(c fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error {\n\treturn doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error {\n\t\treturn cli.DoDeadline(req, resp, deadline)\n\t}, clients...)\n}\n\n// DoTimeout performs the given request and waits for response during the given timeout duration.\n// This method can be used within a fiber.Handler\nfunc DoTimeout(c fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error {\n\treturn doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error {\n\t\treturn cli.DoTimeout(req, resp, timeout)\n\t}, clients...)\n}\n\nfunc doAction(\n\tc fiber.Ctx,\n\taddr string,\n\taction func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error,\n\tclients ...*fasthttp.Client,\n) error {\n\tlock.RLock()\n\tglobalClient := client\n\tlock.RUnlock()\n\n\tcli, err := selectClient(globalClient, clients...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq := c.Request()\n\tres := c.Response()\n\toriginalURL := utils.CopyString(c.OriginalURL())\n\tdefer req.SetRequestURI(originalURL)\n\n\tcopiedURL := utils.CopyString(addr)\n\treq.SetRequestURI(copiedURL)\n\t// NOTE: if req.isTLS is true, SetRequestURI keeps the scheme as https.\n\t// Reference: https://github.com/gofiber/fiber/issues/1762\n\tif scheme := getScheme(utils.UnsafeBytes(copiedURL)); len(scheme) > 0 {\n\t\treq.URI().SetSchemeBytes(scheme)\n\t}\n\n\treq.Header.Del(fiber.HeaderConnection)\n\tif err := action(cli, req, res); err != nil {\n\t\treturn err\n\t}\n\tres.Header.Del(fiber.HeaderConnection)\n\treturn nil\n}\n\nfunc selectClient(globalClient *fasthttp.Client, clients ...*fasthttp.Client) (*fasthttp.Client, error) {\n\tif len(clients) != 0 {\n\t\tif clients[0] == nil {\n\t\t\treturn nil, errNilProxyClientOverride\n\t\t}\n\n\t\treturn clients[0], nil\n\t}\n\n\tif globalClient == nil {\n\t\treturn nil, errNilGlobalProxyClient\n\t}\n\n\treturn globalClient, nil\n}\n\nfunc getScheme(uri []byte) []byte {\n\ti := bytes.IndexByte(uri, '/')\n\tif i < 1 || uri[i-1] != ':' || i == len(uri)-1 || uri[i+1] != '/' {\n\t\treturn nil\n\t}\n\treturn uri[:i-1]\n}\n\n// DomainForward performs an http request based on the given domain and populates the given http response.\n// This method will return a fiber.Handler\nfunc DomainForward(hostname, addr string, clients ...*fasthttp.Client) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\thost := utils.UnsafeString(c.Request().Host())\n\t\tif host == hostname {\n\t\t\treturn Do(c, addr+c.OriginalURL(), clients...)\n\t\t}\n\t\treturn nil\n\t}\n}\n\ntype roundrobin struct {\n\tpool []string\n\n\tcurrent int\n\tsync.Mutex\n}\n\n// this method will return a string of addr server from list server.\nfunc (r *roundrobin) get() string {\n\tr.Lock()\n\tdefer r.Unlock()\n\n\tif r.current >= len(r.pool) {\n\t\tr.current %= len(r.pool)\n\t}\n\n\tresult := r.pool[r.current]\n\tr.current++\n\treturn result\n}\n\n// BalancerForward Forward performs the given http request with round robin algorithm to server and fills the given http response.\n// This method will return a fiber.Handler\nfunc BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler {\n\tr := &roundrobin{\n\t\tcurrent: 0,\n\t\tpool:    servers,\n\t}\n\treturn func(c fiber.Ctx) error {\n\t\tserver := r.get()\n\t\tif !strings.HasPrefix(server, \"http\") {\n\t\t\tserver = \"http://\" + server\n\t\t}\n\t\tc.Request().Header.Add(\"X-Real-IP\", c.IP())\n\t\treturn Do(c, server+c.OriginalURL(), clients...)\n\t}\n}\n"
  },
  {
    "path": "middleware/proxy/proxy_test.go",
    "content": "package proxy\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\tclientpkg \"github.com/gofiber/fiber/v3/client\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gofiber/fiber/v3/internal/tlstest\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc startServer(app *fiber.App, ln net.Listener) {\n\tgo func() {\n\t\terr := app.Listener(ln, fiber.ListenConfig{\n\t\t\tDisableStartupMessage: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}()\n}\n\nfunc createProxyTestServer(t *testing.T, handler fiber.Handler, network, address string) (target *fiber.App, addr string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned target app and address for readability\n\tt.Helper()\n\n\ttarget = fiber.New()\n\ttarget.Get(\"/\", handler)\n\n\tln, err := net.Listen(network, address)\n\trequire.NoError(t, err)\n\n\taddr = ln.Addr().String()\n\n\tstartServer(target, ln)\n\n\treturn target, addr\n}\n\nfunc createProxyTestServerIPv4(t *testing.T, handler fiber.Handler) (target *fiber.App, addr string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned target app and address for readability\n\tt.Helper()\n\treturn createProxyTestServer(t, handler, fiber.NetworkTCP4, \"127.0.0.1:0\")\n}\n\nfunc createProxyTestServerIPv6(t *testing.T, handler fiber.Handler) (target *fiber.App, addr string) { //nolint:nonamedreturns // gocritic unnamedResult prefers naming returned target app and address for readability\n\tt.Helper()\n\treturn createProxyTestServer(t, handler, fiber.NetworkTCP6, \"[::1]:0\")\n}\n\nfunc createRedirectServer(t *testing.T) string {\n\tt.Helper()\n\tapp := fiber.New()\n\n\tvar addr string\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Location(\"http://\" + addr + \"/final\")\n\t\treturn c.Status(fiber.StatusMovedPermanently).SendString(\"redirect\")\n\t})\n\tapp.Get(\"/final\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"final\")\n\t})\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tln.Close() //nolint:errcheck // It is fine to ignore the error here\n\t})\n\taddr = ln.Addr().String()\n\n\tstartServer(app, ln)\n\n\treturn addr\n}\n\n// go test -run Test_Proxy_Empty_Host\nfunc Test_Proxy_Empty_Upstream_Servers(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif r != \"Servers cannot be empty\" {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{Servers: []string{}}))\n}\n\n// go test -run Test_Proxy_Empty_Config\nfunc Test_Proxy_Empty_Config(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif r != \"Servers cannot be empty\" {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{}))\n}\n\n// go test -run Test_Proxy_Next\nfunc Test_Proxy_Next(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers: []string{\"127.0.0.1\"},\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_Proxy\nfunc Test_Proxy(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New()\n\n\tapp.Use(Balancer(Config{Servers: []string{addr}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\n// go test -run Test_Proxy_Balancer_WithTlsConfig\nfunc Test_Proxy_Balancer_WithTlsConfig(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, _, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tln = tls.NewListener(ln, serverTLSConf)\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/tlsbalancer\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"tls balancer\")\n\t})\n\n\taddr := ln.Addr().String()\n\tclientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine\n\n\t// disable certificate verification in Balancer\n\tapp.Use(Balancer(Config{\n\t\tServers:   []string{addr},\n\t\tTLSConfig: clientTLSConf,\n\t}))\n\n\tstartServer(app, ln)\n\n\tclient := clientpkg.New()\n\tclient.SetTLSConfig(clientTLSConf)\n\n\tresp, err := client.Get(\"https://\" + addr + \"/tlsbalancer\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"tls balancer\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -run Test_Proxy_Balancer_IPv6_Upstream\nfunc Test_Proxy_Balancer_IPv6_Upstream(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv6(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New()\n\n\tapp.Use(Balancer(Config{Servers: []string{addr}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n}\n\n// go test -run Test_Proxy_Balancer_IPv6_Upstream\nfunc Test_Proxy_Balancer_IPv6_Upstream_With_DialDualStack(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv6(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New()\n\n\tapp.Use(Balancer(Config{\n\t\tServers:       []string{addr},\n\t\tDialDualStack: true,\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\n// go test -run Test_Proxy_Balancer_IPv6_Upstream\nfunc Test_Proxy_Balancer_IPv4_Upstream_With_DialDualStack(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New()\n\n\tapp.Use(Balancer(Config{\n\t\tServers:       []string{addr},\n\t\tDialDualStack: true,\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\n// go test -run Test_Proxy_Forward_WithTlsConfig_To_Http\nfunc Test_Proxy_Forward_WithTlsConfig_To_Http(t *testing.T) {\n\tt.Parallel()\n\n\t_, targetAddr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello from target\")\n\t})\n\n\tproxyServerTLSConf, _, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tproxyServerLn, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tproxyServerLn = tls.NewListener(proxyServerLn, proxyServerTLSConf)\n\tproxyAddr := proxyServerLn.Addr().String()\n\n\tapp := fiber.New()\n\tapp.Use(Forward(\"http://\" + targetAddr))\n\tstartServer(app, proxyServerLn)\n\n\tclient := clientpkg.New()\n\tclient.SetTimeout(5 * time.Second)\n\tclient.TLSConfig().InsecureSkipVerify = true\n\n\tresp, err := client.Get(\"https://\" + proxyAddr)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"hello from target\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -run Test_Proxy_Forward\nfunc Test_Proxy_Forward(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"forwarded\")\n\t})\n\n\tapp.Use(Forward(\"http://\" + addr))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"forwarded\", string(b))\n}\n\n// go test -run Test_Proxy_Forward_WithClient_TLSConfig\nfunc Test_Proxy_Forward_WithClient_TLSConfig(t *testing.T) {\n\tt.Parallel()\n\n\tserverTLSConf, _, err := tlstest.GetTLSConfigs()\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tln = tls.NewListener(ln, serverTLSConf)\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/tlsfwd\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"tls forward\")\n\t})\n\n\taddr := ln.Addr().String()\n\tclientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine\n\n\t// disable certificate verification\n\tWithClient(&fasthttp.Client{\n\t\tTLSConfig: clientTLSConf,\n\t})\n\tapp.Use(Forward(\"https://\" + addr + \"/tlsfwd\"))\n\n\tstartServer(app, ln)\n\n\tclient := clientpkg.New()\n\tclient.SetTLSConfig(clientTLSConf)\n\n\tresp, err := client.Get(\"https://\" + addr)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"tls forward\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -run Test_Proxy_Modify_Response\nfunc Test_Proxy_Modify_Response(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.Status(500).SendString(\"not modified\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers: []string{addr},\n\t\tModifyResponse: func(c fiber.Ctx) error {\n\t\t\tc.Response().SetStatusCode(fiber.StatusOK)\n\t\t\treturn c.SendString(\"modified response\")\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"modified response\", string(b))\n}\n\n// go test -run Test_Proxy_Modify_Request\nfunc Test_Proxy_Modify_Request(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\tb := c.Request().Body()\n\t\treturn c.SendString(string(b))\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers: []string{addr},\n\t\tModifyRequest: func(c fiber.Ctx) error {\n\t\t\tc.Request().SetBody([]byte(\"modified request\"))\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"modified request\", string(b))\n}\n\n// go test -run Test_Proxy_Timeout_Slow_Server\nfunc Test_Proxy_Timeout_Slow_Server(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\ttime.Sleep(300 * time.Millisecond)\n\t\treturn c.SendString(\"fiber is awesome\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers: []string{addr},\n\t\tTimeout: 600 * time.Millisecond,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"fiber is awesome\", string(b))\n}\n\n// go test -run Test_Proxy_With_Timeout\nfunc Test_Proxy_With_Timeout(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\ttime.Sleep(1 * time.Second)\n\t\treturn c.SendString(\"fiber is awesome\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers: []string{addr},\n\t\tTimeout: 100 * time.Millisecond,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"timeout\", string(b))\n}\n\n// go test -run Test_Proxy_Buffer_Size_Response\nfunc Test_Proxy_Buffer_Size_Response(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\tlong := strings.Join(make([]string, 5000), \"-\")\n\t\tc.Set(\"Very-Long-Header\", long)\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{Servers: []string{addr}, KeepConnectionHeader: true}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\tapp = fiber.New()\n\tapp.Use(Balancer(Config{\n\t\tServers:        []string{addr},\n\t\tReadBufferSize: 1024 * 8,\n\t}))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -race -run Test_Proxy_Do_RestoreOriginalURL\nfunc Test_Proxy_Do_RestoreOriginalURL(t *testing.T) {\n\tt.Parallel()\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn Do(c, \"http://\"+addr)\n\t})\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err1)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxied\", string(body))\n}\n\n// go test -race -run Test_Proxy_Do_WithRealURL\nfunc Test_Proxy_Do_WithRealURL(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"real url\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn Do(c, \"http://\"+addr)\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err1)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"real url\", string(body))\n}\n\n// go test -race -run Test_Proxy_Do_WithRedirect\nfunc Test_Proxy_Do_WithRedirect(t *testing.T) {\n\tt.Parallel()\n\n\taddr := createRedirectServer(t)\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn Do(c, \"http://\"+addr)\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err1)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"redirect\", string(body))\n\trequire.Equal(t, fiber.StatusMovedPermanently, resp.StatusCode)\n}\n\n// go test -race -run Test_Proxy_DoRedirects_RestoreOriginalURL\nfunc Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) {\n\tt.Parallel()\n\n\taddr := createRedirectServer(t)\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoRedirects(c, \"http://\"+addr, 1)\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err1)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"final\", string(body))\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n}\n\n// go test -race -run Test_Proxy_DoRedirects_TooManyRedirects\nfunc Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) {\n\tt.Parallel()\n\n\taddr := createRedirectServer(t)\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoRedirects(c, \"http://\"+addr, 0)\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err1)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"too many redirects detected when doing the request\", string(body))\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n}\n\n// go test -race -run Test_Proxy_DoTimeout_RestoreOriginalURL\nfunc Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoTimeout(c, \"http://\"+addr, time.Second)\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err1)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxied\", string(body))\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n}\n\n// go test -race -run Test_Proxy_DoTimeout_Timeout\nfunc Test_Proxy_DoTimeout_Timeout(t *testing.T) {\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\ttime.Sleep(time.Second * 5)\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoTimeout(c, \"http://\"+addr, time.Second)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"timeout\", string(body))\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n}\n\n// go test -race -run Test_Proxy_DoDeadline_RestoreOriginalURL\nfunc Test_Proxy_DoDeadline_RestoreOriginalURL(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoDeadline(c, \"http://\"+addr, time.Now().Add(time.Second))\n\t})\n\n\tresp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err1)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxied\", string(body))\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"/test\", resp.Request.URL.String())\n}\n\n// go test -race -run Test_Proxy_DoDeadline_PastDeadline\nfunc Test_Proxy_DoDeadline_PastDeadline(t *testing.T) {\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\ttime.Sleep(time.Second * 5)\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn DoDeadline(c, \"http://\"+addr, time.Now().Add(2*time.Second))\n\t})\n\n\t_, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       1 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.Equal(t, os.ErrDeadlineExceeded, err1)\n}\n\n// go test -race -run Test_Proxy_Do_HTTP_Prefix_URL\nfunc Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello world\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\tpath := c.OriginalURL()\n\t\turl := strings.TrimPrefix(path, \"/\")\n\n\t\trequire.Equal(t, \"http://\"+addr, url)\n\t\tif err := Do(c, url); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.Response().Header.Del(fiber.HeaderServer)\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/http://\"+addr, http.NoBody))\n\trequire.NoError(t, err)\n\ts, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"hello world\", string(s))\n}\n\n// go test -race -run Test_Proxy_Forward_Global_Client\nfunc Test_Proxy_Forward_Global_Client(t *testing.T) {\n\tt.Parallel()\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tWithClient(&fasthttp.Client{\n\t\tNoDefaultUserAgentHeader: true,\n\t\tDisablePathNormalizing:   true,\n\t})\n\tapp := fiber.New()\n\tapp.Get(\"/test_global_client\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"test_global_client\")\n\t})\n\n\taddr := ln.Addr().String()\n\tapp.Use(Forward(\"http://\" + addr + \"/test_global_client\"))\n\tstartServer(app, ln)\n\n\tclient := clientpkg.New()\n\tresp, err := client.Get(\"http://\" + addr)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"test_global_client\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -race -run Test_Proxy_Forward_Local_Client\nfunc Test_Proxy_Forward_Local_Client(t *testing.T) {\n\tt.Parallel()\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tapp := fiber.New()\n\tapp.Get(\"/test_local_client\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"test_local_client\")\n\t})\n\n\taddr := ln.Addr().String()\n\tapp.Use(Forward(\"http://\"+addr+\"/test_local_client\", &fasthttp.Client{\n\t\tNoDefaultUserAgentHeader: true,\n\t\tDisablePathNormalizing:   true,\n\n\t\tDial: fasthttp.Dial,\n\t}))\n\tstartServer(app, ln)\n\n\tclient := clientpkg.New()\n\tresp, err := client.Get(\"http://\" + addr)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"test_local_client\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -run Test_Proxy_WithClient_Nil_Panics\nfunc Test_Proxy_WithClient_Nil_Panics(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithValue(t, \"proxy: WithClient requires a non-nil *fasthttp.Client\", func() {\n\t\tWithClient(nil)\n\t})\n}\n\n// go test -run Test_Proxy_Do_NilClientOverride\nfunc Test_Proxy_Do_NilClientOverride(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn Do(c, \"http://\"+addr, nil)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, errNilProxyClientOverride.Error(), string(body))\n}\n\n// go test -run Test_Proxy_Do_NonNilClientOverride\nfunc Test_Proxy_Do_NonNilClientOverride(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn Do(c, \"http://\"+addr, &fasthttp.Client{\n\t\t\tNoDefaultUserAgentHeader: true,\n\t\t\tDisablePathNormalizing:   true,\n\t\t})\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"proxied\", string(body))\n}\n\n// go test -run Test_Proxy_SelectClient_NilGlobal\nfunc Test_Proxy_SelectClient_NilGlobal(t *testing.T) {\n\tt.Parallel()\n\n\tselectedClient, err := selectClient(nil)\n\trequire.ErrorIs(t, err, errNilGlobalProxyClient)\n\trequire.Nil(t, selectedClient)\n}\n\n// go test -run Test_Proxy_NilClientOverride_AcrossHelpers\nfunc Test_Proxy_NilClientOverride_AcrossHelpers(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"proxied\")\n\t})\n\n\ttests := map[string]func(c fiber.Ctx) error{\n\t\t\"DoRedirects\": func(c fiber.Ctx) error {\n\t\t\treturn DoRedirects(c, \"http://\"+addr, 1, nil)\n\t\t},\n\t\t\"DoDeadline\": func(c fiber.Ctx) error {\n\t\t\treturn DoDeadline(c, \"http://\"+addr, time.Now().Add(time.Second), nil)\n\t\t},\n\t\t\"DoTimeout\": func(c fiber.Ctx) error {\n\t\t\treturn DoTimeout(c, \"http://\"+addr, time.Second, nil)\n\t\t},\n\t}\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tapp := fiber.New()\n\t\t\tapp.Get(\"/test\", run)\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, errNilProxyClientOverride.Error(), string(body))\n\t\t})\n\t}\n}\n\n// go test -run Test_ProxyBalancer_Custom_Client\nfunc Test_ProxyBalancer_Custom_Client(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New()\n\n\tapp.Use(Balancer(Config{Client: &fasthttp.LBClient{\n\t\tClients: []fasthttp.BalancingClient{\n\t\t\t&fasthttp.HostClient{\n\t\t\t\tNoDefaultUserAgentHeader: true,\n\t\t\t\tDisablePathNormalizing:   true,\n\t\t\t\tAddr:                     addr,\n\t\t\t},\n\t\t},\n\t\tTimeout: time.Second,\n\t}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\n// go test -run Test_Proxy_Domain_Forward_Local\nfunc Test_Proxy_Domain_Forward_Local(t *testing.T) {\n\tt.Parallel()\n\tln, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tapp := fiber.New()\n\n\t// target server\n\tln1, err := net.Listen(fiber.NetworkTCP4, \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\tapp1 := fiber.New()\n\n\tapp1.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"test_local_client:\" + c.Query(\"query_test\"))\n\t})\n\n\tproxyAddr := ln.Addr().String()\n\ttargetAddr := ln1.Addr().String()\n\tlocalDomain := strings.Replace(proxyAddr, \"127.0.0.1\", \"localhost\", 1)\n\tapp.Use(DomainForward(localDomain, \"http://\"+targetAddr, &fasthttp.Client{\n\t\tNoDefaultUserAgentHeader: true,\n\t\tDisablePathNormalizing:   true,\n\n\t\tDial: fasthttp.Dial,\n\t}))\n\tstartServer(app, ln)\n\tstartServer(app1, ln1)\n\n\tclient := clientpkg.New()\n\tresp, err := client.Get(\"http://\" + localDomain + \"/test?query_test=true\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode())\n\trequire.Equal(t, \"test_local_client:true\", string(resp.Body()))\n\tresp.Close()\n}\n\n// go test -run Test_Proxy_Balancer_Forward_Local\nfunc Test_Proxy_Balancer_Forward_Local(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"forwarded\")\n\t})\n\n\tapp.Use(BalancerForward([]string{addr}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"forwarded\", string(b))\n}\n\nfunc Test_Proxy_Immutable(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t})\n\n\tresp, err := target.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody), fiber.TestConfig{\n\t\tTimeout:       2 * time.Second,\n\t\tFailOnTimeout: true,\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\n\tapp := fiber.New(fiber.Config{Immutable: true})\n\n\tapp.Use(Balancer(Config{Servers: []string{addr}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\nfunc Test_Proxy_KeepConnectionHeader(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderConnection, \"backend\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{Servers: []string{addr}, KeepConnectionHeader: true}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\treq.Header.Set(fiber.HeaderConnection, \"keep-alive\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"backend\", resp.Header.Get(fiber.HeaderConnection))\n}\n\nfunc Test_Proxy_DropConnectionHeader(t *testing.T) {\n\tt.Parallel()\n\n\t_, addr := createProxyTestServerIPv4(t, func(c fiber.Ctx) error {\n\t\tc.Set(fiber.HeaderConnection, \"backend\")\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Balancer(Config{Servers: []string{addr}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Host = addr\n\treq.Header.Set(fiber.HeaderConnection, \"keep-alive\")\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderConnection))\n}\n"
  },
  {
    "path": "middleware/recover/config.go",
    "content": "package recover //nolint:predeclared // TODO: Rename to some non-builtin\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// PanicHandler defines a function to customize the error produced from a recovered panic/result.\n\t//\n\t// Optional. Default: DefaultPanicHandler\n\tPanicHandler func(c fiber.Ctx, r any) error\n\n\t// StackTraceHandler defines a function to handle stack trace\n\t//\n\t// Optional. Default: defaultStackTraceHandler\n\tStackTraceHandler func(c fiber.Ctx, e any)\n\n\t// EnableStackTrace enables handling stack trace\n\t//\n\t// Optional. Default: false\n\tEnableStackTrace bool\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:              nil,\n\tEnableStackTrace:  false,\n\tStackTraceHandler: defaultStackTraceHandler,\n\tPanicHandler:      DefaultPanicHandler,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\tif cfg.EnableStackTrace && cfg.StackTraceHandler == nil {\n\t\tcfg.StackTraceHandler = defaultStackTraceHandler\n\t}\n\tif cfg.PanicHandler == nil {\n\t\tcfg.PanicHandler = DefaultPanicHandler\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/recover/recover.go",
    "content": "package recover //nolint:predeclared // TODO: Rename to some non-builtin\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nfunc defaultStackTraceHandler(_ fiber.Ctx, e any) {\n\tfmt.Fprintf(os.Stderr, \"panic: %v\\n\\n%s\\n\", e, debug.Stack())\n}\n\n// DefaultPanicHandler returns r directly if it's an error, and creates a new one with the %v verb otherwise.\nfunc DefaultPanicHandler(_ fiber.Ctx, r any) error {\n\tif err, ok := r.(error); ok {\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"%v\", r)\n}\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) (err error) { //nolint:nonamedreturns // Uses recover() to overwrite the error\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Catch panics\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tif cfg.EnableStackTrace {\n\t\t\t\t\tcfg.StackTraceHandler(c, r)\n\t\t\t\t}\n\n\t\t\t\t// Set error that will call the global error handler\n\t\t\t\terr = cfg.PanicHandler(c, r)\n\t\t\t}\n\t\t}()\n\n\t\t// Return err if exist, else move to next handler\n\t\treturn c.Next()\n\t}\n}\n"
  },
  {
    "path": "middleware/recover/recover_test.go",
    "content": "package recover //nolint:predeclared // TODO: Rename to some non-builtin\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_Recover\nfunc Test_Recover(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tpanicVal     any\n\t\tpanicHandler func(c fiber.Ctx, r any) error\n\t\terrorMsg     string\n\t}{\n\t\t{\n\t\t\tname:         \"non-error panic will be handled by default\",\n\t\t\tpanicVal:     \"Hi, I'm an error!\",\n\t\t\tpanicHandler: nil,\n\t\t\terrorMsg:     \"Hi, I'm an error!\",\n\t\t},\n\t\t{\n\t\t\tname:         \"error panic will be handled by default\",\n\t\t\tpanicVal:     errors.New(\"hi, I'm an error object\"),\n\t\t\tpanicHandler: nil,\n\t\t\terrorMsg:     \"hi, I'm an error object\",\n\t\t},\n\t\t{\n\t\t\tname:     \"non-error panic will be handled\",\n\t\t\tpanicVal: \"Hi, I'm an error!\",\n\t\t\tpanicHandler: func(c fiber.Ctx, r any) error {\n\t\t\t\treturn fmt.Errorf(\"[RECOVERED]: %w\", DefaultPanicHandler(c, r))\n\t\t\t},\n\t\t\terrorMsg: \"[RECOVERED]: Hi, I'm an error!\",\n\t\t},\n\t\t{\n\t\t\tname:     \"error panic will be handled\",\n\t\t\tpanicVal: errors.New(\"hi, I'm an error object\"),\n\t\t\tpanicHandler: func(c fiber.Ctx, r any) error {\n\t\t\t\treturn fmt.Errorf(\"[RECOVERED]: %w\", DefaultPanicHandler(c, r))\n\t\t\t},\n\t\t\terrorMsg: \"[RECOVERED]: hi, I'm an error object\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tapp := fiber.New(fiber.Config{\n\t\t\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\t\t\trequire.Equal(t, tc.errorMsg, err.Error())\n\t\t\t\t\treturn c.SendStatus(fiber.StatusTeapot)\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tapp.Use(New(Config{PanicHandler: tc.panicHandler}))\n\n\t\t\tapp.Get(\"/panic\", func(_ fiber.Ctx) error {\n\t\t\t\tpanic(tc.panicVal)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/panic\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\t\t})\n\t}\n}\n\n// go test -run Test_Recover_Next\nfunc Test_Recover_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Recover_EnableStackTrace(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tEnableStackTrace: true,\n\t}))\n\n\tapp.Get(\"/panic\", func(_ fiber.Ctx) error {\n\t\tpanic(\"Hi, I'm an error!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/panic\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n}\n"
  },
  {
    "path": "middleware/redirect/config.go",
    "content": "package redirect\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Filter defines a function to skip middleware.\n\t// Optional. Default: nil\n\tNext func(fiber.Ctx) bool\n\n\t// Rules defines the URL path rewrite rules. The values captured in asterisk can be\n\t// retrieved by index e.g. $1, $2 and so on.\n\t// Required. Example:\n\t// \"/old\":              \"/new\",\n\t// \"/api/*\":            \"/$1\",\n\t// \"/js/*\":             \"/public/javascript/$1\",\n\t// \"/users/*/orders/*\": \"/user/$1/order/$2\",\n\tRules map[string]string\n\n\trulesRegex map[*regexp.Regexp]string\n\n\t// The status code when redirecting\n\t// This is ignored if Redirect is disabled\n\t// Optional. Default: 302 Temporary Redirect\n\tStatusCode int\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tStatusCode: fiber.StatusFound,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.StatusCode == 0 {\n\t\tcfg.StatusCode = ConfigDefault.StatusCode\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/redirect/redirect.go",
    "content": "package redirect\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\t// Initialize\n\tcfg.rulesRegex = map[*regexp.Regexp]string{}\n\tfor k, v := range cfg.Rules {\n\t\tk = strings.ReplaceAll(k, \"*\", \"(.*)\")\n\t\tk += \"$\"\n\t\tcfg.rulesRegex[regexp.MustCompile(k)] = v\n\t}\n\n\t// Middleware function\n\treturn func(c fiber.Ctx) error {\n\t\t// Next request to skip middleware\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\t\t// Rewrite\n\t\tfor k, v := range cfg.rulesRegex {\n\t\t\treplacer := captureTokens(k, c.Path())\n\t\t\tif replacer != nil {\n\t\t\t\tqueryString := utils.UnsafeString(c.RequestCtx().QueryArgs().QueryString())\n\t\t\t\tif queryString != \"\" {\n\t\t\t\t\tqueryString = \"?\" + queryString\n\t\t\t\t}\n\t\t\t\treturn c.Redirect().Status(cfg.StatusCode).To(replacer.Replace(v) + queryString)\n\t\t\t}\n\t\t}\n\t\treturn c.Next()\n\t}\n}\n\n// https://github.com/labstack/echo/blob/master/middleware/rewrite.go\nfunc captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {\n\tif len(input) > 1 {\n\t\tinput = utils.TrimRight(input, '/')\n\t}\n\tgroups := pattern.FindAllStringSubmatch(input, -1)\n\tif groups == nil {\n\t\treturn nil\n\t}\n\tvalues := groups[0][1:]\n\treplace := make([]string, 2*len(values))\n\tfor i, v := range values {\n\t\tj := 2 * i\n\t\treplace[j] = \"$\" + strconv.Itoa(i+1)\n\t\treplace[j+1] = v\n\t}\n\treturn strings.NewReplacer(replace...)\n}\n"
  },
  {
    "path": "middleware/redirect/redirect_test.go",
    "content": "package redirect\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_Redirect(t *testing.T) {\n\tapp := *fiber.New()\n\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/default\": \"google.com\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/default/*\": \"fiber.wiki\",\n\t\t},\n\t\tStatusCode: fiber.StatusTemporaryRedirect,\n\t}))\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/redirect/*\": \"$1\",\n\t\t},\n\t\tStatusCode: fiber.StatusSeeOther,\n\t}))\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/pattern/*\": \"golang.org\",\n\t\t},\n\t\tStatusCode: fiber.StatusFound,\n\t}))\n\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/\": \"/swagger\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/params\": \"/with_params\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\tapp.Get(\"/api/*\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"API\")\n\t})\n\n\tapp.Get(\"/new\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World!\")\n\t})\n\n\ttests := []struct {\n\t\tname       string\n\t\turl        string\n\t\tredirectTo string\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\tname:       \"should be returns status StatusFound without a wildcard\",\n\t\t\turl:        \"/default\",\n\t\t\tredirectTo: \"google.com\",\n\t\t\tstatusCode: fiber.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\tname:       \"should be returns status StatusTemporaryRedirect  using wildcard\",\n\t\t\turl:        \"/default/xyz\",\n\t\t\tredirectTo: \"fiber.wiki\",\n\t\t\tstatusCode: fiber.StatusTemporaryRedirect,\n\t\t},\n\t\t{\n\t\t\tname:       \"should be returns status StatusSeeOther without set redirectTo to use the default\",\n\t\t\turl:        \"/redirect/github.com/gofiber/redirect\",\n\t\t\tredirectTo: \"github.com/gofiber/redirect\",\n\t\t\tstatusCode: fiber.StatusSeeOther,\n\t\t},\n\t\t{\n\t\t\tname:       \"should return the status code default\",\n\t\t\turl:        \"/pattern/xyz\",\n\t\t\tredirectTo: \"golang.org\",\n\t\t\tstatusCode: fiber.StatusFound,\n\t\t},\n\t\t{\n\t\t\tname:       \"access URL without rule\",\n\t\t\turl:        \"/new\",\n\t\t\tstatusCode: fiber.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"redirect to swagger route\",\n\t\t\turl:        \"/\",\n\t\t\tredirectTo: \"/swagger\",\n\t\t\tstatusCode: fiber.StatusMovedPermanently,\n\t\t},\n\t\t{\n\t\t\tname:       \"no redirect to swagger route\",\n\t\t\turl:        \"/api/\",\n\t\t\tstatusCode: fiber.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"no redirect to swagger route #2\",\n\t\t\turl:        \"/api/test\",\n\t\t\tstatusCode: fiber.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:       \"redirect with query params\",\n\t\t\turl:        \"/params?query=abc\",\n\t\t\tredirectTo: \"/with_params?query=abc\",\n\t\t\tstatusCode: fiber.StatusMovedPermanently,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, tt.url, http.NoBody)\n\t\t\trequire.NoError(t, err)\n\t\t\treq.Header.Set(\"Location\", \"github.com/gofiber/redirect\")\n\t\t\tresp, err := app.Test(req)\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.statusCode, resp.StatusCode)\n\t\t\trequire.Equal(t, tt.redirectTo, resp.Header.Get(\"Location\"))\n\t\t})\n\t}\n}\n\nfunc Test_Next(t *testing.T) {\n\t// Case 1 : Next function always returns true\n\tapp := *fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tRules: map[string]string{\n\t\t\t\"/default\": \"google.com\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t// Case 2 : Next function always returns false\n\tapp = *fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(fiber.Ctx) bool {\n\t\t\treturn false\n\t\t},\n\t\tRules: map[string]string{\n\t\t\t\"/default\": \"google.com\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, fiber.StatusMovedPermanently, resp.StatusCode)\n\trequire.Equal(t, \"google.com\", resp.Header.Get(\"Location\"))\n}\n\nfunc Test_NoRules(t *testing.T) {\n\t// Case 1: No rules with default route defined\n\tapp := *fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t// Case 2: No rules and no default route defined\n\tapp = *fiber.New()\n\n\tapp.Use(New(Config{\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_DefaultConfig(t *testing.T) {\n\t// Case 1: Default config and no default route\n\tapp := *fiber.New()\n\n\tapp.Use(New())\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\t// Case 2: Default config and default route\n\tapp = *fiber.New()\n\n\tapp.Use(New())\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_RegexRules(t *testing.T) {\n\t// Case 1: Rules regex is empty\n\tapp := *fiber.New()\n\tapp.Use(New(Config{\n\t\tRules:      map[string]string{},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t// Case 2: Rules regex map contains valid regex and well-formed replacement URLs\n\tapp = *fiber.New()\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/default\": \"google.com\",\n\t\t},\n\t\tStatusCode: fiber.StatusMovedPermanently,\n\t}))\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/default\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusMovedPermanently, resp.StatusCode)\n\trequire.Equal(t, \"google.com\", resp.Header.Get(\"Location\"))\n\n\t// Case 3: Test invalid regex throws panic\n\tapp = *fiber.New()\n\trequire.Panics(t, func() {\n\t\tapp.Use(New(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"(\": \"google.com\",\n\t\t\t},\n\t\t\tStatusCode: fiber.StatusMovedPermanently,\n\t\t}))\n\t})\n}\n"
  },
  {
    "path": "middleware/requestid/config.go",
    "content": "package requestid\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Generator defines a function to generate the unique identifier.\n\t//\n\t// Optional. Default: utils.SecureToken\n\tGenerator func() string\n\n\t// Header is the header key where to get/set the unique request ID\n\t//\n\t// Optional. Default: \"X-Request-ID\"\n\tHeader string\n}\n\n// ConfigDefault is the default config\n// It uses a secure token generator for better privacy and security.\nvar ConfigDefault = Config{\n\tNext:      nil,\n\tHeader:    fiber.HeaderXRequestID,\n\tGenerator: utils.SecureToken,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Header == \"\" {\n\t\tcfg.Header = ConfigDefault.Header\n\t}\n\tif cfg.Generator == nil {\n\t\tcfg.Generator = ConfigDefault.Generator\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/requestid/requestid.go",
    "content": "package requestid\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\n// The keys for the values in context\nconst (\n\trequestIDKey contextKey = iota\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\t\trid := sanitizeRequestID(c.Get(cfg.Header), cfg.Generator)\n\n\t\t// Set new id to response header\n\t\tc.Set(cfg.Header, rid)\n\n\t\t// Add the request ID to locals\n\t\tfiber.StoreInContext(c, requestIDKey, rid)\n\n\t\t// Continue stack\n\t\treturn c.Next()\n\t}\n}\n\n// sanitizeRequestID returns the provided request ID when it is valid, otherwise\n// it tries up to three values from the configured generator, then falls back to SecureToken.\nfunc sanitizeRequestID(rid string, generator func() string) string {\n\tif isValidRequestID(rid) {\n\t\treturn rid\n\t}\n\n\tfor range 3 {\n\t\trid = generator()\n\t\tif isValidRequestID(rid) {\n\t\t\treturn rid\n\t\t}\n\t}\n\n\t// Final fallback: SecureToken always produces a valid ID\n\treturn utils.SecureToken()\n}\n\n// isValidRequestID reports whether the request ID contains only visible ASCII\n// characters (0x20–0x7E) and is non-empty.\nfunc isValidRequestID(rid string) bool {\n\tif rid == \"\" {\n\t\treturn false\n\t}\n\n\tfor i := 0; i < len(rid); i++ {\n\t\tc := rid[i]\n\t\tif c < 0x20 || c > 0x7e {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// FromContext returns the request ID from context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// If there is no request ID, an empty string is returned.\nfunc FromContext(ctx any) string {\n\tif rid, ok := fiber.ValueFromContext[string](ctx, requestIDKey); ok {\n\t\treturn rid\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "middleware/requestid/requestid_test.go",
    "content": "package requestid\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_RequestID\nfunc Test_RequestID(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello, World 👋!\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\treqid := resp.Header.Get(fiber.HeaderXRequestID)\n\trequire.Len(t, reqid, 43)\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\treq.Header.Add(fiber.HeaderXRequestID, reqid)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, reqid, resp.Header.Get(fiber.HeaderXRequestID))\n}\n\nfunc Test_RequestID_InvalidHeaderValue(t *testing.T) {\n\tt.Parallel()\n\n\trid := sanitizeRequestID(\"bad\\r\\nid\", func() string {\n\t\treturn \"clean-generated-id\"\n\t})\n\n\trequire.Equal(t, \"clean-generated-id\", rid)\n}\n\nfunc Test_RequestID_InvalidGeneratedValue(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tGenerator: func() string {\n\t\t\treturn \"bad\\r\\nid\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\trid := resp.Header.Get(fiber.HeaderXRequestID)\n\trequire.NotEmpty(t, rid)\n\trequire.NotContains(t, rid, \"\\r\")\n\trequire.NotContains(t, rid, \"\\n\")\n\trequire.Len(t, rid, 43, \"Fallback should produce a SecureToken\")\n}\n\nfunc Test_RequestID_GeneratorAlwaysInvalid(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tGenerator: func() string {\n\t\t\treturn \"invalid\\x00id\" // Always invalid due to null byte\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\trid := resp.Header.Get(fiber.HeaderXRequestID)\n\trequire.NotEmpty(t, rid)\n\trequire.Len(t, rid, 43, \"Should fall back to SecureToken after 3 invalid attempts\")\n}\n\nfunc Test_RequestID_CustomGenerator(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tGenerator: func() string {\n\t\t\treturn \"custom-valid-id\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\trid := resp.Header.Get(fiber.HeaderXRequestID)\n\trequire.Equal(t, \"custom-valid-id\", rid)\n}\n\nfunc Test_isValidRequestID_VisibleASCII(t *testing.T) {\n\tt.Parallel()\n\n\trequire.True(t, isValidRequestID(\"request-id-09AZaz ~\"))\n}\n\nfunc Test_isValidRequestID_Boundaries(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"allows space and tilde\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.True(t, isValidRequestID(\" ~\"))\n\t})\n\n\tt.Run(\"rejects out of range\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.False(t, isValidRequestID(string([]byte{0x1f})))\n\t\trequire.False(t, isValidRequestID(string([]byte{0x7f})))\n\t})\n\n\tt.Run(\"rejects empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.False(t, isValidRequestID(\"\"))\n\t})\n}\n\nfunc Test_isValidRequestID_RejectsObsText(t *testing.T) {\n\tt.Parallel()\n\n\trequire.False(t, isValidRequestID(\"valid\\xff\"))\n}\n\n// go test -run Test_RequestID_Next\nfunc Test_RequestID_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderXRequestID))\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_RequestID_Locals\nfunc Test_RequestID_FromContext(t *testing.T) {\n\tt.Parallel()\n\treqID := \"ThisIsARequestId\"\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tGenerator: func() string {\n\t\t\treturn reqID\n\t\t},\n\t}))\n\n\tvar ctxVal string\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tctxVal = FromContext(c)\n\t\treturn c.Next()\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, reqID, ctxVal)\n}\n\nfunc Test_RequestID_FromContext_Empty(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\t// No middleware\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tctxVal := FromContext(c)\n\t\trequire.Empty(t, ctxVal)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n}\n\nfunc Test_RequestID_FromContext_Types(t *testing.T) {\n\tt.Parallel()\n\n\treqID := \"request-id-123\"\n\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New(Config{\n\t\tGenerator: func() string {\n\t\t\treturn reqID\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\trequire.Equal(t, reqID, FromContext(c))\n\t\tcustomCtx, ok := c.(fiber.CustomCtx)\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, reqID, FromContext(customCtx))\n\t\trequire.Equal(t, reqID, FromContext(c.RequestCtx()))\n\t\trequire.Equal(t, reqID, FromContext(c.Context()))\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n"
  },
  {
    "path": "middleware/responsetime/config.go",
    "content": "package responsetime\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// Header is the header key used to set the response time.\n\t//\n\t// Optional. Default: \"X-Response-Time\"\n\tHeader string\n}\n\n// ConfigDefault is the default config.\nvar ConfigDefault = Config{\n\tNext:   nil,\n\tHeader: fiber.HeaderXResponseTime,\n}\n\n// Helper function to set default values.\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.Header == \"\" {\n\t\tcfg.Header = ConfigDefault.Header\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/responsetime/responsetime.go",
    "content": "package responsetime\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler.\nfunc New(config ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tstart := time.Now()\n\n\t\terr := c.Next()\n\n\t\tc.Set(cfg.Header, time.Since(start).String())\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "middleware/responsetime/responsetime_test.go",
    "content": "package responsetime\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestResponseTimeMiddleware(t *testing.T) {\n\tt.Parallel()\n\n\tboom := errors.New(\"boom\")\n\n\ttests := []struct {\n\t\tname                  string\n\t\texpectedStatus        int\n\t\tuseCustomErrorHandler bool\n\t\treturnError           bool\n\t\texpectHeader          bool\n\t\tskipWithNext          bool\n\t}{\n\t\t{\n\t\t\tname:           \"sets duration header\",\n\t\t\texpectedStatus: fiber.StatusOK,\n\t\t\texpectHeader:   true,\n\t\t},\n\t\t{\n\t\t\tname:           \"skips when Next returns true\",\n\t\t\texpectedStatus: fiber.StatusOK,\n\t\t\texpectHeader:   false,\n\t\t\tskipWithNext:   true,\n\t\t},\n\t\t{\n\t\t\tname:                  \"propagates errors\",\n\t\t\texpectedStatus:        fiber.StatusTeapot,\n\t\t\tuseCustomErrorHandler: true,\n\t\t\treturnError:           true,\n\t\t\texpectHeader:          true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconfigs := []Config(nil)\n\t\t\tif tt.skipWithNext {\n\t\t\t\tconfigs = []Config{{\n\t\t\t\t\tNext: func(fiber.Ctx) bool {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t}\n\n\t\t\tappConfig := fiber.Config{}\n\t\t\tif tt.useCustomErrorHandler {\n\t\t\t\tappConfig.ErrorHandler = func(c fiber.Ctx, err error) error {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\trequire.ErrorIs(t, err, boom)\n\n\t\t\t\t\treturn c.Status(fiber.StatusTeapot).SendString(err.Error())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tapp := fiber.New(appConfig)\n\t\t\tapp.Use(New(configs...))\n\n\t\t\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\t\t\tif tt.returnError {\n\t\t\t\t\treturn boom\n\t\t\t\t}\n\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedStatus, resp.StatusCode)\n\n\t\t\theader := resp.Header.Get(fiber.HeaderXResponseTime)\n\t\t\tif tt.expectHeader {\n\t\t\t\trequire.NotEmpty(t, header)\n\n\t\t\t\t_, parseErr := time.ParseDuration(header)\n\t\t\t\trequire.NoError(t, parseErr)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Empty(t, header)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/rewrite/config.go",
    "content": "package rewrite\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Next defines a function to skip middleware.\n\t// Optional. Default: nil\n\tNext func(fiber.Ctx) bool\n\n\t// Rules defines the URL path rewrite rules. The values captured in asterisk can be\n\t// retrieved by index e.g. $1, $2 and so on.\n\t// Required. Example:\n\t// \"/old\":              \"/new\",\n\t// \"/api/*\":            \"/$1\",\n\t// \"/js/*\":             \"/public/javascript/$1\",\n\t// \"/users/*/orders/*\": \"/user/$1/order/$2\",\n\tRules map[string]string\n\n\trulesRegex map[*regexp.Regexp]string\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn Config{}\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/rewrite/rewrite.go",
    "content": "package rewrite\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New creates a new middleware handler\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\t// Initialize\n\tcfg.rulesRegex = map[*regexp.Regexp]string{}\n\tfor k, v := range cfg.Rules {\n\t\tk = strings.ReplaceAll(k, \"*\", \"(.*)\")\n\t\tk += \"$\"\n\t\tcfg.rulesRegex[regexp.MustCompile(k)] = v\n\t}\n\t// Middleware function\n\treturn func(c fiber.Ctx) error {\n\t\t// Next request to skip middleware\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\t\t// Rewrite\n\t\tfor k, v := range cfg.rulesRegex {\n\t\t\treplacer := captureTokens(k, c.Path())\n\t\t\tif replacer != nil {\n\t\t\t\tc.Path(replacer.Replace(v))\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn c.Next()\n\t}\n}\n\n// https://github.com/labstack/echo/blob/master/middleware/rewrite.go\nfunc captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {\n\tgroups := pattern.FindAllStringSubmatch(input, -1)\n\tif groups == nil {\n\t\treturn nil\n\t}\n\tvalues := groups[0][1:]\n\treplace := make([]string, 2*len(values))\n\tfor i, v := range values {\n\t\tj := 2 * i\n\t\treplace[j] = \"$\" + strconv.Itoa(i+1)\n\t\treplace[j+1] = v\n\t}\n\treturn strings.NewReplacer(replace...)\n}\n"
  },
  {
    "path": "middleware/rewrite/rewrite_test.go",
    "content": "package rewrite\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_New(t *testing.T) {\n\t// Test with no config\n\tm := New()\n\n\tif m == nil {\n\t\tt.Error(\"Expected middleware to be returned, got nil\")\n\t}\n\n\t// Test with config\n\tm = New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/old\": \"/new\",\n\t\t},\n\t})\n\n\tif m == nil {\n\t\tt.Error(\"Expected middleware to be returned, got nil\")\n\t}\n\n\t// Test with full config\n\tm = New(Config{\n\t\tNext: func(fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tRules: map[string]string{\n\t\t\t\"/old\": \"/new\",\n\t\t},\n\t})\n\n\tif m == nil {\n\t\tt.Error(\"Expected middleware to be returned, got nil\")\n\t}\n}\n\nfunc Test_Rewrite(t *testing.T) {\n\t// Case 1: Next function always returns true\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tRules: map[string]string{\n\t\t\t\"/old\": \"/new\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/old\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Rewrite Successful\")\n\t})\n\n\treq, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/old\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tbodyString := string(body)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"Rewrite Successful\", bodyString)\n\n\t// Case 2: Next function always returns false\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tNext: func(fiber.Ctx) bool {\n\t\t\treturn false\n\t\t},\n\t\tRules: map[string]string{\n\t\t\t\"/old\": \"/new\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/new\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Rewrite Successful\")\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/old\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tbodyString = string(body)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"Rewrite Successful\", bodyString)\n\n\t// Case 3: check for captured tokens in rewrite rule\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/user/:userID/order/:orderID\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fmt.Sprintf(\"User ID: %s, Order ID: %s\", c.Params(\"userID\"), c.Params(\"orderID\")))\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/users/123/orders/456\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tbodyString = string(body)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"User ID: 123, Order ID: 456\", bodyString)\n\n\t// Case 4: Send non-matching request, handled by default route\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/user/:userID/order/:orderID\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fmt.Sprintf(\"User ID: %s, Order ID: %s\", c.Params(\"userID\"), c.Params(\"orderID\")))\n\t})\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/not-matching-any-rule\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tbodyString = string(body)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\trequire.Equal(t, \"OK\", bodyString)\n\n\t// Case 4: Send non-matching request, with no default route\n\tapp = fiber.New()\n\tapp.Use(New(Config{\n\t\tRules: map[string]string{\n\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t},\n\t}))\n\n\tapp.Get(\"/user/:userID/order/:orderID\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(fmt.Sprintf(\"User ID: %s, Order ID: %s\", c.Params(\"userID\"), c.Params(\"orderID\")))\n\t})\n\n\treq, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, \"/not-matching-any-rule\", http.NoBody)\n\trequire.NoError(t, err)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Benchmark_Rewrite(b *testing.B) {\n\t// Helper function to create a new Fiber app with rewrite middleware\n\tcreateApp := func(config Config) *fiber.App {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(config))\n\t\treturn app\n\t}\n\n\t// Benchmark: Rewrite with Next function always returns true\n\tb.Run(\"Next always true\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tNext: func(fiber.Ctx) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/old\": \"/new\",\n\t\t\t},\n\t\t})\n\n\t\treqCtx := &fasthttp.RequestCtx{}\n\t\treqCtx.Request.SetRequestURI(\"/old\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tapp.Handler()(reqCtx)\n\t\t}\n\t})\n\n\t// Benchmark: Rewrite with Next function always returns false\n\tb.Run(\"Next always false\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tNext: func(fiber.Ctx) bool {\n\t\t\t\treturn false\n\t\t\t},\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/old\": \"/new\",\n\t\t\t},\n\t\t})\n\n\t\treqCtx := &fasthttp.RequestCtx{}\n\t\treqCtx.Request.SetRequestURI(\"/old\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tapp.Handler()(reqCtx)\n\t\t}\n\t})\n\n\t// Benchmark: Rewrite with tokens\n\tb.Run(\"Rewrite with tokens\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\n\t\treqCtx := &fasthttp.RequestCtx{}\n\t\treqCtx.Request.SetRequestURI(\"/users/123/orders/456\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tapp.Handler()(reqCtx)\n\t\t}\n\t})\n\n\t// Benchmark: Non-matching request, handled by default route\n\tb.Run(\"NonMatch with default\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\n\t\treqCtx := &fasthttp.RequestCtx{}\n\t\treqCtx.Request.SetRequestURI(\"/not-matching-any-rule\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tapp.Handler()(reqCtx)\n\t\t}\n\t})\n\n\t// Benchmark: Non-matching request, with no default route\n\tb.Run(\"NonMatch without default\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\n\t\treqCtx := &fasthttp.RequestCtx{}\n\t\treqCtx.Request.SetRequestURI(\"/not-matching-any-rule\")\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tapp.Handler()(reqCtx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Rewrite_Parallel(b *testing.B) {\n\t// Helper function to create a new Fiber app with rewrite middleware\n\tcreateApp := func(config Config) *fiber.App {\n\t\tapp := fiber.New()\n\t\tapp.Use(New(config))\n\t\treturn app\n\t}\n\n\t// Parallel Benchmark: Rewrite with Next function always returns true\n\tb.Run(\"Next always true\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tNext: func(fiber.Ctx) bool {\n\t\t\t\treturn true\n\t\t\t},\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/old\": \"/new\",\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\treqCtx := &fasthttp.RequestCtx{}\n\t\t\treqCtx.Request.SetRequestURI(\"/old\")\n\t\t\tfor pb.Next() {\n\t\t\t\tapp.Handler()(reqCtx)\n\t\t\t}\n\t\t})\n\t})\n\n\t// Parallel Benchmark: Rewrite with Next function always returns false\n\tb.Run(\"Next always false\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tNext: func(fiber.Ctx) bool {\n\t\t\t\treturn false\n\t\t\t},\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/old\": \"/new\",\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\treqCtx := &fasthttp.RequestCtx{}\n\t\t\treqCtx.Request.SetRequestURI(\"/old\")\n\t\t\tfor pb.Next() {\n\t\t\t\tapp.Handler()(reqCtx)\n\t\t\t}\n\t\t})\n\t})\n\n\t// Parallel Benchmark: Rewrite with tokens\n\tb.Run(\"Rewrite with tokens\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\treqCtx := &fasthttp.RequestCtx{}\n\t\t\treqCtx.Request.SetRequestURI(\"/users/123/orders/456\")\n\t\t\tfor pb.Next() {\n\t\t\t\tapp.Handler()(reqCtx)\n\t\t\t}\n\t\t})\n\t})\n\n\t// Parallel Benchmark: Non-matching request, handled by default route\n\tb.Run(\"NonMatch with default\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\t\tapp.Use(func(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\treqCtx := &fasthttp.RequestCtx{}\n\t\t\treqCtx.Request.SetRequestURI(\"/not-matching-any-rule\")\n\t\t\tfor pb.Next() {\n\t\t\t\tapp.Handler()(reqCtx)\n\t\t\t}\n\t\t})\n\t})\n\n\t// Parallel Benchmark: Non-matching request, with no default route\n\tb.Run(\"NonMatch without default\", func(b *testing.B) {\n\t\tapp := createApp(Config{\n\t\t\tRules: map[string]string{\n\t\t\t\t\"/users/*/orders/*\": \"/user/$1/order/$2\",\n\t\t\t},\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\treqCtx := &fasthttp.RequestCtx{}\n\t\t\treqCtx.Request.SetRequestURI(\"/not-matching-any-rule\")\n\t\t\tfor pb.Next() {\n\t\t\t\tapp.Handler()(reqCtx)\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/session/config.go",
    "content": "package session\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Config defines the configuration for the session middleware.\ntype Config struct {\n\t// Storage interface for storing session data.\n\t//\n\t// Optional. Default: memory.New()\n\tStorage fiber.Storage\n\n\t// Store defines the session store.\n\t//\n\t// Required.\n\tStore *Store\n\n\t// Next defines a function to skip this middleware when it returns true.\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// ErrorHandler defines a function to handle errors.\n\t//\n\t// Optional. Default: nil\n\tErrorHandler func(fiber.Ctx, error)\n\n\t// KeyGenerator generates the session key.\n\t//\n\t// Optional. Default: utils.SecureToken\n\tKeyGenerator func() string\n\n\t// CookieDomain defines the domain of the session cookie.\n\t//\n\t// Optional. Default: \"\"\n\tCookieDomain string\n\n\t// CookiePath defines the path of the session cookie.\n\t//\n\t// Optional. Default: \"\"\n\tCookiePath string\n\n\t// CookieSameSite specifies the SameSite attribute of the cookie.\n\t//\n\t// Optional. Default: \"Lax\"\n\tCookieSameSite string\n\n\t// Extractor is used to extract the session ID from the request.\n\t// See: https://docs.gofiber.io/guide/extractors\n\t//\n\t// Optional. Default: extractors.FromCookie(\"session_id\")\n\tExtractor extractors.Extractor\n\n\t// IdleTimeout defines the maximum duration of inactivity before the session expires.\n\t//\n\t// Note: The idle timeout is updated on each `Save()` call. If a middleware handler is used, `Save()` is called automatically.\n\t//\n\t// Optional. Default: 30 * time.Minute\n\tIdleTimeout time.Duration\n\n\t// AbsoluteTimeout defines the maximum duration of the session before it expires.\n\t//\n\t// If set to 0, the session will not have an absolute timeout, and will expire after the idle timeout.\n\t//\n\t// Optional. Default: 0\n\tAbsoluteTimeout time.Duration\n\n\t// CookieSecure specifies if the session cookie should be secure.\n\t//\n\t// Optional. Default: false\n\tCookieSecure bool\n\n\t// CookieHTTPOnly specifies if the session cookie should be HTTP-only.\n\t//\n\t// Optional. Default: false\n\tCookieHTTPOnly bool\n\n\t// CookieSessionOnly determines if the cookie should expire when the browser session ends.\n\t//\n\t// If true, the cookie will be deleted when the browser is closed.\n\t// Note: This will not delete the session data from the store.\n\t//\n\t// Optional. Default: false\n\tCookieSessionOnly bool\n}\n\n// ConfigDefault provides the default configuration.\nvar ConfigDefault = Config{\n\tIdleTimeout:    30 * time.Minute,\n\tKeyGenerator:   utils.SecureToken,\n\tExtractor:      extractors.FromCookie(\"session_id\"),\n\tCookieSameSite: \"Lax\",\n}\n\n// DefaultErrorHandler logs the error and sends a 500 status code.\n//\n// Parameters:\n//   - c: The Fiber context.\n//   - err: The error to handle.\n//\n// Usage:\n//\n//\tDefaultErrorHandler(c, err)\nfunc DefaultErrorHandler(c fiber.Ctx, err error) {\n\tlog.Errorf(\"session error: %v\", err)\n\tif sendErr := c.Status(fiber.StatusInternalServerError).SendString(\"Internal Server Error\"); sendErr != nil {\n\t\tlog.Errorf(\"failed to send error response: %v\", sendErr)\n\t}\n}\n\n// configDefault sets default values for the Config struct.\n//\n// This function ensures that all necessary fields have sensible defaults\n// if they are not explicitly set by the user.\n//\n// Parameters:\n//   - config: Variadic parameter to override default config.\n//\n// Returns:\n//   - Config: The configuration with defaults applied.\n//\n// Usage:\n//\n//\tcfg := configDefault()\n//\tcfg := configDefault(customConfig)\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif cfg.IdleTimeout <= 0 {\n\t\tcfg.IdleTimeout = ConfigDefault.IdleTimeout\n\t}\n\n\t// Ensure AbsoluteTimeout is greater than or equal to IdleTimeout.\n\tif cfg.AbsoluteTimeout > 0 && cfg.AbsoluteTimeout < cfg.IdleTimeout {\n\t\tpanic(\"[session] AbsoluteTimeout must be greater than or equal to IdleTimeout\")\n\t}\n\n\t// Check if we have a zero-value Extractor\n\tif cfg.Extractor.Extract == nil {\n\t\tcfg.Extractor = ConfigDefault.Extractor\n\t}\n\n\tif cfg.KeyGenerator == nil {\n\t\tcfg.KeyGenerator = ConfigDefault.KeyGenerator\n\t}\n\n\tif cfg.CookieSameSite == \"\" {\n\t\tcfg.CookieSameSite = ConfigDefault.CookieSameSite\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/session/config_test.go",
    "content": "package session\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc TestConfigDefault(t *testing.T) {\n\t// Test default config\n\tcfg := configDefault()\n\trequire.Equal(t, 30*time.Minute, cfg.IdleTimeout)\n\trequire.NotNil(t, cfg.KeyGenerator)\n\trequire.NotNil(t, cfg.Extractor)\n\trequire.Equal(t, \"session_id\", cfg.Extractor.Key)\n\trequire.Equal(t, \"Lax\", cfg.CookieSameSite)\n}\n\nfunc TestConfigDefaultWithCustomConfig(t *testing.T) {\n\t// Test custom config\n\tcustomConfig := Config{\n\t\tIdleTimeout:  48 * time.Hour,\n\t\tExtractor:    extractors.FromHeader(\"X-Custom-Session\"),\n\t\tKeyGenerator: func() string { return \"custom_key\" },\n\t}\n\tcfg := configDefault(customConfig)\n\trequire.Equal(t, 48*time.Hour, cfg.IdleTimeout)\n\trequire.NotNil(t, cfg.KeyGenerator)\n\trequire.NotNil(t, cfg.Extractor)\n\trequire.Equal(t, \"X-Custom-Session\", cfg.Extractor.Key)\n}\n\nfunc TestDefaultErrorHandler(t *testing.T) {\n\t// Create a new Fiber app\n\tapp := fiber.New()\n\n\t// Create a new context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// Test DefaultErrorHandler\n\tDefaultErrorHandler(ctx, fiber.ErrInternalServerError)\n\trequire.Equal(t, fiber.StatusInternalServerError, ctx.Response().StatusCode())\n}\n\nfunc TestAbsoluteTimeoutValidation(t *testing.T) {\n\trequire.PanicsWithValue(t, \"[session] AbsoluteTimeout must be greater than or equal to IdleTimeout\", func() {\n\t\tconfigDefault(Config{\n\t\t\tIdleTimeout:     30 * time.Minute,\n\t\t\tAbsoluteTimeout: 15 * time.Minute, // Less than IdleTimeout\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/session/data.go",
    "content": "package session\n\nimport (\n\t\"sync\"\n)\n\n// msgp -file=\"data.go\" -o=\"data_msgp.go\" -tests=true -unexported\n// Session state should remain small to fit common storage payload limits.\n//\n//go:generate msgp -o=data_msgp.go -tests=true -unexported\n//msgp:ignore data\ntype data struct {\n\tData         map[any]any // Session key counts are expected to be bounded.\n\tsync.RWMutex `msg:\"-\"`\n}\n\nvar dataPool = sync.Pool{\n\tNew: func() any {\n\t\td := new(data)\n\t\td.Data = make(map[any]any)\n\t\treturn d\n\t},\n}\n\n// acquireData returns a new data object from the pool.\n//\n// Returns:\n//   - *data: The data object.\n//\n// Usage:\n//\n//\td := acquireData()\nfunc acquireData() *data {\n\tobj := dataPool.Get()\n\tif d, ok := obj.(*data); ok {\n\t\treturn d\n\t}\n\t// Handle unexpected type in the pool\n\tpanic(\"unexpected type in data pool\")\n}\n\n// Reset clears the data map and resets the data object.\n//\n// Usage:\n//\n//\td.Reset()\nfunc (d *data) Reset() {\n\td.Lock()\n\tdefer d.Unlock()\n\tclear(d.Data)\n}\n\n// Get retrieves a value from the data map by key.\n//\n// Parameters:\n//   - key: The key to retrieve.\n//\n// Returns:\n//   - any: The value associated with the key.\n//\n// Usage:\n//\n//\tvalue := d.Get(\"key\")\nfunc (d *data) Get(key any) any {\n\td.RLock()\n\tdefer d.RUnlock()\n\treturn d.Data[key]\n}\n\n// Set updates or creates a new key-value pair in the data map.\n//\n// Parameters:\n//   - key: The key to set.\n//   - value: The value to set.\n//\n// Usage:\n//\n//\td.Set(\"key\", \"value\")\nfunc (d *data) Set(key, value any) {\n\td.Lock()\n\tdefer d.Unlock()\n\td.Data[key] = value\n}\n\n// Delete removes a key-value pair from the data map.\n//\n// Parameters:\n//   - key: The key to delete.\n//\n// Usage:\n//\n//\td.Delete(\"key\")\nfunc (d *data) Delete(key any) {\n\td.Lock()\n\tdefer d.Unlock()\n\tdelete(d.Data, key)\n}\n\n// Keys retrieves all keys in the data map.\n//\n// Returns:\n//   - []any: A slice of all keys in the data map.\n//\n// Usage:\n//\n//\tkeys := d.Keys()\nfunc (d *data) Keys() []any {\n\td.RLock()\n\tdefer d.RUnlock()\n\tkeys := make([]any, 0, len(d.Data))\n\tfor k := range d.Data {\n\t\tkeys = append(keys, k)\n\t}\n\treturn keys\n}\n\n// Len returns the number of key-value pairs in the data map.\n//\n// Returns:\n//   - int: The number of key-value pairs.\n//\n// Usage:\n//\n//\tlength := d.Len()\nfunc (d *data) Len() int {\n\td.RLock()\n\tdefer d.RUnlock()\n\treturn len(d.Data)\n}\n"
  },
  {
    "path": "middleware/session/data_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage session\n"
  },
  {
    "path": "middleware/session/data_msgp_test.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage session\n"
  },
  {
    "path": "middleware/session/data_test.go",
    "content": "package session\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestKeys(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case: Empty data\n\tt.Run(\"Empty data\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\tkeys := d.Keys()\n\t\trequire.Empty(t, keys, \"Expected no keys in empty data\")\n\t})\n\n\t// Test case: Single key\n\tt.Run(\"Single key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\tkeys := d.Keys()\n\t\trequire.Len(t, keys, 1, \"Expected one key\")\n\t\trequire.Contains(t, keys, \"key1\", \"Expected key1 to be present\")\n\t})\n\n\t// Test case: Multiple keys\n\tt.Run(\"Multiple keys\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Set(\"key2\", \"value2\")\n\t\td.Set(\"key3\", \"value3\")\n\t\tkeys := d.Keys()\n\t\trequire.Len(t, keys, 3, \"Expected three keys\")\n\t\trequire.Contains(t, keys, \"key1\", \"Expected key1 to be present\")\n\t\trequire.Contains(t, keys, \"key2\", \"Expected key2 to be present\")\n\t\trequire.Contains(t, keys, \"key3\", \"Expected key3 to be present\")\n\t})\n\n\t// Test case: Concurrent access\n\tt.Run(\"Concurrent access\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Set(\"key2\", \"value2\")\n\t\td.Set(\"key3\", \"value3\")\n\n\t\tdone := make(chan bool)\n\t\tgo func() {\n\t\t\tkeys := d.Keys()\n\t\t\tassert.Len(t, keys, 3, \"Expected three keys\")\n\t\t\tdone <- true\n\t\t}()\n\t\tgo func() {\n\t\t\tkeys := d.Keys()\n\t\t\tassert.Len(t, keys, 3, \"Expected three keys\")\n\t\t\tdone <- true\n\t\t}()\n\t\t<-done\n\t\t<-done\n\t})\n}\n\nfunc TestData_Len(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case: Empty data\n\tt.Run(\"Empty data\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\tlength := d.Len()\n\t\trequire.Equal(t, 0, length, \"Expected length to be 0 for empty data\")\n\t})\n\n\t// Test case: Single key\n\tt.Run(\"Single key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\tlength := d.Len()\n\t\trequire.Equal(t, 1, length, \"Expected length to be 1 when one key is set\")\n\t})\n\n\t// Test case: Multiple keys\n\tt.Run(\"Multiple keys\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Set(\"key2\", \"value2\")\n\t\td.Set(\"key3\", \"value3\")\n\t\tlength := d.Len()\n\t\trequire.Equal(t, 3, length, \"Expected length to be 3 when three keys are set\")\n\t})\n\n\t// Test case: Concurrent access\n\tt.Run(\"Concurrent access\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Set(\"key2\", \"value2\")\n\t\td.Set(\"key3\", \"value3\")\n\n\t\tdone := make(chan bool, 2) // Buffered channel with size 2\n\t\tgo func() {\n\t\t\tlength := d.Len()\n\t\t\tassert.Equal(t, 3, length, \"Expected length to be 3 during concurrent access\")\n\t\t\tdone <- true\n\t\t}()\n\t\tgo func() {\n\t\t\tlength := d.Len()\n\t\t\tassert.Equal(t, 3, length, \"Expected length to be 3 during concurrent access\")\n\t\t\tdone <- true\n\t\t}()\n\t\t<-done\n\t\t<-done\n\t})\n}\n\nfunc TestData_Get(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case: Nonexistent key\n\tt.Run(\"Nonexistent key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\tvalue := d.Get(\"nonexistent-key\")\n\t\trequire.Nil(t, value, \"Expected nil for nonexistent key\")\n\t})\n\n\t// Test case: Existing key\n\tt.Run(\"Existing key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\tvalue := d.Get(\"key1\")\n\t\trequire.Equal(t, \"value1\", value, \"Expected value1 for key1\")\n\t})\n}\n\nfunc TestData_Reset(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case: Reset data\n\tt.Run(\"Reset data\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Set(\"key2\", \"value2\")\n\t\td.Reset()\n\t\trequireDataEmpty(t, d, \"Expected data map to be empty after reset\")\n\t})\n}\n\nfunc mapPointer(m map[any]any) uintptr {\n\treturn reflect.ValueOf(m).Pointer()\n}\n\nfunc lockedMapPointer(d *data) uintptr {\n\td.RLock()\n\tdefer d.RUnlock()\n\treturn mapPointer(d.Data)\n}\n\nfunc requireDataEmpty(t *testing.T, d *data, msg string) {\n\tt.Helper()\n\td.RLock()\n\tdefer d.RUnlock()\n\trequire.Empty(t, d.Data, msg)\n}\n\nfunc TestData_ResetPreservesAllocation(t *testing.T) {\n\tt.Parallel()\n\n\td := acquireData()\n\td.Reset() // Ensure clean state from pool\n\tt.Cleanup(func() {\n\t\td.Reset()\n\t\tdataPool.Put(d)\n\t})\n\n\toriginalPtr := lockedMapPointer(d)\n\n\td.Set(\"key1\", \"value1\")\n\td.Set(\"key2\", \"value2\")\n\trequire.Equal(t, originalPtr, lockedMapPointer(d), \"Expected map pointer to stay constant after writes\")\n\n\td.Reset()\n\trequireDataEmpty(t, d, \"Expected data map to be empty after reset\")\n\trequire.Equal(t, originalPtr, lockedMapPointer(d), \"Expected reset to preserve underlying map\")\n\n\td.Set(\"key3\", \"value3\")\n\trequire.Nil(t, d.Get(\"key1\"), \"Expected cleared key not to leak after reset\")\n\trequire.Equal(t, originalPtr, lockedMapPointer(d), \"Expected map pointer to remain stable after further writes\")\n}\n\nfunc TestData_PoolReuseDoesNotLeakEntries(t *testing.T) {\n\tt.Parallel()\n\n\tacquired := make([]*data, 0, 6)\n\tt.Cleanup(func() {\n\t\tfor _, item := range acquired {\n\t\t\titem.Reset()\n\t\t\tdataPool.Put(item)\n\t\t}\n\t})\n\n\tacquireWithCleanup := func() *data {\n\t\td := acquireData()\n\t\tacquired = append(acquired, d)\n\t\treturn d\n\t}\n\n\tfirst := acquireWithCleanup()\n\tfirst.Set(\"key1\", \"value1\")\n\tfirst.Set(\"key2\", \"value2\")\n\tfirst.Reset()\n\n\toriginalPtr := lockedMapPointer(first)\n\tdataPool.Put(first)\n\n\tvar reused *data\n\tfor i := 0; i < 5; i++ {\n\t\tcandidate := acquireWithCleanup()\n\t\tif lockedMapPointer(candidate) == originalPtr {\n\t\t\treused = candidate\n\t\t\tbreak\n\t\t}\n\t\trequireDataEmpty(t, candidate, \"Expected pooled data to be empty when new instance is returned\")\n\t\trequire.Nil(t, candidate.Get(\"key2\"), \"Expected no leakage of prior entries on alternate pooled instance\")\n\t}\n\n\tif reused == nil {\n\t\tt.Skip(\"sync.Pool returned a different instance; reuse cannot be asserted\")\n\t\treturn\n\t}\n\n\trequire.Equal(t, originalPtr, lockedMapPointer(reused), \"Expected pooled data to reuse cleared map\")\n\trequireDataEmpty(t, reused, \"Expected pooled data to be empty after reuse\")\n\trequire.Nil(t, reused.Get(\"key2\"), \"Expected no leakage of prior entries on reuse\")\n\n\treused.Set(\"key4\", \"value4\")\n\trequire.Equal(t, \"value4\", reused.Get(\"key4\"), \"Expected pooled map to accept new values\")\n}\n\nfunc TestData_Delete(t *testing.T) {\n\tt.Parallel()\n\n\t// Test case: Delete existing key\n\tt.Run(\"Delete existing key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Set(\"key1\", \"value1\")\n\t\td.Delete(\"key1\")\n\t\tvalue := d.Get(\"key1\")\n\t\trequire.Nil(t, value, \"Expected nil for deleted key\")\n\t})\n\n\t// Test case: Delete nonexistent key\n\tt.Run(\"Delete nonexistent key\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\td := acquireData()\n\t\td.Reset() // Ensure clean state from pool\n\t\tdefer dataPool.Put(d)\n\t\tdefer d.Reset()\n\t\td.Delete(\"nonexistent-key\")\n\t\t// No assertion needed, just ensure no panic or error\n\t})\n}\n"
  },
  {
    "path": "middleware/session/middleware.go",
    "content": "// Package session provides session management middleware for Fiber.\n// This middleware handles user sessions, including storing session data in the store.\npackage session\n\nimport (\n\t\"errors\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Middleware holds session data and configuration.\ntype Middleware struct {\n\tSession   *Session\n\tctx       fiber.Ctx\n\tconfig    Config\n\tmu        sync.RWMutex\n\tdestroyed bool\n}\n\n// Context key for session middleware lookup.\ntype middlewareKey int\n\nconst (\n\t// middlewareContextKey is the key used to store the *Middleware in the context locals.\n\tmiddlewareContextKey middlewareKey = iota\n)\n\nvar (\n\t// ErrTypeAssertionFailed occurs when a type assertion fails.\n\tErrTypeAssertionFailed = errors.New(\"failed to type-assert to *Middleware\")\n\n\t// Pool for reusing middleware instances.\n\tmiddlewarePool = &sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn &Middleware{}\n\t\t},\n\t}\n)\n\n// New initializes session middleware with optional configuration.\n//\n// Parameters:\n//   - config: Variadic parameter to override default config.\n//\n// Returns:\n//   - fiber.Handler: The Fiber handler for the session middleware.\n//\n// Usage:\n//\n//\tapp.Use(session.New())\n//\n// Usage:\n//\n//\tapp.Use(session.New())\nfunc New(config ...Config) fiber.Handler {\n\tif len(config) > 0 {\n\t\thandler, _ := NewWithStore(config[0])\n\t\treturn handler\n\t}\n\thandler, _ := NewWithStore()\n\treturn handler\n}\n\n// NewWithStore creates session middleware with an optional custom store.\n//\n// Parameters:\n//   - config: Variadic parameter to override default config.\n//\n// Returns:\n//   - fiber.Handler: The Fiber handler for the session middleware.\n//   - *Store: The session store.\n//\n// Usage:\n//\n//\thandler, store := session.NewWithStore()\nfunc NewWithStore(config ...Config) (fiber.Handler, *Store) {\n\tcfg := configDefault(config...)\n\n\tif cfg.Store == nil {\n\t\tcfg.Store = NewStore(cfg)\n\t}\n\n\thandler := func(c fiber.Ctx) error {\n\t\tif cfg.Next != nil && cfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Acquire session middleware\n\t\tm := acquireMiddleware()\n\t\tm.initialize(c, &cfg)\n\n\t\tstackErr := c.Next()\n\n\t\tm.mu.RLock()\n\t\tdestroyed := m.destroyed\n\t\tm.mu.RUnlock()\n\n\t\tif !destroyed {\n\t\t\tm.saveSession()\n\t\t}\n\n\t\treleaseMiddleware(m)\n\t\treturn stackErr\n\t}\n\n\treturn handler, cfg.Store\n}\n\n// initialize sets up middleware for the request.\nfunc (m *Middleware) initialize(c fiber.Ctx, cfg *Config) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tsession, err := cfg.Store.getSession(c)\n\tif err != nil {\n\t\tpanic(err) // handle or log this error appropriately in production\n\t}\n\n\tm.config = *cfg\n\tm.Session = session\n\tm.ctx = c\n\n\tfiber.StoreInContext(c, middlewareContextKey, m)\n}\n\n// saveSession handles session saving and error management after the response.\nfunc (m *Middleware) saveSession() {\n\tif err := m.Session.saveSession(); err != nil {\n\t\tif m.config.ErrorHandler != nil {\n\t\t\tm.config.ErrorHandler(m.ctx, err)\n\t\t} else {\n\t\t\tDefaultErrorHandler(m.ctx, err)\n\t\t}\n\t}\n\n\treleaseSession(m.Session)\n}\n\n// acquireMiddleware retrieves a middleware instance from the pool.\nfunc acquireMiddleware() *Middleware {\n\tm, ok := middlewarePool.Get().(*Middleware)\n\tif !ok {\n\t\tpanic(ErrTypeAssertionFailed.Error())\n\t}\n\treturn m\n}\n\n// releaseMiddleware resets and returns middleware to the pool.\n//\n// Parameters:\n//   - m: The middleware object to release.\n//\n// Usage:\n//\n//\treleaseMiddleware(m)\nfunc releaseMiddleware(m *Middleware) {\n\tm.mu.Lock()\n\tm.config = Config{}\n\tm.Session = nil\n\tm.ctx = nil\n\tm.destroyed = false\n\tm.mu.Unlock()\n\tmiddlewarePool.Put(m)\n}\n\n// FromContext returns the Middleware from the Fiber context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n//\n// Parameters:\n//   - c: The Fiber context.\n//\n// Returns:\n//   - *Middleware: The middleware object if found; otherwise, nil.\n//\n// Usage:\n//\n//\tm := session.FromContext(c)\nfunc FromContext(ctx any) *Middleware {\n\tif m, ok := fiber.ValueFromContext[*Middleware](ctx, middlewareContextKey); ok {\n\t\treturn m\n\t}\n\n\treturn nil\n}\n\n// Set sets a key-value pair in the session.\n//\n// Parameters:\n//   - key: The key to set.\n//   - value: The value to set.\n//\n// Usage:\n//\n//\tm.Set(\"key\", \"value\")\nfunc (m *Middleware) Set(key, value any) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.Session.Set(key, value)\n}\n\n// Get retrieves a value from the session by key.\n//\n// Parameters:\n//   - key: The key to retrieve.\n//\n// Returns:\n//   - any: The value associated with the key.\n//\n// Usage:\n//\n//\tvalue := m.Get(\"key\")\nfunc (m *Middleware) Get(key any) any {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\treturn m.Session.Get(key)\n}\n\n// Delete removes a key-value pair from the session.\n//\n// Parameters:\n//   - key: The key to delete.\n//\n// Usage:\n//\n//\tm.Delete(\"key\")\nfunc (m *Middleware) Delete(key any) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tm.Session.Delete(key)\n}\n\n// Keys returns all keys in the current session.\n//\n// Returns:\n//   - []any: A slice of all keys in the session.\n//\n// Usage:\n//\n//\tkeys := m.Keys()\nfunc (m *Middleware) Keys() []any {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\treturn m.Session.Keys()\n}\n\n// Destroy destroys the session.\n//\n// Returns:\n//   - error: An error if the destruction fails.\n//\n// Usage:\n//\n//\terr := m.Destroy()\nfunc (m *Middleware) Destroy() error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\terr := m.Session.Destroy()\n\tm.destroyed = true\n\treturn err\n}\n\n// Fresh checks if the session is fresh.\n//\n// Returns:\n//   - bool: True if the session is fresh; otherwise, false.\n//\n// Usage:\n//\n//\tisFresh := m.Fresh()\nfunc (m *Middleware) Fresh() bool {\n\treturn m.Session.Fresh()\n}\n\n// ID returns the session ID.\n//\n// Returns:\n//   - string: The session ID.\n//\n// Usage:\n//\n//\tid := m.ID()\nfunc (m *Middleware) ID() string {\n\treturn m.Session.ID()\n}\n\n// Reset resets the session.\n//\n// Returns:\n//   - error: An error if the reset fails.\n//\n// Usage:\n//\n//\terr := m.Reset()\nfunc (m *Middleware) Reset() error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\treturn m.Session.Reset()\n}\n\n// Regenerate generates a new session ID while preserving session data.\n//\n// This method is commonly used after authentication to prevent session fixation attacks.\n// Unlike Reset(), this method preserves all existing session data.\n//\n// Returns:\n//   - error: An error if the regeneration fails.\n//\n// Usage:\n//\n//\terr := m.Regenerate()\nfunc (m *Middleware) Regenerate() error {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\treturn m.Session.Regenerate()\n}\n\n// Store returns the session store.\n//\n// Returns:\n//   - *Store: The session store.\n//\n// Usage:\n//\n//\tstore := m.Store()\nfunc (m *Middleware) Store() *Store {\n\treturn m.config.Store\n}\n"
  },
  {
    "path": "middleware/session/middleware_test.go",
    "content": "package session\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_Session_Middleware(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/get\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tvalue, ok := sess.Get(\"key\").(string)\n\t\tif !ok {\n\t\t\treturn c.Status(fiber.StatusNotFound).SendString(\"key not found\")\n\t\t}\n\t\treturn c.SendString(\"value=\" + value)\n\t})\n\n\tapp.Post(\"/set\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// get a value from the body\n\t\tvalue := c.FormValue(\"value\")\n\t\tsess.Set(\"key\", value)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/delete\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tsess.Delete(\"key\")\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/reset\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\n\t\t// Set a value to ensure it is cleared after reset\n\t\tsess.Set(\"key\", \"value\")\n\n\t\tif err := sess.Reset(); err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// Ensure value is cleared\n\t\tvalue, ok := sess.Get(\"key\").(string)\n\t\tif ok || value != \"\" {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/regenerate\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// Set a value to ensure it is preserved after regeneration\n\t\tsess.Set(\"key\", \"value\")\n\n\t\t// Regenerate the session ID\n\t\tif err := sess.Regenerate(); err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// Ensure the session ID has changed but session data is preserved\n\t\tnewID := sess.ID()\n\t\tif newID == \"\" {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// Check if the session data is still accessible\n\t\tvalue, ok := sess.Get(\"key\").(string)\n\t\tif !ok || value != \"value\" {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/destroy\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tif err := sess.Destroy(); err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Post(\"/fresh\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// Reset the session to make it fresh\n\t\tif err := sess.Reset(); err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tif sess.Fresh() {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t})\n\n\tapp.Post(\"/keys\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\t// get a value from the body\n\t\tvalue := c.FormValue(\"keys\")\n\t\tfor rawKey := range strings.SplitSeq(value, \",\") {\n\t\t\tkey := utils.TrimSpace(rawKey)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Set each key in the session\n\t\t\tsess.Set(key, \"value_\"+key)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tapp.Get(\"/keys\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tkeys := sess.Keys()\n\t\tif len(keys) == 0 {\n\t\t\treturn c.SendStatus(fiber.StatusNotFound)\n\t\t}\n\t\t// Keys may be of any type, so convert to string for display\n\t\tstrKeys := []string{}\n\t\tfor _, key := range keys {\n\t\t\tstrKeys = append(strKeys, fmt.Sprintf(\"%v\", key))\n\t\t}\n\t\treturn c.SendString(\"keys=\" + strings.Join(strKeys, \",\"))\n\t})\n\n\t// Test GET, SET, DELETE, RESET, REGENERATE, DESTROY by sending requests to the respective routes\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/get\")\n\th := app.Handler()\n\th(ctx)\n\trequire.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode())\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, token, \"Expected Set-Cookie header to be present\")\n\ttokenParts := strings.SplitN(strings.SplitN(token, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, tokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\ttoken = tokenParts[1]\n\trequire.Equal(t, \"key not found\", string(ctx.Response.Body()))\n\n\t// Test POST /set\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/set\")\n\tctx.Request.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\") // Set the Content-Type\n\tctx.Request.SetBodyString(\"value=hello\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test GET /get to check if the value was set\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/get\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=hello\", string(ctx.Response.Body()))\n\n\t// Test POST /delete to delete the value\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/delete\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test GET /get to check if the value was deleted\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/get\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode())\n\trequire.Equal(t, \"key not found\", string(ctx.Response.Body()))\n\n\t// Test POST /reset to reset the session\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/reset\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// verify we have a new session token\n\tnewToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, newToken, \"Expected Set-Cookie header to be present\")\n\tnewTokenParts := strings.SplitN(strings.SplitN(newToken, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, newTokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\tnewToken = newTokenParts[1]\n\trequire.NotEqual(t, token, newToken)\n\ttoken = newToken\n\n\t// Test POST /regenerate to regenerate the session ID\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/regenerate\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// verify we have a new session token\n\tnewToken = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, newToken, \"Expected Set-Cookie header to be present\")\n\tnewTokenParts = strings.SplitN(strings.SplitN(newToken, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, newTokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\tnewToken = newTokenParts[1]\n\trequire.NotEqual(t, token, newToken)\n\ttoken = newToken\n\n\t// Test POST /destroy to destroy the session\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/destroy\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Verify the session cookie has expired\n\tsetCookieHeader := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.Contains(t, setCookieHeader, \"max-age=0\")\n\n\t// Sleep so that the session expires\n\ttime.Sleep(1 * time.Second)\n\n\t// Test GET /get to check if the session was destroyed\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/get\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusNotFound, ctx.Response.StatusCode())\n\t// check that we have a new session token\n\tnewToken = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, newToken, \"Expected Set-Cookie header to be present\")\n\tparts := strings.Split(newToken, \";\")\n\trequire.Greater(t, len(parts), 1)\n\tvalueParts := strings.Split(parts[0], \"=\")\n\trequire.Greater(t, len(valueParts), 1)\n\tnewToken = valueParts[1]\n\trequire.NotEqual(t, token, newToken)\n\ttoken = newToken\n\n\t// Test POST /fresh to check if the session is fresh\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/fresh\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// check that we have a new session token\n\tnewToken = string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, newToken, \"Expected Set-Cookie header to be present\")\n\tnewTokenParts = strings.SplitN(strings.SplitN(newToken, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, newTokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\tnewToken = newTokenParts[1]\n\trequire.NotEqual(t, token, newToken)\n\n\ttoken = newToken\n\n\t// Test POST /keys to set multiple keys\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.SetRequestURI(\"/keys\")\n\tctx.Request.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\") // Set the Content-Type\n\tctx.Request.SetBodyString(\"keys=key1,key2\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test GET /keys to check if the session has the keys\n\tctx.Request.Reset()\n\tctx.Response.Reset()\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.SetRequestURI(\"/keys\")\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\tbody := string(ctx.Response.Body())\n\trequire.True(t, strings.HasPrefix(body, \"keys=\"))\n\tparts = strings.Split(strings.TrimPrefix(body, \"keys=\"), \",\")\n\trequire.Len(t, parts, 2, \"Expected two keys in the session\")\n\tsort.Strings(parts)\n\trequire.Equal(t, []string{\"key1\", \"key2\"}, parts)\n}\n\nfunc Test_Session_NewWithStore(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tid := sess.ID()\n\t\treturn c.SendString(\"value=\" + id)\n\t})\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tid := sess.ID()\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"session_id\",\n\t\t\tValue: id,\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\t// Test GET request without cookie\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// Get session cookie\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, token, \"Expected Set-Cookie header to be present\")\n\ttokenParts := strings.SplitN(strings.SplitN(token, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, tokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\ttoken = tokenParts[1]\n\trequire.Equal(t, \"value=\"+token, string(ctx.Response.Body()))\n\n\t// Test GET request with cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=\"+token, string(ctx.Response.Body()))\n}\n\nfunc Test_Session_FromSession(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tsess := FromContext(app.AcquireCtx(&fasthttp.RequestCtx{}))\n\trequire.Nil(t, sess)\n\n\tapp.Use(New())\n}\n\nfunc Test_Session_FromContext_Types(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\trequire.NotNil(t, FromContext(c))\n\t\tcustomCtx, ok := c.(fiber.CustomCtx)\n\t\trequire.True(t, ok)\n\t\trequire.NotNil(t, FromContext(customCtx))\n\t\trequire.NotNil(t, FromContext(c.RequestCtx()))\n\t\trequire.NotNil(t, FromContext(c.Context()))\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_Session_WithConfig(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Get(\"key\") == \"value\"\n\t\t},\n\t\tIdleTimeout: 1 * time.Second,\n\t\tExtractor:   extractors.FromCookie(\"session_id_test\"),\n\t\tKeyGenerator: func() string {\n\t\t\treturn \"test\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tid := sess.ID()\n\t\treturn c.SendString(\"value=\" + id)\n\t})\n\n\tapp.Get(\"/isFresh\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess.Fresh() {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t})\n\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tid := sess.ID()\n\t\tc.Cookie(&fiber.Cookie{\n\t\t\tName:  \"session_id_test\",\n\t\t\tValue: id,\n\t\t})\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\t// Test GET request without cookie\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// Get session cookie\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, token, \"Expected Set-Cookie header to be present\")\n\ttokenParts := strings.SplitN(strings.SplitN(token, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, tokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\ttoken = tokenParts[1]\n\trequire.Equal(t, \"value=\"+token, string(ctx.Response.Body()))\n\n\t// Test GET request with cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"session_id_test\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\trequire.Equal(t, \"value=\"+token, string(ctx.Response.Body()))\n\n\t// Test POST request with cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(\"session_id_test\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test POST request without cookie\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test POST request with wrong key\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(\"session_id\", token)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Test POST request with wrong value\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodPost)\n\tctx.Request.Header.SetCookie(\"session_id_test\", \"wrong\")\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\n\t// Check idle timeout not expired\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"session_id_test\", token)\n\tctx.Request.SetRequestURI(\"/isFresh\")\n\th(ctx)\n\trequire.Equal(t, fiber.StatusInternalServerError, ctx.Response.StatusCode())\n\n\t// Test idle timeout\n\ttime.Sleep(1200 * time.Millisecond)\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\tctx.Request.Header.SetCookie(\"session_id_test\", token)\n\tctx.Request.SetRequestURI(\"/isFresh\")\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n}\n\nfunc Test_Session_Next(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tdoNext bool\n\t\tmuNext sync.RWMutex\n\t)\n\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\tmuNext.RLock()\n\t\t\tdefer muNext.RUnlock()\n\t\t\treturn doNext\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tif sess == nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\tid := sess.ID()\n\t\treturn c.SendString(\"value=\" + id)\n\t})\n\n\th := app.Handler()\n\n\t// Test with Next returning false\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n\t// Get session cookie\n\ttoken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie))\n\trequire.NotEmpty(t, token, \"Expected Set-Cookie header to be present\")\n\ttokenParts := strings.SplitN(strings.SplitN(token, \";\", 2)[0], \"=\", 2)\n\trequire.Len(t, tokenParts, 2, \"Expected Set-Cookie header to contain a token\")\n\ttoken = tokenParts[1]\n\trequire.Equal(t, \"value=\"+token, string(ctx.Response.Body()))\n\n\t// Test with Next returning true\n\tmuNext.Lock()\n\tdoNext = true\n\tmuNext.Unlock()\n\n\tctx = &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusInternalServerError, ctx.Response.StatusCode())\n}\n\nfunc Test_Session_Middleware_Store(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thandler, sessionStore := NewWithStore()\n\n\tapp.Use(handler)\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tsess := FromContext(c)\n\t\tst := sess.Store()\n\t\tif st != sessionStore {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\th := app.Handler()\n\n\t// Test GET request\n\tctx := &fasthttp.RequestCtx{}\n\tctx.Request.Header.SetMethod(fiber.MethodGet)\n\th(ctx)\n\trequire.Equal(t, fiber.StatusOK, ctx.Response.StatusCode())\n}\n"
  },
  {
    "path": "middleware/session/session.go",
    "content": "package session\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Session represents a user session.\ntype Session struct {\n\tctx         fiber.Ctx     // fiber context\n\tconfig      *Store        // store configuration\n\tdata        *data         // key value data\n\tid          string        // session id\n\tidleTimeout time.Duration // idleTimeout of this session\n\tmu          sync.RWMutex  // Mutex to protect non-data fields\n\tfresh       bool          // if new session\n}\n\ntype absExpirationKeyType int\n\nconst (\n\t// sessionIDContextKey is the key used to store the session ID in the context locals.\n\tabsExpirationKey absExpirationKeyType = iota\n)\n\n// Session pool for reusing byte buffers.\nvar byteBufferPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new(bytes.Buffer)\n\t},\n}\n\nvar sessionPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &Session{}\n\t},\n}\n\n// acquireSession returns a new Session from the pool.\n//\n// Returns:\n//   - *Session: The session object.\n//\n// Usage:\n//\n//\ts := acquireSession()\nfunc acquireSession() *Session {\n\ts := sessionPool.Get().(*Session) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n\tif s.data == nil {\n\t\ts.data = acquireData()\n\t}\n\ts.fresh = true\n\treturn s\n}\n\n// Release releases the session back to the pool.\n//\n// This function should be called after the session is no longer needed.\n// This function is used to reduce the number of allocations and\n// to improve the performance of the session store.\n//\n// The session should not be used after calling this function.\n//\n// Important: The Release function should only be used when accessing the session directly,\n// for example, when you have called func (s *Session) Get(ctx) to get the session.\n// It should not be used when using the session with a *Middleware handler in the request\n// call stack, as the middleware will still need to access the session.\n//\n// Usage:\n//\n//\tsess := session.Get(ctx)\n//\tdefer sess.Release()\nfunc (s *Session) Release() {\n\tif s == nil {\n\t\treturn\n\t}\n\treleaseSession(s)\n}\n\nfunc releaseSession(s *Session) {\n\ts.mu.Lock()\n\ts.id = \"\"\n\ts.idleTimeout = 0\n\ts.ctx = nil\n\ts.config = nil\n\tif s.data != nil {\n\t\ts.data.Reset()\n\t}\n\ts.mu.Unlock()\n\tsessionPool.Put(s)\n}\n\n// Fresh returns whether the session is new\n//\n// Returns:\n//   - bool: True if the session is fresh; otherwise, false.\n//\n// Usage:\n//\n//\tisFresh := s.Fresh()\nfunc (s *Session) Fresh() bool {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\treturn s.fresh\n}\n\n// ID returns the session ID\n//\n// Returns:\n//   - string: The session ID.\n//\n// Usage:\n//\n//\tid := s.ID()\nfunc (s *Session) ID() string {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\treturn s.id\n}\n\n// Get returns the value associated with the given key.\n//\n// Parameters:\n//   - key: The key to retrieve.\n//\n// Returns:\n//   - any: The value associated with the key.\n//\n// Usage:\n//\n//\tvalue := s.Get(\"key\")\nfunc (s *Session) Get(key any) any {\n\tif s.data == nil {\n\t\treturn nil\n\t}\n\treturn s.data.Get(key)\n}\n\n// Set updates or creates a new key-value pair in the session.\n//\n// Parameters:\n//   - key: The key to set.\n//   - val: The value to set.\n//\n// Usage:\n//\n//\ts.Set(\"key\", \"value\")\nfunc (s *Session) Set(key, val any) {\n\tif s.data == nil {\n\t\treturn\n\t}\n\ts.data.Set(key, val)\n}\n\n// Delete removes the key-value pair from the session.\n//\n// Parameters:\n//   - key: The key to delete.\n//\n// Usage:\n//\n//\ts.Delete(\"key\")\nfunc (s *Session) Delete(key any) {\n\tif s.data == nil {\n\t\treturn\n\t}\n\ts.data.Delete(key)\n}\n\n// Destroy deletes the session from storage and expires the session cookie.\n//\n// Returns:\n//   - error: An error if the destruction fails.\n//\n// Usage:\n//\n//\terr := s.Destroy()\nfunc (s *Session) Destroy() error {\n\tif s.data == nil {\n\t\treturn nil\n\t}\n\n\t// Reset local data\n\ts.data.Reset()\n\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\t// Use external Storage if exist\n\tvar ctx context.Context = s.ctx\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\tif err := s.config.Storage.DeleteWithContext(ctx, s.id); err != nil {\n\t\treturn err\n\t}\n\n\t// Expire session\n\ts.delSession()\n\treturn nil\n}\n\n// Regenerate generates a new session id and deletes the old one from storage.\n//\n// Returns:\n//   - error: An error if the regeneration fails.\n//\n// Usage:\n//\n//\terr := s.Regenerate()\nfunc (s *Session) Regenerate() error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\t// Delete old id from storage\n\tvar ctx context.Context = s.ctx\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\tif err := s.config.Storage.DeleteWithContext(ctx, s.id); err != nil {\n\t\treturn err\n\t}\n\n\t// Generate a new session, and set session.fresh to true\n\ts.refresh()\n\n\treturn nil\n}\n\n// Reset generates a new session id, deletes the old one from storage, and resets the associated data.\n//\n// Returns:\n//   - error: An error if the reset fails.\n//\n// Usage:\n//\n//\terr := s.Reset()\nfunc (s *Session) Reset() error {\n\t// Reset local data\n\tif s.data != nil {\n\t\ts.data.Reset()\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\t// Reset expiration\n\ts.idleTimeout = 0\n\n\t// Delete old id from storage\n\tvar ctx context.Context = s.ctx\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\tif err := s.config.Storage.DeleteWithContext(ctx, s.id); err != nil {\n\t\treturn err\n\t}\n\n\t// Expire session\n\ts.delSession()\n\n\t// Generate a new session, and set session.fresh to true\n\ts.refresh()\n\n\treturn nil\n}\n\n// refresh generates a new session, and sets session.fresh to be true.\nfunc (s *Session) refresh() {\n\ts.id = s.config.KeyGenerator()\n\ts.fresh = true\n}\n\n// Save saves the session data and updates the cookie\n//\n// Note: If the session is being used in the handler, calling Save will have\n// no effect and the session will automatically be saved when the handler returns.\n//\n// Returns:\n//   - error: An error if the save operation fails.\n//\n// Usage:\n//\n//\terr := s.Save()\nfunc (s *Session) Save() error {\n\tif s.ctx == nil {\n\t\treturn s.saveSession()\n\t}\n\n\t// If the session is being used in the handler, it should not be saved\n\tif m, ok := s.ctx.Locals(middlewareContextKey).(*Middleware); ok {\n\t\tif m.Session == s {\n\t\t\t// Session is in use, so we do nothing and return\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn s.saveSession()\n}\n\n// saveSession encodes session data to saves it to storage.\nfunc (s *Session) saveSession() error {\n\tif s.data == nil {\n\t\treturn nil\n\t}\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\t// Set idleTimeout if not already set\n\tif s.idleTimeout <= 0 {\n\t\ts.idleTimeout = s.config.IdleTimeout\n\t}\n\n\t// Update client cookie\n\ts.setSession()\n\n\t// Encode session data\n\ts.data.RLock()\n\tencodedBytes, err := s.encodeSessionData()\n\ts.data.RUnlock()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode data: %w\", err)\n\t}\n\n\t// Pass copied bytes with session id to provider\n\tvar ctx context.Context = s.ctx\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\treturn s.config.Storage.SetWithContext(ctx, s.id, encodedBytes, s.idleTimeout)\n}\n\n// Keys retrieves all keys in the current session.\n//\n// Returns:\n//   - []any: A slice of all keys in the session.\n//\n// Usage:\n//\n//\tkeys := s.Keys()\nfunc (s *Session) Keys() []any {\n\tif s.data == nil {\n\t\treturn []any{}\n\t}\n\treturn s.data.Keys()\n}\n\n// SetIdleTimeout used when saving the session on the next call to `Save()`.\n//\n// Parameters:\n//   - idleTimeout: The duration for the idle timeout.\n//\n// Usage:\n//\n//\ts.SetIdleTimeout(time.Hour)\nfunc (s *Session) SetIdleTimeout(idleTimeout time.Duration) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.idleTimeout = idleTimeout\n}\n\n// getExtractorInfo returns all cookie and header extractors from the chain\nfunc (s *Session) getExtractorInfo() []extractors.Extractor {\n\tif s.config == nil {\n\t\treturn []extractors.Extractor{{Source: extractors.SourceCookie, Key: \"session_id\"}} // Safe default\n\t}\n\n\textractor := s.config.Extractor\n\tvar relevantExtractors []extractors.Extractor\n\n\t// If it's a chained extractor, collect all cookie/header extractors\n\tif len(extractor.Chain) > 0 {\n\t\tfor _, chainExtractor := range extractor.Chain {\n\t\t\tif chainExtractor.Source == extractors.SourceCookie || chainExtractor.Source == extractors.SourceHeader {\n\t\t\t\trelevantExtractors = append(relevantExtractors, chainExtractor)\n\t\t\t}\n\t\t}\n\t} else if extractor.Source == extractors.SourceCookie || extractor.Source == extractors.SourceHeader {\n\t\t// Single extractor - only include if it's cookie or header\n\t\trelevantExtractors = append(relevantExtractors, extractor)\n\t}\n\n\t// If no cookie/header extractors found and the config has a store but no explicit cookie/header extractors,\n\t// we should not default to cookie. This allows for read-only configurations (e.g., query/param/form/custom).\n\t// Only add default cookie extractor if we have no extractors at all (nil config case is handled above)\n\n\treturn relevantExtractors\n}\n\nfunc (s *Session) setSession() {\n\tif s.ctx == nil {\n\t\treturn\n\t}\n\n\t// Get all relevant extractors\n\trelevantExtractors := s.getExtractorInfo()\n\n\t// Set session ID for each extractor type\n\tfor _, ext := range relevantExtractors {\n\t\tswitch ext.Source {\n\t\tcase extractors.SourceHeader:\n\t\t\ts.ctx.Response().Header.SetBytesV(ext.Key, utils.UnsafeBytes(s.id))\n\t\tcase extractors.SourceCookie:\n\t\t\tfcookie := fasthttp.AcquireCookie()\n\n\t\t\tfcookie.SetKey(ext.Key)\n\t\t\tfcookie.SetValue(s.id)\n\t\t\tfcookie.SetPath(s.config.CookiePath)\n\t\t\tfcookie.SetDomain(s.config.CookieDomain)\n\t\t\t// Cookies are also session cookies if they do not specify the Expires or Max-Age attribute.\n\t\t\t// refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie\n\t\t\tif !s.config.CookieSessionOnly {\n\t\t\t\tfcookie.SetMaxAge(int(s.idleTimeout.Seconds()))\n\t\t\t\tfcookie.SetExpire(time.Now().Add(s.idleTimeout))\n\t\t\t}\n\n\t\t\ts.setCookieAttributes(fcookie)\n\t\t\ts.ctx.Response().Header.SetCookie(fcookie)\n\t\t\tfasthttp.ReleaseCookie(fcookie)\n\t\tdefault:\n\t\t\t// For non-cookie/header sources, do nothing (read-only)\n\t\t}\n\t}\n}\n\nfunc (s *Session) delSession() {\n\tif s.ctx == nil {\n\t\treturn\n\t}\n\n\t// Get all relevant extractors\n\trelevantExtractors := s.getExtractorInfo()\n\n\t// Delete session ID for each extractor type\n\tfor _, ext := range relevantExtractors {\n\t\tswitch ext.Source {\n\t\tcase extractors.SourceHeader:\n\t\t\ts.ctx.Request().Header.Del(ext.Key)\n\t\t\ts.ctx.Response().Header.Del(ext.Key)\n\t\tcase extractors.SourceCookie:\n\t\t\ts.ctx.Request().Header.DelCookie(ext.Key)\n\t\t\ts.ctx.Response().Header.DelCookie(ext.Key)\n\n\t\t\tfcookie := fasthttp.AcquireCookie()\n\n\t\t\tfcookie.SetKey(ext.Key)\n\t\t\tfcookie.SetPath(s.config.CookiePath)\n\t\t\tfcookie.SetDomain(s.config.CookieDomain)\n\t\t\tfcookie.SetMaxAge(-1)\n\t\t\tfcookie.SetExpire(time.Now().Add(-1 * time.Minute))\n\n\t\t\ts.setCookieAttributes(fcookie)\n\t\t\ts.ctx.Response().Header.SetCookie(fcookie)\n\t\t\tfasthttp.ReleaseCookie(fcookie)\n\t\tdefault:\n\t\t\t// For non-cookie/header sources, do nothing (read-only)\n\t\t}\n\t}\n}\n\n// setCookieAttributes sets the cookie attributes based on the session config.\nfunc (s *Session) setCookieAttributes(fcookie *fasthttp.Cookie) {\n\t// Set SameSite attribute\n\tswitch {\n\tcase utils.EqualFold(s.config.CookieSameSite, fiber.CookieSameSiteStrictMode):\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)\n\tcase utils.EqualFold(s.config.CookieSameSite, fiber.CookieSameSiteNoneMode):\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)\n\tdefault:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)\n\t}\n\n\t// The Secure attribute is required for SameSite=None\n\tif fcookie.SameSite() == fasthttp.CookieSameSiteNoneMode {\n\t\tfcookie.SetSecure(true)\n\t} else {\n\t\tfcookie.SetSecure(s.config.CookieSecure)\n\t}\n\n\tfcookie.SetHTTPOnly(s.config.CookieHTTPOnly)\n}\n\n// decodeSessionData decodes session data from raw bytes\n//\n// Parameters:\n//   - rawData: The raw byte data to decode.\n//\n// Returns:\n//   - error: An error if the decoding fails.\n//\n// Usage:\n//\n//\terr := s.decodeSessionData(rawData)\nfunc (s *Session) decodeSessionData(rawData []byte) error {\n\tbyteBuffer := byteBufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n\tdefer byteBufferPool.Put(byteBuffer)\n\tdefer byteBuffer.Reset()\n\t_, _ = byteBuffer.Write(rawData)\n\tdecCache := gob.NewDecoder(byteBuffer)\n\tif err := decCache.Decode(&s.data.Data); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode session data: %w\", err)\n\t}\n\treturn nil\n}\n\n// encodeSessionData encodes session data to raw bytes\n//\n// Parameters:\n//   - rawData: The raw byte data to encode.\n//\n// Returns:\n//   - error: An error if the encoding fails.\n//\n// Usage:\n//\n//\terr := s.encodeSessionData(rawData)\nfunc (s *Session) encodeSessionData() ([]byte, error) {\n\tbyteBuffer := byteBufferPool.Get().(*bytes.Buffer) //nolint:forcetypeassert,errcheck // We store nothing else in the pool\n\tdefer byteBufferPool.Put(byteBuffer)\n\tdefer byteBuffer.Reset()\n\tencCache := gob.NewEncoder(byteBuffer)\n\tif err := encCache.Encode(&s.data.Data); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encode session data: %w\", err)\n\t}\n\t// Copy the bytes\n\t// Copy the data in buffer\n\tencodedBytes := make([]byte, byteBuffer.Len())\n\tcopy(encodedBytes, byteBuffer.Bytes())\n\n\treturn encodedBytes, nil\n}\n\n// absExpiration returns the session absolute expiration time or a zero time if not set.\n//\n// Returns:\n//   - time.Time: The session absolute expiration time. Zero time if not set.\n//\n// Usage:\n//\n//\texpiration := s.absExpiration()\nfunc (s *Session) absExpiration() time.Time {\n\tabsExpiration, ok := s.Get(absExpirationKey).(time.Time)\n\tif ok {\n\t\treturn absExpiration\n\t}\n\treturn time.Time{}\n}\n\n// isAbsExpired returns true if the session is expired.\n//\n// If the session has an absolute expiration time set, this function will return true if the\n// current time is after the absolute expiration time.\n//\n// Returns:\n//   - bool: True if the session is expired; otherwise, false.\nfunc (s *Session) isAbsExpired() bool {\n\tabsExpiration := s.absExpiration()\n\treturn !absExpiration.IsZero() && time.Now().After(absExpiration)\n}\n\n// setAbsExpiration sets the absolute session expiration time.\n//\n// Parameters:\n//   - expiration: The session expiration time.\n//\n// Usage:\n//\n//\ts.setAbsExpiration(time.Now().Add(time.Hour))\nfunc (s *Session) setAbsExpiration(absExpiration time.Time) {\n\ts.Set(absExpirationKey, absExpiration)\n}\n"
  },
  {
    "path": "middleware/session/session_test.go",
    "content": "package session\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// go test -run Test_Session\nfunc Test_Session(t *testing.T) {\n\tt.Parallel()\n\n\t// session store\n\tstore := NewStore()\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// Get a new session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\ttoken := sess.ID()\n\trequire.NoError(t, sess.Save())\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// set session using default cookie extractor\n\tctx.Request().Header.SetCookie(\"session_id\", token)\n\n\t// get session\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.False(t, sess.Fresh())\n\n\t// get keys\n\tkeys := sess.Keys()\n\trequire.Equal(t, []any{}, keys)\n\n\t// get value\n\tname := sess.Get(\"name\")\n\trequire.Nil(t, name)\n\n\t// set value\n\tsess.Set(\"name\", \"john\")\n\n\t// get value\n\tname = sess.Get(\"name\")\n\trequire.Equal(t, \"john\", name)\n\n\tkeys = sess.Keys()\n\trequire.Equal(t, []any{\"name\"}, keys)\n\n\t// delete key\n\tsess.Delete(\"name\")\n\n\t// get value\n\tname = sess.Get(\"name\")\n\trequire.Nil(t, name)\n\n\t// get keys\n\tkeys = sess.Keys()\n\trequire.Equal(t, []any{}, keys)\n\n\t// get id\n\tid := sess.ID()\n\trequire.Equal(t, token, id)\n\n\t// save the old session first\n\terr = sess.Save()\n\trequire.NoError(t, err)\n\n\t// release the session\n\tsess.Release()\n\t// release the context\n\tapp.ReleaseCtx(ctx)\n\n\t// requesting entirely new context to prevent falsy tests\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\t// this id should be randomly generated as session key was deleted\n\trequire.Len(t, sess.ID(), 43)\n\n\tsess.Release()\n\n\t// when we use the original session for the second time\n\t// the session be should be same if the session is not expired\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\t// request the server with the old session\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\tsess, err = store.Get(ctx)\n\tdefer sess.Release()\n\trequire.NoError(t, err)\n\trequire.False(t, sess.Fresh())\n\trequire.Equal(t, sess.id, id)\n}\n\n// go test -run Test_Session_Types\nfunc Test_Session_Types(t *testing.T) {\n\tt.Parallel()\n\n\t// session store\n\tstore := NewStore()\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// set cookie\n\tctx.Request().Header.SetCookie(\"session_id\", \"123\")\n\n\t// get session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\t// the session string is no longer be 123\n\tnewSessionIDString := sess.ID()\n\n\ttype User struct {\n\t\tName string\n\t}\n\tstore.RegisterType(User{})\n\tvuser := User{\n\t\tName: \"John\",\n\t}\n\t// set value\n\tvar (\n\t\tvbool                  = true\n\t\tvstring                = \"str\"\n\t\tvint                   = 13\n\t\tvint8       int8       = 13\n\t\tvint16      int16      = 13\n\t\tvint32      int32      = 13\n\t\tvint64      int64      = 13\n\t\tvuint       uint       = 13\n\t\tvuint8      uint8      = 13\n\t\tvuint16     uint16     = 13\n\t\tvuint32     uint32     = 13\n\t\tvuint64     uint64     = 13\n\t\tvuintptr    uintptr    = 13\n\t\tvbyte       byte       = 'k'\n\t\tvrune                  = 'k'\n\t\tvfloat32    float32    = 13\n\t\tvfloat64    float64    = 13\n\t\tvcomplex64  complex64  = 13\n\t\tvcomplex128 complex128 = 13\n\t)\n\tsess.Set(\"vuser\", vuser)\n\tsess.Set(\"vbool\", vbool)\n\tsess.Set(\"vstring\", vstring)\n\tsess.Set(\"vint\", vint)\n\tsess.Set(\"vint8\", vint8)\n\tsess.Set(\"vint16\", vint16)\n\tsess.Set(\"vint32\", vint32)\n\tsess.Set(\"vint64\", vint64)\n\tsess.Set(\"vuint\", vuint)\n\tsess.Set(\"vuint8\", vuint8)\n\tsess.Set(\"vuint16\", vuint16)\n\tsess.Set(\"vuint32\", vuint32)\n\tsess.Set(\"vuint32\", vuint32)\n\tsess.Set(\"vuint64\", vuint64)\n\tsess.Set(\"vuintptr\", vuintptr)\n\tsess.Set(\"vbyte\", vbyte)\n\tsess.Set(\"vrune\", vrune)\n\tsess.Set(\"vfloat32\", vfloat32)\n\tsess.Set(\"vfloat64\", vfloat64)\n\tsess.Set(\"vcomplex64\", vcomplex64)\n\tsess.Set(\"vcomplex128\", vcomplex128)\n\n\t// save session\n\terr = sess.Save()\n\trequire.NoError(t, err)\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tctx.Request().Header.SetCookie(\"session_id\", newSessionIDString)\n\n\t// get session\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.False(t, sess.Fresh())\n\n\t// get value\n\tvuserResult, ok := sess.Get(\"vuser\").(User)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuser, vuserResult)\n\n\tvboolResult, ok := sess.Get(\"vbool\").(bool)\n\trequire.True(t, ok)\n\trequire.Equal(t, vbool, vboolResult)\n\n\tvstringResult, ok := sess.Get(\"vstring\").(string)\n\trequire.True(t, ok)\n\trequire.Equal(t, vstring, vstringResult)\n\n\tvintResult, ok := sess.Get(\"vint\").(int)\n\trequire.True(t, ok)\n\trequire.Equal(t, vint, vintResult)\n\n\tvint8Result, ok := sess.Get(\"vint8\").(int8)\n\trequire.True(t, ok)\n\trequire.Equal(t, vint8, vint8Result)\n\n\tvint16Result, ok := sess.Get(\"vint16\").(int16)\n\trequire.True(t, ok)\n\trequire.Equal(t, vint16, vint16Result)\n\n\tvint32Result, ok := sess.Get(\"vint32\").(int32)\n\trequire.True(t, ok)\n\trequire.Equal(t, vint32, vint32Result)\n\n\tvint64Result, ok := sess.Get(\"vint64\").(int64)\n\trequire.True(t, ok)\n\trequire.Equal(t, vint64, vint64Result)\n\n\tvuintResult, ok := sess.Get(\"vuint\").(uint)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuint, vuintResult)\n\n\tvuint8Result, ok := sess.Get(\"vuint8\").(uint8)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuint8, vuint8Result)\n\n\tvuint16Result, ok := sess.Get(\"vuint16\").(uint16)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuint16, vuint16Result)\n\n\tvuint32Result, ok := sess.Get(\"vuint32\").(uint32)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuint32, vuint32Result)\n\n\tvuint64Result, ok := sess.Get(\"vuint64\").(uint64)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuint64, vuint64Result)\n\n\tvuintptrResult, ok := sess.Get(\"vuintptr\").(uintptr)\n\trequire.True(t, ok)\n\trequire.Equal(t, vuintptr, vuintptrResult)\n\n\tvbyteResult, ok := sess.Get(\"vbyte\").(byte)\n\trequire.True(t, ok)\n\trequire.Equal(t, vbyte, vbyteResult)\n\n\tvruneResult, ok := sess.Get(\"vrune\").(rune)\n\trequire.True(t, ok)\n\trequire.Equal(t, vrune, vruneResult)\n\n\tvfloat32Result, ok := sess.Get(\"vfloat32\").(float32)\n\trequire.True(t, ok)\n\trequire.InEpsilon(t, vfloat32, vfloat32Result, 0.001)\n\n\tvfloat64Result, ok := sess.Get(\"vfloat64\").(float64)\n\trequire.True(t, ok)\n\trequire.InEpsilon(t, vfloat64, vfloat64Result, 0.001)\n\n\tvcomplex64Result, ok := sess.Get(\"vcomplex64\").(complex64)\n\trequire.True(t, ok)\n\trequire.Equal(t, vcomplex64, vcomplex64Result)\n\n\tvcomplex128Result, ok := sess.Get(\"vcomplex128\").(complex128)\n\trequire.True(t, ok)\n\trequire.Equal(t, vcomplex128, vcomplex128Result)\n\n\tsess.Release()\n\n\tapp.ReleaseCtx(ctx)\n}\n\n// go test -run Test_Session_Store_Reset\nfunc Test_Session_Store_Reset(t *testing.T) {\n\tt.Parallel()\n\t// session store\n\tstore := NewStore()\n\t// fiber instance\n\tapp := fiber.New()\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// get session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\t// make sure its new\n\trequire.True(t, sess.Fresh())\n\t// set value & save\n\tsess.Set(\"hello\", \"world\")\n\tctx.Request().Header.SetCookie(\"session_id\", sess.ID())\n\trequire.NoError(t, sess.Save())\n\n\t// reset store\n\trequire.NoError(t, store.Reset(ctx))\n\tid := sess.ID()\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\n\t// make sure the session is recreated\n\tsess, err = store.Get(ctx)\n\tdefer sess.Release()\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\trequire.Nil(t, sess.Get(\"hello\"))\n}\n\nfunc Test_Session_KeyTypes(t *testing.T) {\n\t// Note: This test cannot run in parallel because it registers types\n\t// in the global gob registry via store.RegisterType(), which would\n\t// cause race conditions with other parallel tests.\n\n\t// session store\n\tstore := NewStore()\n\t// fiber instance\n\tapp := fiber.New()\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t// get session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\ttype Person struct {\n\t\tName string\n\t}\n\n\ttype unexportedKey int\n\n\t// register non-default types\n\tstore.RegisterType(Person{})\n\tstore.RegisterType(unexportedKey(0))\n\n\ttype unregisteredKeyType int\n\ttype unregisteredValueType int\n\n\t// verify unregistered keys types are not allowed\n\tvar (\n\t\tunregisteredKey   unregisteredKeyType\n\t\tunregisteredValue unregisteredValueType\n\t)\n\tsess.Set(unregisteredKey, \"test\")\n\terr = sess.Save()\n\trequire.Error(t, err)\n\tsess.Delete(unregisteredKey)\n\terr = sess.Save()\n\trequire.NoError(t, err)\n\tsess.Set(\"abc\", unregisteredValue)\n\terr = sess.Save()\n\trequire.Error(t, err)\n\tsess.Delete(\"abc\")\n\terr = sess.Save()\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, sess.Reset())\n\n\t// Release session before continuing\n\tsess.Release()\n\n\t// Get a new session after reset\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess.Fresh())\n\n\tvar (\n\t\tkbool                     = true\n\t\tkstring                   = \"str\"\n\t\tkint                      = 13\n\t\tkint8          int8       = 13\n\t\tkint16         int16      = 13\n\t\tkint32         int32      = 13\n\t\tkint64         int64      = 13\n\t\tkuint          uint       = 13\n\t\tkuint8         uint8      = 13\n\t\tkuint16        uint16     = 13\n\t\tkuint32        uint32     = 13\n\t\tkuint64        uint64     = 13\n\t\tkuintptr       uintptr    = 13\n\t\tkbyte          byte       = 'k'\n\t\tkrune                     = 'k'\n\t\tkfloat32       float32    = 13\n\t\tkfloat64       float64    = 13\n\t\tkcomplex64     complex64  = 13\n\t\tkcomplex128    complex128 = 13\n\t\tkuser                     = Person{Name: \"John\"}\n\t\tkunexportedKey            = unexportedKey(13)\n\t)\n\n\tvar (\n\t\tvbool                     = true\n\t\tvstring                   = \"str\"\n\t\tvint                      = 13\n\t\tvint8          int8       = 13\n\t\tvint16         int16      = 13\n\t\tvint32         int32      = 13\n\t\tvint64         int64      = 13\n\t\tvuint          uint       = 13\n\t\tvuint8         uint8      = 13\n\t\tvuint16        uint16     = 13\n\t\tvuint32        uint32     = 13\n\t\tvuint64        uint64     = 13\n\t\tvuintptr       uintptr    = 13\n\t\tvbyte          byte       = 'k'\n\t\tvrune                     = 'k'\n\t\tvfloat32       float32    = 13\n\t\tvfloat64       float64    = 13\n\t\tvcomplex64     complex64  = 13\n\t\tvcomplex128    complex128 = 13\n\t\tvuser                     = Person{Name: \"John\"}\n\t\tvunexportedKey            = unexportedKey(13)\n\t)\n\n\tkeys := []any{\n\t\tkbool,\n\t\tkstring,\n\t\tkint,\n\t\tkint8,\n\t\tkint16,\n\t\tkint32,\n\t\tkint64,\n\t\tkuint,\n\t\tkuint8,\n\t\tkuint16,\n\t\tkuint32,\n\t\tkuint64,\n\t\tkuintptr,\n\t\tkbyte,\n\t\tkrune,\n\t\tkfloat32,\n\t\tkfloat64,\n\t\tkcomplex64,\n\t\tkcomplex128,\n\t\tkuser,\n\t\tkunexportedKey,\n\t}\n\n\tvalues := []any{\n\t\tvbool,\n\t\tvstring,\n\t\tvint,\n\t\tvint8,\n\t\tvint16,\n\t\tvint32,\n\t\tvint64,\n\t\tvuint,\n\t\tvuint8,\n\t\tvuint16,\n\t\tvuint32,\n\t\tvuint64,\n\t\tvuintptr,\n\t\tvbyte,\n\t\tvrune,\n\t\tvfloat32,\n\t\tvfloat64,\n\t\tvcomplex64,\n\t\tvcomplex128,\n\t\tvuser,\n\t\tvunexportedKey,\n\t}\n\n\t// loop test all key value pairs\n\tfor i, key := range keys {\n\t\tsess.Set(key, values[i])\n\t}\n\n\tid := sess.ID()\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\t// save session\n\terr = sess.Save()\n\trequire.NoError(t, err)\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\n\t// get session\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\tdefer sess.Release()\n\trequire.False(t, sess.Fresh())\n\n\t// loop test all key value pairs\n\tfor i, key := range keys {\n\t\t// get value\n\t\tresult := sess.Get(key)\n\t\trequire.Equal(t, values[i], result)\n\t}\n}\n\n// go test -run Test_Session_Save\nfunc Test_Session_Save(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"save to cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\t\tsess.Release()\n\t})\n\n\tt.Run(\"save to header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.FromHeader(\"session_id\"),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, sess.ID(), string(ctx.Response().Header.Peek(\"session_id\")))\n\t\tsess.Release()\n\t})\n}\n\n// Test chained extractors to ensure both cookie and header are set when both are present\nfunc Test_Session_ChainedExtractors(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"cookie and header chain\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store with chained extractors\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.Chain(extractors.FromCookie(\"session_id\"), extractors.FromHeader(\"x-session-id\")),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// verify both cookie and header are set\n\t\tcookie := ctx.Response().Header.PeekCookie(\"session_id\")\n\t\trequire.NotNil(t, cookie)\n\t\trequire.Contains(t, string(cookie), sess.ID())\n\n\t\theader := string(ctx.Response().Header.Peek(\"x-session-id\"))\n\t\trequire.Equal(t, sess.ID(), header)\n\n\t\tsess.Release()\n\t})\n\n\tt.Run(\"header and cookie chain\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store with chained extractors (different order)\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.Chain(extractors.FromHeader(\"x-session-id\"), extractors.FromCookie(\"session_id\")),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// verify both header and cookie are set\n\t\theader := string(ctx.Response().Header.Peek(\"x-session-id\"))\n\t\trequire.Equal(t, sess.ID(), header)\n\n\t\tcookie := ctx.Response().Header.PeekCookie(\"session_id\")\n\t\trequire.NotNil(t, cookie)\n\t\trequire.Contains(t, string(cookie), sess.ID())\n\n\t\tsess.Release()\n\t})\n\n\tt.Run(\"only SourceOther extractors - no response setting\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store with only query/form extractors\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.Chain(extractors.FromQuery(\"session_id\"), extractors.FromForm(\"session_id\")),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// verify no cookie or header is set\n\t\tcookie := ctx.Response().Header.PeekCookie(\"session_id\")\n\t\trequire.Nil(t, cookie)\n\n\t\theader := string(ctx.Response().Header.Peek(\"session_id\"))\n\t\trequire.Empty(t, header)\n\n\t\tsess.Release()\n\t})\n\n\tt.Run(\"mixed chain with SourceOther\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store with mixed extractors including SourceOther\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.Chain(extractors.FromCookie(\"session_id\"), extractors.FromQuery(\"session_id\"), extractors.FromHeader(\"x-session-id\")),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// verify both cookie and header are set (query is ignored for response)\n\t\tcookie := ctx.Response().Header.PeekCookie(\"session_id\")\n\t\trequire.NotNil(t, cookie)\n\t\trequire.Contains(t, string(cookie), sess.ID())\n\n\t\theader := string(ctx.Response().Header.Peek(\"x-session-id\"))\n\t\trequire.Equal(t, sess.ID(), header)\n\n\t\tsess.Release()\n\t})\n}\n\nfunc Test_Session_Save_IdleTimeout(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"save to cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconst sessionDuration = 5 * time.Second\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\ttoken := sess.ID()\n\n\t\t// expire this session in 5 seconds\n\t\tsess.SetIdleTimeout(sessionDuration)\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\tsess.Release()\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// here you need to get the old session yet\n\t\tctx.Request().Header.SetCookie(\"session_id\", token)\n\t\tsess, err = store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, token, sess.ID(), \"session ID should match before expiration\")\n\t\tname := sess.Get(\"name\")\n\t\trequire.Equal(t, \"john\", name, \"session should contain the saved value before expiration\")\n\n\t\t// just to make sure the session has been expired\n\t\t// Add extra buffer time to ensure expiration is processed\n\t\ttime.Sleep(sessionDuration + (100 * time.Millisecond))\n\n\t\tsess.Release()\n\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// here you should get a new session\n\t\tctx.Request().Header.SetCookie(\"session_id\", token)\n\t\tsess, err = store.Get(ctx)\n\t\tdefer sess.Release()\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, sess.Get(\"name\"))\n\t\trequire.NotEqual(t, sess.ID(), token)\n\t})\n}\n\nfunc Test_Session_Save_AbsoluteTimeout(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"save to cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconst absoluteTimeout = 2 * time.Second // extra headroom to avoid flakiness under -race\n\t\t// session store\n\t\tstore := NewStore(Config{\n\t\t\tIdleTimeout:     absoluteTimeout,\n\t\t\tAbsoluteTimeout: absoluteTimeout,\n\t\t})\n\n\t\t// force change to IdleTimeout\n\t\tstore.IdleTimeout = 10 * time.Second\n\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\t// set value\n\t\tsess.Set(\"name\", \"john\")\n\n\t\ttoken := sess.ID()\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\tsess.Release()\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// here you need to get the old session yet\n\t\tctx.Request().Header.SetCookie(\"session_id\", token)\n\t\tsess, err = store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"john\", sess.Get(\"name\"))\n\n\t\t// just to make sure the session has been expired\n\t\ttime.Sleep(absoluteTimeout + (200 * time.Millisecond))\n\n\t\tsess.Release()\n\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// here you should get a new session\n\t\tctx.Request().Header.SetCookie(\"session_id\", token)\n\t\tsess, err = store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.Nil(t, sess.Get(\"name\"))\n\t\trequire.NotEqual(t, sess.ID(), token)\n\t\trequire.True(t, sess.Fresh())\n\t\trequire.IsType(t, time.Time{}, sess.Get(absExpirationKey))\n\n\t\ttoken = sess.ID()\n\n\t\tsess.Set(\"name\", \"john\")\n\n\t\t// save session\n\t\terr = sess.Save()\n\t\trequire.NoError(t, err)\n\n\t\tsess.Release()\n\t\tapp.ReleaseCtx(ctx)\n\n\t\t// just to make sure the session has been expired\n\t\ttime.Sleep(absoluteTimeout + (200 * time.Millisecond))\n\n\t\t// try to get expired session by id\n\t\tsess, err = store.GetByID(ctx, token)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, ErrSessionIDNotFoundInStore)\n\t\trequire.Nil(t, sess)\n\t})\n}\n\n// go test -run Test_Session_Destroy\nfunc Test_Session_Destroy(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"destroy from cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\tdefer sess.Release()\n\t\trequire.NoError(t, err)\n\n\t\tsess.Set(\"name\", \"fenny\")\n\t\trequire.NoError(t, sess.Destroy())\n\t\tname := sess.Get(\"name\")\n\t\trequire.Nil(t, name)\n\t})\n\n\tt.Run(\"destroy from header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.FromHeader(\"session_id\"),\n\t\t})\n\t\t// fiber instance\n\t\tapp := fiber.New()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tsess, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\t// set value & save\n\t\tsess.Set(\"name\", \"fenny\")\n\t\tid := sess.ID()\n\t\trequire.NoError(t, sess.Save())\n\n\t\tsess.Release()\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// get session\n\t\tctx.Request().Header.Set(\"session_id\", id)\n\t\tsess, err = store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\tdefer sess.Release()\n\n\t\terr = sess.Destroy()\n\t\trequire.NoError(t, err)\n\t\trequire.Empty(t, string(ctx.Response().Header.Peek(\"session_id\")))\n\t})\n}\n\n// go test -run Test_Session_Custom_Config\nfunc Test_Session_Custom_Config(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewStore(Config{IdleTimeout: time.Hour, KeyGenerator: func() string { return \"very random\" }})\n\trequire.Equal(t, time.Hour, store.IdleTimeout)\n\trequire.Equal(t, \"very random\", store.KeyGenerator())\n\n\tstore = NewStore(Config{IdleTimeout: 0})\n\trequire.Equal(t, ConfigDefault.IdleTimeout, store.IdleTimeout)\n}\n\n// go test -run Test_Session_Cookie\nfunc Test_Session_Cookie(t *testing.T) {\n\tt.Parallel()\n\t// session store\n\tstore := NewStore()\n\t// fiber instance\n\tapp := fiber.New()\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\t// get session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.NoError(t, sess.Save())\n\n\tsess.Release()\n\n\t// cookie should be set on Save ( even if empty data )\n\tcookie := ctx.Response().Header.PeekCookie(\"session_id\")\n\trequire.NotNil(t, cookie)\n\trequire.Regexp(t, `^session_id=[A-Za-z0-9\\-_]{43}; max-age=\\d+; path=/; SameSite=Lax$`, string(cookie))\n}\n\n// go test -run Test_Session_Cookie_SameSite\nfunc Test_Session_Cookie_SameSite(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpectedInHeader string\n\t\tname             string\n\t\tsameSite         string\n\t\tinitialSecure    bool\n\t}{\n\t\t{\n\t\t\tname:             \"Lax should not force secure\",\n\t\t\tsameSite:         \"Lax\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=Lax\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Lax with secure should stay secure\",\n\t\t\tsameSite:         \"Lax\",\n\t\t\tinitialSecure:    true,\n\t\t\texpectedInHeader: \"SameSite=Lax; secure\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Strict should not force secure\",\n\t\t\tsameSite:         \"Strict\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=Strict\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Strict with secure should stay secure\",\n\t\t\tsameSite:         \"Strict\",\n\t\t\tinitialSecure:    true,\n\t\t\texpectedInHeader: \"SameSite=Strict; secure\",\n\t\t},\n\t\t{\n\t\t\tname:             \"None should force secure\",\n\t\t\tsameSite:         \"None\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=None; secure\",\n\t\t},\n\t\t{\n\t\t\tname:             \"None with secure should stay secure\",\n\t\t\tsameSite:         \"None\",\n\t\t\tinitialSecure:    true,\n\t\t\texpectedInHeader: \"SameSite=None; secure\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Case-insensitive none should force secure\",\n\t\t\tsameSite:         \"none\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=None; secure\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Case-insensitive strict should not force secure\",\n\t\t\tsameSite:         \"strict\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=Strict\",\n\t\t},\n\t\t{\n\t\t\tname:             \"Default should be Lax\",\n\t\t\tsameSite:         \"invalid\",\n\t\t\tinitialSecure:    false,\n\t\t\texpectedInHeader: \"SameSite=Lax\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// session store\n\t\t\tstore := NewStore(Config{\n\t\t\t\tCookieSameSite: tc.sameSite,\n\t\t\t\tCookieSecure:   tc.initialSecure,\n\t\t\t})\n\n\t\t\t// fiber instance\n\t\t\tapp := fiber.New()\n\n\t\t\t// fiber context\n\t\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t\t// get session\n\t\t\tsess, err := store.Get(ctx)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer sess.Release()\n\n\t\t\t// save session to trigger cookie setting\n\t\t\terr = sess.Save()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// check cookie\n\t\t\tcookie := string(ctx.Response().Header.PeekCookie(\"session_id\"))\n\t\t\t// The order of attributes in the cookie string is not guaranteed.\n\t\t\t// Instead of checking for a single substring, we check for the presence of each part.\n\t\t\tparts := strings.SplitSeq(tc.expectedInHeader, \"; \")\n\t\t\tfor part := range parts {\n\t\t\t\trequire.Contains(t, cookie, part)\n\t\t\t}\n\n\t\t\t// Also check that secure is NOT present when it shouldn't be\n\t\t\tif !tc.initialSecure && tc.sameSite != \"None\" && tc.sameSite != \"none\" {\n\t\t\t\trequire.NotContains(t, cookie, \"secure\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// go test -run Test_Session_Cookie_In_Response\n// Regression: https://github.com/gofiber/fiber/pull/1191\nfunc Test_Session_Cookie_In_Middleware_Chain(t *testing.T) {\n\tstore := NewStore()\n\tapp := fiber.New()\n\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\t// get session\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\tsess.Set(\"id\", \"1\")\n\trequire.True(t, sess.Fresh())\n\tid := sess.ID()\n\trequire.NoError(t, sess.Save())\n\n\tsess.Release()\n\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\tdefer sess.Release()\n\tsess.Set(\"name\", \"john\")\n\trequire.False(t, sess.Fresh())  // Session should not be fresh - it reuses the same ID from context locals\n\trequire.Equal(t, id, sess.ID()) // session id should be the same\n\n\trequire.Equal(t, \"1\", sess.Get(\"id\"))\n\trequire.Equal(t, \"john\", sess.Get(\"name\"))\n}\n\n// go test -run Test_Session_Deletes_Single_Key\n// Regression: https://github.com/gofiber/fiber/issues/1365\nfunc Test_Session_Deletes_Single_Key(t *testing.T) {\n\tt.Parallel()\n\tstore := NewStore()\n\tapp := fiber.New()\n\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tsess, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\tid := sess.ID()\n\tsess.Set(\"id\", \"1\")\n\trequire.NoError(t, sess.Save())\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\n\tsess, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\tsess.Delete(\"id\")\n\trequire.NoError(t, sess.Save())\n\n\tsess.Release()\n\tapp.ReleaseCtx(ctx)\n\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx.Request().Header.SetCookie(\"session_id\", id)\n\n\tsess, err = store.Get(ctx)\n\tdefer sess.Release()\n\trequire.NoError(t, err)\n\trequire.False(t, sess.Fresh())\n\trequire.Nil(t, sess.Get(\"id\"))\n\n\tapp.ReleaseCtx(ctx)\n}\n\n// go test -run Test_Session_Reset\nfunc Test_Session_Reset(t *testing.T) {\n\tt.Parallel()\n\t// fiber instance\n\tapp := fiber.New()\n\n\t// session store\n\tstore := NewStore()\n\n\tt.Run(\"reset session data and id, and set fresh to be true\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t// a random session uuid\n\t\toriginalSessionUUIDString := \"\"\n\n\t\t// now the session is in the storage\n\t\tfreshSession, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\toriginalSessionUUIDString = freshSession.ID()\n\n\t\t// set a value\n\t\tfreshSession.Set(\"name\", \"fenny\")\n\t\tfreshSession.Set(\"email\", \"fenny@example.com\")\n\n\t\terr = freshSession.Save()\n\t\trequire.NoError(t, err)\n\n\t\tfreshSession.Release()\n\t\tapp.ReleaseCtx(ctx)\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// set cookie\n\t\tctx.Request().Header.SetCookie(\"session_id\", originalSessionUUIDString)\n\n\t\t// as the session is in the storage, session.fresh should be false\n\t\tacquiredSession, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\trequire.False(t, acquiredSession.Fresh())\n\n\t\terr = acquiredSession.Reset()\n\t\trequire.NoError(t, err)\n\n\t\trequire.NotEqual(t, originalSessionUUIDString, acquiredSession.ID())\n\n\t\t// acquiredSession.fresh should be true after resetting\n\t\trequire.True(t, acquiredSession.Fresh())\n\n\t\t// Check that the session data has been reset\n\t\tkeys := acquiredSession.Keys()\n\t\trequire.Equal(t, []any{}, keys)\n\n\t\t// Set a new value for 'name' and check that it's updated\n\t\tacquiredSession.Set(\"name\", \"john\")\n\t\trequire.Equal(t, \"john\", acquiredSession.Get(\"name\"))\n\t\trequire.Nil(t, acquiredSession.Get(\"email\"))\n\n\t\t// Save after resetting\n\t\terr = acquiredSession.Save()\n\t\trequire.NoError(t, err)\n\n\t\tacquiredSession.Release()\n\n\t\t// Check that the session id is not in the header or cookie anymore\n\t\trequire.Empty(t, string(ctx.Response().Header.Peek(\"session_id\")))\n\t\trequire.Empty(t, string(ctx.Request().Header.Peek(\"session_id\")))\n\n\t\tapp.ReleaseCtx(ctx)\n\t})\n}\n\n// go test -run Test_Session_Regenerate\n// Regression: https://github.com/gofiber/fiber/issues/1395\nfunc Test_Session_Regenerate(t *testing.T) {\n\tt.Parallel()\n\t// fiber instance\n\tapp := fiber.New()\n\tt.Run(\"set fresh to be true when regenerating a session\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// a random session uuid\n\t\toriginalSessionUUIDString := \"\"\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// now the session is in the storage\n\t\tfreshSession, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\toriginalSessionUUIDString = freshSession.ID()\n\n\t\terr = freshSession.Save()\n\t\trequire.NoError(t, err)\n\n\t\tfreshSession.Release()\n\n\t\t// release the context\n\t\tapp.ReleaseCtx(ctx)\n\n\t\t// acquire a new context\n\t\tctx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t// set cookie\n\t\tctx.Request().Header.SetCookie(\"session_id\", originalSessionUUIDString)\n\n\t\t// as the session is in the storage, session.fresh should be false\n\t\tacquiredSession, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\t\tdefer acquiredSession.Release()\n\t\trequire.False(t, acquiredSession.Fresh())\n\n\t\terr = acquiredSession.Regenerate()\n\t\trequire.NoError(t, err)\n\n\t\trequire.NotEqual(t, originalSessionUUIDString, acquiredSession.ID())\n\n\t\t// acquiredSession.fresh should be true after regenerating\n\t\trequire.True(t, acquiredSession.Fresh())\n\n\t\t// release the context\n\t\tapp.ReleaseCtx(ctx)\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Session -benchmem -count=4\nfunc Benchmark_Session(b *testing.B) {\n\tb.Run(\"default\", func(b *testing.B) {\n\t\tapp, store := fiber.New(), NewStore()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tsess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark\n\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t_ = sess.Save() //nolint:errcheck // We're inside a benchmark\n\n\t\t\tsess.Release()\n\t\t}\n\t})\n\n\tb.Run(\"storage\", func(b *testing.B) {\n\t\tapp := fiber.New()\n\t\tstore := NewStore(Config{\n\t\t\tStorage: memory.New(),\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tsess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark\n\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t_ = sess.Save() //nolint:errcheck // We're inside a benchmark\n\n\t\t\tsess.Release()\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Session_Parallel -benchmem -count=4\nfunc Benchmark_Session_Parallel(b *testing.B) {\n\tb.Run(\"default\", func(b *testing.B) {\n\t\tapp, store := fiber.New(), NewStore()\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\t\t\tsess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark\n\t\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t\t_ = sess.Save() //nolint:errcheck // We're inside a benchmark\n\n\t\t\t\tsess.Release()\n\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"storage\", func(b *testing.B) {\n\t\tapp := fiber.New()\n\t\tstore := NewStore(Config{\n\t\t\tStorage: memory.New(),\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\t\t\tsess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark\n\t\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t\t_ = sess.Save() //nolint:errcheck // We're inside a benchmark\n\n\t\t\t\tsess.Release()\n\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Session_Asserted -benchmem -count=4\nfunc Benchmark_Session_Asserted(b *testing.B) {\n\tb.Run(\"default\", func(b *testing.B) {\n\t\tapp, store := fiber.New(), NewStore()\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tsess, err := store.Get(c)\n\t\t\trequire.NoError(b, err)\n\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\terr = sess.Save()\n\t\t\trequire.NoError(b, err)\n\t\t\tsess.Release()\n\t\t}\n\t})\n\n\tb.Run(\"storage\", func(b *testing.B) {\n\t\tapp := fiber.New()\n\t\tstore := NewStore(Config{\n\t\t\tStorage: memory.New(),\n\t\t})\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(c)\n\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\tb.ReportAllocs()\n\t\tfor b.Loop() {\n\t\t\tsess, err := store.Get(c)\n\t\t\trequire.NoError(b, err)\n\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\terr = sess.Save()\n\t\t\trequire.NoError(b, err)\n\t\t\tsess.Release()\n\t\t}\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Session_Asserted_Parallel -benchmem -count=4\nfunc Benchmark_Session_Asserted_Parallel(b *testing.B) {\n\tb.Run(\"default\", func(b *testing.B) {\n\t\tapp, store := fiber.New(), NewStore()\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\t\t\tsess, err := store.Get(c)\n\t\t\t\trequire.NoError(b, err)\n\t\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t\trequire.NoError(b, sess.Save())\n\t\t\t\tsess.Release()\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n\n\tb.Run(\"storage\", func(b *testing.B) {\n\t\tapp := fiber.New()\n\t\tstore := NewStore(Config{\n\t\t\tStorage: memory.New(),\n\t\t})\n\t\tb.ReportAllocs()\n\t\tb.ResetTimer()\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tfor pb.Next() {\n\t\t\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\t\tc.Request().Header.SetCookie(\"session_id\", \"12356789\")\n\n\t\t\t\tsess, err := store.Get(c)\n\t\t\t\trequire.NoError(b, err)\n\t\t\t\tsess.Set(\"john\", \"doe\")\n\t\t\t\trequire.NoError(b, sess.Save())\n\t\t\t\tsess.Release()\n\t\t\t\tapp.ReleaseCtx(c)\n\t\t\t}\n\t\t})\n\t})\n}\n\n// go test -v -race -run Test_Session_Concurrency ./...\nfunc Test_Session_Concurrency(t *testing.T) {\n\tapp := fiber.New()\n\tstore := NewStore()\n\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, 10) // Buffered channel to collect errors\n\tconst numGoroutines = 10        // Number of concurrent goroutines to test\n\n\t// Start numGoroutines goroutines\n\tfor range numGoroutines {\n\t\twg.Go(func() {\n\t\t\tlocalCtx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\t\t\tsess, err := store.getSession(localCtx)\n\t\t\tif err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Set a value\n\t\t\tsess.Set(\"name\", \"john\")\n\n\t\t\t// get the session id\n\t\t\tid := sess.ID()\n\n\t\t\t// Check if the session is fresh\n\t\t\tif !sess.Fresh() {\n\t\t\t\terrChan <- errors.New(\"session should be fresh\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Save the session\n\t\t\tif saveErr := sess.Save(); saveErr != nil {\n\t\t\t\terrChan <- saveErr\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// release the session\n\t\t\tsess.Release()\n\n\t\t\t// Release the context\n\t\t\tapp.ReleaseCtx(localCtx)\n\n\t\t\t// Acquire a new context\n\t\t\tlocalCtx = app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\t\tdefer app.ReleaseCtx(localCtx)\n\n\t\t\t// Set the session id in the header\n\t\t\tlocalCtx.Request().Header.SetCookie(\"session_id\", id)\n\n\t\t\t// Get the session\n\t\t\tsess, err = store.Get(localCtx)\n\t\t\tif err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer sess.Release()\n\n\t\t\t// Get the value\n\t\t\tname := sess.Get(\"name\")\n\t\t\tif name != \"john\" {\n\t\t\t\terrChan <- errors.New(\"name should be john\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Get ID from the session\n\t\t\tif sess.ID() != id {\n\t\t\t\terrChan <- errors.New(\"id should be the same\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Check if the session is fresh\n\t\t\tif sess.Fresh() {\n\t\t\t\terrChan <- errors.New(\"session should not be fresh\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Delete the key\n\t\t\tsess.Delete(\"name\")\n\n\t\t\t// Get the value\n\t\t\tname = sess.Get(\"name\")\n\t\t\tif name != nil {\n\t\t\t\terrChan <- errors.New(\"name should be nil\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Destroy the session\n\t\t\tif err := sess.Destroy(); err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n\n\twg.Wait()      // Wait for all goroutines to finish\n\tclose(errChan) // Close the channel to signal no more errors will be sent\n\n\t// Check for errors sent to errChan\n\tfor err := range errChan {\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc Test_Session_StoreGetDecodeSessionDataError(t *testing.T) {\n\t// Initialize a new store with default config\n\tstore := NewStore()\n\n\t// Create a new Fiber app\n\tapp := fiber.New()\n\n\t// Generate a fake session ID\n\tsessionID := uuid.New().String()\n\n\t// Store invalid session data to simulate decode error\n\terr := store.Storage.Set(sessionID, []byte(\"invalid data\"), 0)\n\trequire.NoError(t, err, \"Failed to set invalid session data\")\n\n\t// Create a new request context\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\t// Set the session ID in cookies\n\tc.Request().Header.SetCookie(\"session_id\", sessionID)\n\n\t// Attempt to get the session\n\t_, err = store.Get(c)\n\trequire.Error(t, err, \"Expected error due to invalid session data, but got nil\")\n\n\t// Check that the error message is as expected\n\trequire.Contains(t, err.Error(), \"failed to decode session data\", \"Unexpected error message\")\n\n\t// Check that the error is as expected\n\trequire.ErrorContains(t, err, \"failed to decode session data\", \"Unexpected error\")\n\n\t// Attempt to get the session by ID\n\t_, err = store.GetByID(c, sessionID)\n\trequire.Error(t, err, \"Expected error due to invalid session data, but got nil\")\n\n\t// Check that the error message is as expected\n\trequire.ErrorContains(t, err, \"failed to decode session data\", \"Unexpected error\")\n}\n\n// go test -run Test_Session_Fresh_Flag_Bug\n// This test verifies the fix for the fresh flag bug where calling getSession()\n// multiple times in the same request would incorrectly mark the session as fresh\n// when the ID was found in context locals.\nfunc Test_Session_Fresh_Flag_Bug(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewStore()\n\tapp := fiber.New()\n\n\t// Test Case 1: First call with no session cookie - should be fresh\n\tctx1 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tsess1, err := store.Get(ctx1)\n\trequire.NoError(t, err)\n\trequire.True(t, sess1.Fresh(), \"First session should be fresh (no cookie provided)\")\n\tsessionID := sess1.ID()\n\trequire.NoError(t, sess1.Save())\n\tsess1.Release()\n\tapp.ReleaseCtx(ctx1)\n\n\t// Test Case 2: Second call with session cookie - should NOT be fresh\n\tctx2 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx2.Request().Header.SetCookie(\"session_id\", sessionID)\n\tsess2, err := store.Get(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess2.Fresh(), \"Existing session should not be fresh\")\n\trequire.Equal(t, sessionID, sess2.ID())\n\n\t// Test Case 3: Call getSession() again in the same request\n\t// This simulates what happens when CSRF middleware calls store operations\n\t// The session ID is now in context locals from the first getSession() call\n\tsess3, err := store.getSession(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess3.Fresh(), \"Session should still not be fresh on second getSession() call in same request\")\n\trequire.Equal(t, sessionID, sess3.ID())\n\n\tsess2.Release()\n\tsess3.Release()\n\tapp.ReleaseCtx(ctx2)\n\n\t// Test Case 4: Expired session - should generate new ID and be fresh\n\tctx3 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx3.Request().Header.SetCookie(\"session_id\", \"expired-or-nonexistent-id\")\n\tsess4, err := store.Get(ctx3)\n\trequire.NoError(t, err)\n\trequire.True(t, sess4.Fresh(), \"New session (after expired/missing data) should be fresh\")\n\trequire.NotEqual(t, \"expired-or-nonexistent-id\", sess4.ID(), \"Should have generated a new session ID\")\n\n\tsess4.Release()\n\tapp.ReleaseCtx(ctx3)\n}\n\n// go test -run Test_Session_CSRF_Scenario\n// This test simulates the user-reported issue with CSRF + session middleware\n// where a POST without CSRF token would result in a new session_id cookie\nfunc Test_Session_CSRF_Scenario(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewStore(Config{\n\t\tIdleTimeout: 2 * time.Second, // Longer timeout to ensure session persists\n\t})\n\tapp := fiber.New()\n\n\t// Simulate: First GET request creates session\n\tctx1 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tsess1, err := store.Get(ctx1)\n\trequire.NoError(t, err)\n\trequire.True(t, sess1.Fresh())\n\tfirstSessionID := sess1.ID()\n\n\t// Store some data (simulating CSRF token storage)\n\tsess1.Set(\"csrf_token\", \"token-123\")\n\trequire.NoError(t, sess1.Save())\n\tsess1.Release()\n\tapp.ReleaseCtx(ctx1)\n\n\t// Small delay to ensure save completes\n\ttime.Sleep(10 * time.Millisecond)\n\n\t// Simulate: POST request with valid session (before expiration)\n\tctx2 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx2.Request().Header.SetCookie(\"session_id\", firstSessionID)\n\tsess2, err := store.Get(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess2.Fresh(), \"Session should not be fresh - it exists\")\n\trequire.Equal(t, firstSessionID, sess2.ID(), \"Session ID should remain the same\")\n\trequire.Equal(t, \"token-123\", sess2.Get(\"csrf_token\"))\n\n\t// Simulate CSRF validation failure (session is accessed but request fails)\n\t// Session should still maintain the same ID\n\trequire.Equal(t, firstSessionID, sess2.ID())\n\tsess2.Release()\n\tapp.ReleaseCtx(ctx2)\n\n\t// Wait for session to expire\n\ttime.Sleep(2200 * time.Millisecond)\n\n\t// Simulate: POST request with expired session\n\t// This is the scenario the user reported - session data is gone\n\tctx3 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx3.Request().Header.SetCookie(\"session_id\", firstSessionID)\n\tsess3, err := store.Get(ctx3)\n\trequire.NoError(t, err)\n\trequire.True(t, sess3.Fresh(), \"Session should be fresh - old data expired\")\n\trequire.NotEqual(t, firstSessionID, sess3.ID(), \"Should have generated new session ID (expected behavior)\")\n\trequire.Nil(t, sess3.Get(\"csrf_token\"), \"Old session data should be gone\")\n\n\tsess3.Release()\n\tapp.ReleaseCtx(ctx3)\n}\n\n// go test -run Test_Session_Multiple_GetSession_Calls\n// This test ensures that calling getSession() multiple times within the same\n// request context doesn't incorrectly mark the session as fresh due to the\n// session ID being stored in context locals\nfunc Test_Session_Multiple_GetSession_Calls(t *testing.T) {\n\tt.Parallel()\n\n\tstore := NewStore()\n\tapp := fiber.New()\n\n\t// Create initial session\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tsess1, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\trequire.True(t, sess1.Fresh())\n\tsessionID := sess1.ID()\n\tsess1.Set(\"test_key\", \"test_value\")\n\trequire.NoError(t, sess1.Save())\n\tsess1.Release()\n\tapp.ReleaseCtx(ctx)\n\n\t// New request with existing session\n\tctx2 := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tctx2.Request().Header.SetCookie(\"session_id\", sessionID)\n\n\t// First getSession() call - loads from storage\n\tsess2, err := store.getSession(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess2.Fresh(), \"First call: existing session should not be fresh\")\n\trequire.Equal(t, sessionID, sess2.ID())\n\trequire.Equal(t, \"test_value\", sess2.Get(\"test_key\"))\n\n\t// Second getSession() call - ID now in context locals\n\t// This is where the bug would manifest before the fix\n\tsess3, err := store.getSession(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess3.Fresh(), \"Second call: session should STILL not be fresh (bug fix verification)\")\n\trequire.Equal(t, sessionID, sess3.ID())\n\trequire.Equal(t, \"test_value\", sess3.Get(\"test_key\"))\n\n\t// Third call to ensure consistency\n\tsess4, err := store.getSession(ctx2)\n\trequire.NoError(t, err)\n\trequire.False(t, sess4.Fresh(), \"Third call: session should remain not fresh\")\n\trequire.Equal(t, sessionID, sess4.ID())\n\n\tsess2.Release()\n\tsess3.Release()\n\tsess4.Release()\n\tapp.ReleaseCtx(ctx2)\n}\n"
  },
  {
    "path": "middleware/session/store.go",
    "content": "package session\n\nimport (\n\t\"context\"\n\t\"encoding/gob\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/internal/storage/memory\"\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\n// ErrEmptySessionID is an error that occurs when the session ID is empty.\nvar (\n\tErrEmptySessionID                   = errors.New(\"session ID cannot be empty\")\n\tErrSessionAlreadyLoadedByMiddleware = errors.New(\"session already loaded by middleware\")\n\tErrSessionIDNotFoundInStore         = errors.New(\"session ID not found in session store\")\n)\n\n// sessionIDKey is the local key type used to store and retrieve the session ID in context.\ntype sessionIDKey int\n\nconst (\n\t// sessionIDContextKey is the key used to store the session ID in the context locals.\n\tsessionIDContextKey sessionIDKey = iota\n)\n\n// Store manages session data using the configured storage backend.\ntype Store struct {\n\tConfig\n}\n\n// NewStore creates a new session store with the provided configuration.\n//\n// Parameters:\n//   - config: Variadic parameter to override default config.\n//\n// Returns:\n//   - *Store: The session store.\n//\n// Usage:\n//\n//\tstore := session.NewStore()\nfunc NewStore(config ...Config) *Store {\n\t// Set default config\n\tcfg := configDefault(config...)\n\n\tif cfg.Storage == nil {\n\t\tcfg.Storage = memory.New()\n\t}\n\n\tstore := &Store{\n\t\tConfig: cfg,\n\t}\n\n\tif cfg.AbsoluteTimeout > 0 {\n\t\tstore.RegisterType(absExpirationKey)\n\t\tstore.RegisterType(time.Time{})\n\t}\n\n\treturn store\n}\n\n// RegisterType registers a custom type for encoding/decoding into any storage provider.\n//\n// Parameters:\n//   - i: The custom type to register.\n//\n// Usage:\n//\n//\tstore.RegisterType(MyCustomType{})\nfunc (*Store) RegisterType(i any) {\n\tgob.Register(i)\n}\n\n// Get will get/create a session.\n//\n// This function will return an ErrSessionAlreadyLoadedByMiddleware if\n// the session is already loaded by the middleware.\n//\n// Parameters:\n//   - c: The Fiber context.\n//\n// Returns:\n//   - *Session: The session object.\n//   - error: An error if the session retrieval fails or if the session is already loaded by the middleware.\n//\n// Usage:\n//\n//\tsess, err := store.Get(c)\n//\tif err != nil {\n//\t    // handle error\n//\t}\nfunc (s *Store) Get(c fiber.Ctx) (*Session, error) {\n\t// If session is already loaded in the context,\n\t// it should not be loaded again\n\t_, ok := c.Locals(middlewareContextKey).(*Middleware)\n\tif ok {\n\t\treturn nil, ErrSessionAlreadyLoadedByMiddleware\n\t}\n\n\treturn s.getSession(c)\n}\n\n// getSession retrieves a session based on the context.\n//\n// Parameters:\n//   - c: The Fiber context.\n//\n// Returns:\n//   - *Session: The session object.\n//   - error: An error if the session retrieval fails.\n//\n// Usage:\n//\n//\tsess, err := store.getSession(c)\n//\tif err != nil {\n//\t    // handle error\n//\t}\nfunc (s *Store) getSession(c fiber.Ctx) (*Session, error) {\n\tvar rawData []byte\n\tvar err error\n\n\tid, ok := c.Locals(sessionIDContextKey).(string)\n\tif !ok {\n\t\tid = s.getSessionID(c)\n\t}\n\n\tfresh := false // Session is not fresh initially; only set to true if we generate a new ID\n\n\t// Attempt to fetch session data if an ID is provided\n\tif id != \"\" {\n\t\trawData, err = s.Storage.GetWithContext(c, id)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif rawData == nil {\n\t\t\t// Data not found, prepare to generate a new session\n\t\t\tid = \"\"\n\t\t}\n\t}\n\n\t// Generate a new ID if needed\n\tif id == \"\" {\n\t\tfresh = true // The session is fresh if a new ID is generated\n\t\tid = s.KeyGenerator()\n\t\tc.Locals(sessionIDContextKey, id)\n\t}\n\n\t// Create session object\n\tsess := acquireSession()\n\n\tsess.mu.Lock()\n\n\tsess.ctx = c\n\tsess.config = s\n\tsess.id = id\n\tsess.fresh = fresh\n\n\t// Decode session data if found\n\tif rawData != nil {\n\t\tsess.data.Lock()\n\t\terr := sess.decodeSessionData(rawData)\n\t\tsess.data.Unlock()\n\t\tif err != nil {\n\t\t\tsess.mu.Unlock()\n\t\t\tsess.Release()\n\t\t\treturn nil, fmt.Errorf(\"failed to decode session data: %w\", err)\n\t\t}\n\t}\n\n\tsess.mu.Unlock()\n\n\tif fresh && s.AbsoluteTimeout > 0 {\n\t\tsess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))\n\t} else if sess.isAbsExpired() {\n\t\tif err := sess.Reset(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to reset session: %w\", err)\n\t\t}\n\t\tsess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))\n\t}\n\n\treturn sess, nil\n}\n\n// getSessionID returns the session ID using the configured extractor.\n// The extractor is provided by the shared extractors package.\n//\n// Parameters:\n//   - c: The Fiber context.\n//\n// Returns:\n//   - string: The session ID.\n//\n// Usage:\n//\n//\tid := store.getSessionID(c)\nfunc (s *Store) getSessionID(c fiber.Ctx) string {\n\tsessionID, err := s.Extractor.Extract(c)\n\tif err != nil {\n\t\t// If extraction fails, return empty string to generate a new session\n\t\treturn \"\"\n\t}\n\treturn sessionID\n}\n\n// Reset deletes all sessions from the storage.\n//\n// Returns:\n//   - error: An error if the reset operation fails.\n//\n// Usage:\n//\n//\terr := store.Reset()\n//\tif err != nil {\n//\t    // handle error\n//\t}\nfunc (s *Store) Reset(ctx context.Context) error {\n\treturn s.Storage.ResetWithContext(ctx)\n}\n\n// Delete deletes a session by its ID.\n//\n// Parameters:\n//   - id: The unique identifier of the session.\n//\n// Returns:\n//   - error: An error if the deletion fails or if the session ID is empty.\n//\n// Usage:\n//\n//\terr := store.Delete(id)\n//\tif err != nil {\n//\t    // handle error\n//\t}\nfunc (s *Store) Delete(ctx context.Context, id string) error {\n\tif id == \"\" {\n\t\treturn ErrEmptySessionID\n\t}\n\treturn s.Storage.DeleteWithContext(ctx, id)\n}\n\n// GetByID retrieves a session by its ID from the storage.\n// If the session is not found, it returns nil and an error.\n//\n// Unlike session middleware methods, this function does not automatically:\n//\n//   - Load the session into the request context.\n//\n//   - Save the session data to the storage or update the client cookie.\n//\n// Important Notes:\n//\n//   - The session object returned by GetByID does not have a context associated with it.\n//\n//   - When using this method alongside session middleware, there is a potential for collisions,\n//     so be mindful of interactions between manually retrieved sessions and middleware-managed sessions.\n//\n//   - If you modify a session returned by GetByID, you must call session.Save() to persist the changes.\n//\n//   - When you are done with the session, you should call session.Release() to release the session back to the pool.\n//\n// Parameters:\n//   - id: The unique identifier of the session.\n//\n// Returns:\n//   - *Session: The session object if found; otherwise, nil.\n//   - error: An error if the session retrieval fails or if the session ID is empty.\n//\n// Usage:\n//\n//\tsess, err := store.GetByID(id)\n//\tif err != nil {\n//\t    // handle error\n//\t}\nfunc (s *Store) GetByID(ctx context.Context, id string) (*Session, error) {\n\tif id == \"\" {\n\t\treturn nil, ErrEmptySessionID\n\t}\n\n\trawData, err := s.Storage.GetWithContext(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif rawData == nil {\n\t\treturn nil, ErrSessionIDNotFoundInStore\n\t}\n\n\tsess := acquireSession()\n\n\tsess.mu.Lock()\n\n\tsess.config = s\n\tsess.id = id\n\tsess.fresh = false\n\n\tsess.data.Lock()\n\tdecodeErr := sess.decodeSessionData(rawData)\n\tsess.data.Unlock()\n\tsess.mu.Unlock()\n\tif decodeErr != nil {\n\t\tsess.Release()\n\t\treturn nil, fmt.Errorf(\"failed to decode session data: %w\", decodeErr)\n\t}\n\n\tif s.AbsoluteTimeout > 0 {\n\t\tif sess.isAbsExpired() {\n\t\t\tif err := sess.Destroy(); err != nil { //nolint:contextcheck // it is not right\n\t\t\t\tsess.Release()\n\t\t\t\tlog.Errorf(\"failed to destroy session: %v\", err)\n\t\t\t}\n\t\t\treturn nil, ErrSessionIDNotFoundInStore\n\t\t}\n\t}\n\n\treturn sess, nil\n}\n"
  },
  {
    "path": "middleware/session/store_test.go",
    "content": "package session\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// go test -run Test_Store_getSessionID\nfunc Test_Store_getSessionID(t *testing.T) {\n\tt.Parallel()\n\texpectedID := \"test-session-id\"\n\n\t// fiber instance\n\tapp := fiber.New()\n\n\tt.Run(\"from cookie\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// set cookie\n\t\tctx.Request().Header.SetCookie(store.Extractor.Key, expectedID)\n\n\t\trequire.Equal(t, expectedID, store.getSessionID(ctx))\n\t})\n\n\tt.Run(\"from header\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.FromHeader(\"session_id\"),\n\t\t})\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// set header\n\t\tctx.Request().Header.Set(store.Extractor.Key, expectedID)\n\n\t\trequire.Equal(t, expectedID, store.getSessionID(ctx))\n\t})\n\n\tt.Run(\"from url query\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore(Config{\n\t\t\tExtractor: extractors.FromQuery(\"session_id\"),\n\t\t})\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// set url parameter\n\t\tctx.Request().SetRequestURI(fmt.Sprintf(\"/path?%s=%s\", store.Extractor.Key, expectedID))\n\n\t\trequire.Equal(t, expectedID, store.getSessionID(ctx))\n\t})\n}\n\n// go test -run Test_Store_Get\n// Regression: https://github.com/gofiber/fiber/issues/1408\n// Regression: https://github.com/gofiber/fiber/security/advisories/GHSA-98j2-3j3p-fw2v\nfunc Test_Store_Get(t *testing.T) {\n\t// Regression: https://github.com/gofiber/fiber/security/advisories/GHSA-98j2-3j3p-fw2v\n\tt.Parallel()\n\tunexpectedID := \"test-session-id\"\n\t// fiber instance\n\tapp := fiber.New()\n\tt.Run(\"session should be re-generated if it is invalid\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// session store\n\t\tstore := NewStore()\n\t\t// fiber context\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tdefer app.ReleaseCtx(ctx)\n\n\t\t// set cookie\n\t\tctx.Request().Header.SetCookie(store.Extractor.Key, unexpectedID)\n\n\t\tacquiredSession, err := store.Get(ctx)\n\t\trequire.NoError(t, err)\n\n\t\trequire.NotEqual(t, unexpectedID, acquiredSession.ID())\n\t})\n}\n\n// go test -run Test_Store_DeleteSession\nfunc Test_Store_DeleteSession(t *testing.T) {\n\tt.Parallel()\n\t// fiber instance\n\tapp := fiber.New()\n\t// session store\n\tstore := NewStore()\n\n\t// fiber context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\t// Create a new session\n\tsession, err := store.Get(ctx)\n\trequire.NoError(t, err)\n\n\t// Save the session ID\n\tsessionID := session.ID()\n\n\t// Delete the session\n\terr = store.Delete(ctx, sessionID)\n\trequire.NoError(t, err)\n\n\t// Try to get the session again\n\tsession, err = store.Get(ctx)\n\trequire.NoError(t, err)\n\n\t// The session ID should be different now, because the old session was deleted\n\trequire.NotEqual(t, sessionID, session.ID())\n}\n\nfunc TestStore_Get_SessionAlreadyLoaded(t *testing.T) {\n\t// Create a new Fiber app\n\tapp := fiber.New()\n\n\t// Create a new context\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\t// Mock middleware and set it in the context\n\tmiddleware := &Middleware{}\n\tctx.Locals(middlewareContextKey, middleware)\n\n\t// Create a new store\n\tstore := &Store{}\n\n\t// Call the Get method\n\tsess, err := store.Get(ctx)\n\n\t// Assert that the error is ErrSessionAlreadyLoadedByMiddleware\n\trequire.Nil(t, sess)\n\trequire.Equal(t, ErrSessionAlreadyLoadedByMiddleware, err)\n}\n\nfunc TestStore_Delete(t *testing.T) {\n\t// Create a new store\n\tstore := NewStore()\n\n\tt.Run(\"delete with empty session ID\", func(t *testing.T) {\n\t\terr := store.Delete(context.Background(), \"\")\n\t\trequire.Error(t, err)\n\t\trequire.Equal(t, ErrEmptySessionID, err)\n\t})\n\n\tt.Run(\"delete non-existing session\", func(t *testing.T) {\n\t\terr := store.Delete(context.Background(), \"non-existing-session-id\")\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc Test_Store_GetByID(t *testing.T) {\n\tt.Parallel()\n\t// Create a new store\n\tstore := NewStore()\n\n\tt.Run(\"empty session ID\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tsess, err := store.GetByID(context.Background(), \"\")\n\t\trequire.Error(t, err)\n\t\trequire.Nil(t, sess)\n\t\trequire.Equal(t, ErrEmptySessionID, err)\n\t})\n\n\tt.Run(\"nonexistent session ID\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tsess, err := store.GetByID(context.Background(), \"nonexistent-session-id\")\n\t\trequire.Error(t, err)\n\t\trequire.Nil(t, sess)\n\t\trequire.Equal(t, ErrSessionIDNotFoundInStore, err)\n\t})\n\n\tt.Run(\"valid session ID\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := fiber.New()\n\t\t// Create a new session\n\t\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\t\tsession, err := store.Get(ctx)\n\t\tdefer session.Release()\n\t\tdefer app.ReleaseCtx(ctx)\n\t\trequire.NoError(t, err)\n\n\t\t// Save the session ID\n\t\tsessionID := session.ID()\n\n\t\t// Save the session\n\t\terr = session.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// Retrieve the session by ID\n\t\tretrievedSession, err := store.GetByID(context.Background(), sessionID)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, retrievedSession)\n\t\trequire.Equal(t, sessionID, retrievedSession.ID())\n\n\t\t// Call Save on the retrieved session\n\t\tretrievedSession.Set(\"key\", \"value\")\n\t\terr = retrievedSession.Save()\n\t\trequire.NoError(t, err)\n\n\t\t// Call Other Session methods\n\t\trequire.Equal(t, \"value\", retrievedSession.Get(\"key\"))\n\t\trequire.False(t, retrievedSession.Fresh())\n\n\t\trequire.NoError(t, retrievedSession.Reset())\n\t\trequire.NoError(t, retrievedSession.Destroy())\n\t\trequire.IsType(t, []any{}, retrievedSession.Keys())\n\t\trequire.NoError(t, retrievedSession.Regenerate())\n\t\trequire.NotPanics(t, func() {\n\t\t\tretrievedSession.Release()\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "middleware/skip/skip.go",
    "content": "package skip\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// New returns a middleware that calls the provided predicate for each request.\n// If the predicate evaluates to true the wrapped handler is skipped and the next\n// handler in the chain is executed.\nfunc New(handler fiber.Handler, exclude func(c fiber.Ctx) bool) fiber.Handler {\n\tif exclude == nil {\n\t\treturn handler\n\t}\n\n\treturn func(c fiber.Ctx) error {\n\t\tif exclude(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\treturn handler(c)\n\t}\n}\n"
  },
  {
    "path": "middleware/skip/skip_test.go",
    "content": "package skip_test\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/skip\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_Skip\nfunc Test_Skip(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(skip.New(errTeapotHandler, func(fiber.Ctx) bool { return true }))\n\tapp.Get(\"/\", helloWorldHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\n// go test -run Test_SkipFalse\nfunc Test_SkipFalse(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(skip.New(errTeapotHandler, func(fiber.Ctx) bool { return false }))\n\tapp.Get(\"/\", helloWorldHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\n// go test -run Test_SkipNilFunc\nfunc Test_SkipNilFunc(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Use(skip.New(errTeapotHandler, nil))\n\tapp.Get(\"/\", helloWorldHandler)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n}\n\nfunc helloWorldHandler(c fiber.Ctx) error {\n\treturn c.SendString(\"Hello, World 👋!\")\n}\n\nfunc errTeapotHandler(fiber.Ctx) error {\n\treturn fiber.ErrTeapot\n}\n"
  },
  {
    "path": "middleware/static/config.go",
    "content": "package static\n\nimport (\n\t\"io/fs\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// FS is the file system to serve the static files from.\n\t// You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.\n\t//\n\t// Optional. Default: nil\n\tFS fs.FS\n\n\t// Next defines a function to skip this middleware when returned true.\n\t//\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// ModifyResponse defines a function that allows you to alter the response.\n\t//\n\t// Optional. Default: nil\n\tModifyResponse fiber.Handler\n\n\t// NotFoundHandler defines a function to handle when the path is not found.\n\t//\n\t// Optional. Default: nil\n\tNotFoundHandler fiber.Handler\n\n\t// The names of the index files for serving a directory.\n\t//\n\t// Optional. Default: []string{\"index.html\"}.\n\tIndexNames []string `json:\"index\"`\n\n\t// Expiration duration for inactive file handlers.\n\t// Use a negative time.Duration to disable it.\n\t//\n\t// Optional. Default: 10 * time.Second.\n\tCacheDuration time.Duration `json:\"cache_duration\"`\n\n\t// The value for the Cache-Control HTTP-header\n\t// that is set on the file response. MaxAge is defined in seconds.\n\t//\n\t// Optional. Default: 0.\n\tMaxAge int `json:\"max_age\"`\n\n\t// When set to true, the server tries minimizing CPU usage by caching compressed files.\n\t// This works differently than the github.com/gofiber/compression middleware.\n\t//\n\t// Optional. Default: false\n\tCompress bool `json:\"compress\"`\n\n\t// When set to true, enables byte range requests.\n\t//\n\t// Optional. Default: false\n\tByteRange bool `json:\"byte_range\"`\n\n\t// When set to true, enables directory browsing.\n\t//\n\t// Optional. Default: false.\n\tBrowse bool `json:\"browse\"`\n\n\t// When set to true, enables direct download.\n\t//\n\t// Optional. Default: false.\n\tDownload bool `json:\"download\"`\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tIndexNames:    []string{\"index.html\"},\n\tCacheDuration: 10 * time.Second,\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) Config {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\t// Set default values\n\tif len(cfg.IndexNames) == 0 {\n\t\tcfg.IndexNames = ConfigDefault.IndexNames\n\t}\n\n\tif cfg.CacheDuration == 0 {\n\t\tcfg.CacheDuration = ConfigDefault.CacheDuration\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/static/static.go",
    "content": "package static\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/url\"\n\t\"os\"\n\tpathpkg \"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nvar ErrInvalidPath = errors.New(\"invalid path\")\n\n// sanitizePath validates and cleans the requested path.\n// It returns an error if the path attempts to traverse directories.\nfunc sanitizePath(p []byte, filesystem fs.FS) ([]byte, error) {\n\tvar s string\n\n\thasTrailingSlash := len(p) > 0 && p[len(p)-1] == '/'\n\n\tif bytes.IndexByte(p, '\\\\') >= 0 {\n\t\tb := make([]byte, len(p))\n\t\tcopy(b, p)\n\t\tfor i := range b {\n\t\t\tif b[i] == '\\\\' {\n\t\t\t\tb[i] = '/'\n\t\t\t}\n\t\t}\n\t\ts = utils.UnsafeString(b)\n\t} else {\n\t\ts = utils.UnsafeString(p)\n\t}\n\n\t// repeatedly unescape until it no longer changes, catching errors\n\tfor strings.IndexByte(s, '%') >= 0 {\n\t\tus, err := url.PathUnescape(s)\n\t\tif err != nil {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t\tif us == s {\n\t\t\tbreak\n\t\t}\n\t\ts = us\n\t}\n\n\tif strings.IndexByte(s, '\\\\') >= 0 {\n\t\treturn nil, ErrInvalidPath\n\t}\n\n\t// reject any null bytes\n\tif strings.IndexByte(s, '\\x00') >= 0 {\n\t\treturn nil, ErrInvalidPath\n\t}\n\n\tnormalized := filepath.ToSlash(s)\n\tif filesystem == nil && strings.HasPrefix(normalized, \"//\") {\n\t\treturn nil, ErrInvalidPath\n\t}\n\n\ts = pathpkg.Clean(\"/\" + normalized)\n\n\ttrimmed := utils.TrimLeft(s, '/')\n\tif trimmed != \"\" {\n\t\tif slices.Contains(strings.Split(trimmed, \"/\"), \"..\") {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t}\n\n\tif filesystem == nil {\n\t\tnormalizedClean := filepath.ToSlash(trimmed)\n\t\tif strings.HasPrefix(normalizedClean, \"//\") {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t\tif volume := filepath.VolumeName(normalizedClean); volume != \"\" {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t\tif len(normalizedClean) >= 2 && normalizedClean[1] == ':' {\n\t\t\tdrive := normalizedClean[0]\n\t\t\tif (drive >= 'a' && drive <= 'z') || (drive >= 'A' && drive <= 'Z') {\n\t\t\t\treturn nil, ErrInvalidPath\n\t\t\t}\n\t\t}\n\t\tif strings.HasPrefix(filepath.ToSlash(s), \"//\") {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t}\n\n\tif filesystem != nil {\n\t\ts = trimmed\n\t\tif s == \"\" {\n\t\t\treturn []byte(\"/\"), nil\n\t\t}\n\t\tif !fs.ValidPath(s) {\n\t\t\treturn nil, ErrInvalidPath\n\t\t}\n\t\ts = \"/\" + s\n\t}\n\n\tif hasTrailingSlash && len(s) > 1 && s[len(s)-1] != '/' {\n\t\ts += \"/\"\n\t}\n\n\treturn utils.UnsafeBytes(s), nil\n}\n\n// New creates a new middleware handler.\n// The root argument specifies the root directory from which to serve static assets.\n//\n// Note: Root has to be string or fs.FS; otherwise, it will panic.\nfunc New(root string, cfg ...Config) fiber.Handler {\n\tconfig := configDefault(cfg...)\n\n\tvar createFS sync.Once\n\tvar fileHandler fasthttp.RequestHandler\n\tvar cacheControlValue string\n\tvar rootIsFile bool\n\n\t// adjustments for io/fs compatibility\n\tif config.FS != nil && root == \"\" {\n\t\troot = \".\"\n\t}\n\n\treturn func(c fiber.Ctx) error {\n\t\t// Don't execute middleware if Next returns true\n\t\tif config.Next != nil && config.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// We only serve static assets on GET or HEAD methods\n\t\tmethod := c.Method()\n\t\tif method != fiber.MethodGet && method != fiber.MethodHead {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Initialize FS\n\t\tcreateFS.Do(func() {\n\t\t\tprefix := c.Route().Path\n\n\t\t\tif check, err := isFile(root, config.FS); err == nil {\n\t\t\t\trootIsFile = check\n\t\t\t}\n\n\t\t\t// Is prefix a partial wildcard?\n\t\t\tif before, _, found := strings.Cut(prefix, \"*\"); found {\n\t\t\t\t// /john* -> /john\n\t\t\t\tprefix = before\n\t\t\t}\n\n\t\t\tprefixLen := len(prefix)\n\t\t\tif prefixLen > 1 && prefix[prefixLen-1:] == \"/\" {\n\t\t\t\t// /john/ -> /john\n\t\t\t\tprefixLen--\n\t\t\t}\n\n\t\t\tfileServer := &fasthttp.FS{\n\t\t\t\tRoot:                   root,\n\t\t\t\tFS:                     config.FS,\n\t\t\t\tAllowEmptyRoot:         true,\n\t\t\t\tGenerateIndexPages:     config.Browse,\n\t\t\t\tAcceptByteRange:        config.ByteRange,\n\t\t\t\tCompress:               config.Compress,\n\t\t\t\tCompressBrotli:         config.Compress, // Brotli compression won't work without this\n\t\t\t\tCompressZstd:           config.Compress, // Zstd compression won't work without this\n\t\t\t\tCompressedFileSuffixes: c.App().Config().CompressedFileSuffixes,\n\t\t\t\tCacheDuration:          config.CacheDuration,\n\t\t\t\tSkipCache:              config.CacheDuration < 0,\n\t\t\t\tIndexNames:             config.IndexNames,\n\t\t\t\tPathNotFound: func(fctx *fasthttp.RequestCtx) {\n\t\t\t\t\tfctx.Response.SetStatusCode(fiber.StatusNotFound)\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfileServer.PathRewrite = func(fctx *fasthttp.RequestCtx) []byte {\n\t\t\t\tpath := fctx.Path()\n\n\t\t\t\tif len(path) >= prefixLen {\n\t\t\t\t\tcheckFile, err := isFile(root, fileServer.FS)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn path\n\t\t\t\t\t}\n\n\t\t\t\t\t// If the root is a file, we need to reset the path to \"/\" always.\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase checkFile && fileServer.FS == nil:\n\t\t\t\t\t\tpath = []byte(\"/\")\n\t\t\t\t\tcase checkFile && fileServer.FS != nil:\n\t\t\t\t\t\tpath = utils.UnsafeBytes(root)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tpath = path[prefixLen:]\n\t\t\t\t\t\tif len(path) == 0 || path[len(path)-1] != '/' {\n\t\t\t\t\t\t\tpath = append(path, '/')\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(path) > 0 && path[0] != '/' {\n\t\t\t\t\tpath = append([]byte(\"/\"), path...)\n\t\t\t\t}\n\n\t\t\t\tsanitized, err := sanitizePath(path, fileServer.FS)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// return a guaranteed-missing path so fs responds with 404\n\t\t\t\t\treturn []byte(\"/__fiber_invalid__\")\n\t\t\t\t}\n\t\t\t\treturn sanitized\n\t\t\t}\n\n\t\t\tmaxAge := config.MaxAge\n\t\t\tif maxAge > 0 {\n\t\t\t\tcacheControlValue = \"public, max-age=\" + strconv.Itoa(maxAge)\n\t\t\t}\n\n\t\t\tfileHandler = fileServer.NewRequestHandler()\n\t\t})\n\n\t\t// Serve file\n\t\tfileHandler(c.RequestCtx())\n\n\t\t// Sets the response Content-Disposition header to attachment if the Download option is true\n\t\tif config.Download {\n\t\t\tname := filepath.Base(c.Path())\n\t\t\tif rootIsFile {\n\t\t\t\tname = filepath.Base(root)\n\t\t\t}\n\t\t\tc.Attachment(name)\n\t\t}\n\n\t\t// Return request if found and not forbidden\n\t\tstatus := c.RequestCtx().Response.StatusCode()\n\n\t\tif status != fiber.StatusNotFound && status != fiber.StatusForbidden {\n\t\t\tif cacheControlValue != \"\" {\n\t\t\t\tc.RequestCtx().Response.Header.Set(fiber.HeaderCacheControl, cacheControlValue)\n\t\t\t}\n\n\t\t\tif config.ModifyResponse != nil {\n\t\t\t\treturn config.ModifyResponse(c)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\t// Return custom 404 handler if provided.\n\t\tif config.NotFoundHandler != nil {\n\t\t\treturn config.NotFoundHandler(c)\n\t\t}\n\n\t\t// Reset response to default\n\t\tc.RequestCtx().SetContentType(\"\") // Issue #420\n\t\tc.RequestCtx().Response.SetStatusCode(fiber.StatusOK)\n\t\tc.RequestCtx().Response.SetBodyString(\"\")\n\n\t\t// Next middleware\n\t\treturn c.Next()\n\t}\n}\n\n// isFile checks if the root is a file.\nfunc isFile(root string, filesystem fs.FS) (bool, error) {\n\tvar file fs.File\n\tvar err error\n\n\tif filesystem != nil {\n\t\tfile, err = filesystem.Open(root)\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"static: %w\", err)\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = file.Close() //nolint:errcheck // not needed\n\t\t}()\n\t} else {\n\t\tfile, err = os.Open(filepath.Clean(root))\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"static: %w\", err)\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = file.Close() //nolint:errcheck // not needed\n\t\t}()\n\t}\n\n\tstat, err := file.Stat()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"static: %w\", err)\n\t}\n\n\treturn stat.Mode().IsRegular(), nil\n}\n"
  },
  {
    "path": "middleware/static/static_test.go",
    "content": "package static\n\nimport (\n\t\"embed\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst (\n\twinOS      = \"windows\"\n\ttestCSSDir = \"../../.github/testdata/fs/css\"\n)\n\nvar testConfig = fiber.TestConfig{\n\tTimeout:       10 * time.Second,\n\tFailOnTimeout: true,\n}\n\n// go test -run Test_Static_Index_Default\nfunc Test_Static_Index_Default(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/prefix\", New(\"../../.github/workflows\"))\n\n\tapp.Get(\"\", New(\"../../.github/\"))\n\n\tapp.Get(\"test\", New(\"\", Config{\n\t\tIndexNames: []string{\"index.html\"},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Hello, World!\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/not-found\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Not Found\", string(body))\n}\n\n// go test -run Test_Static_Index\nfunc Test_Static_Direct(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/*\", New(\"../../.github\"))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/index.html\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Hello, World!\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodPost, \"/index.html\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 405, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/testdata/testRoutes.json\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get(\"Content-Type\"))\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"test_routes\")\n}\n\n// go test -run Test_Static_MaxAge\nfunc Test_Static_MaxAge(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/*\", New(\"../../.github\", Config{\n\t\tMaxAge: 100,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/index.html\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, \"text/html; charset=utf-8\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"public, max-age=100\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\n// go test -run Test_Static_Custom_CacheControl\nfunc Test_Static_Custom_CacheControl(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/*\", New(\"../../.github\", Config{\n\t\tModifyResponse: func(c fiber.Ctx) error {\n\t\t\tif strings.Contains(c.GetRespHeader(\"Content-Type\"), \"text/html\") {\n\t\t\t\tc.Response().Header.Set(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/index.html\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"no-cache, no-store, must-revalidate\", resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n\n\tnormalResp, normalErr := app.Test(httptest.NewRequest(fiber.MethodGet, \"/config.yml\", http.NoBody))\n\trequire.NoError(t, normalErr, \"app.Test(req)\")\n\trequire.Empty(t, normalResp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n}\n\nfunc Test_Static_Disable_Cache(t *testing.T) {\n\t// Skip on Windows. It's not possible to delete a file that is in use.\n\tif runtime.GOOS == winOS {\n\t\tt.SkipNow()\n\t}\n\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tfile, err := os.Create(\"../../.github/test.txt\")\n\trequire.NoError(t, err)\n\t_, err = file.WriteString(\"Hello, World!\")\n\trequire.NoError(t, err)\n\trequire.NoError(t, file.Close())\n\n\t// Remove the file even if the test fails\n\tdefer func() {\n\t\t_ = os.Remove(\"../../.github/test.txt\") //nolint:errcheck // not needed\n\t}()\n\n\tapp.Get(\"/*\", New(\"../../.github/\", Config{\n\t\tCacheDuration: -1,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/test.txt\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Hello, World!\")\n\n\trequire.NoError(t, os.Remove(\"../../.github/test.txt\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/test.txt\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), \"CacheControl Control\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Not Found\", string(body))\n}\n\nfunc Test_Static_NotFoundHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/*\", New(\"../../.github\", Config{\n\t\tNotFoundHandler: func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"Custom 404\")\n\t\t},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/not-found\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"Custom 404\", string(body))\n}\n\n// go test -run Test_Static_Download\nfunc Test_Static_Download(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/fiber.png\", New(\"../../.github/testdata/fs/img/fiber.png\", Config{\n\t\tDownload: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/fiber.png\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, \"image/png\", resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, `attachment; filename=\"fiber.png\"`, resp.Header.Get(fiber.HeaderContentDisposition))\n}\n\nfunc Test_Static_Download_NonASCII(t *testing.T) {\n\t// Skip on Windows. It's not possible to delete a file that is in use.\n\tif runtime.GOOS == \"windows\" {\n\t\tt.SkipNow()\n\t}\n\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\tfname := \"файл.txt\"\n\tpath := filepath.Join(dir, fname)\n\trequire.NoError(t, os.WriteFile(path, []byte(\"x\"), 0o644)) //nolint:gosec // Not a concern\n\n\tapp := fiber.New()\n\tapp.Get(\"/file\", New(path, Config{Download: true}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/file\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\texpect := \"attachment; filename=\\\"\" + fname + \"\\\"; filename*=UTF-8''\" + url.PathEscape(fname)\n\trequire.Equal(t, expect, resp.Header.Get(fiber.HeaderContentDisposition))\n}\n\n// go test -run Test_Static_Group\nfunc Test_Static_Group(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tgrp := app.Group(\"/v1\", func(c fiber.Ctx) error {\n\t\tc.Set(\"Test-Header\", \"123\")\n\t\treturn c.Next()\n\t})\n\n\tgrp.Get(\"/v2*\", New(\"../../.github/index.html\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/v1/v2\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\trequire.Equal(t, \"123\", resp.Header.Get(\"Test-Header\"))\n\n\tgrp = app.Group(\"/v2\")\n\tgrp.Get(\"/v3*\", New(\"../../.github/index.html\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/v2/v3/john/doe\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n}\n\nfunc Test_Static_Wildcard(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"*\", New(\"../../.github/index.html\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/yesyes/john/doe\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Test file\")\n}\n\nfunc Test_Static_Prefix_Wildcard(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/test*\", New(\"../../.github/index.html\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/test/john/doe\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Get(\"/my/nameisjohn*\", New(\"../../.github/index.html\"))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/my/nameisjohn/no/its/not\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Test file\")\n}\n\nfunc Test_Static_Prefix(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Get(\"/john*\", New(\"../../.github\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/john/index.html\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Get(\"/prefix*\", New(\"../../.github/testdata\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/prefix/index.html\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Get(\"/single*\", New(\"../../.github/testdata/testRoutes.json\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/single\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType))\n}\n\nfunc Test_Static_Trailing_Slash(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tapp.Get(\"/john*\", New(\"../../.github\"))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/john/\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Get(\"/john_without_index*\", New(testCSSDir))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/john_without_index/\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Use(\"/john\", New(\"../../.github\"))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/john/\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/john\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tapp.Use(\"/john_without_index/\", New(testCSSDir))\n\n\treq = httptest.NewRequest(fiber.MethodGet, \"/john_without_index/\", http.NoBody)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n}\n\nfunc Test_Static_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/*\", New(\"../../.github\", Config{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Get(\"X-Custom-Header\") == \"skip\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/*\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"You've skipped app.Static\")\n\t})\n\n\tt.Run(\"app.Static is skipped: invoking Get handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(\"X-Custom-Header\", \"skip\")\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, resp.StatusCode)\n\t\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\t\trequire.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(body), \"You've skipped app.Static\")\n\t})\n\n\tt.Run(\"app.Static is not skipped: serving index.html\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody)\n\t\treq.Header.Set(\"X-Custom-Header\", \"don't skip\")\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, resp.StatusCode)\n\t\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\t\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, string(body), \"Hello, World!\")\n\t})\n}\n\nfunc Test_Route_Static_Root(t *testing.T) {\n\tt.Parallel()\n\n\tdir := testCSSDir\n\tapp := fiber.New()\n\tapp.Get(\"/*\", New(dir, Config{\n\t\tBrowse: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tapp = fiber.New()\n\tapp.Get(\"/*\", New(dir))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n}\n\nfunc Test_Route_Static_HasPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tdir := testCSSDir\n\tapp := fiber.New()\n\tapp.Get(\"/static*\", New(dir, Config{\n\t\tBrowse: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/static\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tapp = fiber.New()\n\tapp.Get(\"/static/*\", New(dir, Config{\n\t\tBrowse: true,\n\t}))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tapp = fiber.New()\n\tapp.Get(\"/static*\", New(dir))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tapp = fiber.New()\n\tapp.Get(\"/static*\", New(dir))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 404, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n}\n\nfunc Test_Static_FS(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Get(\"/*\", New(\"\", Config{\n\t\tFS:     os.DirFS(\"../../.github/testdata/fs\"),\n\t\tBrowse: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/css/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n}\n\n/*func Test_Static_FS_DifferentRoot(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\tapp.Get(\"/*\", New(\"fs\", Config{\n\t\tFS:         os.DirFS(\"../../.github/testdata\"),\n\t\tIndexNames: []string{\"index2.html\"},\n\t\tBrowse:     true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"<h1>Hello, World!</h1>\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/css/style.css\", nil))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n}*/\n\n//go:embed static.go config.go\nvar fsTestFilesystem embed.FS\n\nfunc Test_Static_FS_Browse(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/embed*\", New(\"\", Config{\n\t\tFS:     fsTestFilesystem,\n\t\tBrowse: true,\n\t}))\n\n\tapp.Get(\"/dirfs*\", New(\"\", Config{\n\t\tFS:     os.DirFS(testCSSDir),\n\t\tBrowse: true,\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/dirfs\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"style.css\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/dirfs/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/dirfs/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/dirfs/test/style2.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/embed\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"static.go\")\n}\n\nfunc Test_Static_FS_Prefix_Wildcard(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/test*\", New(\"index.html\", Config{\n\t\tFS:         os.DirFS(\"../../.github\"),\n\t\tIndexNames: []string{\"not_index.html\"},\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/test/john/doe\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))\n\trequire.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(body), \"Test file\")\n}\n\nfunc Test_isFile(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tfilesystem fs.FS\n\t\tgotError   error\n\t\tname       string\n\t\tpath       string\n\t\texpected   bool\n\t}{\n\t\t{\n\t\t\tname:       \"file\",\n\t\t\tpath:       \"index.html\",\n\t\t\tfilesystem: os.DirFS(\"../../.github\"),\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tname:       \"file\",\n\t\t\tpath:       \"index2.html\",\n\t\t\tfilesystem: os.DirFS(\"../../.github\"),\n\t\t\texpected:   false,\n\t\t\tgotError:   fs.ErrNotExist,\n\t\t},\n\t\t{\n\t\t\tname:       \"directory\",\n\t\t\tpath:       \".\",\n\t\t\tfilesystem: os.DirFS(\"../../.github\"),\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"directory\",\n\t\t\tpath:       \"not_exists\",\n\t\t\tfilesystem: os.DirFS(\"../../.github\"),\n\t\t\texpected:   false,\n\t\t\tgotError:   fs.ErrNotExist,\n\t\t},\n\t\t{\n\t\t\tname:       \"directory\",\n\t\t\tpath:       \".\",\n\t\t\tfilesystem: os.DirFS(testCSSDir),\n\t\t\texpected:   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"file\",\n\t\t\tpath:       testCSSDir + \"/style.css\",\n\t\t\tfilesystem: nil,\n\t\t\texpected:   true,\n\t\t},\n\t\t{\n\t\t\tname:       \"file\",\n\t\t\tpath:       testCSSDir + \"/style2.css\",\n\t\t\tfilesystem: nil,\n\t\t\texpected:   false,\n\t\t\tgotError:   fs.ErrNotExist,\n\t\t},\n\t\t{\n\t\t\tname:       \"directory\",\n\t\t\tpath:       testCSSDir,\n\t\t\tfilesystem: nil,\n\t\t\texpected:   false,\n\t\t},\n\t}\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tc := c\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := isFile(c.path, c.filesystem)\n\t\t\trequire.ErrorIs(t, err, c.gotError)\n\t\t\trequire.Equal(t, c.expected, actual)\n\t\t})\n\t}\n}\n\nfunc Test_Static_Compress(t *testing.T) {\n\tt.Parallel()\n\tdir := \"../../.github/testdata/fs\"\n\tapp := fiber.New()\n\tapp.Get(\"/*\", New(dir, Config{\n\t\tCompress: true,\n\t}))\n\n\t// Note: deflate is not supported by fasthttp.FS\n\talgorithms := []string{\"zstd\", \"gzip\", \"br\"}\n\n\tfor _, algo := range algorithms {\n\t\tt.Run(algo+\"_compression\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// request non-compressible file (less than 200 bytes), Content Length will remain the same\n\t\t\treq := httptest.NewRequest(fiber.MethodGet, \"/css/style.css\", http.NoBody)\n\t\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\t\t\tresp, err := app.Test(req, testConfig)\n\n\t\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\t\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\t\t\trequire.Equal(t, \"46\", resp.Header.Get(fiber.HeaderContentLength))\n\n\t\t\t// request compressible file, ContentLength will change\n\t\t\treq = httptest.NewRequest(fiber.MethodGet, \"/index.html\", http.NoBody)\n\t\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\t\t\tresp, err = app.Test(req, testConfig)\n\n\t\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\t\trequire.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding))\n\t\t\trequire.Greater(t, \"299\", resp.Header.Get(fiber.HeaderContentLength))\n\t\t})\n\t}\n}\n\nfunc Test_Static_Compress_WithoutEncoding(t *testing.T) {\n\tt.Parallel()\n\tdir := \"../../.github/testdata/fs\"\n\tapp := fiber.New()\n\tapp.Get(\"/*\", New(dir, Config{\n\t\tCompress:      true,\n\t\tCacheDuration: 1 * time.Second,\n\t}))\n\n\t// request compressible file without encoding\n\treq := httptest.NewRequest(fiber.MethodGet, \"/index.html\", http.NoBody)\n\tresp, err := app.Test(req, testConfig)\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))\n\trequire.Equal(t, \"299\", resp.Header.Get(fiber.HeaderContentLength))\n\n\t// request compressible file with different encodings\n\talgorithms := []string{\"zstd\", \"gzip\", \"br\"}\n\tfileSuffixes := map[string]string{\n\t\t\"gzip\": \".fiber.gz\",\n\t\t\"br\":   \".fiber.br\",\n\t\t\"zstd\": \".fiber.zst\",\n\t}\n\n\tfor _, algo := range algorithms {\n\t\t// Wait for cache to expire\n\t\ttime.Sleep(2 * time.Second)\n\t\tfileName := \"index.html\"\n\t\tcompressedFileName := dir + \"/index.html\" + fileSuffixes[algo]\n\n\t\treq = httptest.NewRequest(fiber.MethodGet, \"/\"+fileName, http.NoBody)\n\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\t\tresp, err = app.Test(req, testConfig)\n\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\trequire.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding))\n\t\trequire.Greater(t, \"299\", resp.Header.Get(fiber.HeaderContentLength))\n\n\t\t// verify suffixed file was created\n\t\t_, err := os.Stat(compressedFileName)\n\t\trequire.NoError(t, err, \"File should exist\")\n\t}\n}\n\nfunc Test_Static_Compress_WithFileSuffixes(t *testing.T) {\n\tt.Parallel()\n\tdir := \"../../.github/testdata/fs\"\n\tfileSuffixes := map[string]string{\n\t\t\"gzip\": \".test.gz\",\n\t\t\"br\":   \".test.br\",\n\t\t\"zstd\": \".test.zst\",\n\t}\n\n\tapp := fiber.New(fiber.Config{\n\t\tCompressedFileSuffixes: fileSuffixes,\n\t})\n\tapp.Get(\"/*\", New(dir, Config{\n\t\tCompress:      true,\n\t\tCacheDuration: 1 * time.Second,\n\t}))\n\n\t// request compressible file with different encodings\n\talgorithms := []string{\"zstd\", \"gzip\", \"br\"}\n\n\tfor _, algo := range algorithms {\n\t\t// Wait for cache to expire\n\t\ttime.Sleep(2 * time.Second)\n\t\tfileName := \"index.html\"\n\t\tcompressedFileName := dir + \"/index.html\" + fileSuffixes[algo]\n\n\t\treq := httptest.NewRequest(fiber.MethodGet, \"/\"+fileName, http.NoBody)\n\t\treq.Header.Set(\"Accept-Encoding\", algo)\n\t\tresp, err := app.Test(req, testConfig)\n\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\t\trequire.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding))\n\t\trequire.Greater(t, \"299\", resp.Header.Get(fiber.HeaderContentLength))\n\n\t\t// verify suffixed file was created\n\t\t_, err = os.Stat(compressedFileName)\n\t\trequire.NoError(t, err, \"File should exist\")\n\t}\n}\n\nfunc Test_Router_Mount_n_Static(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(\"/static\", New(testCSSDir, Config{Browse: true}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Home\")\n\t})\n\n\tsubApp := fiber.New()\n\tapp.Use(\"/mount\", subApp)\n\tsubApp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Hello from /test\")\n\t})\n\n\tapp.Use(func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusNotFound).SendString(\"Not Found\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/static/style.css\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Static_PathTraversal(t *testing.T) {\n\t// Skip this test if running on Windows\n\tif runtime.GOOS == winOS {\n\t\tt.Skip(\"Skipping Windows-specific tests\")\n\t}\n\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Serve only from testCSSDir\n\t// This directory should contain `style.css` but not `index.html` or anything above it.\n\trootDir := testCSSDir\n\tapp.Get(\"/*\", New(rootDir))\n\n\t// A valid request: should succeed\n\tvalidReq := httptest.NewRequest(fiber.MethodGet, \"/style.css\", http.NoBody)\n\tvalidResp, err := app.Test(validReq)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, validResp.StatusCode, \"Status code\")\n\trequire.Equal(t, fiber.MIMETextCSSCharsetUTF8, validResp.Header.Get(fiber.HeaderContentType))\n\tvalidBody, err := io.ReadAll(validResp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(validBody), \"color\")\n\n\t// Helper function to assert that a given path is blocked.\n\t// Blocked can mean different status codes depending on what triggered the block.\n\t// We'll accept 400 or 404 as \"blocked\" statuses:\n\t// - 404 is the expected blocked response in most cases.\n\t// - 400 might occur if fasthttp rejects the request before it's even processed (e.g., null bytes).\n\tassertTraversalBlocked := func(path string) {\n\t\treq := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\n\t\tstatus := resp.StatusCode\n\t\trequire.Truef(t, status == 400 || status == 404,\n\t\t\t\"Status code for path traversal %s should be 400 or 404, got %d\", path, status)\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\n\t\t// If we got a 404, we expect the \"Not Found\" message because that's how fiber handles NotFound by default.\n\t\tif status == 404 {\n\t\t\trequire.Contains(t, string(body), \"Not Found\",\n\t\t\t\t\"Blocked traversal should have a \\\"Not Found\\\" message for %s\", path)\n\t\t} else {\n\t\t\trequire.Contains(t, string(body), \"Are you a hacker?\",\n\t\t\t\t\"Blocked traversal should have a \\\"Not Found\\\" message for %s\", path)\n\t\t}\n\t}\n\n\t// Basic attempts to escape the directory\n\tassertTraversalBlocked(\"/index.html..\")\n\tassertTraversalBlocked(\"/style.css..\")\n\tassertTraversalBlocked(\"/../index.html\")\n\tassertTraversalBlocked(\"/../../index.html\")\n\tassertTraversalBlocked(\"/../../../index.html\")\n\n\t// Attempts with double slashes\n\tassertTraversalBlocked(\"//../index.html\")\n\tassertTraversalBlocked(\"/..//index.html\")\n\n\t// Encoded attempts: `%2e` is '.' and `%2f` is '/'\n\tassertTraversalBlocked(\"/..%2findex.html\")        // ../index.html\n\tassertTraversalBlocked(\"/%2e%2e/index.html\")      // ../index.html\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e/secret\") // ../../../secret\n\n\t// Mixed encoded and normal attempts\n\tassertTraversalBlocked(\"/%2e%2e/../index.html\")  // ../../index.html\n\tassertTraversalBlocked(\"/..%2f..%2fsecret.json\") // ../../../secret.json\n\n\t// Attempts with current directory references\n\tassertTraversalBlocked(\"/./../index.html\")\n\tassertTraversalBlocked(\"/././../index.html\")\n\n\t// Trailing slashes\n\tassertTraversalBlocked(\"/../\")\n\tassertTraversalBlocked(\"/../../\")\n\n\t// Attempts to load files from an absolute path outside the root\n\tassertTraversalBlocked(\"/\" + rootDir + \"/../../index.html\")\n\n\t// Additional edge cases:\n\n\t// Double-encoded `..`\n\tassertTraversalBlocked(\"/%252e%252e/index.html\") // double-encoded .. -> ../index.html after double decoding\n\n\t// Multiple levels of encoding and traversal\n\tassertTraversalBlocked(\"/%2e%2e%2F..%2f%2e%2e%2fWINDOWS\")       // multiple ups and unusual pattern\n\tassertTraversalBlocked(\"/%2e%2e%2F..%2f%2e%2e%2f%2e%2e/secret\") // more complex chain of ../\n\n\t// Null byte attempts\n\tassertTraversalBlocked(\"/index.html%00.jpg\")\n\tassertTraversalBlocked(\"/%00index.html\")\n\tassertTraversalBlocked(\"/somefolder%00/something\")\n\tassertTraversalBlocked(\"/%00/index.html\")\n\n\t// Attempts to access known system files\n\tassertTraversalBlocked(\"/etc/passwd\")\n\tassertTraversalBlocked(\"/etc/\")\n\n\t// Complex mixed attempts with encoded slashes and dots\n\tassertTraversalBlocked(\"/..%2F..%2F..%2F..%2Fetc%2Fpasswd\")\n\n\t// Attempts inside subdirectories with encoded traversal\n\tassertTraversalBlocked(\"/somefolder/%2e%2e%2findex.html\")\n\tassertTraversalBlocked(\"/somefolder/%2e%2e%2f%2e%2e%2findex.html\")\n\n\t// Backslash encoded attempts\n\tassertTraversalBlocked(\"/%5C..%5Cindex.html\")\n\tassertTraversalBlocked(\"/%5c..%5c..%5cetc%5cpasswd\")\n\tassertTraversalBlocked(\"/%255c..%255c..%255cetc%255cpasswd\")\n\tassertTraversalBlocked(\"/..%5c..%5cetc%5cpasswd\")\n\tassertTraversalBlocked(\"/%2e%2e%5c%2e%2e%5cetc%5cpasswd\")\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e%5cetc%5cpasswd\")\n\tassertTraversalBlocked(\"/%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd\")\n\tassertTraversalBlocked(\"/.%2e/.%2e/etc/passwd\")\n\tassertTraversalBlocked(\"/..%2f..%2f..%2f..%2fetc%2fpasswd\")\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd\")\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e%2fetc%2fshadow\")\n\tassertTraversalBlocked(\"/%2e%2e/%2e%2e/var/log/auth.log\")\n\tassertTraversalBlocked(\"/..%2f..%2fvar%2flog%2fauth.log\")\n\tassertTraversalBlocked(\"/%2e%2e//%2e%2e//etc/passwd\")\n\tassertTraversalBlocked(\"/..//..//etc/passwd\")\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e%2f%2e%2e%2fproc%2fself%2fenviron\")\n\tassertTraversalBlocked(\"/%2e%2e%2f%2e%2e%2f%2e%2e%2froot%2f.ssh%2fauthorized_keys\")\n}\n\nfunc Test_Static_PathTraversal_WindowsOnly(t *testing.T) {\n\t// Skip this test if not running on Windows\n\tif runtime.GOOS != winOS {\n\t\tt.Skip(\"Skipping Windows-specific tests\")\n\t}\n\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Serve only from testCSSDir\n\trootDir := testCSSDir\n\tapp.Get(\"/*\", New(rootDir))\n\n\t// A valid request (relative path without backslash):\n\tvalidReq := httptest.NewRequest(fiber.MethodGet, \"/style.css\", http.NoBody)\n\tvalidResp, err := app.Test(validReq)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, validResp.StatusCode, \"Status code for valid file on Windows\")\n\tbody, err := io.ReadAll(validResp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Contains(t, string(body), \"color\")\n\n\t// Helper to test blocked responses\n\tassertTraversalBlocked := func(path string) {\n\t\treq := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)\n\t\tresp, err := app.Test(req)\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\n\t\t// We expect a blocked request to return either 400 or 404\n\t\tstatus := resp.StatusCode\n\t\trequire.Containsf(t, []int{400, 404}, status,\n\t\t\t\"Status code for path traversal %s should be 400 or 404, got %d\", path, status)\n\n\t\t// If it's a 404, we expect a \"Not Found\" message\n\t\tif status == 404 {\n\t\t\trespBody, err := io.ReadAll(resp.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Contains(t, string(respBody), \"Not Found\",\n\t\t\t\t\"Blocked traversal should have a \\\"Not Found\\\" message for %s\", path)\n\t\t} else {\n\t\t\trequire.Contains(t, string(body), \"Are you a hacker?\",\n\t\t\t\t\"Blocked traversal should have a \\\"Not Found\\\" message for %s\", path)\n\t\t}\n\t}\n\n\t// Windows-specific traversal attempts\n\t// Backslashes are treated as directory separators on Windows.\n\tassertTraversalBlocked(\"/..\\\\index.html\")\n\tassertTraversalBlocked(\"/..\\\\..\\\\index.html\")\n\tassertTraversalBlocked(\"/..\\\\..\\\\..\\\\Windows\\\\win.ini\")\n\tassertTraversalBlocked(\"/..\\\\..\\\\..\\\\Windows\\\\System32\\\\drivers\\\\etc\\\\hosts\")\n\tassertTraversalBlocked(\"/%5C..%5C..%5CWindows%5Cwin.ini\")\n\tassertTraversalBlocked(\"/%255C..%255C..%255CWindows%255Cwin.ini\")\n\tassertTraversalBlocked(\"/%5c..%5c..%5cWindows%5cSystem32%5cdrivers%5cetc%5chosts\")\n\tassertTraversalBlocked(\"/C:\\\\Windows\\\\System32\\\\cmd.exe\")\n\tassertTraversalBlocked(\"/C:%5CWindows%5CSystem32%5Ccmd.exe\")\n\tassertTraversalBlocked(\"/%43:%5CWindows%5CSystem32%5Ccmd.exe\")\n\tassertTraversalBlocked(\"/%5c%5cserver%5cshare%5csecret.txt\")\n\tassertTraversalBlocked(\"//server\\\\share\\\\secret.txt\")\n\tassertTraversalBlocked(\"//server/share/secret.txt\")\n\tassertTraversalBlocked(\"/%2F%2Fserver%2Fshare%2Fsecret.txt\")\n\n\t// Attempt with a path that might try to reference Windows drives or absolute paths\n\t// Note: These are artificial tests to ensure no drive-letter escapes are allowed.\n\tassertTraversalBlocked(\"/C:\\\\Windows\\\\System32\\\\cmd.exe\")\n\tassertTraversalBlocked(\"/C:/Windows/System32/cmd.exe\")\n\n\t// Attempt with UNC-like paths (though unlikely in a web context, good to test)\n\tassertTraversalBlocked(\"//server\\\\share\\\\secret.txt\")\n\n\t// Attempt using a mixture of forward and backward slashes\n\tassertTraversalBlocked(\"/..\\\\..\\\\/index.html\")\n\n\t// Attempt that includes a null-byte on Windows\n\tassertTraversalBlocked(\"/index.html%00.txt\")\n\n\t// Check behavior on an obviously nonexistent and suspicious file\n\tassertTraversalBlocked(\"/\\\\this\\\\path\\\\does\\\\not\\\\exist\\\\..\")\n\n\t// Attempts involving relative traversal and current directory reference\n\tassertTraversalBlocked(\"/.\\\\../index.html\")\n\tassertTraversalBlocked(\"/./..\\\\index.html\")\n}\n\nfunc Benchmark_SanitizePath(b *testing.B) {\n\tbench := func(name string, filesystem fs.FS, path []byte) {\n\t\tb.Run(name, func(b *testing.B) {\n\t\t\tb.ReportAllocs()\n\t\t\tfor b.Loop() {\n\t\t\t\tif _, err := sanitizePath(path, filesystem); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tbench(\"nilFS - urlencoded chars\", nil, []byte(\"/foo%2Fbar/../baz%20qux/index.html\"))\n\tbench(\"dirFS - urlencoded chars\", os.DirFS(\".\"), []byte(\"/foo%2Fbar/../baz%20qux/index.html\"))\n\tbench(\"nilFS - slashes\", nil, []byte(\"\\\\foo%2Fbar\\\\baz%20qux\\\\index.html\"))\n}\n\nfunc Test_SanitizePath(t *testing.T) {\n\tt.Parallel()\n\n\ttype testCase struct {\n\t\tfilesystem fs.FS\n\t\tname       string\n\t\texpectPath string\n\t\tinput      []byte\n\t}\n\n\ttestCases := []testCase{\n\t\t{name: \"simple path\", input: []byte(\"/foo/bar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"traversal attempt\", input: []byte(\"/foo/../../bar.txt\"), expectPath: \"/bar.txt\"},\n\t\t{name: \"encoded traversal\", input: []byte(\"/foo/%2e%2e/bar.txt\"), expectPath: \"/bar.txt\"},\n\t\t{name: \"double encoded traversal\", input: []byte(\"/%252e%252e/bar.txt\"), expectPath: \"/bar.txt\"},\n\t\t{name: \"current dir reference\", input: []byte(\"/foo/./bar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"encoded slash\", input: []byte(\"/foo%2Fbar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"empty path\", input: []byte(\"\"), expectPath: \"/\"},\n\t\t{name: \"dot segments\", input: []byte(\"/foo/./bar/../baz.txt\"), expectPath: \"/foo/baz.txt\"},\n\t\t{name: \"leading dot segment\", input: []byte(\"/./foo/bar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"encoded space\", input: []byte(\"/foo%20bar/baz.txt\"), expectPath: \"/foo bar/baz.txt\"},\n\t\t{name: \"encoded plus literal\", input: []byte(\"/foo+bar/baz.txt\"), expectPath: \"/foo+bar/baz.txt\"},\n\t\t// windows-specific paths\n\t\t{name: \"backslash path\", input: []byte(\"\\\\foo\\\\bar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"backslash traversal\", input: []byte(\"\\\\foo\\\\..\\\\..\\\\bar.txt\"), expectPath: \"/bar.txt\"},\n\t\t{name: \"mixed slashes\", input: []byte(\"/foo\\\\bar.txt\"), expectPath: \"/foo/bar.txt\"},\n\t\t{name: \"trailing slash preserved\", input: []byte(\"/foo/bar/\"), expectPath: \"/foo/bar/\"},\n\t\t{name: \"encoded trailing slash\", input: []byte(\"/foo/bar%2F\"), expectPath: \"/foo/bar\"},\n\t\t{filesystem: os.DirFS(\".\"), name: \"filesystem empty path\", input: []byte(\"\"), expectPath: \"/\"},\n\t\t{filesystem: os.DirFS(\".\"), name: \"filesystem trailing slash\", input: []byte(\"/foo/\"), expectPath: \"/foo/\"},\n\t\t{filesystem: os.DirFS(\".\"), name: \"filesystem traversal clean\", input: []byte(\"/foo/../bar.txt\"), expectPath: \"/bar.txt\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := sanitizePath(tc.input, tc.filesystem)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tc.expectPath, string(got))\n\t\t})\n\t}\n}\n\nfunc Test_SanitizePath_Error(t *testing.T) {\n\tt.Parallel()\n\n\ttype testCase struct {\n\t\tfilesystem fs.FS\n\t\tname       string\n\t\tinput      []byte\n\t}\n\n\ttestCases := []testCase{\n\t\t{name: \"null byte\", input: []byte(\"/foo/bar.txt%00\")},\n\t\t{name: \"encoded backslash traversal\", input: []byte(\"/foo%5C..%5Cbar.txt\")},\n\t\t{name: \"double encoded backslash traversal\", input: []byte(\"/%255C..%255C..%255CWindows%255Cwin.ini\")},\n\t\t{name: \"encoded backslash absolute\", input: []byte(\"/%5CWindows%5CSystem32%5Cdrivers%5Cetc%5Chosts\")},\n\t\t{name: \"double encoded backslash absolute\", input: []byte(\"/%255CWindows%255CSystem32%255Cdrivers%255Cetc%255Chosts\")},\n\t\t{name: \"encoded backslash mixed slashes\", input: []byte(\"/..%5C..%5Cetc%5Cpasswd\")},\n\t\t{name: \"encoded backslash mixed encoding\", input: []byte(\"/%2e%2e%5c%2e%2e%5cetc%5cpasswd\")},\n\t\t{name: \"encoded backslash with encoded slash\", input: []byte(\"/%2e%2e%2f%2e%2e%5cetc%5cpasswd\")},\n\t\t{name: \"encoded backslash unc path\", input: []byte(\"//server%5Cshare%5Csecret.txt\")},\n\t\t{name: \"encoded backslash drive letter\", input: []byte(\"/C:%5CWindows%5CSystem32%5Ccmd.exe\")},\n\t\t{name: \"double slash path\", input: []byte(\"//foo//bar.txt\")},\n\t\t{name: \"drive letter\", input: []byte(\"C:/Windows/System32/cmd.exe\")},\n\t\t{name: \"drive letter with leading slash\", input: []byte(\"/C:/Windows/System32/cmd.exe\")},\n\t\t{name: \"encoded drive letter\", input: []byte(\"/%43:%5CWindows%5CSystem32%5Ccmd.exe\")},\n\t\t{name: \"unc path\", input: []byte(\"//server/share/secret.txt\")},\n\t\t{name: \"encoded unc path\", input: []byte(\"/%2F%2Fserver%2Fshare%2Fsecret.txt\")},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t_, err := sanitizePath(tc.input, tc.filesystem)\n\t\t\trequire.ErrorIs(t, err, ErrInvalidPath, \"Expected ErrInvalidPath for input: %s\", tc.input)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "middleware/timeout/config.go",
    "content": "package timeout\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config holds the configuration for the timeout middleware.\ntype Config struct {\n\t// Next defines a function to skip this middleware.\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\n\t// OnTimeout is executed when a timeout occurs.\n\t// Optional. Default: nil (return fiber.ErrRequestTimeout)\n\tOnTimeout fiber.Handler\n\n\t// Errors defines custom errors that are treated as timeouts.\n\t// Optional. Default: nil\n\tErrors []error\n\n\t// Timeout defines the timeout duration for all routes.\n\t// Optional. Default: 0 (no timeout)\n\tTimeout time.Duration\n}\n\n// ConfigDefault is the default configuration.\nvar ConfigDefault = Config{\n\tNext:      nil,\n\tTimeout:   0,\n\tOnTimeout: nil,\n\tErrors:    nil,\n}\n\n// configDefault returns the first Config value or ConfigDefault.\nfunc configDefault(config ...Config) Config {\n\tif len(config) < 1 {\n\t\treturn ConfigDefault\n\t}\n\n\tcfg := config[0]\n\n\tif cfg.Timeout < 0 {\n\t\tcfg.Timeout = ConfigDefault.Timeout\n\t}\n\tif cfg.Errors == nil {\n\t\tcfg.Errors = ConfigDefault.Errors\n\t}\n\tif cfg.OnTimeout == nil {\n\t\tcfg.OnTimeout = ConfigDefault.OnTimeout\n\t}\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\treturn cfg\n}\n"
  },
  {
    "path": "middleware/timeout/timeout.go",
    "content": "package timeout\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"runtime/debug\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\n// New enforces a timeout for each incoming request. It replaces the request's\n// context with one that has the configured deadline, which is exposed through\n// c.Context(). Handlers can detect the timeout by listening on c.Context().Done()\n// and return early.\n//\n// When a timeout occurs, the middleware returns immediately with fiber.ErrRequestTimeout\n// (or the result of OnTimeout if configured). The handler goroutine can continue\n// safely, and resources are recycled when it finishes via the Abandon/ForceRelease\n// mechanism.\nfunc New(h fiber.Handler, config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\treturn func(ctx fiber.Ctx) error {\n\t\tif cfg.Next != nil && cfg.Next(ctx) {\n\t\t\treturn h(ctx)\n\t\t}\n\n\t\ttimeout := cfg.Timeout\n\t\tif timeout <= 0 {\n\t\t\treturn h(ctx)\n\t\t}\n\n\t\t// Create timeout context - handler can check c.Context().Done()\n\t\tparent := ctx.Context()\n\t\ttCtx, cancel := context.WithTimeout(parent, timeout)\n\t\tctx.SetContext(tCtx)\n\n\t\t// Channels for handler result and panics\n\t\tdone := make(chan error, 1)\n\t\tpanicChan := make(chan any, 1)\n\n\t\t// Run handler in goroutine so we can race against the timeout\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif p := recover(); p != nil {\n\t\t\t\t\tlog.Errorw(\"panic recovered in timeout handler\", \"panic\", p, \"stack\", string(debug.Stack()))\n\t\t\t\t\tselect {\n\t\t\t\t\tcase panicChan <- p:\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// Middleware already returned, panic value discarded\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t\terr := h(ctx)\n\t\t\tselect {\n\t\t\tcase done <- err:\n\t\t\tdefault:\n\t\t\t\t// Middleware already returned, error discarded\n\t\t\t}\n\t\t}()\n\n\t\t// Wait for handler completion, panic, or timeout\n\t\tselect {\n\t\tcase err := <-done:\n\t\t\t// Handler finished normally - cleanup and return\n\t\t\tcancel()\n\t\t\tctx.SetContext(parent)\n\t\t\treturn handleResult(err, ctx, cfg)\n\n\t\tcase <-panicChan:\n\t\t\t// Handler panicked - cleanup and return error\n\t\t\tcancel()\n\t\t\tctx.SetContext(parent)\n\t\t\treturn fiber.ErrInternalServerError\n\n\t\tcase <-tCtx.Done():\n\t\t\t// Timeout occurred - abandon context and return immediately\n\t\t\t// The cleanup goroutine will cancel the timeout context once the handler finishes;\n\t\t\t// the abandoned fiber.Ctx stays out of the pool.\n\t\t\treturn handleTimeout(parent, ctx, cancel, done, panicChan, cfg)\n\t\t}\n\t}\n}\n\n// handleResult processes the handler's return value\nfunc handleResult(err error, ctx fiber.Ctx, cfg Config) error {\n\tif err != nil && isTimeoutError(err, cfg.Errors) {\n\t\treturn invokeOnTimeout(ctx, cfg)\n\t}\n\treturn err\n}\n\n// handleTimeout handles the timeout case using the Abandon mechanism\nfunc handleTimeout(\n\tparent context.Context,\n\tctx fiber.Ctx,\n\tcancel context.CancelFunc,\n\tdone <-chan error,\n\tpanicChan <-chan any,\n\tcfg Config,\n) error {\n\t// Mark fiber context as abandoned - ReleaseCtx will skip pooling.\n\t// The context will NOT be returned to the pool. This is an intentional\n\t// trade-off: we accept the small memory cost of not recycling timed-out\n\t// contexts in exchange for complete race-freedom.\n\t//\n\t// This is the same approach fasthttp uses - timed-out RequestCtx objects\n\t// are never returned to the pool (see fasthttp's releaseCtx which panics\n\t// if timeoutResponse is set).\n\tctx.Abandon()\n\n\t// Prepare the timeout response before marking the RequestCtx as timed out so\n\t// custom OnTimeout handlers can shape the response body.\n\ttimeoutErr := invokeOnTimeout(ctx, cfg)\n\n\t// If no OnTimeout handler is configured or the response is still the default\n\t// 200/empty, ensure a sensible timeout response is captured for fasthttp to send.\n\tif cfg.OnTimeout == nil || (ctx.Response().StatusCode() == fiber.StatusOK && len(ctx.Response().Body()) == 0) {\n\t\tctx.Response().SetStatusCode(fiber.StatusRequestTimeout)\n\t\tif len(ctx.Response().Body()) == 0 {\n\t\t\tctx.Response().SetBodyString(fiber.ErrRequestTimeout.Message)\n\t\t}\n\t}\n\n\t// Tell fasthttp to not recycle the RequestCtx - it will acquire a new one\n\t// for the response and send the captured payload (either default or from\n\t// OnTimeout). All ctx mutations after this call are ignored by fasthttp.\n\tctx.RequestCtx().TimeoutErrorWithResponse(&ctx.RequestCtx().Response)\n\n\t// Spawn cleanup goroutine that waits for handler to finish.\n\t// This only does context cleanup (cancel + restore parent), NOT ctx release.\n\t// The fiber.Ctx is intentionally NOT released to avoid races with requestHandler\n\t// which may still access ctx (e.g., ErrorHandler) after this function returns.\n\t// ForceRelease cannot be called safely here for the same reason.\n\tgo func() {\n\t\tselect {\n\t\tcase <-done:\n\t\tcase <-panicChan:\n\t\t}\n\t\t// Handler finished - cancel timeout context and restore parent\n\t\tcancel()\n\t\tctx.SetContext(parent)\n\n\t\t// TODO: Currently the ctx is not returned to the pool (memory leak for timed-out requests).\n\t\t// Future improvement: Implement a concurrent \"garbage collector\" list where abandoned\n\t\t// contexts are queued after both the handler AND requestHandler are done. A background\n\t\t// goroutine would periodically process this list and call ForceRelease() to recycle\n\t\t// the contexts safely. This would require tracking when requestHandler finishes\n\t\t// (e.g., via a channel signaled in ReleaseCtx) without adding per-request overhead\n\t\t// for non-timeout cases.\n\t}()\n\n\treturn timeoutErr\n}\n\n// invokeOnTimeout calls the OnTimeout handler if configured\nfunc invokeOnTimeout(ctx fiber.Ctx, cfg Config) error {\n\tif cfg.OnTimeout != nil {\n\t\treturn cfg.OnTimeout(ctx)\n\t}\n\treturn fiber.ErrRequestTimeout\n}\n\n// isTimeoutError checks if err is a timeout-like error (context.DeadlineExceeded\n// or any of the custom errors).\nfunc isTimeoutError(err error, customErrors []error) bool {\n\tif errors.Is(err, context.DeadlineExceeded) {\n\t\treturn true\n\t}\n\tif len(customErrors) > 0 {\n\t\tfor _, e := range customErrors {\n\t\t\tif errors.Is(err, e) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "middleware/timeout/timeout_test.go",
    "content": "package timeout\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nvar (\n\t// Custom error that we treat like a timeout when returned by the handler.\n\terrCustomTimeout = errors.New(\"custom timeout error\")\n\n\t// Some unrelated error that should NOT trigger a request timeout.\n\terrUnrelated = errors.New(\"unmatched error\")\n)\n\n// sleepWithContext simulates a task that takes `d` time, but returns `te` if the context is canceled.\nfunc sleepWithContext(ctx context.Context, d time.Duration, te error) error {\n\ttimer := time.NewTimer(d)\n\tdefer timer.Stop() // Clean up the timer\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn te\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n\n// TestTimeout_Success tests a handler that completes within the allotted timeout.\nfunc TestTimeout_Success(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// Our middleware wraps a handler that sleeps for 10ms, well under the 50ms limit.\n\tapp.Get(\"/fast\", New(func(c fiber.Ctx) error {\n\t\t// Simulate some work\n\t\tif err := sleepWithContext(c.Context(), 10*time.Millisecond, context.DeadlineExceeded); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.SendString(\"OK\")\n\t}, Config{Timeout: 50 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/fast\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Expected 200 OK for fast requests\")\n}\n\n// TestTimeout_Exceeded tests a handler that exceeds the provided timeout.\nfunc TestTimeout_Exceeded(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// This handler listens for context cancelation and returns early when timeout occurs.\n\tapp.Get(\"/slow\", New(func(c fiber.Ctx) error {\n\t\tif err := sleepWithContext(c.Context(), 200*time.Millisecond, context.DeadlineExceeded); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.SendString(\"Should never get here\")\n\t}, Config{Timeout: 50 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/slow\", http.NoBody)\n\tstart := time.Now()\n\tresp, err := app.Test(req)\n\telapsed := time.Since(start)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, \"Expected 408 Request Timeout\")\n\t// Handler should return shortly after timeout (not wait full 200ms)\n\trequire.Less(t, elapsed, 150*time.Millisecond, \"handler should return early on context cancelation\")\n}\n\n// TestTimeout_ContextPropagation verifies that the timeout context is properly\n// passed to the handler so it can detect cancelation (Issue #3671).\nfunc TestTimeout_ContextPropagation(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\terrCh := make(chan error, 1)\n\n\tapp.Get(\"/context-aware\", New(func(c fiber.Ctx) error {\n\t\ttimer := time.NewTimer(500 * time.Millisecond)\n\t\tdefer timer.Stop()\n\n\t\tselect {\n\t\tcase <-timer.C:\n\t\t\terrCh <- nil\n\t\t\treturn c.SendString(\"completed\")\n\n\t\tcase <-c.Context().Done():\n\t\t\terrCh <- c.Context().Err()\n\t\t\treturn c.Context().Err()\n\t\t}\n\t}, Config{Timeout: 50 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/context-aware\", http.NoBody)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\n\tsafety := time.NewTimer(1 * time.Second)\n\tdefer safety.Stop()\n\n\tselect {\n\tcase handlerErr := <-errCh:\n\t\trequire.ErrorIs(t, handlerErr, context.DeadlineExceeded, \"handler should report DeadlineExceeded\")\n\n\tcase <-safety.C:\n\t\tt.Fatal(\"timed out waiting for handler to report context state\")\n\t}\n}\n\n// TestTimeout_HandlerReturnsEarlyOnCancel verifies that handlers checking context\n// can return early, making the overall request faster than the handler's work time.\nfunc TestTimeout_HandlerReturnsEarlyOnCancel(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/early-return\", New(func(c fiber.Ctx) error {\n\t\t// Handler that would take 500ms but checks context\n\t\tfor i := 0; i < 50; i++ {\n\t\t\tselect {\n\t\t\tcase <-c.Context().Done():\n\t\t\t\treturn c.Context().Err()\n\t\t\tcase <-time.After(10 * time.Millisecond):\n\t\t\t\t// Continue work\n\t\t\t}\n\t\t}\n\t\treturn c.SendString(\"completed\")\n\t}, Config{Timeout: 30 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/early-return\", http.NoBody)\n\tstart := time.Now()\n\tresp, err := app.Test(req)\n\telapsed := time.Since(start)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\t// Should complete much faster than 500ms because handler checks context\n\trequire.Less(t, elapsed, 100*time.Millisecond)\n}\n\n// TestTimeout_CustomError tests that returning a user-defined error is also treated as a timeout.\nfunc TestTimeout_CustomError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\t// This handler sleeps 50ms and returns errCustomTimeout if canceled.\n\tapp.Get(\"/custom\", New(func(c fiber.Ctx) error {\n\t\t// Sleep might time out, or might return early. If the context is canceled,\n\t\t// we treat errCustomTimeout as a 'timeout-like' condition.\n\t\tif err := sleepWithContext(c.Context(), 200*time.Millisecond, errCustomTimeout); err != nil {\n\t\t\treturn fmt.Errorf(\"wrapped: %w\", err)\n\t\t}\n\t\treturn c.SendString(\"Should never get here\")\n\t}, Config{Timeout: 50 * time.Millisecond, Errors: []error{errCustomTimeout}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/custom\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, \"Expected 408 for custom timeout error\")\n}\n\n// TestTimeout_UnmatchedError checks that if the handler returns an error\n// that is neither a deadline exceeded nor a custom 'timeout' error, it is\n// propagated as a regular 500 (internal server error).\nfunc TestTimeout_UnmatchedError(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/unmatched\", New(func(_ fiber.Ctx) error {\n\t\treturn errUnrelated // Not in the custom error list\n\t}, Config{Timeout: 100 * time.Millisecond, Errors: []error{errCustomTimeout}}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/unmatched\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode,\n\t\t\"Expected 500 because the error is not recognized as a timeout error\")\n}\n\n// TestTimeout_ZeroDuration tests the edge case where the timeout is set to zero.\n// Usually this means the request can never exceed a 'deadline' – effectively no timeout.\nfunc TestTimeout_ZeroDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/zero\", New(func(c fiber.Ctx) error {\n\t\t// Sleep 50ms, but there's no real 'deadline' since zero-timeout.\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\treturn c.SendString(\"No timeout used\")\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/zero\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Expected 200 OK with zero timeout\")\n}\n\n// TestTimeout_NegativeDuration ensures negative timeout values fall back to zero.\nfunc TestTimeout_NegativeDuration(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/negative\", New(func(c fiber.Ctx) error {\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\treturn c.SendString(\"No timeout used\")\n\t}, Config{Timeout: -100 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/negative\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err, \"app.Test(req) should not fail\")\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Expected 200 OK with zero timeout\")\n}\n\n// TestTimeout_CustomHandler ensures that a custom handler runs on timeout.\nfunc TestTimeout_CustomHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tvar called atomic.Int32\n\n\tapp.Get(\"/custom-handler\", New(func(c fiber.Ctx) error {\n\t\tif err := sleepWithContext(c.Context(), 100*time.Millisecond, context.DeadlineExceeded); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn c.SendString(\"should not reach\")\n\t}, Config{\n\t\tTimeout: 20 * time.Millisecond,\n\t\tOnTimeout: func(c fiber.Ctx) error {\n\t\t\tcalled.Add(1)\n\t\t\treturn c.Status(408).JSON(fiber.Map{\"error\": \"timeout\"})\n\t\t},\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/custom-handler\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\trequire.Equal(t, int32(1), called.Load())\n\n\tbody, readErr := io.ReadAll(resp.Body)\n\trequire.NoError(t, readErr)\n\trequire.JSONEq(t, `{\"error\":\"timeout\"}`, string(body))\n}\n\n// TestTimeout_PanicInHandler verifies that panics in the handler return 500.\nfunc TestTimeout_PanicInHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/panic\", New(func(_ fiber.Ctx) error {\n\t\tpanic(\"test panic\")\n\t}, Config{Timeout: 100 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/panic\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\t// Panic in handler results in 500 Internal Server Error\n\trequire.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n}\n\n// TestIsTimeoutError_DeadlineExceeded ensures context.DeadlineExceeded triggers timeout.\nfunc TestIsTimeoutError_DeadlineExceeded(t *testing.T) {\n\tt.Parallel()\n\n\trequire.True(t, isTimeoutError(context.DeadlineExceeded, nil))\n\trequire.True(t, isTimeoutError(fmt.Errorf(\"wrap: %w\", context.DeadlineExceeded), nil))\n}\n\n// TestIsTimeoutError_CustomErrors verifies custom errors are detected.\nfunc TestIsTimeoutError_CustomErrors(t *testing.T) {\n\tt.Parallel()\n\n\tcustomErr := errors.New(\"custom timeout\")\n\trequire.True(t, isTimeoutError(customErr, []error{customErr}))\n\trequire.True(t, isTimeoutError(fmt.Errorf(\"wrap: %w\", customErr), []error{customErr}))\n\trequire.False(t, isTimeoutError(errUnrelated, []error{customErr}))\n}\n\n// TestIsTimeoutError_WithOnTimeout verifies that custom OnTimeout is called for custom errors.\nfunc TestIsTimeoutError_WithOnTimeout(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(ctx)\n\n\tcalled := false\n\tcfg := Config{\n\t\tTimeout: 100 * time.Millisecond,\n\t\tErrors:  []error{errCustomTimeout},\n\t\tOnTimeout: func(_ fiber.Ctx) error {\n\t\t\tcalled = true\n\t\t\treturn errors.New(\"handled\")\n\t\t},\n\t}\n\n\t// Test via full middleware to ensure OnTimeout is called\n\thandler := New(func(_ fiber.Ctx) error {\n\t\treturn fmt.Errorf(\"wrap: %w\", errCustomTimeout)\n\t}, cfg)\n\n\terr := handler(ctx)\n\trequire.True(t, called)\n\trequire.EqualError(t, err, \"handled\")\n}\n\n// TestTimeout_ImmediateReturn verifies that the middleware returns immediately on timeout\n// without waiting for the handler to finish (using Abandon mechanism).\nfunc TestTimeout_ImmediateReturn(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thandlerStarted := make(chan struct{})\n\thandlerDone := make(chan struct{})\n\n\tapp.Get(\"/immediate\", New(func(_ fiber.Ctx) error {\n\t\tclose(handlerStarted)\n\t\t// Handler takes 500ms but middleware should return after 20ms\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tclose(handlerDone)\n\t\treturn nil\n\t}, Config{Timeout: 20 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/immediate\", http.NoBody)\n\tstart := time.Now()\n\tresp, err := app.Test(req)\n\telapsed := time.Since(start)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\t// Middleware should return immediately after timeout, not wait 500ms\n\trequire.Less(t, elapsed, 100*time.Millisecond, \"middleware should return immediately on timeout\")\n\n\t// Wait for handler to verify it was abandoned properly\n\t<-handlerStarted\n\tselect {\n\tcase <-handlerDone:\n\t\t// Handler finished - cleanup goroutine should have released context\n\tcase <-time.After(1 * time.Second):\n\t\tt.Log(\"Handler still running (expected for abandoned context)\")\n\t}\n}\n\n// TestTimeout_PanicAfterTimeout ensures panics after a timeout are handled.\nfunc TestTimeout_PanicAfterTimeout(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tpanicDone := make(chan struct{})\n\tapp.Get(\"/panic-after-timeout\", New(func(c fiber.Ctx) error {\n\t\t<-c.Context().Done()\n\t\tdefer close(panicDone)\n\t\tpanic(\"panic after timeout\")\n\t}, Config{Timeout: 20 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/panic-after-timeout\", http.NoBody)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\t// With immediate return, we get 408 (not 500) because panic happens after middleware returned\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\n\t// Wait for panic to occur and be handled by cleanup goroutine\n\tselect {\n\tcase <-panicDone:\n\tcase <-time.After(200 * time.Millisecond):\n\t\tt.Fatal(\"panic did not occur\")\n\t}\n}\n\n// TestTimeout_Next verifies the Next function skips the middleware.\nfunc TestTimeout_Next(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\tapp.Get(\"/skip\", New(func(c fiber.Ctx) error {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn c.SendString(\"OK\")\n\t}, Config{\n\t\tTimeout: 10 * time.Millisecond,\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true // Always skip\n\t\t},\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/skip\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusOK, resp.StatusCode, \"Middleware should be skipped\")\n}\n\n// TestTimeout_ContextCleanup verifies that the context is properly released\n// after the handler finishes (even after timeout).\nfunc TestTimeout_ContextCleanup(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\n\thandlerDone := make(chan struct{})\n\tapp.Get(\"/cleanup\", New(func(c fiber.Ctx) error {\n\t\tdefer close(handlerDone)\n\t\t<-c.Context().Done()\n\t\t// Small delay to simulate cleanup\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\treturn nil\n\t}, Config{Timeout: 20 * time.Millisecond}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/cleanup\", http.NoBody)\n\tresp, err := app.Test(req)\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode)\n\n\t// Wait for handler to finish - cleanup goroutine should release context\n\tselect {\n\tcase <-handlerDone:\n\t\t// Give cleanup goroutine time to run\n\t\ttime.Sleep(20 * time.Millisecond)\n\tcase <-time.After(200 * time.Millisecond):\n\t\tt.Fatal(\"handler did not finish\")\n\t}\n}\n\n// TestTimeout_AbandonMechanism verifies the Abandon mechanism works correctly.\nfunc TestTimeout_AbandonMechanism(t *testing.T) {\n\tt.Parallel()\n\tapp := fiber.New()\n\tctx := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tt.Cleanup(ctx.ForceRelease)\n\n\t// Initially not abandoned\n\trequire.False(t, ctx.IsAbandoned())\n\n\t// Abandon it\n\tctx.Abandon()\n\trequire.True(t, ctx.IsAbandoned())\n\n\t// ReleaseCtx should be a no-op when abandoned\n\tapp.ReleaseCtx(ctx)\n\trequire.True(t, ctx.IsAbandoned(), \"ReleaseCtx should not release abandoned context\")\n\n\t// Note: We intentionally do NOT test ForceRelease here.\n\t// In the timeout middleware, abandoned contexts are NOT released back to the pool\n\t// to avoid race conditions with requestHandler. This is the same approach\n\t// fasthttp uses for timed-out RequestCtx objects.\n}\n"
  },
  {
    "path": "mount.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// Put fields related to mounting.\ntype mountFields struct {\n\t// Mounted and main apps\n\tappList map[string]*App\n\t// Prefix of app if it was mounted\n\tmountPath string\n\t// Ordered keys of apps (sorted by key length for Render)\n\tappListKeys []string\n\t// check added routes of sub-apps\n\tsubAppsRoutesAdded sync.Once\n\t// check mounted sub-apps\n\tsubAppsProcessed sync.Once\n}\n\n// Create empty mountFields instance\nfunc newMountFields(app *App) *mountFields {\n\treturn &mountFields{\n\t\tappList:     map[string]*App{\"\": app},\n\t\tappListKeys: make([]string, 0),\n\t}\n}\n\n// Mount attaches another app instance as a sub-router along a routing path.\n// It's very useful to split up a large API as many independent routers and\n// compose them as a single service using Mount. The fiber's error handler and\n// any of the fiber's sub apps are added to the application's error handlers\n// to be invoked on errors that happen within the prefix route.\nfunc (app *App) mount(prefix string, subApp *App) Router {\n\tprefix = utils.TrimRight(prefix, '/')\n\tif prefix == \"\" {\n\t\tprefix = \"/\"\n\t}\n\n\tapp.mutex.Lock()\n\t// Support for configs of mounted-apps and sub-mounted-apps\n\tfor mountedPrefixes, subApp := range subApp.mountFields.appList {\n\t\tpath := getGroupPath(prefix, mountedPrefixes)\n\n\t\tsubApp.mountFields.mountPath = path\n\t\tapp.mountFields.appList[path] = subApp\n\t}\n\tapp.mutex.Unlock()\n\n\t// register mounted group\n\tmountGroup := &Group{Prefix: prefix, app: subApp}\n\tapp.register([]string{methodUse}, prefix, mountGroup)\n\n\t// Execute onMount hooks\n\tif err := subApp.hooks.executeOnMountHooks(app); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn app\n}\n\n// Mount attaches another app instance as a sub-router along a routing path.\n// It's very useful to split up a large API as many independent routers and\n// compose them as a single service using Mount.\nfunc (grp *Group) mount(prefix string, subApp *App) Router {\n\tgroupPath := getGroupPath(grp.Prefix, prefix)\n\tgroupPath = utils.TrimRight(groupPath, '/')\n\tif groupPath == \"\" {\n\t\tgroupPath = \"/\"\n\t}\n\n\tgrp.app.mutex.Lock()\n\t// Support for configs of mounted-apps and sub-mounted-apps\n\tfor mountedPrefixes, subApp := range subApp.mountFields.appList {\n\t\tpath := getGroupPath(groupPath, mountedPrefixes)\n\n\t\tsubApp.mountFields.mountPath = path\n\t\tgrp.app.mountFields.appList[path] = subApp\n\t}\n\tgrp.app.mutex.Unlock()\n\n\t// register mounted group\n\tmountGroup := &Group{Prefix: groupPath, app: subApp}\n\tgrp.app.register([]string{methodUse}, groupPath, mountGroup)\n\n\t// Execute onMount hooks\n\tif err := subApp.hooks.executeOnMountHooks(grp.app); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn grp\n}\n\n// MountPath returns the route pattern where the current app instance was mounted as a sub-application.\nfunc (app *App) MountPath() string {\n\treturn app.mountFields.mountPath\n}\n\n// hasMountedApps Checks if there are any mounted apps in the current application.\nfunc (app *App) hasMountedApps() bool {\n\treturn len(app.mountFields.appList) > 1\n}\n\n// mountStartupProcess Handles the startup process of mounted apps by appending sub-app routes, generating app list keys, and processing sub-app routes.\nfunc (app *App) mountStartupProcess() {\n\tif app.hasMountedApps() {\n\t\t// add routes of sub-apps\n\t\tapp.mountFields.subAppsProcessed.Do(func() {\n\t\t\tapp.appendSubAppLists(app.mountFields.appList)\n\t\t\tapp.generateAppListKeys()\n\t\t})\n\t\t// adds the routes of the sub-apps to the current application.\n\t\tapp.mountFields.subAppsRoutesAdded.Do(func() {\n\t\t\tapp.processSubAppsRoutes()\n\t\t})\n\t}\n}\n\n// generateAppListKeys generates app list keys for Render, should work after appendSubAppLists\nfunc (app *App) generateAppListKeys() {\n\tfor key := range app.mountFields.appList {\n\t\tapp.mountFields.appListKeys = append(app.mountFields.appListKeys, key)\n\t}\n\n\tsort.Slice(app.mountFields.appListKeys, func(i, j int) bool {\n\t\treturn len(app.mountFields.appListKeys[i]) < len(app.mountFields.appListKeys[j])\n\t})\n}\n\n// appendSubAppLists supports nested for sub apps\nfunc (app *App) appendSubAppLists(appList map[string]*App, parent ...string) {\n\t// Optimize: Cache parent prefix\n\tparentPrefix := \"\"\n\tif len(parent) > 0 {\n\t\tparentPrefix = parent[0]\n\t}\n\n\tfor prefix, subApp := range appList {\n\t\t// skip real app\n\t\tif prefix == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif parentPrefix != \"\" {\n\t\t\tprefix = getGroupPath(parentPrefix, prefix)\n\t\t}\n\n\t\tif _, ok := app.mountFields.appList[prefix]; !ok {\n\t\t\tapp.mountFields.appList[prefix] = subApp\n\t\t}\n\n\t\t// The first element of appList is always the app itself. If there are no other sub apps, we should skip appending nested apps.\n\t\tif len(subApp.mountFields.appList) > 1 {\n\t\t\tapp.appendSubAppLists(subApp.mountFields.appList, prefix)\n\t\t}\n\t}\n}\n\n// processSubAppsRoutes adds routes of sub-apps recursively when the server is started\nfunc (app *App) processSubAppsRoutes() {\n\tfor prefix, subApp := range app.mountFields.appList {\n\t\t// skip real app\n\t\tif prefix == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// process the inner routes\n\t\tif subApp.hasMountedApps() {\n\t\t\tsubApp.mountFields.subAppsRoutesAdded.Do(func() {\n\t\t\t\tsubApp.processSubAppsRoutes()\n\t\t\t})\n\t\t}\n\t}\n\tvar handlersCount uint32\n\t// Iterate over the stack of the parent app\n\tfor m := range app.stack {\n\t\t// Iterate over each route in the stack\n\t\tstackLen := len(app.stack[m])\n\t\tfor i := 0; i < stackLen; i++ {\n\t\t\troute := app.stack[m][i]\n\t\t\t// Check if the route has a mounted app\n\t\t\tif !route.mount {\n\t\t\t\tif !route.use || (route.use && m == 0) {\n\t\t\t\t\thandlersCount += uint32(len(route.Handlers)) //nolint:gosec // G115 - handler count is always small\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Create a slice to hold the sub-app's routes\n\t\t\tsubRoutes := make([]*Route, len(route.group.app.stack[m]))\n\n\t\t\t// Iterate over the sub-app's routes\n\t\t\tfor j, subAppRoute := range route.group.app.stack[m] {\n\t\t\t\t// Clone the sub-app's route\n\t\t\t\tsubAppRouteClone := app.copyRoute(subAppRoute)\n\n\t\t\t\t// Add the parent route's path as a prefix to the sub-app's route\n\t\t\t\tapp.addPrefixToRoute(route.path, subAppRouteClone)\n\n\t\t\t\t// Add the cloned sub-app's route to the slice of sub-app routes\n\t\t\t\tsubRoutes[j] = subAppRouteClone\n\t\t\t}\n\n\t\t\t// Insert the sub-app's routes into the parent app's stack\n\t\t\tnewStack := make([]*Route, len(app.stack[m])+len(subRoutes)-1)\n\t\t\tcopy(newStack[:i], app.stack[m][:i])\n\t\t\tcopy(newStack[i:i+len(subRoutes)], subRoutes)\n\t\t\tcopy(newStack[i+len(subRoutes):], app.stack[m][i+1:])\n\t\t\tapp.stack[m] = newStack\n\n\t\t\ti--\n\n\t\t\t// Mark the parent app's routes as refreshed\n\t\t\tapp.routesRefreshed = true\n\t\t\t// update stackLen after appending subRoutes to app.stack[m]\n\t\t\tstackLen = len(app.stack[m])\n\t\t}\n\t}\n\tatomic.StoreUint32(&app.handlersCount, handlersCount)\n}\n"
  },
  {
    "path": "mount_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -run Test_App_Mount\nfunc Test_App_Mount(t *testing.T) {\n\tt.Parallel()\n\tmicro := New()\n\tmicro.Get(\"/doe\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp := New()\n\tapp.Use(\"/john\", micro)\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/john/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, uint32(2), app.handlersCount)\n}\n\nfunc Test_App_Mount_RootPath_Nested(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tdynamic := New()\n\tapiserver := New()\n\n\tapiroutes := apiserver.Group(\"/v1\")\n\tapiroutes.Get(\"/home\", func(c Ctx) error {\n\t\treturn c.SendString(\"home\")\n\t})\n\n\tdynamic.Use(\"/api\", apiserver)\n\tapp.Use(\"/\", dynamic)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api/v1/home\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, uint32(2), app.handlersCount)\n}\n\n// go test -run Test_App_Mount_Nested\nfunc Test_App_Mount_Nested(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tone := New()\n\ttwo := New()\n\tthree := New()\n\n\ttwo.Use(\"/three\", three)\n\tapp.Use(\"/one\", one)\n\tone.Use(\"/two\", two)\n\n\tone.Get(\"/doe\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\ttwo.Get(\"/nested\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tthree.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/one/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/one/two/nested\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/one/two/three/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\trequire.Equal(t, uint32(6), app.handlersCount)\n}\n\n// go test -run Test_App_Mount_Express_Behavior\nfunc Test_App_Mount_Express_Behavior(t *testing.T) {\n\tt.Parallel()\n\tcreateTestHandler := func(body string) func(c Ctx) error {\n\t\treturn func(c Ctx) error {\n\t\t\treturn c.SendString(body)\n\t\t}\n\t}\n\ttestEndpoint := func(app *App, route, expectedBody string, expectedStatusCode int) {\n\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody))\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, expectedStatusCode, resp.StatusCode, \"Status code\")\n\t\trequire.Equal(t, expectedBody, string(body), \"Unexpected response body\")\n\t}\n\n\tapp := New()\n\tsubApp := New()\n\n\t// app setup\n\tsubApp.Get(\"/hello\", createTestHandler(\"subapp hello!\"))\n\tsubApp.Get(\"/world\", createTestHandler(\"subapp world!\")) // <- wins\n\n\tapp.Get(\"/hello\", createTestHandler(\"app hello!\")) // <- wins\n\tapp.Use(\"/\", subApp)                               // <- subApp registration\n\tapp.Get(\"/world\", createTestHandler(\"app world!\"))\n\n\tapp.Get(\"/bar\", createTestHandler(\"app bar!\"))\n\tsubApp.Get(\"/bar\", createTestHandler(\"subapp bar!\")) // <- wins\n\n\tsubApp.Get(\"/foo\", createTestHandler(\"subapp foo!\")) // <- wins\n\tapp.Get(\"/foo\", createTestHandler(\"app foo!\"))\n\n\t// 404 Handler\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.SendStatus(StatusNotFound)\n\t})\n\t// expectation check\n\ttestEndpoint(app, \"/world\", \"subapp world!\", StatusOK)\n\ttestEndpoint(app, \"/hello\", \"app hello!\", StatusOK)\n\ttestEndpoint(app, \"/bar\", \"subapp bar!\", StatusOK)\n\ttestEndpoint(app, \"/foo\", \"subapp foo!\", StatusOK)\n\ttestEndpoint(app, \"/unknown\", ErrNotFound.Message, StatusNotFound)\n\n\trequire.Equal(t, uint32(17), app.handlersCount)\n}\n\n// go test -run Test_App_Mount_RoutePositions\nfunc Test_App_Mount_RoutePositions(t *testing.T) {\n\tt.Parallel()\n\ttestEndpoint := func(app *App, route, expectedBody string) {\n\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody))\n\t\trequire.NoError(t, err, \"app.Test(req)\")\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\t\trequire.Equal(t, expectedBody, string(body), \"Unexpected response body\")\n\t}\n\n\tapp := New()\n\tsubApp1 := New()\n\tsubApp2 := New()\n\t// app setup\n\t{\n\t\tapp.Use(func(c Ctx) error {\n\t\t\t// set initial value\n\t\t\tc.Locals(\"world\", \"world\")\n\t\t\treturn c.Next()\n\t\t})\n\t\tapp.Use(\"/subApp1\", subApp1)\n\t\tapp.Use(func(c Ctx) error {\n\t\t\treturn c.Next()\n\t\t})\n\t\tapp.Get(\"/bar\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\t\tapp.Use(func(c Ctx) error {\n\t\t\t// is overwritten when the positioning is not correct\n\t\t\tc.Locals(\"world\", \"hello\")\n\t\t\treturn c.Next()\n\t\t})\n\t\tmethods := subApp2.Group(\"/subApp2\")\n\t\tmethods.Get(\"/world\", func(c Ctx) error {\n\t\t\tv, ok := c.Locals(\"world\").(string)\n\t\t\tif !ok {\n\t\t\t\tpanic(\"unexpected data type\")\n\t\t\t}\n\t\t\treturn c.SendString(v)\n\t\t})\n\t\tapp.Use(\"\", subApp2)\n\t}\n\n\ttestEndpoint(app, \"/subApp2/world\", \"hello\")\n\n\trouteStackGET := app.Stack()[0]\n\trequire.True(t, routeStackGET[0].use)\n\trequire.Equal(t, \"/\", routeStackGET[0].path)\n\n\trequire.True(t, routeStackGET[1].use)\n\trequire.Equal(t, \"/\", routeStackGET[1].path)\n\n\trequire.False(t, routeStackGET[2].use)\n\trequire.Equal(t, \"/bar\", routeStackGET[2].path)\n\n\trequire.True(t, routeStackGET[3].use)\n\trequire.Equal(t, \"/\", routeStackGET[3].path)\n\n\trequire.False(t, routeStackGET[4].use)\n\trequire.Equal(t, \"/subapp2/world\", routeStackGET[4].path)\n\n\trequire.Len(t, routeStackGET, 5)\n}\n\n// go test -run Test_App_MountPath\nfunc Test_App_MountPath(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tone := New()\n\ttwo := New()\n\tthree := New()\n\n\ttwo.Use(\"/three\", three)\n\tone.Use(\"/two\", two)\n\tapp.Use(\"/one\", one)\n\n\trequire.Equal(t, \"/one\", one.MountPath())\n\trequire.Equal(t, \"/one/two\", two.MountPath())\n\trequire.Equal(t, \"/one/two/three\", three.MountPath())\n\trequire.Empty(t, app.MountPath())\n}\n\nfunc Test_App_ErrorHandler_GroupMount(t *testing.T) {\n\tt.Parallel()\n\tmicro := New(Config{\n\t\tErrorHandler: func(c Ctx, err error) error {\n\t\t\trequire.Equal(t, \"0: GET error\", err.Error())\n\t\t\treturn c.Status(500).SendString(\"1: custom error\")\n\t\t},\n\t})\n\tmicro.Get(\"/doe\", func(_ Ctx) error {\n\t\treturn errors.New(\"0: GET error\")\n\t})\n\n\tapp := New()\n\tv1 := app.Group(\"/v1\")\n\tv1.Use(\"/john\", micro)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/john/doe\", http.NoBody))\n\ttestErrorResponse(t, err, resp, \"1: custom error\")\n}\n\nfunc Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) {\n\tt.Parallel()\n\tmicro := New(Config{\n\t\tErrorHandler: func(c Ctx, err error) error {\n\t\t\trequire.Equal(t, \"0: GET error\", err.Error())\n\t\t\treturn c.Status(500).SendString(\"1: custom error\")\n\t\t},\n\t})\n\tmicro.Get(\"/john/doe\", func(_ Ctx) error {\n\t\treturn errors.New(\"0: GET error\")\n\t})\n\n\tapp := New()\n\tv1 := app.Group(\"/v1\")\n\tv1.Use(\"/\", micro)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/john/doe\", http.NoBody))\n\ttestErrorResponse(t, err, resp, \"1: custom error\")\n}\n\n// go test -run Test_App_Group_Mount\nfunc Test_App_Group_Mount(t *testing.T) {\n\tt.Parallel()\n\tmicro := New()\n\tmicro.Get(\"/doe\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp := New()\n\tv1 := app.Group(\"/v1\")\n\tv1.Use(\"/john\", micro)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/john/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\trequire.Equal(t, uint32(2), app.handlersCount)\n}\n\nfunc Test_App_UseParentErrorHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := New(Config{\n\t\tErrorHandler: func(ctx Ctx, _ error) error {\n\t\t\treturn ctx.Status(500).SendString(\"hi, i'm a custom error\")\n\t\t},\n\t})\n\n\tfiber := New()\n\tfiber.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\n\tapp.Use(\"/api\", fiber)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api\", http.NoBody))\n\ttestErrorResponse(t, err, resp, \"hi, i'm a custom error\")\n}\n\nfunc Test_App_UseMountedErrorHandler(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tfiber := New(Config{\n\t\tErrorHandler: func(c Ctx, _ error) error {\n\t\t\treturn c.Status(500).SendString(\"hi, i'm a custom error\")\n\t\t},\n\t})\n\tfiber.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\n\tapp.Use(\"/api\", fiber)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api\", http.NoBody))\n\ttestErrorResponse(t, err, resp, \"hi, i'm a custom error\")\n}\n\nfunc Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tfiber := New(Config{\n\t\tErrorHandler: func(c Ctx, _ error) error {\n\t\t\treturn c.Status(500).SendString(\"hi, i'm a custom error\")\n\t\t},\n\t})\n\tfiber.Get(\"/api\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\n\tapp.Use(\"/\", fiber)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api\", http.NoBody))\n\ttestErrorResponse(t, err, resp, \"hi, i'm a custom error\")\n}\n\nfunc Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\ttsf := func(c Ctx, _ error) error {\n\t\treturn c.Status(200).SendString(\"hi, i'm a custom sub fiber error 2\")\n\t}\n\ttripleSubFiber := New(Config{\n\t\tErrorHandler: tsf,\n\t})\n\ttripleSubFiber.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\n\tsf := func(c Ctx, _ error) error {\n\t\treturn c.Status(200).SendString(\"hi, i'm a custom sub fiber error\")\n\t}\n\tsubfiber := New(Config{\n\t\tErrorHandler: sf,\n\t})\n\tsubfiber.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\tsubfiber.Use(\"/third\", tripleSubFiber)\n\n\tf := func(c Ctx, _ error) error {\n\t\treturn c.Status(200).SendString(\"hi, i'm a custom error\")\n\t}\n\tfiber := New(Config{\n\t\tErrorHandler: f,\n\t})\n\tfiber.Get(\"/\", func(_ Ctx) error {\n\t\treturn errors.New(\"something happened\")\n\t})\n\tfiber.Use(\"/sub\", subfiber)\n\n\tapp.Use(\"/api\", fiber)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api/sub\", http.NoBody))\n\trequire.NoError(t, err, \"/api/sub req\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tb, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"iotuil.ReadAll()\")\n\trequire.Equal(t, \"hi, i'm a custom sub fiber error\", string(b), \"Response body\")\n\n\tresp2, err := app.Test(httptest.NewRequest(MethodGet, \"/api/sub/third\", http.NoBody))\n\trequire.NoError(t, err, \"/api/sub/third req\")\n\trequire.Equal(t, 200, resp2.StatusCode, \"Status code\")\n\n\tb, err = io.ReadAll(resp2.Body)\n\trequire.NoError(t, err, \"iotuil.ReadAll()\")\n\trequire.Equal(t, \"hi, i'm a custom sub fiber error 2\", string(b), \"Third fiber Response body\")\n}\n\n// go test -run Test_Mount_Route_Names\nfunc Test_Mount_Route_Names(t *testing.T) {\n\tt.Parallel()\n\t// create sub-app with 2 handlers:\n\tsubApp1 := New()\n\tsubApp1.Get(\"/users\", func(c Ctx) error {\n\t\turl, err := c.GetRouteURL(\"add-user\", Map{})\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"/app1/users\", url, \"handler: app1.add-user\") // the prefix is /app1 because of the mount\n\t\t// if subApp1 is not mounted, expected url just /users\n\t\treturn nil\n\t}).Name(\"get-users\")\n\tsubApp1.Post(\"/users\", func(c Ctx) error {\n\t\troute := c.App().GetRoute(\"get-users\")\n\t\trequire.Equal(t, MethodGet, route.Method, \"handler: app1.get-users method\")\n\t\trequire.Equal(t, \"/app1/users\", route.Path, \"handler: app1.get-users path\")\n\t\treturn nil\n\t}).Name(\"add-user\")\n\n\t// create sub-app with 2 handlers inside a group:\n\tsubApp2 := New()\n\tapp2Grp := subApp2.Group(\"/users\").Name(\"users.\")\n\tapp2Grp.Get(\"\", emptyHandler).Name(\"get\")\n\tapp2Grp.Post(\"\", emptyHandler).Name(\"add\")\n\n\t// put both sub-apps into root app\n\trootApp := New()\n\t_ = rootApp.Use(\"/app1\", subApp1)\n\t_ = rootApp.Use(\"/app2\", subApp2)\n\n\trootApp.startupProcess()\n\n\t// take route directly from sub-app\n\troute := subApp1.GetRoute(\"get-users\")\n\trequire.Equal(t, MethodGet, route.Method)\n\trequire.Equal(t, \"/users\", route.Path)\n\n\troute = subApp1.GetRoute(\"add-user\")\n\trequire.Equal(t, MethodPost, route.Method)\n\trequire.Equal(t, \"/users\", route.Path)\n\n\t// take route directly from sub-app with group\n\troute = subApp2.GetRoute(\"users.get\")\n\trequire.Equal(t, MethodGet, route.Method)\n\trequire.Equal(t, \"/users\", route.Path)\n\n\troute = subApp2.GetRoute(\"users.add\")\n\trequire.Equal(t, MethodPost, route.Method)\n\trequire.Equal(t, \"/users\", route.Path)\n\n\t// take route from root app (using names of sub-apps)\n\troute = rootApp.GetRoute(\"add-user\")\n\trequire.Equal(t, MethodPost, route.Method)\n\trequire.Equal(t, \"/app1/users\", route.Path)\n\n\troute = rootApp.GetRoute(\"users.add\")\n\trequire.Equal(t, MethodPost, route.Method)\n\trequire.Equal(t, \"/app2/users\", route.Path)\n\n\t// GetRouteURL inside handler\n\treq := httptest.NewRequest(MethodGet, \"/app1/users\", http.NoBody)\n\tresp, err := rootApp.Test(req)\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// ctx.App().GetRoute() inside handler\n\treq = httptest.NewRequest(MethodPost, \"/app1/users\", http.NoBody)\n\tresp, err = rootApp.Test(req)\n\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n}\n\n// go test -run Test_Ctx_Render_Mount\nfunc Test_Ctx_Render_Mount(t *testing.T) {\n\tt.Parallel()\n\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(t, err)\n\n\tsub := New(Config{\n\t\tViews: engine,\n\t})\n\n\tsub.Get(\"/:name\", func(c Ctx) error {\n\t\treturn c.Render(\"hello_world.tmpl\", Map{\n\t\t\t\"Name\": c.Params(\"name\"),\n\t\t})\n\t})\n\n\tapp := New()\n\tapp.Use(\"/hello\", sub)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/hello/a\", http.NoBody))\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.NoError(t, err, \"app.Test(req)\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello a!</h1>\", string(body))\n}\n\n// go test -run Test_Ctx_Render_Mount_ParentOrSubHasViews\nfunc Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) {\n\tt.Parallel()\n\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(t, err)\n\n\tengine2 := &testTemplateEngine{path: \"testdata2\"}\n\terr = engine2.Load()\n\trequire.NoError(t, err)\n\n\tengine3 := &testTemplateEngine{path: \"testdata3\"}\n\terr = engine3.Load()\n\trequire.NoError(t, err)\n\n\tsub := New(Config{\n\t\tViews: engine3,\n\t})\n\n\tsub2 := New(Config{\n\t\tViews: engine2,\n\t})\n\n\tapp := New(Config{\n\t\tViews: engine,\n\t})\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.Render(\"index.tmpl\", Map{\n\t\t\t\"Title\": \"Hello, World!\",\n\t\t})\n\t})\n\n\tsub.Get(\"/world/:name\", func(c Ctx) error {\n\t\treturn c.Render(\"hello_world.tmpl\", Map{\n\t\t\t\"Name\": c.Params(\"name\"),\n\t\t})\n\t})\n\n\tsub2.Get(\"/moment\", func(c Ctx) error {\n\t\treturn c.Render(\"bruh.tmpl\", Map{})\n\t})\n\n\tsub.Use(\"/bruh\", sub2)\n\tapp.Use(\"/hello\", sub)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/hello/world/a\", http.NoBody))\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.NoError(t, err, \"app.Test(req)\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello a!</h1>\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.NoError(t, err, \"app.Test(req)\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello, World!</h1>\", string(body))\n\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/hello/bruh/moment\", http.NoBody))\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\trequire.NoError(t, err, \"app.Test(req)\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>I'm Bruh</h1>\", string(body))\n}\n\nfunc Test_Ctx_Render_MountGroup(t *testing.T) {\n\tt.Parallel()\n\n\tengine := &testTemplateEngine{}\n\terr := engine.Load()\n\trequire.NoError(t, err)\n\n\tmicro := New(Config{\n\t\tViews: engine,\n\t})\n\n\tmicro.Get(\"/doe\", func(c Ctx) error {\n\t\treturn c.Render(\"hello_world.tmpl\", Map{\n\t\t\t\"Name\": \"doe\",\n\t\t})\n\t})\n\n\tapp := New()\n\tv1 := app.Group(\"/v1\")\n\tv1.Use(\"/john\", micro)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/john/doe\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"<h1>Hello doe!</h1>\", string(body))\n}\n"
  },
  {
    "path": "path.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📄 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n// ⚠️ This path parser was inspired by https://github.com/ucarion/urlpath\n// 💖 Maintained and modified for Fiber by @renewerner87\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/google/uuid\"\n)\n\n// routeParser holds the path segments and param names\ntype routeParser struct {\n\tsegs          []*routeSegment // the parsed segments of the route\n\tparams        []string        // that parameter names the parsed route\n\twildCardCount int             // number of wildcard parameters, used internally to give the wildcard parameter its number\n\tplusCount     int             // number of plus parameters, used internally to give the plus parameter its number\n}\n\nvar routerParserPool = &sync.Pool{\n\tNew: func() any {\n\t\treturn &routeParser{}\n\t},\n}\n\n// routeSegment holds the segment metadata\ntype routeSegment struct {\n\t// const information\n\tConst       string        // constant part of the route\n\tParamName   string        // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added\n\tComparePart string        // search part to find the end of the parameter\n\tConstraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default\n\tPartCount   int           // how often is the search part contained in the non-param segments? -> necessary for greedy search\n\tLength      int           // length of the parameter for segment, when its 0 then the length is undetermined\n\t// future TODO: add support for optional groups \"/abc(/def)?\"\n\t// parameter information\n\tIsParam    bool // Truth value that indicates whether it is a parameter or a constant part\n\tIsGreedy   bool // indicates whether the parameter is greedy or not, is used with wildcard and plus\n\tIsOptional bool // indicates whether the parameter is optional or not\n\t// common information\n\tIsLast           bool // shows if the segment is the last one for the route\n\tHasOptionalSlash bool // segment has the possibility of an optional slash\n}\n\n// different special routing signs\nconst (\n\twildcardParam                byte = '*'  // indicates an optional greedy parameter\n\tplusParam                    byte = '+'  // indicates a required greedy parameter\n\toptionalParam                byte = '?'  // concludes a parameter by name and makes it optional\n\tparamStarterChar             byte = ':'  // start character for a parameter with name\n\tslashDelimiter               byte = '/'  // separator for the route, unlike the other delimiters this character at the end can be optional\n\tescapeChar                   byte = '\\\\' // escape character\n\tparamConstraintStart         byte = '<'  // start of type constraint for a parameter\n\tparamConstraintEnd           byte = '>'  // end of type constraint for a parameter\n\tparamConstraintSeparator     byte = ';'  // separator of type constraints for a parameter\n\tparamConstraintDataStart     byte = '('  // start of data of type constraint for a parameter\n\tparamConstraintDataEnd       byte = ')'  // end of data of type constraint for a parameter\n\tparamConstraintDataSeparator byte = ','  // separator of data of type constraint for a parameter\n)\n\n// TypeConstraint parameter constraint types\ntype TypeConstraint uint16\n\n// Constraint describes the validation rules that apply to a dynamic route\n// segment when matching incoming requests.\ntype Constraint struct {\n\tRegexCompiler     *regexp.Regexp\n\tName              string\n\tData              []string\n\tcustomConstraints []CustomConstraint\n\tID                TypeConstraint\n}\n\n// CustomConstraint is an interface for custom constraints\ntype CustomConstraint interface {\n\t// Name returns the name of the constraint.\n\t// This name is used in the constraint matching.\n\tName() string\n\n\t// Execute executes the constraint.\n\t// It returns true if the constraint is matched and right.\n\t// param is the parameter value to check.\n\t// args are the constraint arguments.\n\tExecute(param string, args ...string) bool\n}\n\nconst (\n\tnoConstraint TypeConstraint = 1 << iota\n\tintConstraint\n\tboolConstraint\n\tfloatConstraint\n\talphaConstraint\n\tdatetimeConstraint\n\tguidConstraint\n\tminLenConstraint\n\tmaxLenConstraint\n\tlenConstraint\n\tbetweenLenConstraint\n\tminConstraint\n\tmaxConstraint\n\trangeConstraint\n\tregexConstraint\n)\n\nconst (\n\tneedOneData = minLenConstraint | maxLenConstraint | lenConstraint | minConstraint | maxConstraint | datetimeConstraint | regexConstraint\n\tneedTwoData = betweenLenConstraint | rangeConstraint\n)\n\n// list of possible parameter and segment delimiter\nvar (\n\t// slash has a special role, unlike the other parameters it must not be interpreted as a parameter\n\trouteDelimiter = []byte{slashDelimiter, '-', '.'}\n\t// list of greedy parameters\n\tgreedyParameters = []byte{wildcardParam, plusParam}\n\t// list of chars for the parameter recognizing\n\tparameterStartChars = [256]bool{\n\t\twildcardParam:    true,\n\t\tplusParam:        true,\n\t\tparamStarterChar: true,\n\t}\n\t// list of chars of delimiters and the starting parameter name char\n\tparameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...)\n\t// list of chars to find the end of a parameter\n\tparameterEndChars = [256]bool{\n\t\toptionalParam:    true,\n\t\tparamStarterChar: true,\n\t\tescapeChar:       true,\n\t\tslashDelimiter:   true,\n\t\t'-':              true,\n\t\t'.':              true,\n\t}\n)\n\n// RoutePatternMatch reports whether path matches the provided Fiber route pattern.\n//\n// Patterns use the same syntax as routes registered on an App, including\n// parameters (for example `:id`), wildcards (`*`, `+`), and optional segments.\n// The optional Config argument can be used to control case sensitivity and\n// strict routing behavior. This helper allows checking potential matches\n// without registering a route.\nfunc RoutePatternMatch(path, pattern string, cfg ...Config) bool {\n\t// See logic in (*Route).match and (*App).register\n\tvar ctxParams [maxParams]string\n\n\tconfig := Config{}\n\tif len(cfg) > 0 {\n\t\tconfig = cfg[0]\n\t}\n\n\tif path == \"\" {\n\t\tpath = \"/\"\n\t}\n\n\t// Cannot have an empty pattern\n\tif pattern == \"\" {\n\t\tpattern = \"/\"\n\t}\n\t// Pattern always start with a '/'\n\tif pattern[0] != '/' {\n\t\tpattern = \"/\" + pattern\n\t}\n\n\tpatternPretty := []byte(pattern)\n\n\t// Case-sensitive routing, all to lowercase\n\tif !config.CaseSensitive {\n\t\tpatternPretty = utilsbytes.UnsafeToLower(patternPretty)\n\t\tpath = utilsstrings.ToLower(path)\n\t}\n\t// Strict routing, remove trailing slashes\n\tif !config.StrictRouting && len(patternPretty) > 1 {\n\t\tpatternPretty = utils.TrimRight(patternPretty, '/')\n\t}\n\n\tparser, _ := routerParserPool.Get().(*routeParser) //nolint:errcheck // only contains routeParser\n\tparser.reset()\n\tpatternStr := string(patternPretty)\n\tparser.parseRoute(patternStr)\n\tdefer routerParserPool.Put(parser)\n\n\t// '*' wildcard matches any path\n\tif (patternStr == \"/\" && path == \"/\") || patternStr == \"/*\" {\n\t\treturn true\n\t}\n\n\t// Does this route have parameters\n\tif len(parser.params) > 0 {\n\t\tif match := parser.getMatch(path, path, &ctxParams, false); match {\n\t\t\treturn true\n\t\t}\n\t}\n\t// Check for a simple match\n\tpatternPretty = RemoveEscapeCharBytes(patternPretty)\n\n\treturn string(patternPretty) == path\n}\n\nfunc (parser *routeParser) reset() {\n\tparser.segs = parser.segs[:0]\n\tparser.params = parser.params[:0]\n\tparser.wildCardCount = 0\n\tparser.plusCount = 0\n}\n\n// parseRoute analyzes the route and divides it into segments for constant areas and parameters,\n// this information is needed later when assigning the requests to the declared routes\nfunc (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) {\n\tvar n int\n\tvar seg *routeSegment\n\tfor pattern != \"\" {\n\t\tnextParamPosition := findNextParamPosition(pattern)\n\t\t// handle the parameter part\n\t\tif nextParamPosition == 0 {\n\t\t\tn, seg = parser.analyseParameterPart(pattern, customConstraints...)\n\t\t\tparser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg)\n\t\t} else {\n\t\t\tn, seg = parser.analyseConstantPart(pattern, nextParamPosition)\n\t\t\tparser.segs = append(parser.segs, seg)\n\t\t}\n\t\tpattern = pattern[n:]\n\t}\n\t// mark last segment\n\tif len(parser.segs) > 0 {\n\t\tparser.segs[len(parser.segs)-1].IsLast = true\n\t}\n\tparser.segs = addParameterMetaInfo(parser.segs)\n}\n\n// parseRoute analyzes the route and divides it into segments for constant areas and parameters,\n// this information is needed later when assigning the requests to the declared routes\nfunc parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser {\n\tparser := routeParser{}\n\tparser.parseRoute(pattern, customConstraints...)\n\n\t// Check if the route has too many parameters\n\tif len(parser.params) > maxParams {\n\t\tpanic(fmt.Sprintf(\"Route '%s' has %d parameters, which exceeds the maximum of %d\",\n\t\t\tpattern, len(parser.params), maxParams))\n\t}\n\n\treturn parser\n}\n\n// addParameterMetaInfo add important meta information to the parameter segments\n// to simplify the search for the end of the parameter\nfunc addParameterMetaInfo(segs []*routeSegment) []*routeSegment {\n\tvar comparePart string\n\tsegLen := len(segs)\n\t// loop from end to begin\n\tfor i := segLen - 1; i >= 0; i-- {\n\t\t// set the compare part for the parameter\n\t\tif segs[i].IsParam {\n\t\t\t// important for finding the end of the parameter\n\t\t\tsegs[i].ComparePart = RemoveEscapeChar(comparePart)\n\t\t} else {\n\t\t\tcomparePart = segs[i].Const\n\t\t\tif len(comparePart) > 1 {\n\t\t\t\tcomparePart = utils.TrimRight(comparePart, slashDelimiter)\n\t\t\t}\n\t\t}\n\t}\n\n\t// loop from beginning to end\n\tfor i := range segLen {\n\t\t// check how often the compare part is in the following const parts\n\t\tif segs[i].IsParam {\n\t\t\t// check if parameter segments are directly after each other;\n\t\t\t// when neither this parameter nor the next parameter are greedy, we only want one character\n\t\t\tif segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy {\n\t\t\t\tsegs[i].Length = 1\n\t\t\t}\n\t\t\tif segs[i].ComparePart == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor j := i + 1; j <= len(segs)-1; j++ {\n\t\t\t\tif !segs[j].IsParam {\n\t\t\t\t\t// count is important for the greedy match\n\t\t\t\t\tsegs[i].PartCount += strings.Count(segs[j].Const, segs[i].ComparePart)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// check if the end of the segment is an optional slash and then if the segment is optional or the last one\n\t\t} else if segs[i].Const[len(segs[i].Const)-1] == slashDelimiter && (segs[i].IsLast || (segLen > i+1 && segs[i+1].IsOptional)) {\n\t\t\tsegs[i].HasOptionalSlash = true\n\t\t}\n\t}\n\n\treturn segs\n}\n\n// findNextParamPosition search for the next possible parameter start position\nfunc findNextParamPosition(pattern string) int {\n\t// Find the first parameter position\n\tnext := -1\n\tfor i := range pattern {\n\t\tif parameterStartChars[pattern[i]] && (i == 0 || pattern[i-1] != escapeChar) {\n\t\t\tnext = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif next > 0 && pattern[next] != wildcardParam {\n\t\t// checking the found parameterStartChar is a cluster\n\t\tfor i := next + 1; i < len(pattern); i++ {\n\t\t\tif !parameterStartChars[pattern[i]] {\n\t\t\t\treturn i - 1\n\t\t\t}\n\t\t}\n\t\treturn len(pattern) - 1\n\t}\n\treturn next\n}\n\n// analyseConstantPart find the end of the constant part and create the route segment\nfunc (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (int, *routeSegment) {\n\t// handle the constant part\n\tprocessedPart := pattern\n\tif nextParamPosition != -1 {\n\t\t// remove the constant part until the parameter\n\t\tprocessedPart = pattern[:nextParamPosition]\n\t}\n\tconstPart := RemoveEscapeChar(processedPart)\n\treturn len(processedPart), &routeSegment{\n\t\tConst:  constPart,\n\t\tLength: len(constPart),\n\t}\n}\n\n// analyseParameterPart find the parameter end and create the route segment\nfunc (parser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) {\n\tisWildCard := pattern[0] == wildcardParam\n\tisPlusParam := pattern[0] == plusParam\n\n\tparamEndPosition := 0\n\tparamConstraintStartPosition := -1\n\tparamConstraintEndPosition := -1\n\n\t// handle wildcard end\n\tif !isWildCard && !isPlusParam {\n\t\tparamEndPosition = -1\n\t\tsearch := pattern[1:]\n\t\tfor i := range search {\n\t\t\tif paramConstraintStartPosition == -1 && search[i] == paramConstraintStart && (i == 0 || search[i-1] != escapeChar) {\n\t\t\t\tparamConstraintStartPosition = i + 1\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif paramConstraintEndPosition == -1 && search[i] == paramConstraintEnd && (i == 0 || search[i-1] != escapeChar) {\n\t\t\t\tparamConstraintEndPosition = i + 1\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif parameterEndChars[search[i]] {\n\t\t\t\tif (paramConstraintStartPosition == -1 && paramConstraintEndPosition == -1) ||\n\t\t\t\t\t(paramConstraintStartPosition != -1 && paramConstraintEndPosition != -1) {\n\t\t\t\t\tparamEndPosition = i\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tswitch {\n\t\tcase paramEndPosition == -1:\n\t\t\tparamEndPosition = len(pattern) - 1\n\t\tcase bytes.IndexByte(parameterDelimiterChars, pattern[paramEndPosition+1]) == -1:\n\t\t\tparamEndPosition++\n\t\tdefault:\n\t\t\t// do nothing\n\t\t}\n\t}\n\n\t// cut params part\n\tprocessedPart := pattern[0 : paramEndPosition+1]\n\tn := paramEndPosition + 1\n\tparamName := RemoveEscapeChar(GetTrimmedParam(processedPart))\n\n\t// Check has constraint\n\tvar constraints []*Constraint\n\n\tif hasConstraint := paramConstraintStartPosition != -1 && paramConstraintEndPosition != -1; hasConstraint {\n\t\tconstraintString := pattern[paramConstraintStartPosition+1 : paramConstraintEndPosition]\n\t\tuserConstraints := splitNonEscaped(constraintString, paramConstraintSeparator)\n\t\tconstraints = make([]*Constraint, 0, len(userConstraints))\n\n\t\tfor _, c := range userConstraints {\n\t\t\tstart := findNextNonEscapedCharPosition(c, paramConstraintDataStart)\n\t\t\tend := strings.LastIndexByte(c, paramConstraintDataEnd)\n\n\t\t\t// Assign constraint\n\t\t\tif start != -1 && end != -1 {\n\t\t\t\tconstraint := &Constraint{\n\t\t\t\t\tID:                getParamConstraintType(c[:start]),\n\t\t\t\t\tName:              c[:start],\n\t\t\t\t\tcustomConstraints: customConstraints,\n\t\t\t\t}\n\n\t\t\t\t// remove escapes from data\n\t\t\t\tif constraint.ID != regexConstraint {\n\t\t\t\t\tconstraint.Data = splitNonEscaped(c[start+1:end], paramConstraintDataSeparator)\n\t\t\t\t\tif len(constraint.Data) == 1 {\n\t\t\t\t\t\tconstraint.Data[0] = RemoveEscapeChar(constraint.Data[0])\n\t\t\t\t\t} else if len(constraint.Data) == 2 { // This is fine, we simply expect two parts\n\t\t\t\t\t\tconstraint.Data[0] = RemoveEscapeChar(constraint.Data[0])\n\t\t\t\t\t\tconstraint.Data[1] = RemoveEscapeChar(constraint.Data[1])\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Precompile regex if has regex constraint\n\t\t\t\tif constraint.ID == regexConstraint {\n\t\t\t\t\tconstraint.Data = []string{c[start+1 : end]}\n\t\t\t\t\tconstraint.RegexCompiler = regexp.MustCompile(constraint.Data[0])\n\t\t\t\t}\n\n\t\t\t\tconstraints = append(constraints, constraint)\n\t\t\t} else {\n\t\t\t\tconstraints = append(constraints, &Constraint{\n\t\t\t\t\tID:                getParamConstraintType(c),\n\t\t\t\t\tData:              []string{},\n\t\t\t\t\tName:              c,\n\t\t\t\t\tcustomConstraints: customConstraints,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tparamName = RemoveEscapeChar(GetTrimmedParam(pattern[0:paramConstraintStartPosition]))\n\t}\n\n\t// add access iterator to wildcard and plus\n\tif isWildCard {\n\t\tparser.wildCardCount++\n\t\tparamName += strconv.Itoa(parser.wildCardCount)\n\t} else if isPlusParam {\n\t\tparser.plusCount++\n\t\tparamName += strconv.Itoa(parser.plusCount)\n\t}\n\n\tsegment := &routeSegment{\n\t\tParamName:  paramName,\n\t\tIsParam:    true,\n\t\tIsOptional: isWildCard || pattern[paramEndPosition] == optionalParam,\n\t\tIsGreedy:   isWildCard || isPlusParam,\n\t}\n\n\tif len(constraints) > 0 {\n\t\tsegment.Constraints = constraints\n\t}\n\n\treturn n, segment\n}\n\n// findNextNonEscapedCharPosition searches the next char position and skips the escaped characters\nfunc findNextNonEscapedCharPosition(search string, char byte) int {\n\tfor i := 0; i < len(search); i++ {\n\t\tif search[i] == char && (i == 0 || search[i-1] != escapeChar) {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// splitNonEscaped slices s into all substrings separated by sep and returns a slice of the substrings between those separators\n// This function also takes a care of escape char when splitting.\nfunc splitNonEscaped(s string, sep byte) []string {\n\tvar result []string\n\ti := findNextNonEscapedCharPosition(s, sep)\n\n\tfor i > -1 {\n\t\tresult = append(result, s[:i])\n\t\ts = s[i+1:]\n\t\ti = findNextNonEscapedCharPosition(s, sep)\n\t}\n\n\treturn append(result, s)\n}\n\nfunc hasPartialMatchBoundary(path string, matchedLength int) bool {\n\tif matchedLength < 0 || matchedLength > len(path) {\n\t\treturn false\n\t}\n\tif matchedLength == len(path) {\n\t\treturn true\n\t}\n\tif matchedLength == 0 {\n\t\treturn false\n\t}\n\tif path[matchedLength-1] == slashDelimiter {\n\t\treturn true\n\t}\n\tif matchedLength < len(path) && path[matchedLength] == slashDelimiter {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions\nfunc (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint:revive // Accepting a bool param is fine here\n\toriginalDetectionPath := detectionPath\n\tvar i, paramsIterator, partLen int\n\tfor _, segment := range parser.segs {\n\t\tpartLen = len(detectionPath)\n\t\t// check const segment\n\t\tif !segment.IsParam {\n\t\t\ti = segment.Length\n\t\t\t// is optional part or the const part must match with the given string\n\t\t\t// check if the end of the segment is an optional slash\n\t\t\tif segment.HasOptionalSlash && partLen == i-1 && detectionPath == segment.Const[:i-1] {\n\t\t\t\ti--\n\t\t\t} else if i > partLen || detectionPath[:i] != segment.Const {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\t// determine parameter length\n\t\t\ti = findParamLen(detectionPath, segment)\n\t\t\tif !segment.IsOptional && i == 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// take over the params positions\n\t\t\tparams[paramsIterator] = path[:i]\n\n\t\t\tif !segment.IsOptional || i != 0 {\n\t\t\t\t// check constraint\n\t\t\t\tfor _, c := range segment.Constraints {\n\t\t\t\t\tif matched := c.CheckConstraint(params[paramsIterator]); !matched {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tparamsIterator++\n\t\t}\n\n\t\t// reduce founded part from the string\n\t\tif partLen > 0 {\n\t\t\tdetectionPath, path = detectionPath[i:], path[i:]\n\t\t}\n\t}\n\tif detectionPath != \"\" {\n\t\tif !partialCheck {\n\t\t\treturn false\n\t\t}\n\t\tconsumedLength := len(originalDetectionPath) - len(detectionPath)\n\t\tif !hasPartialMatchBoundary(originalDetectionPath, consumedLength) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// findParamLen for the expressjs wildcard behavior (right to left greedy)\n// look at the other segments and take what is left for the wildcard from right to left\nfunc findParamLen(s string, segment *routeSegment) int {\n\tif segment.IsLast {\n\t\treturn findParamLenForLastSegment(s, segment)\n\t}\n\n\tif segment.Length != 0 && len(s) >= segment.Length {\n\t\treturn segment.Length\n\t} else if segment.IsGreedy {\n\t\t// Search the parameters until the next constant part\n\t\t// special logic for greedy params\n\t\tsearchCount := strings.Count(s, segment.ComparePart)\n\t\tif searchCount > 1 {\n\t\t\treturn findGreedyParamLen(s, searchCount, segment)\n\t\t}\n\t}\n\n\tif len(segment.ComparePart) == 1 {\n\t\tif constPosition := strings.IndexByte(s, segment.ComparePart[0]); constPosition != -1 {\n\t\t\treturn constPosition\n\t\t}\n\t} else if constPosition := strings.Index(s, segment.ComparePart); constPosition != -1 {\n\t\t// if the compare part was found, but contains a slash although this part is not greedy, then it must not match\n\t\t// example: /api/:param/fixedEnd -> path: /api/123/456/fixedEnd = no match , /api/123/fixedEnd = match\n\t\tif !segment.IsGreedy && strings.IndexByte(s[:constPosition], slashDelimiter) != -1 {\n\t\t\treturn 0\n\t\t}\n\t\treturn constPosition\n\t}\n\n\treturn len(s)\n}\n\n// findParamLenForLastSegment get the length of the parameter if it is the last segment\nfunc findParamLenForLastSegment(s string, seg *routeSegment) int {\n\tif !seg.IsGreedy {\n\t\tif i := strings.IndexByte(s, slashDelimiter); i != -1 {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn len(s)\n}\n\n// findGreedyParamLen get the length of the parameter for greedy segments from right to left\nfunc findGreedyParamLen(s string, searchCount int, segment *routeSegment) int {\n\t// check all from right to left segments\n\tfor i := segment.PartCount; i > 0 && searchCount > 0; i-- {\n\t\tsearchCount--\n\n\t\tconstPosition := strings.LastIndex(s, segment.ComparePart)\n\t\tif constPosition == -1 {\n\t\t\tbreak\n\t\t}\n\t\ts = s[:constPosition]\n\t}\n\n\treturn len(s)\n}\n\n// GetTrimmedParam trims the ':' & '?' from a string\nfunc GetTrimmedParam(param string) string {\n\tstart := 0\n\tend := len(param)\n\n\tif end == 0 || param[start] != paramStarterChar { // is not a param\n\t\treturn param\n\t}\n\tstart++\n\tif param[end-1] == optionalParam { // is ?\n\t\tend--\n\t}\n\n\treturn param[start:end]\n}\n\n// RemoveEscapeChar removes escape characters\nfunc RemoveEscapeChar(word string) string {\n\t// Fast path: check if there are any escape characters first\n\tescapeIdx := strings.IndexByte(word, '\\\\')\n\tif escapeIdx == -1 {\n\t\treturn word // No escape chars, return original string without allocation\n\t}\n\n\t// Slow path: copy and remove escape characters\n\tb := []byte(word)\n\tdst := escapeIdx\n\tfor src := escapeIdx + 1; src < len(b); src++ {\n\t\tif b[src] != '\\\\' {\n\t\t\tb[dst] = b[src]\n\t\t\tdst++\n\t\t}\n\t}\n\treturn string(b[:dst])\n}\n\n// RemoveEscapeCharBytes removes escape characters\nfunc RemoveEscapeCharBytes(word []byte) []byte {\n\tdst := 0\n\tfor src := range word {\n\t\tif word[src] != '\\\\' {\n\t\t\tword[dst] = word[src]\n\t\t\tdst++\n\t\t}\n\t}\n\treturn word[:dst]\n}\n\nfunc getParamConstraintType(constraintPart string) TypeConstraint {\n\tswitch constraintPart {\n\tcase ConstraintInt:\n\t\treturn intConstraint\n\tcase ConstraintBool:\n\t\treturn boolConstraint\n\tcase ConstraintFloat:\n\t\treturn floatConstraint\n\tcase ConstraintAlpha:\n\t\treturn alphaConstraint\n\tcase ConstraintGUID:\n\t\treturn guidConstraint\n\tcase ConstraintMinLen, ConstraintMinLenLower:\n\t\treturn minLenConstraint\n\tcase ConstraintMaxLen, ConstraintMaxLenLower:\n\t\treturn maxLenConstraint\n\tcase ConstraintLen:\n\t\treturn lenConstraint\n\tcase ConstraintBetweenLen, ConstraintBetweenLenLower:\n\t\treturn betweenLenConstraint\n\tcase ConstraintMin:\n\t\treturn minConstraint\n\tcase ConstraintMax:\n\t\treturn maxConstraint\n\tcase ConstraintRange:\n\t\treturn rangeConstraint\n\tcase ConstraintDatetime:\n\t\treturn datetimeConstraint\n\tcase ConstraintRegex:\n\t\treturn regexConstraint\n\tdefault:\n\t\treturn noConstraint\n\t}\n}\n\n// CheckConstraint validates if a param matches the given constraint\n// Returns true if the param passes the constraint check, false otherwise\nfunc (c *Constraint) CheckConstraint(param string) bool {\n\t// First check if there's a custom constraint with the same name\n\t// This allows custom constraints to override built-in constraints\n\tfor _, cc := range c.customConstraints {\n\t\tif cc.Name() == c.Name {\n\t\t\treturn cc.Execute(param, c.Data...)\n\t\t}\n\t}\n\n\tvar (\n\t\terr error\n\t\tnum int\n\t)\n\n\t// Validate constraint has required data\n\tif c.ID&needOneData != 0 && len(c.Data) == 0 {\n\t\treturn false\n\t}\n\n\tif c.ID&needTwoData != 0 && len(c.Data) < 2 {\n\t\treturn false\n\t}\n\n\tswitch c.ID {\n\tcase noConstraint:\n\t\treturn true\n\tcase intConstraint:\n\t\t_, err = strconv.Atoi(param)\n\tcase boolConstraint:\n\t\t_, err = strconv.ParseBool(param)\n\tcase floatConstraint:\n\t\t_, err = strconv.ParseFloat(param, 32)\n\tcase alphaConstraint:\n\t\tfor _, r := range param {\n\t\t\tif !unicode.IsLetter(r) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\tcase guidConstraint:\n\t\t_, err = uuid.Parse(param)\n\tcase minLenConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tif len(param) < data {\n\t\t\treturn false\n\t\t}\n\tcase maxLenConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tif len(param) > data {\n\t\t\treturn false\n\t\t}\n\tcase lenConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tif len(param) != data {\n\t\t\treturn false\n\t\t}\n\tcase betweenLenConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tdata2, parseErr := strconv.Atoi(c.Data[1])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tlength := len(param)\n\t\tif length < data || length > data2 {\n\t\t\treturn false\n\t\t}\n\tcase minConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tnum, err = strconv.Atoi(param)\n\n\t\tif err != nil || num < data {\n\t\t\treturn false\n\t\t}\n\tcase maxConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tnum, err = strconv.Atoi(param)\n\n\t\tif err != nil || num > data {\n\t\t\treturn false\n\t\t}\n\tcase rangeConstraint:\n\t\tdata, parseErr := strconv.Atoi(c.Data[0])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tdata2, parseErr := strconv.Atoi(c.Data[1])\n\t\tif parseErr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tnum, err = strconv.Atoi(param)\n\n\t\tif err != nil || num < data || num > data2 {\n\t\t\treturn false\n\t\t}\n\tcase datetimeConstraint:\n\t\t_, err = time.Parse(c.Data[0], param)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\tcase regexConstraint:\n\t\tif c.RegexCompiler == nil {\n\t\t\treturn false\n\t\t}\n\t\tif match := c.RegexCompiler.MatchString(param); !match {\n\t\t\treturn false\n\t\t}\n\tdefault:\n\t\treturn false\n\t}\n\n\treturn err == nil\n}\n"
  },
  {
    "path": "path_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📝 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// go test -race -run Test_Path_parseRoute\nfunc Test_Path_parseRoute(t *testing.T) {\n\tt.Parallel()\n\tvar rp routeParser\n\n\trp = parseRoute(\"/shop/product/::filter/color::color/size::size\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/shop/product/:\", Length: 15},\n\t\t\t{IsParam: true, ParamName: \"filter\", ComparePart: \"/color:\", PartCount: 1},\n\t\t\t{Const: \"/color:\", Length: 7},\n\t\t\t{IsParam: true, ParamName: \"color\", ComparePart: \"/size:\", PartCount: 1},\n\t\t\t{Const: \"/size:\", Length: 6},\n\t\t\t{IsParam: true, ParamName: \"size\", IsLast: true},\n\t\t},\n\t\tparams: []string{\"filter\", \"color\", \"size\"},\n\t}, rp)\n\n\trp = parseRoute(\"/api/v1/:param/abc/*\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/api/v1/\", Length: 8},\n\t\t\t{IsParam: true, ParamName: \"param\", ComparePart: \"/abc\", PartCount: 1},\n\t\t\t{Const: \"/abc/\", Length: 5, HasOptionalSlash: true},\n\t\t\t{IsParam: true, ParamName: \"*1\", IsGreedy: true, IsOptional: true, IsLast: true},\n\t\t},\n\t\tparams:        []string{\"param\", \"*1\"},\n\t\twildCardCount: 1,\n\t}, rp)\n\n\trp = parseRoute(\"/v1/some/resource/name\\\\:customVerb\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/v1/some/resource/name:customVerb\", Length: 33, IsLast: true},\n\t\t},\n\t\tparams: nil,\n\t}, rp)\n\n\trp = parseRoute(\"/v1/some/resource/:name\\\\:customVerb\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/v1/some/resource/\", Length: 18},\n\t\t\t{IsParam: true, ParamName: \"name\", ComparePart: \":customVerb\", PartCount: 1},\n\t\t\t{Const: \":customVerb\", Length: 11, IsLast: true},\n\t\t},\n\t\tparams: []string{\"name\"},\n\t}, rp)\n\n\t// heavy test with escaped characters\n\trp = parseRoute(\"/v1/some/resource/name\\\\\\\\:customVerb?\\\\?/:param/*\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/v1/some/resource/name:customVerb??/\", Length: 36},\n\t\t\t{IsParam: true, ParamName: \"param\", ComparePart: \"/\", PartCount: 1},\n\t\t\t{Const: \"/\", Length: 1, HasOptionalSlash: true},\n\t\t\t{IsParam: true, ParamName: \"*1\", IsGreedy: true, IsOptional: true, IsLast: true},\n\t\t},\n\t\tparams:        []string{\"param\", \"*1\"},\n\t\twildCardCount: 1,\n\t}, rp)\n\n\trp = parseRoute(\"/api/*/:param/:param2\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/api/\", Length: 5, HasOptionalSlash: true},\n\t\t\t{IsParam: true, ParamName: \"*1\", IsGreedy: true, IsOptional: true, ComparePart: \"/\", PartCount: 2},\n\t\t\t{Const: \"/\", Length: 1},\n\t\t\t{IsParam: true, ParamName: \"param\", ComparePart: \"/\", PartCount: 1},\n\t\t\t{Const: \"/\", Length: 1},\n\t\t\t{IsParam: true, ParamName: \"param2\", IsLast: true},\n\t\t},\n\t\tparams:        []string{\"*1\", \"param\", \"param2\"},\n\t\twildCardCount: 1,\n\t}, rp)\n\n\trp = parseRoute(\"/test:optional?:optional2?\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/test\", Length: 5},\n\t\t\t{IsParam: true, ParamName: \"optional\", IsOptional: true, Length: 1},\n\t\t\t{IsParam: true, ParamName: \"optional2\", IsOptional: true, IsLast: true},\n\t\t},\n\t\tparams: []string{\"optional\", \"optional2\"},\n\t}, rp)\n\n\trp = parseRoute(\"/config/+.json\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/config/\", Length: 8},\n\t\t\t{IsParam: true, ParamName: \"+1\", IsGreedy: true, IsOptional: false, ComparePart: \".json\", PartCount: 1},\n\t\t\t{Const: \".json\", Length: 5, IsLast: true},\n\t\t},\n\t\tparams:    []string{\"+1\"},\n\t\tplusCount: 1,\n\t}, rp)\n\n\trp = parseRoute(\"/api/:day.:month?.:year?\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/api/\", Length: 5},\n\t\t\t{IsParam: true, ParamName: \"day\", IsOptional: false, ComparePart: \".\", PartCount: 2},\n\t\t\t{Const: \".\", Length: 1},\n\t\t\t{IsParam: true, ParamName: \"month\", IsOptional: true, ComparePart: \".\", PartCount: 1},\n\t\t\t{Const: \".\", Length: 1},\n\t\t\t{IsParam: true, ParamName: \"year\", IsOptional: true, IsLast: true},\n\t\t},\n\t\tparams: []string{\"day\", \"month\", \"year\"},\n\t}, rp)\n\n\trp = parseRoute(\"/*v1*/proxy\")\n\trequire.Equal(t, routeParser{\n\t\tsegs: []*routeSegment{\n\t\t\t{Const: \"/\", Length: 1, HasOptionalSlash: true},\n\t\t\t{IsParam: true, ParamName: \"*1\", IsGreedy: true, IsOptional: true, ComparePart: \"v1\", PartCount: 1},\n\t\t\t{Const: \"v1\", Length: 2},\n\t\t\t{IsParam: true, ParamName: \"*2\", IsGreedy: true, IsOptional: true, ComparePart: \"/proxy\", PartCount: 1},\n\t\t\t{Const: \"/proxy\", Length: 6, IsLast: true},\n\t\t},\n\t\tparams:        []string{\"*1\", \"*2\"},\n\t\twildCardCount: 2,\n\t}, rp)\n}\n\n// go test -race -run Test_Path_matchParams\nfunc Test_Path_matchParams(t *testing.T) {\n\tt.Parallel()\n\tvar ctxParams [maxParams]string\n\ttestCaseFn := func(testCollection routeCaseCollection) {\n\t\tparser := parseRoute(testCollection.pattern)\n\t\tfor _, c := range testCollection.testCases {\n\t\t\tmatch := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck)\n\t\t\trequire.Equal(t, c.match, match, \"route: '%s', url: '%s'\", testCollection.pattern, c.url)\n\t\t\tif match && len(c.params) > 0 {\n\t\t\t\trequire.Equal(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], \"route: '%s', url: '%s'\", testCollection.pattern, c.url)\n\t\t\t}\n\t\t}\n\t}\n\tfor _, testCaseCollection := range routeTestCases {\n\t\ttestCaseFn(testCaseCollection)\n\t}\n}\n\n// go test -race -run Test_RoutePatternMatch\nfunc Test_RoutePatternMatch(t *testing.T) {\n\tt.Parallel()\n\ttestCaseFn := func(pattern string, cases []routeTestCase) {\n\t\tfor _, c := range cases {\n\t\t\t// skip all cases for partial checks\n\t\t\tif c.partialCheck {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmatch := RoutePatternMatch(c.url, pattern)\n\t\t\trequire.Equal(t, c.match, match, \"route: '%s', url: '%s'\", pattern, c.url)\n\t\t}\n\t}\n\tfor _, testCase := range routeTestCases {\n\t\ttestCaseFn(testCase.pattern, testCase.testCases)\n\t}\n}\n\nfunc TestHasPartialMatchBoundary(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tpath          string\n\t\tmatchedLength int\n\t\texpected      bool\n\t}{\n\t\t{\n\t\t\tname:          \"negative length\",\n\t\t\tpath:          \"/demo\",\n\t\t\tmatchedLength: -1,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"greater than length\",\n\t\t\tpath:          \"/demo\",\n\t\t\tmatchedLength: 6,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exact match\",\n\t\t\tpath:          \"/demo\",\n\t\t\tmatchedLength: len(\"/demo\"),\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"zero length\",\n\t\t\tpath:          \"/demo\",\n\t\t\tmatchedLength: 0,\n\t\t\texpected:      false,\n\t\t},\n\t\t{\n\t\t\tname:          \"previous rune slash\",\n\t\t\tpath:          \"/demo/child\",\n\t\t\tmatchedLength: len(\"/demo/\"),\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"next rune slash\",\n\t\t\tpath:          \"/demo/child\",\n\t\t\tmatchedLength: len(\"/demo\"),\n\t\t\texpected:      true,\n\t\t},\n\t\t{\n\t\t\tname:          \"no boundary\",\n\t\t\tpath:          \"/demo/child\",\n\t\t\tmatchedLength: len(\"/dem\"),\n\t\t\texpected:      false,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, testCase.expected, hasPartialMatchBoundary(testCase.path, testCase.matchedLength))\n\t\t})\n\t}\n}\n\nfunc Test_Utils_GetTrimmedParam(t *testing.T) {\n\tt.Parallel()\n\tres := GetTrimmedParam(\"\")\n\trequire.Empty(t, res)\n\tres = GetTrimmedParam(\"*\")\n\trequire.Equal(t, \"*\", res)\n\tres = GetTrimmedParam(\":param\")\n\trequire.Equal(t, \"param\", res)\n\tres = GetTrimmedParam(\":param1?\")\n\trequire.Equal(t, \"param1\", res)\n\tres = GetTrimmedParam(\"noParam\")\n\trequire.Equal(t, \"noParam\", res)\n}\n\nfunc Test_Utils_RemoveEscapeChar(t *testing.T) {\n\tt.Parallel()\n\tres := RemoveEscapeChar(\":test\\\\:bla\")\n\trequire.Equal(t, \":test:bla\", res)\n\tres = RemoveEscapeChar(\"\\\\abc\")\n\trequire.Equal(t, \"abc\", res)\n\tres = RemoveEscapeChar(\"noEscapeChar\")\n\trequire.Equal(t, \"noEscapeChar\", res)\n}\n\nfunc Test_ConstraintCheckConstraint_InvalidMetadata(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tparam      string\n\t\tconstraint Constraint\n\t}{\n\t\t{\n\t\t\tname:       \"minLen invalid metadata\",\n\t\t\tconstraint: Constraint{ID: minLenConstraint, Data: []string{\"abc\"}},\n\t\t\tparam:      \"abcd\",\n\t\t},\n\t\t{\n\t\t\tname:       \"maxLen invalid metadata\",\n\t\t\tconstraint: Constraint{ID: maxLenConstraint, Data: []string{\"abc\"}},\n\t\t\tparam:      \"abcd\",\n\t\t},\n\t\t{\n\t\t\tname:       \"len invalid metadata\",\n\t\t\tconstraint: Constraint{ID: lenConstraint, Data: []string{\"abc\"}},\n\t\t\tparam:      \"abcd\",\n\t\t},\n\t\t{\n\t\t\tname:       \"betweenLen invalid first metadata\",\n\t\t\tconstraint: Constraint{ID: betweenLenConstraint, Data: []string{\"abc\", \"5\"}},\n\t\t\tparam:      \"abcd\",\n\t\t},\n\t\t{\n\t\t\tname:       \"betweenLen invalid second metadata\",\n\t\t\tconstraint: Constraint{ID: betweenLenConstraint, Data: []string{\"1\", \"abc\"}},\n\t\t\tparam:      \"abcd\",\n\t\t},\n\t\t{\n\t\t\tname:       \"min invalid metadata\",\n\t\t\tconstraint: Constraint{ID: minConstraint, Data: []string{\"abc\"}},\n\t\t\tparam:      \"10\",\n\t\t},\n\t\t{\n\t\t\tname:       \"max invalid metadata\",\n\t\t\tconstraint: Constraint{ID: maxConstraint, Data: []string{\"abc\"}},\n\t\t\tparam:      \"10\",\n\t\t},\n\t\t{\n\t\t\tname:       \"range invalid first metadata\",\n\t\t\tconstraint: Constraint{ID: rangeConstraint, Data: []string{\"abc\", \"10\"}},\n\t\t\tparam:      \"7\",\n\t\t},\n\t\t{\n\t\t\tname:       \"range invalid second metadata\",\n\t\t\tconstraint: Constraint{ID: rangeConstraint, Data: []string{\"1\", \"abc\"}},\n\t\t\tparam:      \"7\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.False(t, testCase.constraint.CheckConstraint(testCase.param))\n\t\t})\n\t}\n}\n\nfunc Benchmark_Utils_RemoveEscapeChar(b *testing.B) {\n\tb.ReportAllocs()\n\tvar res string\n\tfor b.Loop() {\n\t\tres = RemoveEscapeChar(\":test\\\\:bla\")\n\t}\n\n\trequire.Equal(b, \":test:bla\", res)\n}\n\n// go test -race -run Test_Path_matchParams\nfunc Benchmark_Path_matchParams(t *testing.B) {\n\tvar ctxParams [maxParams]string\n\tbenchCaseFn := func(testCollection routeCaseCollection) {\n\t\tparser := parseRoute(testCollection.pattern)\n\t\tfor _, c := range testCollection.testCases {\n\t\t\tvar matchRes bool\n\t\t\tstate := \"match\"\n\t\t\tif !c.match {\n\t\t\t\tstate = \"not match\"\n\t\t\t}\n\t\t\tt.Run(testCollection.pattern+\" | \"+state+\" | \"+c.url, func(b *testing.B) {\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\tif match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match {\n\t\t\t\t\t\t// Get testCases from the original path\n\t\t\t\t\t\tmatchRes = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trequire.Equal(t, c.match, matchRes, \"route: '%s', url: '%s'\", testCollection.pattern, c.url)\n\t\t\t\tif matchRes && len(c.params) > 0 {\n\t\t\t\t\trequire.Equal(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], \"route: '%s', url: '%s'\", testCollection.pattern, c.url)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, testCollection := range benchmarkCases {\n\t\tbenchCaseFn(testCollection)\n\t}\n}\n\n// go test -race -run Test_RoutePatternMatch\nfunc Benchmark_RoutePatternMatch(t *testing.B) {\n\tbenchCaseFn := func(testCollection routeCaseCollection) {\n\t\tfor _, c := range testCollection.testCases {\n\t\t\t// skip all cases for partial checks\n\t\t\tif c.partialCheck {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar matchRes bool\n\t\t\tstate := \"match\"\n\t\t\tif !c.match {\n\t\t\t\tstate = \"not match\"\n\t\t\t}\n\t\t\tt.Run(testCollection.pattern+\" | \"+state+\" | \"+c.url, func(b *testing.B) {\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\tif match := RoutePatternMatch(c.url, testCollection.pattern); match {\n\t\t\t\t\t\t// Get testCases from the original path\n\t\t\t\t\t\tmatchRes = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trequire.Equal(t, c.match, matchRes, \"route: '%s', url: '%s'\", testCollection.pattern, c.url)\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, testCollection := range benchmarkCases {\n\t\tbenchCaseFn(testCollection)\n\t}\n}\n\nfunc Test_Route_TooManyParams_Panic(t *testing.T) {\n\tt.Parallel()\n\n\t// Test with exactly maxParams (30) - should work\n\tt.Run(\"exactly_maxParams\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\troute := paramsRoute(t, maxParams)\n\t\trequire.NotPanics(t, func() {\n\t\t\tparseRoute(route)\n\t\t})\n\t})\n\n\t// Test with maxParams + 1 (31) - should panic\n\tt.Run(\"maxParams_plus_one\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\troute := paramsRoute(t, maxParams+1)\n\t\trequire.PanicsWithValue(t, \"Route '\"+route+\"' has 31 parameters, which exceeds the maximum of 30\", func() {\n\t\t\tparseRoute(route)\n\t\t})\n\t})\n\n\t// Test with 35 params - should panic\n\tt.Run(\"35_params\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\troute := paramsRoute(t, maxParams+5)\n\t\trequire.PanicsWithValue(t, \"Route '\"+route+\"' has 35 parameters, which exceeds the maximum of 30\", func() {\n\t\t\tparseRoute(route)\n\t\t})\n\t})\n}\n\nfunc Test_App_Register_TooManyParams_Panic(t *testing.T) {\n\tt.Parallel()\n\n\t// Test registering a route with too many params via app\n\tt.Run(\"register_via_Get\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\troute := paramsRoute(t, maxParams+1)\n\n\t\trequire.PanicsWithValue(t, \"Route '\"+route+\"' has 31 parameters, which exceeds the maximum of 30\", func() {\n\t\t\tapp.Get(route, func(c Ctx) error {\n\t\t\t\treturn c.SendString(\"test\")\n\t\t\t})\n\t\t})\n\t})\n\n\t// Test registering a route with maxParams works\n\tt.Run(\"register_maxParams_works\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapp := New()\n\t\troute := paramsRoute(t, maxParams)\n\n\t\trequire.NotPanics(t, func() {\n\t\t\tapp.Get(route, func(c Ctx) error {\n\t\t\t\treturn c.SendString(\"test\")\n\t\t\t})\n\t\t})\n\t})\n}\n\n// paramsRoute generates a route with n parameters for testing parseRoute maxParams condition.\n// Returns a route in the format \"/:p1/:p2/:p3/.../:pN\"\nfunc paramsRoute(t *testing.T, n int) string {\n\tt.Helper()\n\tparams := make([]string, n)\n\tfor i := range params {\n\t\tparams[i] = fmt.Sprintf(\":p%d\", i+1)\n\t}\n\treturn \"/\" + strings.Join(params, \"/\")\n}\n"
  },
  {
    "path": "path_testcases_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📝 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"strings\"\n)\n\ntype routeTestCase struct {\n\turl          string\n\tparams       []string\n\tmatch        bool\n\tpartialCheck bool\n}\n\ntype routeCaseCollection struct {\n\tpattern   string\n\ttestCases []routeTestCase\n}\n\nvar (\n\tbenchmarkCases []routeCaseCollection\n\trouteTestCases []routeCaseCollection\n)\n\nfunc init() {\n\t// smaller list for benchmark cases\n\tbenchmarkCases = []routeCaseCollection{\n\t\t{\n\t\t\tpattern: \"/api/v1/const\",\n\t\t\ttestCases: []routeTestCase{\n\t\t\t\t{url: \"/api/v1/const\", params: []string{}, match: true},\n\t\t\t\t{url: \"/api/v1\", params: nil, match: false},\n\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t\t{url: \"/api/v1/something\", params: nil, match: false},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tpattern: \"/api/:param/fixedEnd\",\n\t\t\ttestCases: []routeTestCase{\n\t\t\t\t{url: \"/api/abc/fixedEnd\", params: []string{\"abc\"}, match: true},\n\t\t\t\t{url: \"/api/abc/def/fixedEnd\", params: nil, match: false},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tpattern: \"/api/v1/:param/*\",\n\t\t\ttestCases: []routeTestCase{\n\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\", \"\"}, match: true},\n\t\t\t\t{url: \"/api/v1/entity/\", params: []string{\"entity\", \"\"}, match: true},\n\t\t\t\t{url: \"/api/v1/entity/1\", params: []string{\"entity\", \"1\"}, match: true},\n\t\t\t\t{url: \"/api/v\", params: nil, match: false},\n\t\t\t\t{url: \"/api/v2\", params: nil, match: false},\n\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t},\n\t\t},\n\t}\n\n\t// combine benchmark cases and other cases\n\trouteTestCases = benchmarkCases\n\trouteTestCases = append(\n\t\trouteTestCases,\n\t\t[]routeCaseCollection{\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param/+\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/entity/\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/entity/1\", params: []string{\"entity\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/v\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v2\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/optional\", params: []string{\"optional\"}, match: true},\n\t\t\t\t\t{url: \"/api/v\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v2\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/xyz\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/v1/some/resource/name\\:customVerb`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/v1/some/resource/name:customVerb\", params: nil, match: true},\n\t\t\t\t\t{url: \"/v1/some/resource/name:test\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/v1/some/resource/:name\\:customVerb`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/v1/some/resource/test:customVerb\", params: []string{\"test\"}, match: true},\n\t\t\t\t\t{url: \"/v1/some/resource/test:test\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/v1/some/resource/name\\\\:customVerb?\\?/:param/*`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/v1/some/resource/name:customVerb??/test/optionalWildCard/character\", params: []string{\"test\", \"optionalWildCard/character\"}, match: true},\n\t\t\t\t\t{url: \"/v1/some/resource/name:customVerb??/test\", params: []string{\"test\", \"\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/*\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/entity/1/2\", params: []string{\"entity/1/2\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/Entity/1/2\", params: []string{\"Entity/1/2\"}, match: true},\n\t\t\t\t\t{url: \"/api/v\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v2\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/abc\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/entity/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param-:param2\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity-entity2\", params: []string{\"entity\", \"entity2\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/entity/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/entity-8728382\", params: []string{\"entity\", \"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:filename.:extension\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/test.pdf\", params: []string{\"test\", \"pdf\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/test/pdf\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/test-pdf\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/test_pdf\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/shop/product/::filter/color::color/size::size\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/shop/product/:test/color:blue/size:xs\", params: []string{\"test\", \"blue\", \"xs\"}, match: true},\n\t\t\t\t\t{url: \"/shop/product/test/color:blue/size:xs\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/::param?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/:hello\", params: []string{\"hello\"}, match: true},\n\t\t\t\t\t{url: \"/:\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// successive parameters, each take one character and the last parameter gets everything\n\t\t\t{\n\t\t\t\tpattern: \"/test:sign:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/test-abc\", params: []string{\"-\", \"abc\"}, match: true},\n\t\t\t\t\t{url: \"/test\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// optional parameters are not greedy\n\t\t\t{\n\t\t\t\tpattern: \"/:param1:param2?:param3\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/abbbc\", params: []string{\"a\", \"b\", \"bbc\"}, match: true},\n\t\t\t\t\t// {url: \"/ac\", testCases: []string{\"a\", \"\", \"c\"}, match: true}, // TODO: fix it\n\t\t\t\t\t{url: \"/test\", params: []string{\"t\", \"e\", \"st\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/test:optional?:mandatory\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t// {url: \"/testo\", testCases: []string{\"\", \"o\"}, match: true}, // TODO: fix it\n\t\t\t\t\t{url: \"/testoaaa\", params: []string{\"o\", \"aaa\"}, match: true},\n\t\t\t\t\t{url: \"/test\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/test:optional?:optional2?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/testo\", params: []string{\"o\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/testoaaa\", params: []string{\"o\", \"aaa\"}, match: true},\n\t\t\t\t\t{url: \"/test\", params: []string{\"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/tes\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/foo:param?bar\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/foofaselbar\", params: []string{\"fasel\"}, match: true},\n\t\t\t\t\t{url: \"/foobar\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/fooba\", params: nil, match: false},\n\t\t\t\t\t{url: \"/fobar\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/foo*bar\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/foofaselbar\", params: []string{\"fasel\"}, match: true},\n\t\t\t\t\t{url: \"/foobar\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/foo+bar\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/foofaselbar\", params: []string{\"fasel\"}, match: true},\n\t\t\t\t\t{url: \"/foobar\", params: nil, match: false},\n\t\t\t\t\t{url: \"/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/a*cde*g/\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/abbbcdefffg\", params: []string{\"bbb\", \"fff\"}, match: true},\n\t\t\t\t\t{url: \"/acdeg\", params: []string{\"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/*v1*/proxy\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/customer/v1/cart/proxy\", params: []string{\"customer/\", \"/cart\"}, match: true},\n\t\t\t\t\t{url: \"/v1/proxy\", params: []string{\"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/v1/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// successive wildcard -> first wildcard is greedy\n\t\t\t{\n\t\t\t\tpattern: \"/foo***bar\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/foo*abar\", params: []string{\"*a\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/foo*bar\", params: []string{\"*\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/foobar\", params: []string{\"\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/fooba\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// chars in front of a parameter\n\t\t\t{\n\t\t\t\tpattern: \"/name::name\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/name:john\", params: []string{\"john\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/@:name\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/@john\", params: []string{\"john\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/-:name\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/-john\", params: []string{\"john\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/.:name\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/.john\", params: []string{\"john\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param/abc/*\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/well/abc/wildcard\", params: []string{\"well\", \"wildcard\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/well/abc/\", params: []string{\"well\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/well/abc\", params: []string{\"well\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/well/ttt\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/:day/:month?/:year?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/1\", params: []string{\"1\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1/\", params: []string{\"1\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1//\", params: []string{\"1\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1/-/\", params: []string{\"1\", \"-\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1-\", params: []string{\"1-\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1.\", params: []string{\"1.\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1/2\", params: []string{\"1\", \"2\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1/2/3\", params: []string{\"1\", \"2\", \"3\"}, match: true},\n\t\t\t\t\t{url: \"/api/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/:day.:month?.:year?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1/\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1.\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1..\", params: []string{\"1\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1.2\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1.2.\", params: []string{\"1\", \"2\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1.2.3\", params: []string{\"1\", \"2\", \"3\"}, match: true},\n\t\t\t\t\t{url: \"/api/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/:day-:month?-:year?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1/\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1-\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1--\", params: []string{\"1\", \"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1-/\", params: nil, match: false},\n\t\t\t\t\t// {url: \"/api/1-/-\", testCases: nil, match: false}, // TODO: fix this part\n\t\t\t\t\t{url: \"/api/1-2\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/1-2-\", params: []string{\"1\", \"2\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/1-2-3\", params: []string{\"1\", \"2\", \"3\"}, match: true},\n\t\t\t\t\t{url: \"/api/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/*\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker\", params: []string{\"joker\"}, match: true},\n\t\t\t\t\t{url: \"/api\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"v1/entity\"}, match: true},\n\t\t\t\t\t{url: \"/api2/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api_ignore/v1/entity\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/partialCheck/foo/\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/partialCheck/foo/\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/bar\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/bar/baz\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foobar\", params: nil, match: false, partialCheck: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/partialCheck/foo/bar/:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/partialCheck/foo/bar/test\", params: []string{\"test\"}, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/bar/test/test2\", params: []string{\"test\"}, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/bar\", params: nil, match: false, partialCheck: true},\n\t\t\t\t\t{url: \"/partialFoo\", params: nil, match: false, partialCheck: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/partialCheck/foo\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/partialCheck/foo\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foo/bar\", params: nil, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/foobar\", params: nil, match: false, partialCheck: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/partialCheck/:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/partialCheck/value\", params: []string{\"value\"}, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/value/\", params: []string{\"value\"}, match: true, partialCheck: true},\n\t\t\t\t\t{url: \"/partialCheck/value/next\", params: []string{\"value\"}, match: true, partialCheck: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api\", params: nil, match: false},\n\t\t\t\t\t{url: \"\", params: []string{}, match: true},\n\t\t\t\t\t{url: \"/\", params: []string{}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/config/abc.json\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/config/abc.json\", params: []string{}, match: true},\n\t\t\t\t\t{url: \"config/abc.json\", params: nil, match: false},\n\t\t\t\t\t{url: \"/config/efg.json\", params: nil, match: false},\n\t\t\t\t\t{url: \"/config\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/config/*.json\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/config/abc.json\", params: []string{\"abc\"}, match: true},\n\t\t\t\t\t{url: \"/config/efg.json\", params: []string{\"efg\"}, match: true},\n\t\t\t\t\t{url: \"/config/.json\", params: []string{\"\"}, match: true},\n\t\t\t\t\t{url: \"/config/efg.csv\", params: nil, match: false},\n\t\t\t\t\t{url: \"config/abc.json\", params: nil, match: false},\n\t\t\t\t\t{url: \"/config\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/config/+.json\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/config/abc.json\", params: []string{\"abc\"}, match: true},\n\t\t\t\t\t{url: \"/config/.json\", params: nil, match: false},\n\t\t\t\t\t{url: \"/config/efg.json\", params: []string{\"efg\"}, match: true},\n\t\t\t\t\t{url: \"/config/efg.csv\", params: nil, match: false},\n\t\t\t\t\t{url: \"config/abc.json\", params: nil, match: false},\n\t\t\t\t\t{url: \"/config\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/xyz\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"xyz\", params: nil, match: false},\n\t\t\t\t\t{url: \"xyz/\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/*/:param?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/\", params: []string{\"\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker\", params: []string{\"joker\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman\", params: []string{\"joker\", \"batman\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker//batman\", params: []string{\"joker/\", \"batman\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin\", params: []string{\"joker/batman\", \"robin\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1\", params: []string{\"joker/batman/robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1/\", params: []string{\"joker/batman/robin/1\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker-batman/robin/1\", params: []string{\"joker-batman/robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker-batman-robin/1\", params: []string{\"joker-batman-robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker-batman-robin-1\", params: []string{\"joker-batman-robin-1\", \"\"}, match: true},\n\t\t\t\t\t{url: \"/api\", params: []string{\"\", \"\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/*/:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/test/abc\", params: []string{\"test\", \"abc\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman\", params: []string{\"joker\", \"batman\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin\", params: []string{\"joker/batman\", \"robin\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1\", params: []string{\"joker/batman/robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman-robin/1\", params: []string{\"joker/batman-robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker-batman-robin-1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/+/:param\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/test/abc\", params: []string{\"test\", \"abc\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1\", params: []string{\"joker/batman/robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/*/:param/:param2\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/test/abc/1\", params: []string{\"test\", \"abc\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/joker/batman-robin/1\", params: []string{\"joker\", \"batman-robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker-batman-robin-1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/test/abc\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/joker/batman/robin\", params: []string{\"joker\", \"batman\", \"robin\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1\", params: []string{\"joker/batman\", \"robin\", \"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/joker/batman/robin/1/2\", params: []string{\"joker/batman/robin\", \"1\", \"2\"}, match: true},\n\t\t\t\t\t{url: \"/api\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/:test\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: []string{\"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<bool>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/true\", params: []string{\"true\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<float>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: []string{\"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/8728382.5\", params: []string{\"8728382.5\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<alpha>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/#!?\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<guid>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d\", params: []string{\"f0fa66cc-d22e-445b-866d-1d76e776371d\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<minLen>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<minLen(5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: []string{\"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/123\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/12345\", params: []string{\"12345\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<maxLen(5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/ent\", params: []string{\"ent\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/12345\", params: []string{\"12345\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<len(5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/12345\", params: []string{\"12345\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<betweenLen(1)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<betweenLen(2,5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/e\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/en\", params: []string{\"en\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/12345\", params: []string{\"12345\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<betweenLen(2,5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/e\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/en\", params: []string{\"en\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/12345\", params: []string{\"12345\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<min(5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/1\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/5\", params: []string{\"5\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<max(5)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/1\", params: []string{\"1\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/5\", params: []string{\"5\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/15\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<range(5,10)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/9\", params: []string{\"9\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/5\", params: []string{\"5\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/15\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/api/v1/:param<datetime(2006\\-01\\-02)>`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/2005-11-01\", params: []string{\"2005-11-01\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<regex(p([a-z]+)ch)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/15\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/peach\", params: []string{\"peach\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/p34ch\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<regex(^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/12\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/xy\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/test\", params: []string{\"test\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/\" + strings.Repeat(\"a\", 64), params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/api/v1/:param<regex(\\d{4}-\\d{2}-\\d{2})}>`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/ent\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/15\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/2022-08-27\", params: []string{\"2022-08-27\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/2022/08-27\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int;bool((>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: []string{\"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int;max(3000)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int;maxLen(10)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/87283827683\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int;range(10,30)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/87283827683\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/25\", params: []string{\"25\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/api/v1/:param<int\\;range(10,30)>`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: []string{\"entity\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/87283827683\", params: []string{\"87283827683\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/25\", params: []string{\"25\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: []string{\"true\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/api/v1/:param<range(10\\,30,1500)>`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/87283827683\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/25\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/1200\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<range(10,1500)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/87283827683\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/25\", params: []string{\"25\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/1200\", params: []string{\"1200\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:lang<len(2)>/videos/:page<range(100,1500)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/try/videos/200\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/videos/1800\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/videos/100\", params: []string{\"tr\", \"100\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/e/videos/10\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:lang<len(2)>/:page<range(100,1500)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/try/200\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/1800\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/100\", params: []string{\"tr\", \"100\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/e/10\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:lang/:page<range(100,1500)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/try/200\", params: []string{\"try\", \"200\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/tr/1800\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/100\", params: []string{\"tr\", \"100\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/e/10\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:lang<len(2)>/:page\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/try/200\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/tr/1800\", params: []string{\"tr\", \"1800\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/tr/100\", params: []string{\"tr\", \"100\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/e/10\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: `/api/v1/:date<datetime(2006\\-01\\-02)>/:regex<regex(p([a-z]+)ch)>`,\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/2005-11-01/a\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/2005-1101/paach\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/2005-11-01/peach\", params: []string{\"2005-11-01\", \"peach\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<int>?\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/entity\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/8728382\", params: []string{\"8728382\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/true\", params: nil, match: false},\n\t\t\t\t\t{url: \"/api/v1/\", params: []string{\"\"}, match: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Add test case for RegexCompiler == nil\n\t\t\t{\n\t\t\t\tpattern: \"/api/v1/:param<regex(\\\\d+)>\",\n\t\t\t\ttestCases: []routeTestCase{\n\t\t\t\t\t{url: \"/api/v1/123\", params: []string{\"123\"}, match: true},\n\t\t\t\t\t{url: \"/api/v1/abc\", params: nil, match: false},\n\t\t\t\t},\n\t\t\t},\n\t\t}...,\n\t)\n}\n"
  },
  {
    "path": "prefork.go",
    "content": "package fiber\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/valyala/fasthttp/reuseport\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n)\n\nconst (\n\tenvPreforkChildKey = \"FIBER_PREFORK_CHILD\"\n\tenvPreforkChildVal = \"1\"\n\tsleepDuration      = 100 * time.Millisecond\n\twindowsOS          = \"windows\"\n)\n\nvar (\n\ttestPreforkMaster = false\n\ttestOnPrefork     = false\n)\n\n// IsChild determines if the current process is a child of Prefork\nfunc IsChild() bool {\n\treturn os.Getenv(envPreforkChildKey) == envPreforkChildVal\n}\n\n// prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature\nfunc (app *App) prefork(addr string, tlsConfig *tls.Config, cfg *ListenConfig) error {\n\tif cfg == nil {\n\t\tcfg = &ListenConfig{}\n\t}\n\tvar ln net.Listener\n\tvar err error\n\n\t// 👶 child process 👶\n\tif IsChild() {\n\t\t// use 1 cpu core per child process\n\t\truntime.GOMAXPROCS(1)\n\t\t// Linux will use SO_REUSEPORT and Windows falls back to SO_REUSEADDR\n\t\t// Only tcp4 or tcp6 is supported when preforking, both are not supported\n\t\tif ln, err = reuseport.Listen(cfg.ListenerNetwork, addr); err != nil {\n\t\t\tif !cfg.DisableStartupMessage {\n\t\t\t\ttime.Sleep(sleepDuration) // avoid colliding with startup message\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"prefork: %w\", err)\n\t\t}\n\t\t// wrap a tls config around the listener if provided\n\t\tif tlsConfig != nil {\n\t\t\tln = tls.NewListener(ln, tlsConfig)\n\t\t}\n\n\t\t// kill current child proc when master exits\n\t\tmasterPID := os.Getppid()\n\t\tgo watchMaster(masterPID)\n\n\t\t// prepare the server for the start\n\t\tapp.startupProcess()\n\n\t\tif cfg.ListenerAddrFunc != nil {\n\t\t\tcfg.ListenerAddrFunc(ln.Addr())\n\t\t}\n\n\t\t// listen for incoming connections\n\t\treturn app.server.Serve(ln)\n\t}\n\n\t// 👮 master process 👮\n\ttype child struct {\n\t\terr error\n\t\tpid int\n\t}\n\t// create variables\n\tmaxProcs := runtime.GOMAXPROCS(0)\n\tchildren := make(map[int]*exec.Cmd)\n\tchannel := make(chan child, maxProcs)\n\n\t// kill child procs when master exits\n\tdefer func() {\n\t\tfor _, proc := range children {\n\t\t\tif err = proc.Process.Kill(); err != nil {\n\t\t\t\tif !errors.Is(err, os.ErrProcessDone) {\n\t\t\t\t\tlog.Errorf(\"prefork: failed to kill child: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// collect child pids\n\tvar childPIDs []int\n\n\t// launch child procs\n\tfor range maxProcs {\n\t\tcmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint:gosec // It's fine to launch the same process again\n\t\tif testPreforkMaster {\n\t\t\t// When test prefork master,\n\t\t\t// just start the child process with a dummy cmd,\n\t\t\t// which will exit soon\n\t\t\tcmd = dummyCmd()\n\t\t}\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\n\t\t// add fiber prefork child flag into child proc env\n\t\tcmd.Env = append(os.Environ(),\n\t\t\tfmt.Sprintf(\"%s=%s\", envPreforkChildKey, envPreforkChildVal),\n\t\t)\n\n\t\tif err = cmd.Start(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start a child prefork process, error: %w\", err)\n\t\t}\n\n\t\t// store child process\n\t\tpid := cmd.Process.Pid\n\t\tchildren[pid] = cmd\n\t\tchildPIDs = append(childPIDs, pid)\n\n\t\t// execute fork hook\n\t\tif app.hooks != nil {\n\t\t\tif testOnPrefork {\n\t\t\t\tapp.hooks.executeOnForkHooks(dummyPid)\n\t\t\t} else {\n\t\t\t\tapp.hooks.executeOnForkHooks(pid)\n\t\t\t}\n\t\t}\n\n\t\t// notify master if child crashes\n\t\tgo func() {\n\t\t\tchannel <- child{pid: pid, err: cmd.Wait()}\n\t\t}()\n\t}\n\n\t// Run onListen hooks\n\t// Hooks have to be run here as different as non-prefork mode due to they should run as child or master\n\tlistenData := app.prepareListenData(addr, tlsConfig != nil, cfg, childPIDs)\n\n\tapp.runOnListenHooks(listenData)\n\n\tapp.startupMessage(listenData, cfg)\n\n\tif cfg.EnablePrintRoutes {\n\t\tapp.printRoutesMessage()\n\t}\n\n\t// return error if child crashes\n\treturn (<-channel).err\n}\n\n// watchMaster watches the master process and exits if it dies.\n// It detects master death by checking if the parent PID has changed,\n// which happens when the master exits and the child is reparented to\n// another process (often init/PID 1, but could be a subreaper).\nfunc watchMaster(masterPID int) {\n\tif runtime.GOOS == windowsOS {\n\t\t// finds parent process,\n\t\t// and waits for it to exit\n\t\tp, err := os.FindProcess(masterPID)\n\t\tif err == nil {\n\t\t\t_, _ = p.Wait() //nolint:errcheck // It is fine to ignore the error here\n\t\t}\n\t\tos.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork\n\t}\n\t// Watch for parent PID changes. When the master exits, the OS\n\t// reparents the child to another process, causing Getppid() to change.\n\t// Comparing against the original PID instead of hardcoding 1 ensures\n\t// this works correctly when the master itself is PID 1 (e.g. in\n\t// Docker containers).\n\tconst watchInterval = 500 * time.Millisecond\n\tfor range time.NewTicker(watchInterval).C {\n\t\tif os.Getppid() != masterPID {\n\t\t\tos.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork\n\t\t}\n\t}\n}\n\nvar (\n\tdummyPid      = 1\n\tdummyChildCmd atomic.Value\n)\n\n// dummyCmd is for internal prefork testing\nfunc dummyCmd() *exec.Cmd {\n\tcommand := \"go\"\n\tif storeCommand := dummyChildCmd.Load(); storeCommand != nil && storeCommand != \"\" {\n\t\tcommand = storeCommand.(string) //nolint:forcetypeassert,errcheck // We always store a string in here\n\t}\n\tif runtime.GOOS == windowsOS {\n\t\treturn exec.Command(\"cmd\", \"/C\", command, \"version\")\n\t}\n\treturn exec.Command(command, \"version\")\n}\n"
  },
  {
    "path": "prefork_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📄 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n// 💖 Maintained and modified for Fiber by @renewerner87\npackage fiber\n\nimport (\n\t\"crypto/tls\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_App_Prefork_Child_Process(t *testing.T) {\n\t// Reset test var\n\ttestPreforkMaster = true\n\n\tsetupIsChild(t)\n\n\tapp := New()\n\n\tcfg := listenConfigDefault()\n\terr := app.prefork(\"invalid\", nil, &cfg)\n\trequire.Error(t, err)\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\tipv6Cfg := ListenConfig{ListenerNetwork: NetworkTCP6}\n\trequire.NoError(t, app.prefork(\"[::1]:\", nil, &ipv6Cfg))\n\n\t// Create tls certificate\n\tcer, err := tls.LoadX509KeyPair(\"./.github/testdata/ssl.pem\", \"./.github/testdata/ssl.key\")\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\t//nolint:gosec // We're in a test so using old ciphers is fine\n\tconfig := &tls.Config{Certificates: []tls.Certificate{cer}}\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\tcfg = listenConfigDefault()\n\trequire.NoError(t, app.prefork(\"127.0.0.1:\", config, &cfg))\n}\n\nfunc Test_App_Prefork_Master_Process(t *testing.T) {\n\t// Reset test var\n\ttestPreforkMaster = true\n\n\tapp := New()\n\n\tgo func() {\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t\tassert.NoError(t, app.Shutdown())\n\t}()\n\n\tcfg := listenConfigDefault()\n\trequire.NoError(t, app.prefork(\":0\", nil, &cfg))\n\n\tdummyChildCmd.Store(\"invalid\")\n\n\tcfg = listenConfigDefault()\n\terr := app.prefork(\"127.0.0.1:\", nil, &cfg)\n\trequire.Error(t, err)\n\n\tdummyChildCmd.Store(\"go\")\n}\n\nfunc Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) {\n\tsetupIsChild(t)\n\n\trescueStdout := os.Stdout\n\tdefer func() { os.Stdout = rescueStdout }()\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\tos.Stdout = w\n\n\tcfg := listenConfigDefault()\n\tapp := New()\n\tapp.startupProcess()\n\tlistenData := app.prepareListenData(\":0\", false, &cfg, nil)\n\tapp.startupMessage(listenData, &cfg)\n\n\trequire.NoError(t, w.Close())\n\n\tout, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\trequire.Empty(t, out)\n}\n\nfunc setupIsChild(t *testing.T) {\n\tt.Helper()\n\n\tt.Setenv(envPreforkChildKey, envPreforkChildVal)\n}\n"
  },
  {
    "path": "readonly.go",
    "content": "//go:build !s390x && !ppc64 && !ppc64le\n\npackage fiber\n\nimport (\n\t\"unsafe\"\n)\n\n//go:linkname runtimeRodata runtime.rodata\nvar runtimeRodata byte\n\n//go:linkname runtimeErodata runtime.erodata\nvar runtimeErodata byte\n\nfunc isReadOnly(p unsafe.Pointer) bool {\n\tstart := uintptr(unsafe.Pointer(&runtimeRodata)) //nolint:gosec // converting runtime symbols\n\tend := uintptr(unsafe.Pointer(&runtimeErodata))  //nolint:gosec // converting runtime symbols\n\taddr := uintptr(p)\n\treturn addr >= start && addr < end\n}\n"
  },
  {
    "path": "readonly_strict.go",
    "content": "//go:build s390x || ppc64 || ppc64le\n\npackage fiber\n\nimport \"unsafe\"\n\nfunc isReadOnly(_ unsafe.Pointer) bool {\n\treturn false\n}\n"
  },
  {
    "path": "redirect.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📝 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"sync\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n\n\t\"github.com/gofiber/fiber/v3/binder\"\n)\n\n// Pool for redirection\nvar (\n\tredirectPool = sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn &Redirect{\n\t\t\t\tstatus:   StatusSeeOther,\n\t\t\t\tmessages: make(redirectionMsgs, 0),\n\t\t\t}\n\t\t},\n\t}\n\toldInputPool = sync.Pool{\n\t\tNew: func() any {\n\t\t\treturn make(map[string]string)\n\t\t},\n\t}\n)\n\nconst maxPoolableMapSize = 64\n\n// FlashCookieName Cookie name to send flash messages when to use redirection.\nconst (\n\tFlashCookieName = \"fiber_flash\"\n)\n\nvar flashCookieNeedle = []byte(FlashCookieName + \"=\")\n\n// hasFlashCookie is on the request hot path and runs on every request/response cycle.\n// Keep this cheap for users who don't use flash messages:\n// 1) a fast raw-header prefilter to avoid unnecessary cookie parsing,\n// 2) an exact cookie lookup to avoid prefix false positives (e.g. fiber_flashX).\nfunc hasFlashCookie(header *fasthttp.RequestHeader) bool {\n\trawHeaders := header.RawHeaders()\n\tif len(rawHeaders) == 0 {\n\t\treturn false\n\t}\n\n\tif !bytes.Contains(rawHeaders, flashCookieNeedle) {\n\t\treturn false\n\t}\n\n\treturn header.Cookie(FlashCookieName) != nil\n}\n\n// redirectionMsgs is a struct that used to store flash messages and old input data in cookie using MSGP.\n// msgp -file=\"redirect.go\" -o=\"redirect_msgp.go\" -unexported\n// Cookie payloads are limited to ~4KB, so keep flash message counts bounded but usable.\n//\n//msgp:limit arrays:256 maps:32 marshal:true\n//msgp:ignore Redirect RedirectConfig OldInputData FlashMessage\ntype redirectionMsg struct {\n\tkey        string\n\tvalue      string\n\tlevel      uint8\n\tisOldInput bool\n}\n\ntype redirectionMsgs []redirectionMsg\n\n// OldInputData is a struct that holds the old input data.\ntype OldInputData struct {\n\tKey   string\n\tValue string\n}\n\n// FlashMessage is a struct that holds the flash message data.\ntype FlashMessage struct {\n\tKey   string\n\tValue string\n\tLevel uint8\n}\n\n// Redirect is a struct that holds the redirect data.\ntype Redirect struct {\n\tc        *DefaultCtx     // Embed ctx\n\tmessages redirectionMsgs // Flash messages and old input data\n\tstatus   int             // Status code of redirection. Default: 303 StatusSeeOther\n}\n\n// RedirectConfig A config to use with Redirect().Route()\n// You can specify queries or route parameters.\n// NOTE: We don't use net/url to parse parameters because of it has poor performance. You have to pass map.\ntype RedirectConfig struct {\n\tParams  Map               // Route parameters\n\tQueries map[string]string // Query map\n}\n\n// AcquireRedirect return default Redirect reference from the redirect pool\nfunc AcquireRedirect() *Redirect {\n\tredirect, ok := redirectPool.Get().(*Redirect)\n\tif !ok {\n\t\tpanic(errRedirectTypeAssertion)\n\t}\n\n\treturn redirect\n}\n\n// ReleaseRedirect returns c acquired via Redirect to redirect pool.\n//\n// It is forbidden accessing req and/or its members after returning\n// it to redirect pool.\nfunc ReleaseRedirect(r *Redirect) {\n\tr.release()\n\tredirectPool.Put(r)\n}\n\nfunc (r *Redirect) release() {\n\tr.status = StatusSeeOther\n\tr.messages = r.messages[:0]\n\tr.c = nil\n}\n\nfunc acquireOldInput() map[string]string {\n\toldInput, ok := oldInputPool.Get().(map[string]string)\n\tif !ok {\n\t\treturn make(map[string]string)\n\t}\n\n\treturn oldInput\n}\n\nfunc releaseOldInput(oldInput map[string]string) {\n\tif len(oldInput) > maxPoolableMapSize {\n\t\treturn\n\t}\n\n\tclear(oldInput)\n\toldInputPool.Put(oldInput)\n}\n\n// Status sets the status code of redirection.\n// If status is not specified, status defaults to 303 See Other.\nfunc (r *Redirect) Status(code int) *Redirect {\n\tr.status = code\n\n\treturn r\n}\n\n// With You can send flash messages by using With().\n// They will be sent as a cookie.\n// You can get them by using: Redirect().Messages(), Redirect().Message()\n// Note: You must use escape char before using ',' and ':' chars to avoid wrong parsing.\nfunc (r *Redirect) With(key, value string, level ...uint8) *Redirect {\n\t// Get level\n\tvar msgLevel uint8\n\tif len(level) > 0 {\n\t\tmsgLevel = level[0]\n\t}\n\n\t// Override old message if exists\n\tfor i, msg := range r.messages {\n\t\tif msg.key == key && !msg.isOldInput {\n\t\t\tr.messages[i].value = value\n\t\t\tr.messages[i].level = msgLevel\n\n\t\t\treturn r\n\t\t}\n\t}\n\n\tr.messages = append(r.messages, redirectionMsg{\n\t\tkey:   key,\n\t\tvalue: value,\n\t\tlevel: msgLevel,\n\t})\n\n\treturn r\n}\n\n// WithInput You can send input data by using WithInput().\n// They will be sent as a cookie.\n// This method can send form, multipart form, query data to redirected route.\n// You can get them by using: Redirect().OldInputs(), Redirect().OldInput()\nfunc (r *Redirect) WithInput() *Redirect {\n\t// Get content-type\n\tctype := utils.UnsafeString(utilsbytes.UnsafeToLower(r.c.RequestCtx().Request.Header.ContentType()))\n\tctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))\n\n\toldInput := acquireOldInput()\n\tdefer releaseOldInput(oldInput)\n\n\tswitch ctype {\n\tcase MIMEApplicationForm, MIMEMultipartForm:\n\t\t_ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed\n\tdefault:\n\t\t_ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed\n\t}\n\n\t// Add old input data\n\tfor k, v := range oldInput {\n\t\tr.messages = append(r.messages, redirectionMsg{\n\t\t\tkey:        k,\n\t\t\tvalue:      v,\n\t\t\tisOldInput: true,\n\t\t})\n\t}\n\n\treturn r\n}\n\n// Messages Get flash messages.\nfunc (r *Redirect) Messages() []FlashMessage {\n\tif len(r.c.flashMessages) == 0 {\n\t\treturn nil\n\t}\n\n\tflashMessages := make([]FlashMessage, 0, len(r.c.flashMessages))\n\twriteIdx := 0\n\n\tfor _, msg := range r.c.flashMessages {\n\t\tif msg.isOldInput {\n\t\t\tr.c.flashMessages[writeIdx] = msg\n\t\t\twriteIdx++\n\t\t\tcontinue\n\t\t}\n\n\t\tflashMessages = append(flashMessages, FlashMessage{\n\t\t\tKey:   msg.key,\n\t\t\tValue: msg.value,\n\t\t\tLevel: msg.level,\n\t\t})\n\t}\n\n\tfor i := writeIdx; i < len(r.c.flashMessages); i++ {\n\t\tr.c.flashMessages[i] = redirectionMsg{}\n\t}\n\n\tr.c.flashMessages = r.c.flashMessages[:writeIdx]\n\n\treturn flashMessages\n}\n\n// Message Get flash message by key.\nfunc (r *Redirect) Message(key string) FlashMessage {\n\tif len(r.c.flashMessages) == 0 {\n\t\treturn FlashMessage{}\n\t}\n\n\tvar flashMessage FlashMessage\n\tfound := false\n\twriteIdx := 0\n\n\tfor _, msg := range r.c.flashMessages {\n\t\tif msg.isOldInput || found || msg.key != key {\n\t\t\tr.c.flashMessages[writeIdx] = msg\n\t\t\twriteIdx++\n\t\t\tcontinue\n\t\t}\n\n\t\tflashMessage = FlashMessage{\n\t\t\tKey:   msg.key,\n\t\t\tValue: msg.value,\n\t\t\tLevel: msg.level,\n\t\t}\n\t\tfound = true\n\t}\n\n\tfor i := writeIdx; i < len(r.c.flashMessages); i++ {\n\t\tr.c.flashMessages[i] = redirectionMsg{}\n\t}\n\n\tr.c.flashMessages = r.c.flashMessages[:writeIdx]\n\n\treturn flashMessage\n}\n\n// OldInputs Get old input data.\nfunc (r *Redirect) OldInputs() []OldInputData {\n\t// Count old inputs first to avoid allocation if none exist\n\tcount := 0\n\tfor _, msg := range r.c.flashMessages {\n\t\tif msg.isOldInput {\n\t\t\tcount++\n\t\t}\n\t}\n\n\tif count == 0 {\n\t\treturn nil\n\t}\n\n\tinputs := make([]OldInputData, 0, count)\n\tfor _, msg := range r.c.flashMessages {\n\t\tif msg.isOldInput {\n\t\t\tinputs = append(inputs, OldInputData{\n\t\t\t\tKey:   msg.key,\n\t\t\t\tValue: msg.value,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn inputs\n}\n\n// OldInput Get old input data by key.\nfunc (r *Redirect) OldInput(key string) OldInputData {\n\tmsgs := r.c.flashMessages\n\n\tfor _, msg := range msgs {\n\t\tif msg.key == key && msg.isOldInput {\n\t\t\treturn OldInputData{\n\t\t\t\tKey:   msg.key,\n\t\t\t\tValue: msg.value,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn OldInputData{}\n}\n\n// To redirect to the URL derived from the specified path, with specified status.\nfunc (r *Redirect) To(location string) error {\n\tr.c.setCanonical(HeaderLocation, location)\n\tr.c.Status(r.status)\n\n\tr.processFlashMessages()\n\n\treturn nil\n}\n\n// Route redirects to the Route registered in the app with appropriate parameters.\n// If you want to send queries or params to route, you should use config parameter.\nfunc (r *Redirect) Route(name string, config ...RedirectConfig) error {\n\t// Check config\n\tcfg := RedirectConfig{}\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\n\t// Get location from route name\n\troute := r.c.App().GetRoute(name)\n\tlocation, err := r.c.getLocationFromRoute(&route, cfg.Params)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check queries\n\tif len(cfg.Queries) > 0 {\n\t\tqueryText := bytebufferpool.Get()\n\t\tdefer bytebufferpool.Put(queryText)\n\n\t\tfirst := true\n\t\tfor k, v := range cfg.Queries {\n\t\t\tif !first {\n\t\t\t\tqueryText.WriteByte('&')\n\t\t\t}\n\t\t\tfirst = false\n\t\t\tqueryText.WriteString(k)\n\t\t\tqueryText.WriteByte('=')\n\t\t\tqueryText.WriteString(v)\n\t\t}\n\n\t\treturn r.To(location + \"?\" + r.c.app.toString(queryText.Bytes()))\n\t}\n\n\treturn r.To(location)\n}\n\n// Back redirect to the URL to referer.\nfunc (r *Redirect) Back(fallback ...string) error {\n\tlocation := r.c.Get(HeaderReferer)\n\tif location == \"\" {\n\t\t// Check fallback URL\n\t\tif len(fallback) == 0 {\n\t\t\terr := ErrRedirectBackNoFallback\n\t\t\tr.c.Status(err.Code)\n\n\t\t\treturn err\n\t\t}\n\t\tlocation = fallback[0]\n\t}\n\n\treturn r.To(location)\n}\n\n// parseAndClearFlashMessages is a method to get flash messages before they are getting removed\nfunc (r *Redirect) parseAndClearFlashMessages() {\n\t// parse flash messages\n\tcookieValue, err := hex.DecodeString(r.c.Cookies(FlashCookieName))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t_, err = r.c.flashMessages.UnmarshalMsg(cookieValue)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tr.c.Cookie(&Cookie{\n\t\tName:   FlashCookieName,\n\t\tValue:  \"\",\n\t\tPath:   \"/\",\n\t\tMaxAge: -1,\n\t})\n}\n\n// processFlashMessages is a helper function to process flash messages and old input data\n// and set them as cookies\nfunc (r *Redirect) processFlashMessages() {\n\tif len(r.messages) == 0 {\n\t\treturn\n\t}\n\n\tval, err := r.messages.MarshalMsg(nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tdst := make([]byte, hex.EncodedLen(len(val)))\n\thex.Encode(dst, val)\n\n\tr.c.Cookie(&Cookie{\n\t\tName:        FlashCookieName,\n\t\tValue:       r.c.app.toString(dst),\n\t\tSessionOnly: true,\n\t})\n}\n"
  },
  {
    "path": "redirect_msgp.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage fiber\n\nimport (\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\n// Size limits for msgp deserialization\nconst (\n\tzc920acdalimitArrays = 256\n\tzc920acdalimitMaps   = 32\n)\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *redirectionMsg) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, err = dc.ReadMapHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tif zb0001 > zc920acdalimitMaps {\n\t\terr = msgp.ErrLimitExceeded\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, err = dc.ReadMapKeyPtr()\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"key\":\n\t\t\tz.key, err = dc.ReadString()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"key\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"value\":\n\t\t\tz.value, err = dc.ReadString()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"value\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"level\":\n\t\t\tz.level, err = dc.ReadUint8()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"level\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"isOldInput\":\n\t\t\tz.isOldInput, err = dc.ReadBool()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"isOldInput\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\terr = dc.Skip()\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z *redirectionMsg) EncodeMsg(en *msgp.Writer) (err error) {\n\t// map header, size 4\n\t// write \"key\"\n\terr = en.Append(0x84, 0xa3, 0x6b, 0x65, 0x79)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteString(z.key)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"key\")\n\t\treturn\n\t}\n\t// write \"value\"\n\terr = en.Append(0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteString(z.value)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"value\")\n\t\treturn\n\t}\n\t// write \"level\"\n\terr = en.Append(0xa5, 0x6c, 0x65, 0x76, 0x65, 0x6c)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteUint8(z.level)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"level\")\n\t\treturn\n\t}\n\t// write \"isOldInput\"\n\terr = en.Append(0xaa, 0x69, 0x73, 0x4f, 0x6c, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74)\n\tif err != nil {\n\t\treturn\n\t}\n\terr = en.WriteBool(z.isOldInput)\n\tif err != nil {\n\t\terr = msgp.WrapError(err, \"isOldInput\")\n\t\treturn\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z *redirectionMsg) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\t// map header, size 4\n\t// string \"key\"\n\to = append(o, 0x84, 0xa3, 0x6b, 0x65, 0x79)\n\to = msgp.AppendString(o, z.key)\n\t// string \"value\"\n\to = append(o, 0xa5, 0x76, 0x61, 0x6c, 0x75, 0x65)\n\to = msgp.AppendString(o, z.value)\n\t// string \"level\"\n\to = append(o, 0xa5, 0x6c, 0x65, 0x76, 0x65, 0x6c)\n\to = msgp.AppendUint8(o, z.level)\n\t// string \"isOldInput\"\n\to = append(o, 0xaa, 0x69, 0x73, 0x4f, 0x6c, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74)\n\to = msgp.AppendBool(o, z.isOldInput)\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *redirectionMsg) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar field []byte\n\t_ = field\n\tvar zb0001 uint32\n\tzb0001, bts, err = msgp.ReadMapHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tif zb0001 > zc920acdalimitMaps {\n\t\terr = msgp.ErrLimitExceeded\n\t\treturn\n\t}\n\tfor zb0001 > 0 {\n\t\tzb0001--\n\t\tfield, bts, err = msgp.ReadMapKeyZC(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msgp.UnsafeString(field) {\n\t\tcase \"key\":\n\t\t\tz.key, bts, err = msgp.ReadStringBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"key\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"value\":\n\t\t\tz.value, bts, err = msgp.ReadStringBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"value\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"level\":\n\t\t\tz.level, bts, err = msgp.ReadUint8Bytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"level\")\n\t\t\t\treturn\n\t\t\t}\n\t\tcase \"isOldInput\":\n\t\t\tz.isOldInput, bts, err = msgp.ReadBoolBytes(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err, \"isOldInput\")\n\t\t\t\treturn\n\t\t\t}\n\t\tdefault:\n\t\t\tbts, err = msgp.Skip(bts)\n\t\t\tif err != nil {\n\t\t\t\terr = msgp.WrapError(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z *redirectionMsg) Msgsize() (s int) {\n\ts = 1 + 4 + msgp.StringPrefixSize + len(z.key) + 6 + msgp.StringPrefixSize + len(z.value) + 6 + msgp.Uint8Size + 11 + msgp.BoolSize\n\treturn\n}\n\n// DecodeMsg implements msgp.Decodable\nfunc (z *redirectionMsgs) DecodeMsg(dc *msgp.Reader) (err error) {\n\tvar zb0002 uint32\n\tzb0002, err = dc.ReadArrayHeader()\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tif zb0002 > zc920acdalimitArrays {\n\t\terr = msgp.ErrLimitExceeded\n\t\treturn\n\t}\n\tif cap((*z)) >= int(zb0002) {\n\t\t(*z) = (*z)[:zb0002]\n\t} else {\n\t\t(*z) = make(redirectionMsgs, zb0002)\n\t}\n\tfor zb0001 := range *z {\n\t\terr = (*z)[zb0001].DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, zb0001)\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// EncodeMsg implements msgp.Encodable\nfunc (z redirectionMsgs) EncodeMsg(en *msgp.Writer) (err error) {\n\terr = en.WriteArrayHeader(uint32(len(z)))\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tif uint32(len(z)) > zc920acdalimitArrays {\n\t\terr = msgp.ErrLimitExceeded\n\t\treturn\n\t}\n\tfor zb0003 := range z {\n\t\terr = z[zb0003].EncodeMsg(en)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, zb0003)\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// MarshalMsg implements msgp.Marshaler\nfunc (z redirectionMsgs) MarshalMsg(b []byte) (o []byte, err error) {\n\to = msgp.Require(b, z.Msgsize())\n\to = msgp.AppendArrayHeader(o, uint32(len(z)))\n\tif uint32(len(z)) > zc920acdalimitArrays {\n\t\treturn nil, msgp.ErrLimitExceeded\n\t}\n\tfor zb0003 := range z {\n\t\to, err = z[zb0003].MarshalMsg(o)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, zb0003)\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// UnmarshalMsg implements msgp.Unmarshaler\nfunc (z *redirectionMsgs) UnmarshalMsg(bts []byte) (o []byte, err error) {\n\tvar zb0002 uint32\n\tzb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)\n\tif err != nil {\n\t\terr = msgp.WrapError(err)\n\t\treturn\n\t}\n\tif zb0002 > zc920acdalimitArrays {\n\t\terr = msgp.ErrLimitExceeded\n\t\treturn\n\t}\n\tif cap((*z)) >= int(zb0002) {\n\t\t(*z) = (*z)[:zb0002]\n\t} else {\n\t\t(*z) = make(redirectionMsgs, zb0002)\n\t}\n\tfor zb0001 := range *z {\n\t\tbts, err = (*z)[zb0001].UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\terr = msgp.WrapError(err, zb0001)\n\t\t\treturn\n\t\t}\n\t}\n\to = bts\n\treturn\n}\n\n// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message\nfunc (z redirectionMsgs) Msgsize() (s int) {\n\ts = msgp.ArrayHeaderSize\n\tfor zb0003 := range z {\n\t\ts += z[zb0003].Msgsize()\n\t}\n\treturn\n}\n"
  },
  {
    "path": "redirect_msgp_test.go",
    "content": "// Code generated by github.com/tinylib/msgp DO NOT EDIT.\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/tinylib/msgp/msgp\"\n)\n\nfunc TestMarshalUnmarshalredirectionMsg(t *testing.T) {\n\tv := redirectionMsg{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgredirectionMsg(b *testing.B) {\n\tv := redirectionMsg{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgredirectionMsg(b *testing.B) {\n\tv := redirectionMsg{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalredirectionMsg(b *testing.B) {\n\tv := redirectionMsg{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecoderedirectionMsg(t *testing.T) {\n\tv := redirectionMsg{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecoderedirectionMsg Msgsize() is inaccurate\")\n\t}\n\n\tvn := redirectionMsg{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncoderedirectionMsg(b *testing.B) {\n\tv := redirectionMsg{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecoderedirectionMsg(b *testing.B) {\n\tv := redirectionMsg{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestMarshalUnmarshalredirectionMsgs(t *testing.T) {\n\tv := redirectionMsgs{}\n\tbts, err := v.MarshalMsg(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleft, err := v.UnmarshalMsg(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after UnmarshalMsg(): %q\", len(left), left)\n\t}\n\n\tleft, err = msgp.Skip(bts)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(left) > 0 {\n\t\tt.Errorf(\"%d bytes left over after Skip(): %q\", len(left), left)\n\t}\n}\n\nfunc BenchmarkMarshalMsgredirectionMsgs(b *testing.B) {\n\tv := redirectionMsgs{}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.MarshalMsg(nil)\n\t}\n}\n\nfunc BenchmarkAppendMsgredirectionMsgs(b *testing.B) {\n\tv := redirectionMsgs{}\n\tbts := make([]byte, 0, v.Msgsize())\n\tbts, _ = v.MarshalMsg(bts[0:0])\n\tb.SetBytes(int64(len(bts)))\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tbts, _ = v.MarshalMsg(bts[0:0])\n\t}\n}\n\nfunc BenchmarkUnmarshalredirectionMsgs(b *testing.B) {\n\tv := redirectionMsgs{}\n\tbts, _ := v.MarshalMsg(nil)\n\tb.ReportAllocs()\n\tb.SetBytes(int64(len(bts)))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := v.UnmarshalMsg(bts)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc TestEncodeDecoderedirectionMsgs(t *testing.T) {\n\tv := redirectionMsgs{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\n\tm := v.Msgsize()\n\tif buf.Len() > m {\n\t\tt.Log(\"WARNING: TestEncodeDecoderedirectionMsgs Msgsize() is inaccurate\")\n\t}\n\n\tvn := redirectionMsgs{}\n\terr := msgp.Decode(&buf, &vn)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tbuf.Reset()\n\tmsgp.Encode(&buf, &v)\n\terr = msgp.NewReader(&buf).Skip()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc BenchmarkEncoderedirectionMsgs(b *testing.B) {\n\tv := redirectionMsgs{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\ten := msgp.NewWriter(msgp.Nowhere)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tv.EncodeMsg(en)\n\t}\n\ten.Flush()\n}\n\nfunc BenchmarkDecoderedirectionMsgs(b *testing.B) {\n\tv := redirectionMsgs{}\n\tvar buf bytes.Buffer\n\tmsgp.Encode(&buf, &v)\n\tb.SetBytes(int64(buf.Len()))\n\trd := msgp.NewEndlessReader(buf.Bytes(), b)\n\tdc := msgp.NewReader(rd)\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\terr := v.DecodeMsg(dc)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "redirect_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📝 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc assertFlashCookieCleared(t *testing.T, setCookie string) {\n\tt.Helper()\n\n\tsetCookie = strings.ToLower(setCookie)\n\trequire.Contains(t, setCookie, FlashCookieName+\"=\")\n\trequire.True(t, strings.Contains(setCookie, \"max-age=0\") || strings.Contains(setCookie, \"max-age=-1\"))\n\trequire.Contains(t, setCookie, \"path=/\")\n}\n\n// go test -run Test_Redirect_To\nfunc Test_Redirect_To(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\tdefer app.ReleaseCtx(c)\n\n\terr := c.Redirect().To(\"http://default.com\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"http://default.com\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\terr = c.Redirect().Status(301).To(\"http://example.com\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, 301, c.Response().StatusCode())\n\trequire.Equal(t, \"http://example.com\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_To_WithFlashMessages\nfunc Test_Redirect_To_WithFlashMessages(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().With(\"success\", \"2\").With(\"success\", \"1\").With(\"message\", \"test\", 2).To(\"http://example.com\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"http://example.com\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\tvar msgs redirectionMsgs\n\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\trequire.NoError(t, err)\n\t_, err = msgs.UnmarshalMsg(decoded)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, msgs, 2)\n\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 2, isOldInput: false})\n}\n\n// go test -run Test_Redirect_Route_WithParams\nfunc Test_Redirect_Route_WithParams(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Route(\"user\", RedirectConfig{\n\t\tParams: Map{\n\t\t\t\"name\": \"fiber\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/user/fiber\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_Route_WithParams_WithQueries\nfunc Test_Redirect_Route_WithParams_WithQueries(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Route(\"user\", RedirectConfig{\n\t\tParams: Map{\n\t\t\t\"name\": \"fiber\",\n\t\t},\n\t\tQueries: map[string]string{\"data[0][name]\": \"john\", \"data[0][age]\": \"10\", \"test\": \"doe\"},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\n\t// analysis of query parameters with url parsing, since a map pass is always randomly ordered\n\tlocation, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation)))\n\trequire.NoError(t, err, \"url.Parse(location)\")\n\trequire.Equal(t, \"/user/fiber\", location.Path)\n\trequire.Equal(t, url.Values{\"data[0][name]\": []string{\"john\"}, \"data[0][age]\": []string{\"10\"}, \"test\": []string{\"doe\"}}, location.Query())\n}\n\n// go test -run Test_Redirect_Route_WithOptionalParams\nfunc Test_Redirect_Route_WithOptionalParams(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/user/:name?\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Route(\"user\", RedirectConfig{\n\t\tParams: Map{\n\t\t\t\"name\": \"fiber\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/user/fiber\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_Route_WithOptionalParamsWithoutValue\nfunc Test_Redirect_Route_WithOptionalParamsWithoutValue(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/user/:name?\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Route(\"user\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/user/\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_Route_WithGreedyParameters\nfunc Test_Redirect_Route_WithGreedyParameters(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/user/+\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"+\"))\n\t}).Name(\"user\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Route(\"user\", RedirectConfig{\n\t\tParams: Map{\n\t\t\t\"+\": \"test/routes\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/user/test/routes\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_Back\nfunc Test_Redirect_Back(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.JSON(\"Home\")\n\t}).Name(\"home\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\terr := c.Redirect().Back(\"/\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\terr = c.Redirect().Back()\n\trequire.Equal(t, 500, c.Response().StatusCode())\n\trequire.ErrorAs(t, err, &ErrRedirectBackNoFallback)\n}\n\n// go test -run Test_Redirect_Back_WithFlashMessages\nfunc Test_Redirect_Back_WithFlashMessages(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\terr := c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").Back(\"/\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\tvar msgs redirectionMsgs\n\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\trequire.NoError(t, err)\n\t_, err = msgs.UnmarshalMsg(decoded)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, msgs, 2)\n\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n}\n\n// go test -run Test_Redirect_Back_WithReferer\nfunc Test_Redirect_Back_WithReferer(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.JSON(\"Home\")\n\t}).Name(\"home\")\n\tapp.Get(\"/back\", func(c Ctx) error {\n\t\treturn c.JSON(\"Back\")\n\t}).Name(\"back\")\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{})\n\n\tc.Request().Header.Set(HeaderReferer, \"/back\")\n\terr := c.Redirect().Back(\"/\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/back\", c.Get(HeaderReferer))\n\trequire.Equal(t, \"/back\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -run Test_Redirect_Route_WithFlashMessages\nfunc Test_Redirect_Route_WithFlashMessages(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\terr := c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").Route(\"user\")\n\n\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(t, \"/user\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\tvar msgs redirectionMsgs\n\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\trequire.NoError(t, err)\n\t_, err = msgs.UnmarshalMsg(decoded)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, msgs, 2)\n\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n}\n\n// go test -run Test_Redirect_Route_WithOldInput\nfunc Test_Redirect_Route_WithOldInput(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Query\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tapp.Get(\"/user\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"user\")\n\t\t}).Name(\"user\")\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\t\tc.Request().URI().SetQueryString(\"id=1&name=tom\")\n\t\terr := c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").WithInput().Route(\"user\")\n\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"id\", value: \"1\", isOldInput: true})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"name\", value: \"tom\", isOldInput: true})\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\t\trequire.Equal(t, \"/user\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\t\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\t\tvar msgs redirectionMsgs\n\t\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\t\trequire.NoError(t, err)\n\t\t_, err = msgs.UnmarshalMsg(decoded)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Len(t, msgs, 4)\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"id\", value: \"1\", level: 0, isOldInput: true})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"name\", value: \"tom\", level: 0, isOldInput: true})\n\t})\n\n\tt.Run(\"Form\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tapp.Post(\"/user\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"user\")\n\t\t}).Name(\"user\")\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\t\tc.Request().Header.Set(HeaderContentType, MIMEApplicationForm)\n\t\tc.Request().SetBodyString(\"id=1&name=tom\")\n\t\terr := c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").WithInput().Route(\"user\")\n\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"id\", value: \"1\", isOldInput: true})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"name\", value: \"tom\", isOldInput: true})\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\t\trequire.Equal(t, \"/user\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\t\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\t\tvar msgs redirectionMsgs\n\t\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\t\trequire.NoError(t, err)\n\t\t_, err = msgs.UnmarshalMsg(decoded)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Len(t, msgs, 4)\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"id\", value: \"1\", level: 0, isOldInput: true})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"name\", value: \"tom\", level: 0, isOldInput: true})\n\t})\n\n\tt.Run(\"MultipartForm\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapp := New()\n\t\tapp.Get(\"/user\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"user\")\n\t\t}).Name(\"user\")\n\n\t\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\t\tbody := &bytes.Buffer{}\n\t\twriter := multipart.NewWriter(body)\n\n\t\trequire.NoError(t, writer.WriteField(\"id\", \"1\"))\n\t\trequire.NoError(t, writer.WriteField(\"name\", \"tom\"))\n\t\trequire.NoError(t, writer.Close())\n\n\t\tc.Request().SetBody(body.Bytes())\n\t\tc.Request().Header.Set(HeaderContentType, writer.FormDataContentType())\n\n\t\terr := c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").WithInput().Route(\"user\")\n\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"id\", value: \"1\", isOldInput: true})\n\t\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"name\", value: \"tom\", isOldInput: true})\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, StatusSeeOther, c.Response().StatusCode())\n\t\trequire.Equal(t, \"/user\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\t\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\t\tvar msgs redirectionMsgs\n\t\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\t\trequire.NoError(t, err)\n\t\t_, err = msgs.UnmarshalMsg(decoded)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Len(t, msgs, 4)\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"id\", value: \"1\", level: 0, isOldInput: true})\n\t\trequire.Contains(t, msgs, redirectionMsg{key: \"name\", value: \"tom\", level: 0, isOldInput: true})\n\t})\n}\n\nfunc Test_Redirect_WithInput_ReusesClearedMap(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\tdefer app.ReleaseCtx(c)\n\n\tc.Request().URI().SetQueryString(\"first=1\")\n\tc.Redirect().WithInput()\n\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"first\", value: \"1\", isOldInput: true})\n\n\tc.redirect.messages = c.redirect.messages[:0]\n\n\tc.Request().URI().SetQueryString(\"second=2\")\n\tc.Redirect().WithInput()\n\n\trequire.Len(t, c.redirect.messages, 1)\n\trequire.Contains(t, c.redirect.messages, redirectionMsg{key: \"second\", value: \"2\", isOldInput: true})\n\trequire.NotContains(t, c.redirect.messages, redirectionMsg{key: \"first\", value: \"1\", isOldInput: true})\n}\n\n// go test -run Test_Redirect_parseAndClearFlashMessages\nfunc Test_Redirect_parseAndClearFlashMessages(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tmsgs := redirectionMsgs{\n\t\t{\n\t\t\tkey:   \"success\",\n\t\t\tvalue: \"1\",\n\t\t},\n\t\t{\n\t\t\tkey:   \"message\",\n\t\t\tvalue: \"test\",\n\t\t},\n\t\t{\n\t\t\tkey:        \"name\",\n\t\t\tvalue:      \"tom\",\n\t\t\tisOldInput: true,\n\t\t},\n\t\t{\n\t\t\tkey:        \"id\",\n\t\t\tvalue:      \"1\",\n\t\t\tisOldInput: true,\n\t\t},\n\t}\n\n\tval, err := msgs.MarshalMsg(nil)\n\trequire.NoError(t, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\n\tc.Redirect().parseAndClearFlashMessages()\n\n\trequire.Equal(t, FlashMessage{\n\t\tKey:   \"success\",\n\t\tValue: \"1\",\n\t\tLevel: 0,\n\t}, c.Redirect().Message(\"success\"))\n\n\trequire.Equal(t, FlashMessage{\n\t\tKey:   \"message\",\n\t\tValue: \"test\",\n\t\tLevel: 0,\n\t}, c.Redirect().Message(\"message\"))\n\n\trequire.Equal(t, FlashMessage{}, c.Redirect().Message(\"success\"))\n\trequire.Equal(t, FlashMessage{}, c.Redirect().Message(\"not_message\"))\n\n\trequire.Empty(t, c.Redirect().Messages())\n\n\trequire.Equal(t, OldInputData{\n\t\tKey:   \"id\",\n\t\tValue: \"1\",\n\t}, c.Redirect().OldInput(\"id\"))\n\n\trequire.Equal(t, OldInputData{\n\t\tKey:   \"name\",\n\t\tValue: \"tom\",\n\t}, c.Redirect().OldInput(\"name\"))\n\n\trequire.Equal(t, OldInputData{}, c.Redirect().OldInput(\"not_name\"))\n\n\trequire.Equal(t, []OldInputData{\n\t\t{\n\t\t\tKey:   \"name\",\n\t\t\tValue: \"tom\",\n\t\t},\n\t\t{\n\t\t\tKey:   \"id\",\n\t\t\tValue: \"1\",\n\t\t},\n\t}, c.Redirect().OldInputs())\n\n\tassertFlashCookieCleared(t, string(c.Response().Header.Peek(HeaderSetCookie)))\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=test\")\n\n\tc.Redirect().parseAndClearFlashMessages()\n\n\trequire.Empty(t, c.Redirect().messages)\n}\n\n// Test_Redirect_parseAndClearFlashMessages_InvalidHex tests the case where hex decoding fails\nfunc Test_Redirect_parseAndClearFlashMessages_InvalidHex(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\t// Setup request and response\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\tdefer app.ReleaseCtx(c)\n\n\t// Create redirect instance\n\tr := AcquireRedirect()\n\tr.c = c\n\n\t// Set invalid hex value in flash cookie\n\tc.Request().Header.SetCookie(FlashCookieName, \"not-a-valid-hex-string\")\n\n\t// Call parseAndClearFlashMessages\n\tr.parseAndClearFlashMessages()\n\n\t// Verify that no flash messages are processed (should be empty)\n\trequire.Empty(t, r.messages)\n\n\t// Release redirect\n\tReleaseRedirect(r)\n}\n\nfunc Test_Redirect_Messages_ClearsFlashMessages(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\tdefer app.ReleaseCtx(c)\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(t, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\tc.Redirect().parseAndClearFlashMessages()\n\n\trequire.Equal(t, []FlashMessage{\n\t\t{\n\t\t\tKey:   \"success\",\n\t\t\tValue: \"1\",\n\t\t\tLevel: 0,\n\t\t},\n\t\t{\n\t\t\tKey:   \"message\",\n\t\t\tValue: \"test\",\n\t\t\tLevel: 0,\n\t\t},\n\t}, c.Redirect().Messages())\n\n\trequire.Empty(t, c.Redirect().Messages())\n\trequire.Equal(t, FlashMessage{}, c.Redirect().Message(\"success\"))\n\n\trequire.Equal(t, []OldInputData{\n\t\t{\n\t\t\tKey:   \"name\",\n\t\t\tValue: \"tom\",\n\t\t},\n\t\t{\n\t\t\tKey:   \"id\",\n\t\t\tValue: \"1\",\n\t\t},\n\t}, c.Redirect().OldInputs())\n}\n\nfunc Test_Redirect_CompleteFlowWithFlashMessages(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\t// First handler that sets flash messages and redirects\n\tapp.Get(\"/source\", func(c Ctx) error {\n\t\t// Redirect to the target handler\n\t\treturn c.Redirect().With(\"string_message\", \"Hello, World!\").\n\t\t\tWith(\"number_message\", \"12345\").\n\t\t\tWith(\"bool_message\", \"true\").\n\t\t\tTo(\"/target\")\n\t})\n\n\t// Second handler that receives and processes flash messages\n\tapp.Get(\"/target\", func(c Ctx) error {\n\t\t// Get all flash messages and return them as a JSON response\n\t\treturn c.JSON(Map{\n\t\t\t\"string_message\": c.Redirect().Message(\"string_message\").Value,\n\t\t\t\"number_message\": c.Redirect().Message(\"number_message\").Value,\n\t\t\t\"bool_message\":   c.Redirect().Message(\"bool_message\").Value,\n\t\t})\n\t})\n\n\t// Step 1: Make the initial request to the source route\n\treq := httptest.NewRequest(MethodGet, \"/source\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, resp.StatusCode)\n\trequire.Equal(t, \"/target\", resp.Header.Get(HeaderLocation))\n\n\t// Verify and get the cookie from the response\n\tcookies := resp.Cookies()\n\tvar flashCookie *http.Cookie\n\tfor _, cookie := range cookies {\n\t\tif cookie.Name == \"fiber_flash\" {\n\t\t\tflashCookie = cookie\n\t\t\tbreak\n\t\t}\n\t}\n\trequire.NotNil(t, flashCookie, \"Flash cookie should be set\")\n\n\t// Step 2: Make the second request to the target route with the cookie\n\treq = httptest.NewRequest(MethodGet, \"/target\", http.NoBody)\n\treq.Header.Set(\"Cookie\", flashCookie.Name+\"=\"+flashCookie.Value)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tassertFlashCookieCleared(t, resp.Header.Get(HeaderSetCookie))\n\n\t// Parse the JSON response and verify flash messages\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\terr = json.Unmarshal(body, &result)\n\trequire.NoError(t, err)\n\n\t// Verify all flash messages were received correctly\n\trequire.Equal(t, \"Hello, World!\", result[\"string_message\"])\n\trequire.Equal(t, \"12345\", result[\"number_message\"]) // JSON numbers are float64\n\trequire.Equal(t, \"true\", result[\"bool_message\"])\n}\n\nfunc Test_Redirect_FlashMessagesWithSpecialChars(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\t// Handler that sets flash messages with special characters and redirects\n\tapp.Get(\"/special-source\", func(c Ctx) error {\n\t\t// Create a large message to test encoding of larger data\n\t\treturn c.Redirect().With(\"null_bytes\", \"Contains\\x00null\\x00bytes\").\n\t\t\tWith(\"control_chars\", \"Contains\\r\\ncontrol\\tcharacters\").\n\t\t\tWith(\"unicode\", \"Unicode: 你好世界\").\n\t\t\tWith(\"emoji\", \"Emoji: 🔥🚀😊\").\n\t\t\tTo(\"/special-target\")\n\t})\n\n\t// Target handler that receives the flash messages\n\tapp.Get(\"/special-target\", func(c Ctx) error {\n\t\treturn c.JSON(Map{\n\t\t\t\"null_bytes\":    c.Redirect().Message(\"null_bytes\").Value,\n\t\t\t\"control_chars\": c.Redirect().Message(\"control_chars\").Value,\n\t\t\t\"unicode\":       c.Redirect().Message(\"unicode\").Value,\n\t\t\t\"emoji\":         c.Redirect().Message(\"emoji\").Value,\n\t\t})\n\t})\n\n\t// Step 1: Make the initial request\n\treq := httptest.NewRequest(MethodGet, \"/special-source\", http.NoBody)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusSeeOther, resp.StatusCode)\n\trequire.Equal(t, \"/special-target\", resp.Header.Get(HeaderLocation))\n\n\t// Get the flash cookie\n\tvar flashCookie *http.Cookie\n\tfor _, cookie := range resp.Cookies() {\n\t\tif cookie.Name == \"fiber_flash\" {\n\t\t\tflashCookie = cookie\n\t\t\tbreak\n\t\t}\n\t}\n\trequire.NotNil(t, flashCookie, \"Flash cookie should be set\")\n\n\t// Step 2: Make the second request with the cookie\n\treq = httptest.NewRequest(MethodGet, \"/special-target\", http.NoBody)\n\treq.Header.Set(\"Cookie\", flashCookie.Name+\"=\"+flashCookie.Value)\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, StatusOK, resp.StatusCode)\n\n\tassertFlashCookieCleared(t, resp.Header.Get(HeaderSetCookie))\n\n\t// Parse and verify the response\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\terr = json.Unmarshal(body, &result)\n\trequire.NoError(t, err)\n\n\t// Verify special character handling\n\trequire.Equal(t, \"Contains\\x00null\\x00bytes\", result[\"null_bytes\"])\n\trequire.Equal(t, \"Contains\\r\\ncontrol\\tcharacters\", result[\"control_chars\"])\n\trequire.Equal(t, \"Unicode: 你好世界\", result[\"unicode\"])\n\trequire.Equal(t, \"Emoji: 🔥🚀😊\", result[\"emoji\"])\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_Route -benchmem -count=4\nfunc Benchmark_Redirect_Route(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\n\tvar err error\n\n\tfor b.Loop() {\n\t\terr = c.Redirect().Route(\"user\", RedirectConfig{\n\t\t\tParams: Map{\n\t\t\t\t\"name\": \"fiber\",\n\t\t\t},\n\t\t})\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(b, \"/user/fiber\", string(c.Response().Header.Peek(HeaderLocation)))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_Route_WithQueries -benchmem -count=4\nfunc Benchmark_Redirect_Route_WithQueries(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user/:name\", func(c Ctx) error {\n\t\treturn c.JSON(c.Params(\"name\"))\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\n\tvar err error\n\n\tfor b.Loop() {\n\t\terr = c.Redirect().Route(\"user\", RedirectConfig{\n\t\t\tParams: Map{\n\t\t\t\t\"name\": \"fiber\",\n\t\t\t},\n\t\t\tQueries: map[string]string{\"a\": \"a\", \"b\": \"b\"},\n\t\t})\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, StatusSeeOther, c.Response().StatusCode())\n\t// analysis of query parameters with url parsing, since a map pass is always randomly ordered\n\tlocation, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation)))\n\trequire.NoError(b, err, \"url.Parse(location)\")\n\trequire.Equal(b, \"/user/fiber\", location.Path)\n\trequire.Equal(b, url.Values{\"a\": []string{\"a\"}, \"b\": []string{\"b\"}}, location.Query())\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_Route_WithFlashMessages -benchmem -count=4\nfunc Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tb.ReportAllocs()\n\n\tvar err error\n\n\tfor b.Loop() {\n\t\terr = c.Redirect().With(\"success\", \"1\").With(\"message\", \"test\").Route(\"user\")\n\t}\n\n\trequire.NoError(b, err)\n\trequire.Equal(b, StatusSeeOther, c.Response().StatusCode())\n\trequire.Equal(b, \"/user\", string(c.Response().Header.Peek(HeaderLocation)))\n\n\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\tvar msgs redirectionMsgs\n\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\trequire.NoError(b, err)\n\t_, err = msgs.UnmarshalMsg(decoded)\n\trequire.NoError(b, err)\n\n\trequire.Contains(b, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(b, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n}\n\nvar testredirectionMsgs = redirectionMsgs{\n\t{\n\t\tkey:   \"success\",\n\t\tvalue: \"1\",\n\t},\n\t{\n\t\tkey:   \"message\",\n\t\tvalue: \"test\",\n\t},\n\t{\n\t\tkey:        \"name\",\n\t\tvalue:      \"tom\",\n\t\tisOldInput: true,\n\t},\n\t{\n\t\tkey:        \"id\",\n\t\tvalue:      \"1\",\n\t\tisOldInput: true,\n\t},\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_parseAndClearFlashMessages -benchmem -count=4\nfunc Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(b, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tc.Redirect().parseAndClearFlashMessages()\n\t}\n\n\trequire.Equal(b, FlashMessage{\n\t\tKey:   \"success\",\n\t\tValue: \"1\",\n\t}, c.Redirect().Message(\"success\"))\n\n\trequire.Equal(b, FlashMessage{\n\t\tKey:   \"message\",\n\t\tValue: \"test\",\n\t}, c.Redirect().Message(\"message\"))\n\n\trequire.Equal(b, OldInputData{\n\t\tKey:   \"id\",\n\t\tValue: \"1\",\n\t}, c.Redirect().OldInput(\"id\"))\n\n\trequire.Equal(b, OldInputData{\n\t\tKey:   \"name\",\n\t\tValue: \"tom\",\n\t}, c.Redirect().OldInput(\"name\"))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_processFlashMessages -benchmem -count=4\nfunc Benchmark_Redirect_processFlashMessages(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tc.Redirect().With(\"success\", \"1\").With(\"message\", \"test\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tc.Redirect().processFlashMessages()\n\t}\n\n\tc.RequestCtx().Request.Header.Set(HeaderCookie, c.GetRespHeader(HeaderSetCookie)) // necessary for testing\n\n\tvar msgs redirectionMsgs\n\tdecoded, err := hex.DecodeString(c.Cookies(FlashCookieName))\n\trequire.NoError(b, err)\n\t_, err = msgs.UnmarshalMsg(decoded)\n\trequire.NoError(b, err)\n\n\trequire.Len(b, msgs, 2)\n\trequire.Contains(b, msgs, redirectionMsg{key: \"success\", value: \"1\", level: 0, isOldInput: false})\n\trequire.Contains(b, msgs, redirectionMsg{key: \"message\", value: \"test\", level: 0, isOldInput: false})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_Messages -benchmem -count=4\nfunc Benchmark_Redirect_Messages(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(b, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\tc.Redirect().parseAndClearFlashMessages()\n\n\tvar msgs []FlashMessage\n\n\tmsgTemplate := testredirectionMsgs\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tc.flashMessages = c.flashMessages[:0]\n\t\tc.flashMessages = append(c.flashMessages, msgTemplate...)\n\t\tmsgs = c.Redirect().Messages()\n\t}\n\n\trequire.Contains(b, msgs, FlashMessage{\n\t\tKey:   \"success\",\n\t\tValue: \"1\",\n\t\tLevel: 0,\n\t})\n\n\trequire.Contains(b, msgs, FlashMessage{\n\t\tKey:   \"message\",\n\t\tValue: \"test\",\n\t\tLevel: 0,\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_OldInputs -benchmem -count=4\nfunc Benchmark_Redirect_OldInputs(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(b, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\tc.Redirect().parseAndClearFlashMessages()\n\n\tvar oldInputs []OldInputData\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\toldInputs = c.Redirect().OldInputs()\n\t}\n\n\trequire.Contains(b, oldInputs, OldInputData{\n\t\tKey:   \"name\",\n\t\tValue: \"tom\",\n\t})\n\n\trequire.Contains(b, oldInputs, OldInputData{\n\t\tKey:   \"id\",\n\t\tValue: \"1\",\n\t})\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_Message -benchmem -count=4\nfunc Benchmark_Redirect_Message(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(b, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\tc.Redirect().parseAndClearFlashMessages()\n\n\tvar msg FlashMessage\n\n\tmsgTemplate := testredirectionMsgs\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tc.flashMessages = c.flashMessages[:0]\n\t\tc.flashMessages = append(c.flashMessages, msgTemplate...)\n\t\tmsg = c.Redirect().Message(\"message\")\n\t}\n\n\trequire.Equal(b, FlashMessage{\n\t\tKey:   \"message\",\n\t\tValue: \"test\",\n\t\tLevel: 0,\n\t}, msg)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Redirect_OldInput -benchmem -count=4\nfunc Benchmark_Redirect_OldInput(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/user\", func(c Ctx) error {\n\t\treturn c.SendString(\"user\")\n\t}).Name(\"user\")\n\n\tc := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tval, err := testredirectionMsgs.MarshalMsg(nil)\n\trequire.NoError(b, err)\n\n\tc.Request().Header.Set(HeaderCookie, \"fiber_flash=\"+hex.EncodeToString(val))\n\tc.Redirect().parseAndClearFlashMessages()\n\n\tvar input OldInputData\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tinput = c.Redirect().OldInput(\"name\")\n\t}\n\n\trequire.Equal(b, OldInputData{\n\t\tKey:   \"name\",\n\t\tValue: \"tom\",\n\t}, input)\n}\n"
  },
  {
    "path": "register.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\n// Register defines all router handle interface generate by RouteChain().\ntype Register interface {\n\tAll(handler any, handlers ...any) Register\n\tGet(handler any, handlers ...any) Register\n\tHead(handler any, handlers ...any) Register\n\tPost(handler any, handlers ...any) Register\n\tPut(handler any, handlers ...any) Register\n\tDelete(handler any, handlers ...any) Register\n\tConnect(handler any, handlers ...any) Register\n\tOptions(handler any, handlers ...any) Register\n\tTrace(handler any, handlers ...any) Register\n\tPatch(handler any, handlers ...any) Register\n\n\tAdd(methods []string, handler any, handlers ...any) Register\n\n\tRouteChain(path string) Register\n}\n\nvar _ Register = (*Registering)(nil)\n\n// Registering provides route registration helpers for a specific path on the\n// application instance.\ntype Registering struct {\n\tapp   *App\n\tgroup *Group\n\n\tpath string\n}\n\n// All registers a middleware route that will match requests\n// with the provided path which is stored in register struct.\n//\n//\tapp.RouteChain(\"/\").All(func(c fiber.Ctx) error {\n//\t     return c.Next()\n//\t})\n//\tapp.RouteChain(\"/api\").All(func(c fiber.Ctx) error {\n//\t     return c.Next()\n//\t})\n//\tapp.RouteChain(\"/api\").All(handler, func(c fiber.Ctx) error {\n//\t     return c.Next()\n//\t})\n//\n// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...\nfunc (r *Registering) All(handler any, handlers ...any) Register {\n\tconverted := collectHandlers(\"register\", append([]any{handler}, handlers...)...)\n\tr.app.register([]string{methodUse}, r.path, r.group, converted...)\n\treturn r\n}\n\n// Get registers a route for GET methods that requests a representation\n// of the specified resource. Requests using GET should only retrieve data.\nfunc (r *Registering) Get(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodGet}, handler, handlers...)\n}\n\n// Head registers a route for HEAD methods that asks for a response identical\n// to that of a GET request, but without the response body.\nfunc (r *Registering) Head(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodHead}, handler, handlers...)\n}\n\n// Post registers a route for POST methods that is used to submit an entity to the\n// specified resource, often causing a change in state or side effects on the server.\nfunc (r *Registering) Post(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodPost}, handler, handlers...)\n}\n\n// Put registers a route for PUT methods that replaces all current representations\n// of the target resource with the request payload.\nfunc (r *Registering) Put(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodPut}, handler, handlers...)\n}\n\n// Delete registers a route for DELETE methods that deletes the specified resource.\nfunc (r *Registering) Delete(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodDelete}, handler, handlers...)\n}\n\n// Connect registers a route for CONNECT methods that establishes a tunnel to the\n// server identified by the target resource.\nfunc (r *Registering) Connect(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodConnect}, handler, handlers...)\n}\n\n// Options registers a route for OPTIONS methods that is used to describe the\n// communication options for the target resource.\nfunc (r *Registering) Options(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodOptions}, handler, handlers...)\n}\n\n// Trace registers a route for TRACE methods that performs a message loop-back\n// test along the r.Path to the target resource.\nfunc (r *Registering) Trace(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodTrace}, handler, handlers...)\n}\n\n// Patch registers a route for PATCH methods that is used to apply partial\n// modifications to a resource.\nfunc (r *Registering) Patch(handler any, handlers ...any) Register {\n\treturn r.Add([]string{MethodPatch}, handler, handlers...)\n}\n\n// Add allows you to specify multiple HTTP methods to register a route.\n// The provided handlers are executed in order, starting with `handler` and then the variadic `handlers`.\nfunc (r *Registering) Add(methods []string, handler any, handlers ...any) Register {\n\tconverted := collectHandlers(\"register\", append([]any{handler}, handlers...)...)\n\tr.app.register(methods, r.path, r.group, converted...)\n\treturn r\n}\n\n// RouteChain returns a new Register instance whose route path takes\n// the path in the current instance as its prefix.\nfunc (r *Registering) RouteChain(path string) Register {\n\t// Create new group\n\troute := &Registering{app: r.app, group: r.group, path: getGroupPath(r.path, path)}\n\n\treturn route\n}\n"
  },
  {
    "path": "req.go",
    "content": "package fiber\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsbytes \"github.com/gofiber/utils/v2/bytes\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n\t\"golang.org/x/net/idna\"\n)\n\n// Pre-allocated byte slices for common header comparisons to avoid allocations\nvar (\n\txForwardedPrefix        = []byte(\"X-Forwarded-\")\n\txForwardedProtoBytes    = []byte(HeaderXForwardedProto)\n\txForwardedProtocolBytes = []byte(HeaderXForwardedProtocol)\n\txForwardedSslBytes      = []byte(HeaderXForwardedSsl)\n\txURLSchemeBytes         = []byte(HeaderXUrlScheme)\n\tonBytes                 = []byte(\"on\")\n)\n\n// Range represents the parsed HTTP Range header extracted by DefaultReq.Range.\ntype Range struct {\n\tType   string\n\tRanges []RangeSet\n}\n\n// RangeSet represents a single content range from a request.\ntype RangeSet struct {\n\tStart int64\n\tEnd   int64\n}\n\n// DefaultReq is the default implementation of Req used by DefaultCtx.\n//\n//go:generate ifacemaker --file req.go --struct DefaultReq --iface Req --pkg fiber --output req_interface_gen.go --not-exported true --iface-comment \"Req is an interface for request-related Ctx methods.\"\ntype DefaultReq struct {\n\tc *DefaultCtx\n}\n\n// Accepts checks if the specified extensions or content types are acceptable.\nfunc (r *DefaultReq) Accepts(offers ...string) string {\n\theader := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAccept))\n\treturn getOffer(header, acceptsOfferType, offers...)\n}\n\n// AcceptsCharsets checks if the specified charset is acceptable.\nfunc (r *DefaultReq) AcceptsCharsets(offers ...string) string {\n\theader := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptCharset))\n\treturn getOffer(header, acceptsOffer, offers...)\n}\n\n// AcceptsEncodings checks if the specified encoding is acceptable.\nfunc (r *DefaultReq) AcceptsEncodings(offers ...string) string {\n\theader := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptEncoding))\n\treturn getOffer(header, acceptsOffer, offers...)\n}\n\n// AcceptsLanguages checks if the specified language is acceptable using\n// RFC 4647 Basic Filtering.\nfunc (r *DefaultReq) AcceptsLanguages(offers ...string) string {\n\theader := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))\n\treturn getOffer(header, acceptsLanguageOfferBasic, offers...)\n}\n\n// AcceptsLanguagesExtended checks if the specified language is acceptable using\n// RFC 4647 Extended Filtering.\nfunc (r *DefaultReq) AcceptsLanguagesExtended(offers ...string) string {\n\theader := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))\n\treturn getOffer(header, acceptsLanguageOfferExtended, offers...)\n}\n\n// App returns the *App reference to the instance of the Fiber application\nfunc (r *DefaultReq) App() *App {\n\treturn r.c.app\n}\n\n// BaseURL returns (protocol + host + base path).\nfunc (r *DefaultReq) BaseURL() string {\n\treturn r.c.BaseURL()\n}\n\n// BodyRaw contains the raw body submitted in a POST request.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultReq) BodyRaw() []byte {\n\treturn r.getBody()\n}\n\n//nolint:nonamedreturns // gocritic unnamedResult prefers naming decoded body, decode count, and error\nfunc (r *DefaultReq) tryDecodeBodyInOrder(\n\toriginalBody *[]byte,\n\tencodings []string,\n) (body []byte, decodesRealized uint8, err error) {\n\trequest := &r.c.fasthttp.Request\n\tfor idx := range encodings {\n\t\ti := len(encodings) - 1 - idx\n\t\tencoding := encodings[i]\n\t\tdecodesRealized++\n\t\tvar decodeErr error\n\t\tswitch encoding {\n\t\tcase StrGzip, \"x-gzip\":\n\t\t\tbody, decodeErr = request.BodyGunzip()\n\t\tcase StrBr, StrBrotli:\n\t\t\tbody, decodeErr = request.BodyUnbrotli()\n\t\tcase StrDeflate:\n\t\t\tbody, decodeErr = request.BodyInflate()\n\t\tcase StrZstd:\n\t\t\tbody, decodeErr = request.BodyUnzstd()\n\t\tcase StrIdentity:\n\t\t\tbody = request.Body()\n\t\tcase StrCompress, \"x-compress\":\n\t\t\treturn nil, decodesRealized - 1, ErrNotImplemented\n\t\tdefault:\n\t\t\treturn nil, decodesRealized - 1, ErrUnsupportedMediaType\n\t\t}\n\n\t\tif decodeErr != nil {\n\t\t\treturn nil, decodesRealized, decodeErr\n\t\t}\n\n\t\tif i > 0 && decodesRealized > 0 {\n\t\t\tif i == len(encodings)-1 {\n\t\t\t\ttempBody := request.Body()\n\t\t\t\t*originalBody = make([]byte, len(tempBody))\n\t\t\t\tcopy(*originalBody, tempBody)\n\t\t\t}\n\t\t\trequest.SetBodyRaw(body)\n\t\t}\n\t}\n\n\treturn body, decodesRealized, nil\n}\n\n// Body contains the raw body submitted in a POST request.\n// This method will decompress the body if the 'Content-Encoding' header is provided.\n// It returns the original (or decompressed) body data which is valid only within the handler.\n// Don't store direct references to the returned data.\n// If you need to keep the body's data later, make a copy or use the Immutable option.\nfunc (r *DefaultReq) Body() []byte {\n\tvar (\n\t\terr                error\n\t\tbody, originalBody []byte\n\t\theaderEncoding     string\n\t\tencodingOrder      = []string{\"\", \"\", \"\"}\n\t)\n\n\trequest := &r.c.fasthttp.Request\n\n\t// Get Content-Encoding header\n\theaderEncoding = utils.UnsafeString(utilsbytes.UnsafeToLower(request.Header.ContentEncoding()))\n\n\t// If no encoding is provided, return the original body\n\tif headerEncoding == \"\" {\n\t\treturn r.getBody()\n\t}\n\n\t// Split and get the encodings list, in order to attend the\n\t// rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5\n\tencodingOrder = getSplicedStrList(headerEncoding, encodingOrder)\n\tfor i := range encodingOrder {\n\t\tencodingOrder[i] = utilsstrings.UnsafeToLower(encodingOrder[i])\n\t}\n\tif len(encodingOrder) == 0 {\n\t\treturn r.getBody()\n\t}\n\n\tvar decodesRealized uint8\n\tbody, decodesRealized, err = r.tryDecodeBodyInOrder(&originalBody, encodingOrder)\n\n\t// Ensure that the body will be the original\n\tif originalBody != nil && decodesRealized > 0 {\n\t\trequest.SetBodyRaw(originalBody)\n\t}\n\tif err != nil {\n\t\tswitch {\n\t\tcase errors.Is(err, ErrUnsupportedMediaType):\n\t\t\t_ = r.c.DefaultRes.SendStatus(StatusUnsupportedMediaType) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check\n\t\tcase errors.Is(err, ErrNotImplemented):\n\t\t\t_ = r.c.DefaultRes.SendStatus(StatusNotImplemented) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check\n\t\tdefault:\n\t\t\t// do nothing\n\t\t}\n\t\treturn []byte(err.Error())\n\t}\n\n\treturn r.c.app.GetBytes(body)\n}\n\n// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n// a cancellation signal, and other values across API boundaries.\nfunc (r *DefaultReq) RequestCtx() *fasthttp.RequestCtx {\n\treturn r.c.fasthttp\n}\n\n// FullURL returns the full request URL (protocol + host + original URL).\nfunc (c *DefaultCtx) FullURL() string {\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\tbuf.WriteString(c.Scheme())\n\tbuf.WriteString(\"://\")\n\tbuf.WriteString(c.Host())\n\tbuf.WriteString(c.OriginalURL())\n\n\treturn c.app.toString(buf.Bytes())\n}\n\n// UserAgent returns the User-Agent request header.\nfunc (c *DefaultCtx) UserAgent() string {\n\treturn c.app.toString(c.fasthttp.Request.Header.UserAgent())\n}\n\n// Referer returns the Referer request header.\nfunc (c *DefaultCtx) Referer() string {\n\treturn c.app.toString(c.fasthttp.Request.Header.Referer())\n}\n\n// AcceptLanguage returns the Accept-Language request header.\nfunc (c *DefaultCtx) AcceptLanguage() string {\n\treturn c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptLanguage))\n}\n\n// AcceptEncoding returns the Accept-Encoding request header.\nfunc (c *DefaultCtx) AcceptEncoding() string {\n\treturn c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptEncoding))\n}\n\n// HasHeader reports whether the request includes a header with the given key.\nfunc (c *DefaultCtx) HasHeader(key string) bool {\n\treturn len(c.fasthttp.Request.Header.Peek(key)) > 0\n}\n\n// MediaType returns the MIME type from the Content-Type header without parameters.\nfunc (c *DefaultCtx) MediaType() string {\n\tcontentType := utils.TrimSpace(c.fasthttp.Request.Header.ContentType())\n\tif len(contentType) == 0 {\n\t\treturn \"\"\n\t}\n\tif idx := bytes.IndexByte(contentType, ';'); idx != -1 {\n\t\tcontentType = contentType[:idx]\n\t}\n\tcontentType = utils.TrimSpace(contentType)\n\treturn c.app.toString(contentType)\n}\n\n// Charset returns the charset parameter from the Content-Type header.\nfunc (c *DefaultCtx) Charset() string {\n\tcontentType := c.fasthttp.Request.Header.ContentType()\n\tif len(contentType) == 0 {\n\t\treturn \"\"\n\t}\n\t_, after, ok := bytes.Cut(contentType, []byte{';'})\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tparams := after\n\tfor len(params) > 0 {\n\t\tparams = utils.TrimSpace(params)\n\t\tif len(params) == 0 {\n\t\t\treturn \"\"\n\t\t}\n\t\tparam := params\n\t\tif idx := bytes.IndexByte(params, ';'); idx != -1 {\n\t\t\tparam = params[:idx]\n\t\t\tparams = params[idx+1:]\n\t\t} else {\n\t\t\tparams = nil\n\t\t}\n\n\t\tparam = utils.TrimSpace(param)\n\t\tif len(param) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tbefore, after, ok := bytes.Cut(param, []byte{'='})\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tname := utils.TrimSpace(before)\n\t\tif !bytes.EqualFold(name, []byte(\"charset\")) {\n\t\t\tcontinue\n\t\t}\n\t\tvalue := utils.TrimSpace(after)\n\t\tif len(value) >= 2 && value[0] == '\"' && value[len(value)-1] == '\"' {\n\t\t\tvalue = value[1 : len(value)-1]\n\t\t}\n\t\treturn c.app.toString(value)\n\t}\n\treturn \"\"\n}\n\n// IsJSON reports whether the Content-Type header is JSON.\nfunc (c *DefaultCtx) IsJSON() bool {\n\treturn utils.EqualFold(c.MediaType(), MIMEApplicationJSON)\n}\n\n// IsForm reports whether the Content-Type header is form-encoded.\nfunc (c *DefaultCtx) IsForm() bool {\n\treturn utils.EqualFold(c.MediaType(), MIMEApplicationForm)\n}\n\n// IsMultipart reports whether the Content-Type header is multipart form data.\nfunc (c *DefaultCtx) IsMultipart() bool {\n\treturn utils.EqualFold(c.MediaType(), MIMEMultipartForm)\n}\n\n// AcceptsJSON reports whether the Accept header allows JSON.\nfunc (c *DefaultCtx) AcceptsJSON() bool {\n\treturn c.Accepts(MIMEApplicationJSON) != \"\"\n}\n\n// AcceptsHTML reports whether the Accept header allows HTML.\nfunc (c *DefaultCtx) AcceptsHTML() bool {\n\treturn c.Accepts(MIMETextHTML) != \"\"\n}\n\n// AcceptsXML reports whether the Accept header allows XML.\nfunc (c *DefaultCtx) AcceptsXML() bool {\n\treturn c.Accepts(MIMEApplicationXML, MIMETextXML) != \"\"\n}\n\n// AcceptsEventStream reports whether the Accept header allows text/event-stream.\nfunc (c *DefaultCtx) AcceptsEventStream() bool {\n\treturn c.Accepts(\"text/event-stream\") != \"\"\n}\n\n// Cookies are used for getting a cookie value by key.\n// Defaults to the empty string \"\" if the cookie doesn't exist.\n// If a default value is given, it will return that value if the cookie doesn't exist.\n// The returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (r *DefaultReq) Cookies(key string, defaultValue ...string) string {\n\treturn defaultString(r.c.app.toString(r.c.fasthttp.Request.Header.Cookie(key)), defaultValue)\n}\n\n// Request return the *fasthttp.Request object\n// This allows you to use all fasthttp request methods\n// https://godoc.org/github.com/valyala/fasthttp#Request\nfunc (r *DefaultReq) Request() *fasthttp.Request {\n\treturn &r.c.fasthttp.Request\n}\n\n// FormFile returns the first file by key from a MultipartForm.\nfunc (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {\n\treturn r.c.fasthttp.FormFile(key)\n}\n\n// FormValue returns the first value by key from a MultipartForm.\n// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.\n// Defaults to the empty string \"\" if the form value doesn't exist.\n// If a default value is given, it will return that value if the form value does not exist.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultReq) FormValue(key string, defaultValue ...string) string {\n\treturn defaultString(r.c.app.toString(r.c.fasthttp.FormValue(key)), defaultValue)\n}\n\n// Fresh returns true when the response is still “fresh” in the client's cache,\n// otherwise false is returned to indicate that the client cache is now stale\n// and the full response should be sent.\n// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end\n// reload request, this module will return false to make handling these requests transparent.\n// https://github.com/jshttp/fresh/blob/master/index.js#L33\nfunc (r *DefaultReq) Fresh() bool {\n\theader := &r.c.fasthttp.Request.Header\n\n\t// fields\n\tmodifiedSince := header.Peek(HeaderIfModifiedSince)\n\tnoneMatch := header.Peek(HeaderIfNoneMatch)\n\n\t// unconditional request\n\tif len(modifiedSince) == 0 && len(noneMatch) == 0 {\n\t\treturn false\n\t}\n\n\t// Always return stale when Cache-Control: no-cache\n\t// to support end-to-end reload requests\n\t// https://www.rfc-editor.org/rfc/rfc9111#section-5.2.1.4\n\tcacheControl := header.Peek(HeaderCacheControl)\n\tif len(cacheControl) > 0 && isNoCache(utils.UnsafeString(cacheControl)) {\n\t\treturn false\n\t}\n\n\t// if-none-match\n\tif len(noneMatch) > 0 && (len(noneMatch) != 1 || noneMatch[0] != '*') {\n\t\tapp := r.c.app\n\t\tresponse := &r.c.fasthttp.Response\n\t\tetag := app.toString(response.Header.Peek(HeaderETag))\n\t\tif etag == \"\" {\n\t\t\treturn false\n\t\t}\n\t\tif app.isEtagStale(etag, noneMatch) {\n\t\t\treturn false\n\t\t}\n\n\t\tif len(modifiedSince) > 0 {\n\t\t\tlastModified := response.Header.Peek(HeaderLastModified)\n\t\t\tif len(lastModified) > 0 {\n\t\t\t\tlastModifiedTime, err := fasthttp.ParseHTTPDate(lastModified)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tmodifiedSinceTime, err := fasthttp.ParseHTTPDate(modifiedSince)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn lastModifiedTime.Compare(modifiedSinceTime) != 1\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n// Get returns the HTTP request header specified by field.\n// Field names are case-insensitive\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultReq) Get(key string, defaultValue ...string) string {\n\treturn GetReqHeader(r.c, key, defaultValue...)\n}\n\n// GetReqHeader returns the HTTP request header specified by filed.\n// This function is generic and can handle different headers type values.\n// If the generic type cannot be matched to a supported type, the function\n// returns the default value (if provided) or the zero value of type V.\nfunc GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V {\n\tv, err := genericParseType[V](c.App().toString(c.Request().Header.Peek(key)))\n\tif err != nil && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\n}\n\n// GetHeaders (a.k.a GetReqHeaders) returns the HTTP request headers.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultReq) GetHeaders() map[string][]string {\n\tapp := r.c.app\n\treqHeader := &r.c.fasthttp.Request.Header\n\t// Pre-allocate map with known header count to avoid reallocations\n\theaders := make(map[string][]string, reqHeader.Len())\n\tfor k, v := range reqHeader.All() {\n\t\tkey := app.toString(k)\n\t\theaders[key] = append(headers[key], app.toString(v))\n\t}\n\treturn headers\n}\n\n// Host contains the host derived from the X-Forwarded-Host or Host HTTP header.\n// Returned value is only valid within the handler. Do not store any references.\n// In a network context, `Host` refers to the combination of a hostname and potentially a port number used for connecting,\n// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.\n// Example: URL: https://example.com:8080 -> Host: example.com:8080\n// Make copies or use the Immutable setting instead.\n// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\nfunc (r *DefaultReq) Host() string {\n\tif r.IsProxyTrusted() {\n\t\tif host := r.Get(HeaderXForwardedHost); host != \"\" {\n\t\t\tif before, _, found := strings.Cut(host, \",\"); found {\n\t\t\t\treturn utils.TrimSpace(before)\n\t\t\t}\n\t\t\treturn utils.TrimSpace(host)\n\t\t}\n\t}\n\treturn r.c.app.toString(r.c.fasthttp.Request.URI().Host())\n}\n\n// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method.\n// Returned value is only valid within the handler. Do not store any references.\n// Example: URL: https://example.com:8080 -> Hostname: example.com\n// Make copies or use the Immutable setting instead.\n// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\nfunc (r *DefaultReq) Hostname() string {\n\taddr, _ := parseAddr(r.Host())\n\n\treturn addr\n}\n\n// Port returns the remote port of the request.\nfunc (r *DefaultReq) Port() string {\n\taddr := r.c.fasthttp.RemoteAddr()\n\tif addr == nil {\n\t\treturn \"0\"\n\t}\n\tswitch typedAddr := addr.(type) {\n\tcase *net.TCPAddr:\n\t\treturn strconv.Itoa(typedAddr.Port)\n\tcase *net.UnixAddr:\n\t\treturn \"\"\n\t}\n\n\t_, port, err := net.SplitHostPort(addr.String())\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn port\n}\n\n// IP returns the remote IP address of the request.\n// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.\n// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\nfunc (r *DefaultReq) IP() string {\n\tapp := r.c.app\n\tif r.IsProxyTrusted() && app.config.ProxyHeader != \"\" {\n\t\treturn r.extractIPFromHeader(app.config.ProxyHeader)\n\t}\n\n\tif ip := r.c.fasthttp.RemoteIP(); ip != nil {\n\t\treturn ip.String()\n\t}\n\treturn \"\"\n}\n\n// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear.\n// When IP validation is enabled, any invalid IPs will be omitted.\nfunc (r *DefaultReq) extractIPsFromHeader(header string) []string {\n\t// TODO: Reuse the c.extractIPFromHeader func somehow in here\n\n\theaderValue := r.Get(header)\n\n\t// We can't know how many IPs we will return, but we will try to guess with this constant division.\n\t// Counting ',' makes function slower for about 50ns in general case.\n\tconst maxEstimatedCount = 8\n\testimatedCount := min(len(headerValue)/maxEstimatedCount,\n\t\t// Avoid big allocation on big header\n\t\tmaxEstimatedCount)\n\n\tipsFound := make([]string, 0, estimatedCount)\n\n\ti := 0\n\tj := -1\n\n\tfor {\n\t\tvar v4, v6 bool\n\n\t\t// Manually splitting string without allocating slice, working with parts directly\n\t\ti, j = j+1, j+2\n\n\t\tif j > len(headerValue) {\n\t\t\tbreak\n\t\t}\n\n\t\tfor j < len(headerValue) && headerValue[j] != ',' {\n\t\t\tswitch headerValue[j] {\n\t\t\tcase ':':\n\t\t\t\tv6 = true\n\t\t\tcase '.':\n\t\t\t\tv4 = true\n\t\t\tdefault:\n\t\t\t\t// do nothing\n\t\t\t}\n\t\t\tj++\n\t\t}\n\n\t\tfor i < j && (headerValue[i] == ' ' || headerValue[i] == ',') {\n\t\t\ti++\n\t\t}\n\n\t\ts := utils.TrimRight(headerValue[i:j], ' ')\n\n\t\tif r.c.app.config.EnableIPValidation {\n\t\t\t// Skip validation if IP is clearly not IPv4/IPv6; otherwise, validate without allocations\n\t\t\tif (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tipsFound = append(ipsFound, s)\n\t}\n\n\treturn ipsFound\n}\n\n// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled.\n// currently, it will return the first valid IP address in header.\n// when IP validation is disabled, it will simply return the value of the header without any inspection.\n// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string.\nfunc (r *DefaultReq) extractIPFromHeader(header string) string {\n\tapp := r.c.app\n\tif app.config.EnableIPValidation {\n\t\theaderValue := r.Get(header)\n\n\t\ti := 0\n\t\tj := -1\n\n\t\tfor {\n\t\t\tvar v4, v6 bool\n\n\t\t\t// Manually splitting string without allocating slice, working with parts directly\n\t\t\ti, j = j+1, j+2\n\n\t\t\tif j > len(headerValue) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tfor j < len(headerValue) && headerValue[j] != ',' {\n\t\t\t\tswitch headerValue[j] {\n\t\t\t\tcase ':':\n\t\t\t\t\tv6 = true\n\t\t\t\tcase '.':\n\t\t\t\t\tv4 = true\n\t\t\t\tdefault:\n\t\t\t\t\t// do nothing\n\t\t\t\t}\n\t\t\t\tj++\n\t\t\t}\n\n\t\t\tfor i < j && headerValue[i] == ' ' {\n\t\t\t\ti++\n\t\t\t}\n\n\t\t\ts := utils.TrimRight(headerValue[i:j], ' ')\n\n\t\t\tif app.config.EnableIPValidation {\n\t\t\t\tif (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn s\n\t\t}\n\n\t\tif ip := r.c.fasthttp.RemoteIP(); ip != nil {\n\t\t\treturn ip.String()\n\t\t}\n\t\treturn \"\"\n\t}\n\n\t// default behavior if IP validation is not enabled is just to return whatever value is\n\t// in the proxy header. Even if it is empty or invalid\n\treturn r.Get(app.config.ProxyHeader)\n}\n\n// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header.\n// When IP validation is enabled, only valid IPs are returned.\nfunc (r *DefaultReq) IPs() []string {\n\treturn r.extractIPsFromHeader(HeaderXForwardedFor)\n}\n\n// Is returns the matching content type,\n// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter\nfunc (r *DefaultReq) Is(extension string) bool {\n\textensionHeader := utils.GetMIME(extension)\n\tif extensionHeader == \"\" {\n\t\treturn false\n\t}\n\n\tct := r.c.app.toString(r.c.fasthttp.Request.Header.ContentType())\n\tif i := strings.IndexByte(ct, ';'); i != -1 {\n\t\tct = ct[:i]\n\t}\n\tct = utils.TrimSpace(ct)\n\treturn utils.EqualFold(ct, extensionHeader)\n}\n\n// Locals makes it possible to pass any values under keys scoped to the request\n// and therefore available to all following routes that match the request.\n//\n// All the values are removed from ctx after returning from the top\n// RequestHandler. Additionally, Close method is called on each value\n// implementing io.Closer before removing the value from ctx.\nfunc (r *DefaultReq) Locals(key any, value ...any) any {\n\tif len(value) == 0 {\n\t\treturn r.c.fasthttp.UserValue(key)\n\t}\n\tr.c.fasthttp.SetUserValue(key, value[0])\n\treturn value[0]\n}\n\n// Locals function utilizing Go's generics feature.\n// This function allows for manipulating and retrieving local values within a\n// request context with a more specific data type.\n//\n// All the values are removed from ctx after returning from the top\n// RequestHandler. Additionally, Close method is called on each value\n// implementing io.Closer before removing the value from ctx.\nfunc Locals[V any](c Ctx, key any, value ...V) V {\n\tvar v V\n\tvar ok bool\n\tif len(value) == 0 {\n\t\tv, ok = c.Locals(key).(V)\n\t} else {\n\t\tv, ok = c.Locals(key, value[0]).(V)\n\t}\n\tif !ok {\n\t\treturn v // return zero of type V\n\t}\n\treturn v\n}\n\n// Method returns the HTTP request method for the context, optionally overridden by the provided argument.\n// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context.\n// Otherwise, it updates the context's method and returns the overridden method as a string.\nfunc (r *DefaultReq) Method(override ...string) string {\n\tapp := r.c.app\n\tif len(override) == 0 {\n\t\t// Nothing to override, just return current method from context\n\t\treturn app.method(r.c.methodInt)\n\t}\n\n\tmethod := utilsstrings.ToUpper(override[0])\n\tmethodInt := app.methodInt(method)\n\tif methodInt == -1 {\n\t\t// Provided override does not valid HTTP method, no override, return current method\n\t\treturn app.method(r.c.methodInt)\n\t}\n\tr.c.methodInt = methodInt\n\treturn method\n}\n\n// MultipartForm parse form entries from binary.\n// This returns a map[string][]string, so given a key, the value will be a string slice.\nfunc (r *DefaultReq) MultipartForm() (*multipart.Form, error) {\n\treturn r.c.fasthttp.MultipartForm()\n}\n\n// OriginalURL contains the original request URL.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (r *DefaultReq) OriginalURL() string {\n\treturn r.c.app.toString(r.c.fasthttp.Request.Header.RequestURI())\n}\n\n// Params is used to get the route parameters.\n// Defaults to empty string \"\" if the param doesn't exist.\n// If a default value is given, it will return that value if the param doesn't exist.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (r *DefaultReq) Params(key string, defaultValue ...string) string {\n\tif key == \"*\" || key == \"+\" {\n\t\tkey += \"1\"\n\t}\n\n\tapp := r.c.app\n\troute := r.c.Route()\n\tvalues := &r.c.values\n\tfor i := range route.Params {\n\t\tif len(key) != len(route.Params[i]) {\n\t\t\tcontinue\n\t\t}\n\t\tif route.Params[i] == key || (!app.config.CaseSensitive && utils.EqualFold(route.Params[i], key)) {\n\t\t\t// if there is no value for the key\n\t\t\tif len(values) <= i || values[i] == \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tval := values[i]\n\t\t\treturn r.c.app.GetString(val)\n\t\t}\n\t}\n\treturn defaultString(\"\", defaultValue)\n}\n\n// Params is used to get the route parameters.\n// This function is generic and can handle different route parameters type values.\n// If the generic type cannot be matched to a supported type, the function\n// returns the default value (if provided) or the zero value of type V.\n//\n// Example:\n//\n// http://example.com/user/:user -> http://example.com/user/john\n// Params[string](c, \"user\") -> returns john\n//\n// http://example.com/id/:id -> http://example.com/user/114\n// Params[int](c, \"id\") ->  returns 114 as integer.\n//\n// http://example.com/id/:number -> http://example.com/id/john\n// Params[int](c, \"number\", 0) -> returns 0 because can't parse 'john' as integer.\nfunc Params[V GenericType](c Ctx, key string, defaultValue ...V) V {\n\tv, err := genericParseType[V](c.Params(key))\n\tif err != nil && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\n}\n\n// Scheme contains the request protocol string: http or https for TLS requests.\n// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\nfunc (r *DefaultReq) Scheme() string {\n\tctx := r.c.fasthttp\n\tif ctx.IsTLS() {\n\t\treturn schemeHTTPS\n\t}\n\tif !r.IsProxyTrusted() {\n\t\treturn schemeHTTP\n\t}\n\n\tapp := r.c.app\n\tscheme := schemeHTTP\n\tconst lenXHeaderName = 12\n\tfor key, val := range ctx.Request.Header.All() {\n\t\tif len(key) < lenXHeaderName {\n\t\t\tcontinue // Neither \"X-Forwarded-\" nor \"X-Url-Scheme\"\n\t\t}\n\t\tswitch {\n\t\tcase utils.EqualFold(key[:len(xForwardedPrefix)], xForwardedPrefix):\n\t\t\tif utils.EqualFold(key, xForwardedProtoBytes) ||\n\t\t\t\tutils.EqualFold(key, xForwardedProtocolBytes) {\n\t\t\t\tv := app.toString(val)\n\t\t\t\tif before, _, found := strings.Cut(v, \",\"); found {\n\t\t\t\t\tscheme = utils.TrimSpace(before)\n\t\t\t\t} else {\n\t\t\t\t\tscheme = utils.TrimSpace(v)\n\t\t\t\t}\n\t\t\t} else if utils.EqualFold(key, xForwardedSslBytes) && utils.EqualFold(val, onBytes) {\n\t\t\t\tscheme = schemeHTTPS\n\t\t\t}\n\n\t\tcase utils.EqualFold(key, xURLSchemeBytes):\n\t\t\tscheme = utils.TrimSpace(app.toString(val))\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn utilsstrings.ToLower(utils.TrimSpace(scheme))\n}\n\n// Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2.\nfunc (r *DefaultReq) Protocol() string {\n\treturn r.c.app.toString(r.c.fasthttp.Request.Header.Protocol())\n}\n\n// Query returns the query string parameter in the url.\n// Defaults to empty string \"\" if the query doesn't exist.\n// If a default value is given, it will return that value if the query doesn't exist.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (r *DefaultReq) Query(key string, defaultValue ...string) string {\n\treturn Query(r.c, key, defaultValue...)\n}\n\n// Queries returns a map of query parameters and their values.\n//\n// GET /?name=alex&wanna_cake=2&id=\n// Queries()[\"name\"] == \"alex\"\n// Queries()[\"wanna_cake\"] == \"2\"\n// Queries()[\"id\"] == \"\"\n//\n// GET /?field1=value1&field1=value2&field2=value3\n// Queries()[\"field1\"] == \"value2\"\n// Queries()[\"field2\"] == \"value3\"\n//\n// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3\n// Queries()[\"list_a\"] == \"3\"\n// Queries()[\"list_b[]\"] == \"3\"\n// Queries()[\"list_c\"] == \"1,2,3\"\n//\n// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending\n// Queries()[\"filters.author.name\"] == \"John\"\n// Queries()[\"filters.category.name\"] == \"Technology\"\n// Queries()[\"filters[customer][name]\"] == \"Alice\"\n// Queries()[\"filters[status]\"] == \"pending\"\nfunc (r *DefaultReq) Queries() map[string]string {\n\tapp := r.c.app\n\tqueryArgs := r.c.fasthttp.QueryArgs()\n\n\tm := make(map[string]string, queryArgs.Len())\n\tfor key, value := range queryArgs.All() {\n\t\tm[app.toString(key)] = app.toString(value)\n\t}\n\treturn m\n}\n\n// Query Retrieves the value of a query parameter from the request's URI.\n// The function is generic and can handle query parameter values of different types.\n// It takes the following parameters:\n// - c: The context object representing the current request.\n// - key: The name of the query parameter.\n// - defaultValue: (Optional) The default value to return if the query parameter is not found or cannot be parsed.\n// The function performs the following steps:\n//  1. Type-asserts the context object to *DefaultCtx.\n//  2. Retrieves the raw query parameter value from the request's URI.\n//  3. Parses the raw value into the appropriate type based on the generic type parameter V.\n//     If parsing fails, the function checks if a default value is provided. If so, it returns the default value.\n//  4. Returns the parsed value.\n//\n// If the generic type cannot be matched to a supported type, the function returns the default value (if provided) or the zero value of type V.\n//\n// Example usage:\n//\n//\tGET /?search=john&age=8\n//\tname := Query[string](c, \"search\") // Returns \"john\"\n//\tage := Query[int](c, \"age\") // Returns 8\n//\tunknown := Query[string](c, \"unknown\", \"default\") // Returns \"default\" since the query parameter \"unknown\" is not found\nfunc Query[V GenericType](c Ctx, key string, defaultValue ...V) V {\n\tq := c.App().toString(c.RequestCtx().QueryArgs().Peek(key))\n\tv, err := genericParseType[V](q)\n\tif err != nil && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\n}\n\n// Range returns a struct containing the type and a slice of ranges.\nfunc (r *DefaultReq) Range(size int64) (Range, error) {\n\tvar (\n\t\trangeData Range\n\t\tranges    string\n\t)\n\trangeStr := utils.TrimSpace(r.Get(HeaderRange))\n\tmaxRanges := r.c.app.config.MaxRanges\n\tconst maxRangePrealloc = 8\n\tprealloc := min(maxRanges, maxRangePrealloc)\n\tif prealloc > 0 {\n\t\trangeData.Ranges = make([]RangeSet, 0, prealloc)\n\t}\n\n\tparseBound := func(value string) (int64, error) {\n\t\tparsed, err := utils.ParseUint(value)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"parse range bound %q: %w\", value, err)\n\t\t}\n\t\tif parsed > (math.MaxUint64 >> 1) {\n\t\t\treturn 0, ErrRangeMalformed\n\t\t}\n\t\treturn int64(parsed), nil\n\t}\n\n\tbefore, after, found := strings.Cut(rangeStr, \"=\")\n\tif !found || strings.IndexByte(after, '=') >= 0 {\n\t\treturn rangeData, ErrRangeMalformed\n\t}\n\trangeData.Type = utilsstrings.ToLower(utils.TrimSpace(before))\n\tif rangeData.Type != \"bytes\" {\n\t\treturn rangeData, ErrRangeMalformed\n\t}\n\tranges = utils.TrimSpace(after)\n\n\tvar (\n\t\tsingleRange string\n\t\tmoreRanges  = ranges\n\t\trangeCount  int\n\t)\n\tfor moreRanges != \"\" {\n\t\trangeCount++\n\t\tif rangeCount > maxRanges {\n\t\t\tr.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)\n\t\t\tr.c.DefaultRes.Set(HeaderContentRange, \"bytes */\"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check\n\t\t\treturn rangeData, ErrRangeTooLarge\n\t\t}\n\t\tsingleRange = moreRanges\n\t\tif i := strings.IndexByte(moreRanges, ','); i >= 0 {\n\t\t\tsingleRange = moreRanges[:i]\n\t\t\tmoreRanges = utils.TrimSpace(moreRanges[i+1:])\n\t\t} else {\n\t\t\tmoreRanges = \"\"\n\t\t}\n\n\t\tsingleRange = utils.TrimSpace(singleRange)\n\n\t\tvar (\n\t\t\tstartStr, endStr string\n\t\t\ti                int\n\t\t)\n\t\tif i = strings.IndexByte(singleRange, '-'); i == -1 {\n\t\t\treturn rangeData, ErrRangeMalformed\n\t\t}\n\t\tstartStr = utils.TrimSpace(singleRange[:i])\n\t\tendStr = utils.TrimSpace(singleRange[i+1:])\n\n\t\tstart, startErr := parseBound(startStr)\n\t\tend, endErr := parseBound(endStr)\n\t\tif errors.Is(startErr, ErrRangeMalformed) || errors.Is(endErr, ErrRangeMalformed) {\n\t\t\treturn rangeData, ErrRangeMalformed\n\t\t}\n\t\tif startErr != nil { // -nnn\n\t\t\tstart = max(size-end, 0)\n\t\t\tend = size - 1\n\t\t} else if endErr != nil { // nnn-\n\t\t\tend = size - 1\n\t\t}\n\t\tif end > size-1 { // limit last-byte-pos to current length\n\t\t\tend = size - 1\n\t\t}\n\t\tif start > end || start < 0 {\n\t\t\tcontinue\n\t\t}\n\t\trangeData.Ranges = append(rangeData.Ranges, RangeSet{\n\t\t\tStart: start,\n\t\t\tEnd:   end,\n\t\t})\n\t}\n\tif len(rangeData.Ranges) < 1 {\n\t\tr.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)\n\t\tr.c.DefaultRes.Set(HeaderContentRange, \"bytes */\"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check\n\t\treturn rangeData, ErrRequestedRangeNotSatisfiable\n\t}\n\n\treturn rangeData, nil\n}\n\n// Route returns the matched Route struct.\nfunc (r *DefaultReq) Route() *Route {\n\treturn r.c.Route()\n}\n\n// Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.\n// If the offset is negative or exceeds the number of subdomains, an empty slice is returned.\n// If the offset is zero every label (no trimming) is returned.\nfunc (r *DefaultReq) Subdomains(offset ...int) []string {\n\to := 2\n\tif len(offset) > 0 {\n\t\to = offset[0]\n\t}\n\n\t// Negative offset, return nothing.\n\tif o < 0 {\n\t\treturn []string{}\n\t}\n\n\t// Normalize host according to RFC 3986\n\thost := r.Hostname()\n\t// Trim the trailing dot of a fully-qualified domain\n\tif strings.HasSuffix(host, \".\") {\n\t\thost = utils.TrimRight(host, '.')\n\t}\n\thost = utilsstrings.ToLower(host)\n\n\t// Decode punycode labels only when necessary\n\tif strings.Contains(host, \"xn--\") {\n\t\tif u, err := idna.Lookup.ToUnicode(host); err == nil {\n\t\t\thost = utilsstrings.ToLower(u)\n\t\t}\n\t}\n\n\t// Return nothing for IP addresses\n\tip := host\n\tif strings.HasPrefix(ip, \"[\") && strings.HasSuffix(ip, \"]\") {\n\t\tip = ip[1 : len(ip)-1]\n\t}\n\tif utils.IsIPv4(ip) || utils.IsIPv6(ip) {\n\t\treturn []string{}\n\t}\n\n\t// Use stack-allocated array for typical domain names (up to 8 labels)\n\t// This avoids heap allocation for most common cases\n\tvar partsBuf [8]string\n\tparts := partsBuf[:0]\n\n\tfor part := range strings.SplitSeq(host, \".\") {\n\t\tparts = append(parts, part)\n\t}\n\n\t// offset == 0, caller wants everything.\n\tif o == 0 {\n\t\t// Need to return a copy since partsBuf is on the stack\n\t\tresult := make([]string, len(parts))\n\t\tcopy(result, parts)\n\t\treturn result\n\t}\n\n\t// If we trim away the whole slice (or more), nothing remains.\n\tif o >= len(parts) {\n\t\treturn []string{}\n\t}\n\n\t// Return a heap-allocated copy of the relevant portion\n\tresult := make([]string, len(parts)-o)\n\tcopy(result, parts[:len(parts)-o])\n\treturn result\n}\n\n// Stale returns the inverse of Fresh, indicating if the client's cached response is considered stale.\nfunc (r *DefaultReq) Stale() bool {\n\treturn !r.Fresh()\n}\n\n// IsProxyTrusted checks trustworthiness of remote ip.\n// If Config.TrustProxy false, it returns false.\n// IsProxyTrusted can check remote ip by proxy ranges and ip map.\nfunc (r *DefaultReq) IsProxyTrusted() bool {\n\tconfig := r.c.app.config\n\tif !config.TrustProxy {\n\t\treturn false\n\t}\n\n\tremoteAddr := r.c.fasthttp.RemoteAddr()\n\tswitch remoteAddr.(type) {\n\tcase *net.UnixAddr:\n\t\treturn config.TrustProxyConfig.UnixSocket\n\tcase *net.TCPAddr, *net.UDPAddr:\n\t\t// Keep existing RemoteIP/IP-map/CIDR checks for TCP/UDP paths as-is.\n\tdefault:\n\t\t// Unknown address type: do not trust by default.\n\t\treturn false\n\t}\n\n\tip := r.c.fasthttp.RemoteIP()\n\tif ip == nil {\n\t\treturn false\n\t}\n\n\tif (config.TrustProxyConfig.Loopback && ip.IsLoopback()) ||\n\t\t(config.TrustProxyConfig.Private && ip.IsPrivate()) ||\n\t\t(config.TrustProxyConfig.LinkLocal && ip.IsLinkLocalUnicast()) {\n\t\treturn true\n\t}\n\n\tif _, trusted := config.TrustProxyConfig.ips[ip.String()]; trusted {\n\t\treturn true\n\t}\n\n\tfor _, ipNet := range config.TrustProxyConfig.ranges {\n\t\tif ipNet.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// IsFromLocal will return true if request came from local.\nfunc (r *DefaultReq) IsFromLocal() bool {\n\t// Unix sockets are inherently local - only processes on the same host can connect.\n\tremoteAddr := r.c.fasthttp.RemoteAddr()\n\tif _, ok := remoteAddr.(*net.UnixAddr); ok {\n\t\treturn true\n\t}\n\n\tif ip := r.c.fasthttp.RemoteIP(); ip != nil {\n\t\treturn ip.IsLoopback()\n\t}\n\treturn false\n}\n\n// Release is a method to reset Req fields when to use ReleaseCtx()\nfunc (r *DefaultReq) release() {\n\tr.c = nil\n}\n\nfunc (r *DefaultReq) getBody() []byte {\n\treturn r.c.app.GetBytes(r.c.fasthttp.Request.Body())\n}\n"
  },
  {
    "path": "req_interface_gen.go",
    "content": "// Code generated by ifacemaker; DO NOT EDIT.\n\npackage fiber\n\nimport (\n\t\"mime/multipart\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Req is an interface for request-related Ctx methods.\ntype Req interface {\n\t// Accepts checks if the specified extensions or content types are acceptable.\n\tAccepts(offers ...string) string\n\t// AcceptsCharsets checks if the specified charset is acceptable.\n\tAcceptsCharsets(offers ...string) string\n\t// AcceptsEncodings checks if the specified encoding is acceptable.\n\tAcceptsEncodings(offers ...string) string\n\t// AcceptsLanguages checks if the specified language is acceptable using\n\t// RFC 4647 Basic Filtering.\n\tAcceptsLanguages(offers ...string) string\n\t// AcceptsLanguagesExtended checks if the specified language is acceptable using\n\t// RFC 4647 Extended Filtering.\n\tAcceptsLanguagesExtended(offers ...string) string\n\t// App returns the *App reference to the instance of the Fiber application\n\tApp() *App\n\t// BaseURL returns (protocol + host + base path).\n\tBaseURL() string\n\t// BodyRaw contains the raw body submitted in a POST request.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tBodyRaw() []byte\n\t//nolint:nonamedreturns // gocritic unnamedResult prefers naming decoded body, decode count, and error\n\ttryDecodeBodyInOrder(originalBody *[]byte, encodings []string) (body []byte, decodesRealized uint8, err error)\n\t// Body contains the raw body submitted in a POST request.\n\t// This method will decompress the body if the 'Content-Encoding' header is provided.\n\t// It returns the original (or decompressed) body data which is valid only within the handler.\n\t// Don't store direct references to the returned data.\n\t// If you need to keep the body's data later, make a copy or use the Immutable option.\n\tBody() []byte\n\t// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n\t// a cancellation signal, and other values across API boundaries.\n\tRequestCtx() *fasthttp.RequestCtx\n\t// Cookies are used for getting a cookie value by key.\n\t// Defaults to the empty string \"\" if the cookie doesn't exist.\n\t// If a default value is given, it will return that value if the cookie doesn't exist.\n\t// The returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tCookies(key string, defaultValue ...string) string\n\t// Request return the *fasthttp.Request object\n\t// This allows you to use all fasthttp request methods\n\t// https://godoc.org/github.com/valyala/fasthttp#Request\n\tRequest() *fasthttp.Request\n\t// FormFile returns the first file by key from a MultipartForm.\n\tFormFile(key string) (*multipart.FileHeader, error)\n\t// FormValue returns the first value by key from a MultipartForm.\n\t// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.\n\t// Defaults to the empty string \"\" if the form value doesn't exist.\n\t// If a default value is given, it will return that value if the form value does not exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tFormValue(key string, defaultValue ...string) string\n\t// Fresh returns true when the response is still “fresh” in the client's cache,\n\t// otherwise false is returned to indicate that the client cache is now stale\n\t// and the full response should be sent.\n\t// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end\n\t// reload request, this module will return false to make handling these requests transparent.\n\t// https://github.com/jshttp/fresh/blob/master/index.js#L33\n\tFresh() bool\n\t// Get returns the HTTP request header specified by field.\n\t// Field names are case-insensitive\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGet(key string, defaultValue ...string) string\n\t// GetHeaders (a.k.a GetReqHeaders) returns the HTTP request headers.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetHeaders() map[string][]string\n\t// Host contains the host derived from the X-Forwarded-Host or Host HTTP header.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// In a network context, `Host` refers to the combination of a hostname and potentially a port number used for connecting,\n\t// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.\n\t// Example: URL: https://example.com:8080 -> Host: example.com:8080\n\t// Make copies or use the Immutable setting instead.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tHost() string\n\t// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Example: URL: https://example.com:8080 -> Hostname: example.com\n\t// Make copies or use the Immutable setting instead.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tHostname() string\n\t// Port returns the remote port of the request.\n\tPort() string\n\t// IP returns the remote IP address of the request.\n\t// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tIP() string\n\t// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear.\n\t// When IP validation is enabled, any invalid IPs will be omitted.\n\textractIPsFromHeader(header string) []string\n\t// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled.\n\t// currently, it will return the first valid IP address in header.\n\t// when IP validation is disabled, it will simply return the value of the header without any inspection.\n\t// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string.\n\textractIPFromHeader(header string) string\n\t// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header.\n\t// When IP validation is enabled, only valid IPs are returned.\n\tIPs() []string\n\t// Is returns the matching content type,\n\t// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter\n\tIs(extension string) bool\n\t// Locals makes it possible to pass any values under keys scoped to the request\n\t// and therefore available to all following routes that match the request.\n\t//\n\t// All the values are removed from ctx after returning from the top\n\t// RequestHandler. Additionally, Close method is called on each value\n\t// implementing io.Closer before removing the value from ctx.\n\tLocals(key any, value ...any) any\n\t// Method returns the HTTP request method for the context, optionally overridden by the provided argument.\n\t// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context.\n\t// Otherwise, it updates the context's method and returns the overridden method as a string.\n\tMethod(override ...string) string\n\t// MultipartForm parse form entries from binary.\n\t// This returns a map[string][]string, so given a key, the value will be a string slice.\n\tMultipartForm() (*multipart.Form, error)\n\t// OriginalURL contains the original request URL.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tOriginalURL() string\n\t// Params is used to get the route parameters.\n\t// Defaults to empty string \"\" if the param doesn't exist.\n\t// If a default value is given, it will return that value if the param doesn't exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tParams(key string, defaultValue ...string) string\n\t// Scheme contains the request protocol string: http or https for TLS requests.\n\t// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.\n\tScheme() string\n\t// Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2.\n\tProtocol() string\n\t// Query returns the query string parameter in the url.\n\t// Defaults to empty string \"\" if the query doesn't exist.\n\t// If a default value is given, it will return that value if the query doesn't exist.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tQuery(key string, defaultValue ...string) string\n\t// Queries returns a map of query parameters and their values.\n\t//\n\t// GET /?name=alex&wanna_cake=2&id=\n\t// Queries()[\"name\"] == \"alex\"\n\t// Queries()[\"wanna_cake\"] == \"2\"\n\t// Queries()[\"id\"] == \"\"\n\t//\n\t// GET /?field1=value1&field1=value2&field2=value3\n\t// Queries()[\"field1\"] == \"value2\"\n\t// Queries()[\"field2\"] == \"value3\"\n\t//\n\t// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3\n\t// Queries()[\"list_a\"] == \"3\"\n\t// Queries()[\"list_b[]\"] == \"3\"\n\t// Queries()[\"list_c\"] == \"1,2,3\"\n\t//\n\t// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending\n\t// Queries()[\"filters.author.name\"] == \"John\"\n\t// Queries()[\"filters.category.name\"] == \"Technology\"\n\t// Queries()[\"filters[customer][name]\"] == \"Alice\"\n\t// Queries()[\"filters[status]\"] == \"pending\"\n\tQueries() map[string]string\n\t// Range returns a struct containing the type and a slice of ranges.\n\tRange(size int64) (Range, error)\n\t// Route returns the matched Route struct.\n\tRoute() *Route\n\t// Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.\n\t// If the offset is negative or exceeds the number of subdomains, an empty slice is returned.\n\t// If the offset is zero every label (no trimming) is returned.\n\tSubdomains(offset ...int) []string\n\t// Stale returns the inverse of Fresh, indicating if the client's cached response is considered stale.\n\tStale() bool\n\t// IsProxyTrusted checks trustworthiness of remote ip.\n\t// If Config.TrustProxy false, it returns false.\n\t// IsProxyTrusted can check remote ip by proxy ranges and ip map.\n\tIsProxyTrusted() bool\n\t// IsFromLocal will return true if request came from local.\n\tIsFromLocal() bool\n\t// Release is a method to reset Req fields when to use ReleaseCtx()\n\trelease()\n\tgetBody() []byte\n}\n"
  },
  {
    "path": "res.go",
    "content": "package fiber\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\tpathpkg \"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/bytebufferpool\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// SendFile defines configuration options when to transfer file with SendFile.\ntype SendFile struct {\n\t// FS is the file system to serve the static files from.\n\t// You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.\n\t//\n\t// Optional. Default: nil\n\tFS fs.FS\n\n\t// When set to true, the server tries minimizing CPU usage by caching compressed files.\n\t// This works differently than the github.com/gofiber/compression middleware.\n\t// You have to set Content-Encoding header to compress the file.\n\t// Available compression methods are gzip, br, and zstd.\n\t//\n\t// Optional. Default: false\n\tCompress bool `json:\"compress\"`\n\n\t// When set to true, enables byte range requests.\n\t//\n\t// Optional. Default: false\n\tByteRange bool `json:\"byte_range\"`\n\n\t// When set to true, enables direct download.\n\t//\n\t// Optional. Default: false\n\tDownload bool `json:\"download\"`\n\n\t// Expiration duration for inactive file handlers.\n\t// Use a negative time.Duration to disable it.\n\t//\n\t// Optional. Default: 10 * time.Second\n\tCacheDuration time.Duration `json:\"cache_duration\"`\n\n\t// The value for the Cache-Control HTTP-header\n\t// that is set on the file response. MaxAge is defined in seconds.\n\t//\n\t// Optional. Default: 0\n\tMaxAge int `json:\"max_age\"`\n}\n\n// sendFileStore is used to keep the SendFile configuration and the handler.\ntype sendFileStore struct {\n\thandler           fasthttp.RequestHandler\n\tcacheControlValue string\n\tconfig            SendFile\n}\n\n// configEqual compares the current SendFile config with the new one\n// and returns true if they are equal.\n//\n// Here we don't use reflect.DeepEqual because it is quite slow compared to manual comparison.\nfunc (sf *sendFileStore) configEqual(cfg SendFile) bool {\n\tif sf.config.FS != cfg.FS {\n\t\treturn false\n\t}\n\n\tif sf.config.Compress != cfg.Compress {\n\t\treturn false\n\t}\n\n\tif sf.config.ByteRange != cfg.ByteRange {\n\t\treturn false\n\t}\n\n\tif sf.config.Download != cfg.Download {\n\t\treturn false\n\t}\n\n\tif sf.config.CacheDuration != cfg.CacheDuration {\n\t\treturn false\n\t}\n\n\tif sf.config.MaxAge != cfg.MaxAge {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Cookie defines the values used when configuring cookies emitted by\n// DefaultRes.Cookie.\ntype Cookie struct {\n\tExpires     time.Time `json:\"expires\"`      // The expiration date of the cookie\n\tName        string    `json:\"name\"`         // The name of the cookie\n\tValue       string    `json:\"value\"`        // The value of the cookie\n\tPath        string    `json:\"path\"`         // Specifies a URL path which is allowed to receive the cookie\n\tDomain      string    `json:\"domain\"`       // Specifies the domain which is allowed to receive the cookie\n\tSameSite    string    `json:\"same_site\"`    // Controls whether or not a cookie is sent with cross-site requests\n\tMaxAge      int       `json:\"max_age\"`      // The maximum age (in seconds) of the cookie\n\tSecure      bool      `json:\"secure\"`       // Indicates that the cookie should only be transmitted over a secure HTTPS connection\n\tHTTPOnly    bool      `json:\"http_only\"`    // Indicates that the cookie is accessible only through the HTTP protocol\n\tPartitioned bool      `json:\"partitioned\"`  // Indicates if the cookie is stored in a partitioned cookie jar\n\tSessionOnly bool      `json:\"session_only\"` // Indicates if the cookie is a session-only cookie\n}\n\n// ResFmt associates a Content Type to a fiber.Handler for c.Format\ntype ResFmt struct {\n\tHandler   func(Ctx) error\n\tMediaType string\n}\n\n// DefaultRes is the default implementation of Res used by DefaultCtx.\n//\n//go:generate ifacemaker --file res.go --struct DefaultRes --iface Res --pkg fiber --output res_interface_gen.go --not-exported true --iface-comment \"Res is an interface for response-related Ctx methods.\"\ntype DefaultRes struct {\n\tc *DefaultCtx\n}\n\n// App returns the *App reference to the instance of the Fiber application\nfunc (r *DefaultRes) App() *App {\n\treturn r.c.app\n}\n\n// Append the specified value to the HTTP response header field.\n// If the header is not already set, it creates the header with the specified value.\nfunc (r *DefaultRes) Append(field string, values ...string) {\n\tif len(values) == 0 {\n\t\treturn\n\t}\n\th := r.c.app.toString(r.c.fasthttp.Response.Header.Peek(field))\n\toriginalH := h\n\tfor _, value := range values {\n\t\tif h == \"\" {\n\t\t\th = value\n\t\t} else if !headerContainsValue(h, value) {\n\t\t\th += \", \" + value\n\t\t}\n\t}\n\tif originalH != h {\n\t\tr.Set(field, h)\n\t}\n}\n\n// headerContainsValue checks if a header value already contains the given value\n// as a comma-separated element. Per RFC 9110, list elements are separated by commas\n// with optional whitespace (OWS) around them.\nfunc headerContainsValue(header, value string) bool {\n\t// Empty value should never match\n\tif value == \"\" {\n\t\treturn false\n\t}\n\n\t// Exact match (single value header)\n\tif header == value {\n\t\treturn true\n\t}\n\n\t// Check each comma-separated element, handling optional whitespace (OWS)\n\tfor part := range strings.SplitSeq(header, \",\") {\n\t\tif utils.TrimSpace(part) == value {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc sanitizeFilename(filename string) string {\n\tfor _, r := range filename {\n\t\tif unicode.IsControl(r) {\n\t\t\tb := make([]byte, 0, len(filename))\n\t\t\tfor _, rr := range filename {\n\t\t\t\tif !unicode.IsControl(rr) {\n\t\t\t\t\tb = utf8.AppendRune(b, rr)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn utils.TrimSpace(string(b))\n\t\t}\n\t}\n\n\treturn utils.TrimSpace(filename)\n}\n\nfunc fallbackFilenameIfInvalid(filename string) string {\n\tif filename == \"\" || filename == \".\" {\n\t\treturn \"download\"\n\t}\n\n\treturn filename\n}\n\n// Attachment sets the HTTP response Content-Disposition header field to attachment.\nfunc (r *DefaultRes) Attachment(filename ...string) {\n\tif len(filename) > 0 {\n\t\tfname := filepath.Base(filename[0])\n\t\tfname = sanitizeFilename(fname)\n\t\tfname = fallbackFilenameIfInvalid(fname)\n\t\tr.Type(filepath.Ext(fname))\n\t\tapp := r.c.app\n\t\tvar quoted string\n\t\tif app.isASCII(fname) {\n\t\t\tquoted = app.quoteString(fname)\n\t\t} else {\n\t\t\tquoted = app.quoteRawString(fname)\n\t\t}\n\t\tdisp := `attachment; filename=\"` + quoted + `\"`\n\t\tif !app.isASCII(fname) {\n\t\t\tdisp += `; filename*=UTF-8''` + url.PathEscape(fname)\n\t\t}\n\t\tr.setCanonical(HeaderContentDisposition, disp)\n\t\treturn\n\t}\n\tr.setCanonical(HeaderContentDisposition, \"attachment\")\n}\n\n// ClearCookie expires a specific cookie by key on the client side.\n// If no key is provided it expires all cookies that came with the request.\nfunc (r *DefaultRes) ClearCookie(key ...string) {\n\trequest := &r.c.fasthttp.Request\n\tresponse := &r.c.fasthttp.Response\n\tif len(key) > 0 {\n\t\tfor i := range key {\n\t\t\tresponse.Header.DelClientCookie(key[i])\n\t\t}\n\t\treturn\n\t}\n\tfor k := range request.Header.Cookies() {\n\t\tresponse.Header.DelClientCookieBytes(k)\n\t}\n}\n\n// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n// a cancellation signal, and other values across API boundaries.\nfunc (r *DefaultRes) RequestCtx() *fasthttp.RequestCtx {\n\treturn r.c.fasthttp\n}\n\n// Cookie sets a cookie by passing a cookie struct.\nfunc (r *DefaultRes) Cookie(cookie *Cookie) {\n\tif cookie.Path == \"\" {\n\t\tcookie.Path = \"/\"\n\t}\n\n\tif cookie.SessionOnly {\n\t\tcookie.MaxAge = 0\n\t\tcookie.Expires = time.Time{}\n\t}\n\n\tvar sameSite http.SameSite\n\n\tswitch {\n\tcase utils.EqualFold(cookie.SameSite, CookieSameSiteStrictMode):\n\t\tsameSite = http.SameSiteStrictMode\n\tcase utils.EqualFold(cookie.SameSite, CookieSameSiteNoneMode):\n\t\tsameSite = http.SameSiteNoneMode\n\t\t// SameSite=None requires Secure=true per RFC and browser requirements\n\t\tcookie.Secure = true\n\tcase utils.EqualFold(cookie.SameSite, CookieSameSiteDisabled):\n\t\tsameSite = 0\n\tcase utils.EqualFold(cookie.SameSite, CookieSameSiteLaxMode):\n\t\tsameSite = http.SameSiteLaxMode\n\tdefault:\n\t\tsameSite = http.SameSiteLaxMode\n\t}\n\n\t// Partitioned requires Secure=true per CHIPS spec\n\tif cookie.Partitioned {\n\t\tcookie.Secure = true\n\t}\n\n\t// create/validate cookie using net/http\n\thc := &http.Cookie{\n\t\tName:        cookie.Name,\n\t\tValue:       cookie.Value,\n\t\tPath:        cookie.Path,\n\t\tDomain:      cookie.Domain,\n\t\tExpires:     cookie.Expires,\n\t\tMaxAge:      cookie.MaxAge,\n\t\tSecure:      cookie.Secure,\n\t\tHttpOnly:    cookie.HTTPOnly,\n\t\tSameSite:    sameSite,\n\t\tPartitioned: cookie.Partitioned,\n\t}\n\n\tif err := hc.Valid(); err != nil {\n\t\t// invalid cookies are ignored, same approach as net/http\n\t\treturn\n\t}\n\n\t// create fasthttp cookie\n\tfcookie := fasthttp.AcquireCookie()\n\tfcookie.SetKey(hc.Name)\n\tfcookie.SetValue(hc.Value)\n\tfcookie.SetPath(hc.Path)\n\tfcookie.SetDomain(hc.Domain)\n\n\tif !cookie.SessionOnly {\n\t\tfcookie.SetMaxAge(hc.MaxAge)\n\t\tfcookie.SetExpire(hc.Expires)\n\t}\n\n\tfcookie.SetSecure(hc.Secure)\n\tfcookie.SetHTTPOnly(hc.HttpOnly)\n\n\tswitch sameSite {\n\tcase http.SameSiteLaxMode:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)\n\tcase http.SameSiteStrictMode:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)\n\tcase http.SameSiteNoneMode:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)\n\tcase http.SameSiteDefaultMode:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteDefaultMode)\n\tdefault:\n\t\tfcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)\n\t}\n\n\tfcookie.SetPartitioned(hc.Partitioned)\n\n\t// Set resp header\n\tr.c.fasthttp.Response.Header.SetCookie(fcookie)\n\tfasthttp.ReleaseCookie(fcookie)\n}\n\n// Download transfers the file from path as an attachment.\n// Typically, browsers will prompt the user for download.\n// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).\n// Override this default with the filename parameter.\nfunc (r *DefaultRes) Download(file string, filename ...string) error {\n\tvar fname string\n\tif len(filename) > 0 {\n\t\tfname = filepath.Base(filename[0])\n\t} else {\n\t\tfname = filepath.Base(file)\n\t}\n\tfname = sanitizeFilename(fname)\n\tfname = fallbackFilenameIfInvalid(fname)\n\tapp := r.c.app\n\tvar quoted string\n\tif app.isASCII(fname) {\n\t\tquoted = app.quoteString(fname)\n\t} else {\n\t\tquoted = app.quoteRawString(fname)\n\t}\n\tdisp := `attachment; filename=\"` + quoted + `\"`\n\tif !app.isASCII(fname) {\n\t\tdisp += `; filename*=UTF-8''` + url.PathEscape(fname)\n\t}\n\tr.setCanonical(HeaderContentDisposition, disp)\n\treturn r.SendFile(file)\n}\n\n// Response return the *fasthttp.Response object\n// This allows you to use all fasthttp response methods\n// https://godoc.org/github.com/valyala/fasthttp#Response\nfunc (r *DefaultRes) Response() *fasthttp.Response {\n\treturn &r.c.fasthttp.Response\n}\n\n// Format performs content-negotiation on the Accept HTTP header.\n// It uses Accepts to select a proper format and calls the matching\n// user-provided handler function.\n// If no accepted format is found, and a format with MediaType \"default\" is given,\n// that default handler is called. If no format is found and no default is given,\n// StatusNotAcceptable is sent.\nfunc (r *DefaultRes) Format(handlers ...ResFmt) error {\n\tif len(handlers) == 0 {\n\t\treturn ErrNoHandlers\n\t}\n\n\tfor i, h := range handlers {\n\t\tif h.Handler == nil {\n\t\t\treturn fmt.Errorf(\"format handler is nil for media type %q at index %d\", h.MediaType, i)\n\t\t}\n\t}\n\n\tr.Vary(HeaderAccept)\n\n\tif r.c.DefaultReq.Get(HeaderAccept) == \"\" {\n\t\tr.c.fasthttp.Response.Header.SetContentType(handlers[0].MediaType)\n\t\treturn handlers[0].Handler(r.c)\n\t}\n\n\t// Using an int literal as the slice capacity allows for the slice to be\n\t// allocated on the stack. The number was chosen arbitrarily as an\n\t// approximation of the maximum number of content types a user might handle.\n\t// If the user goes over, it just causes allocations, so it's not a problem.\n\ttypes := make([]string, 0, 8)\n\tvar defaultHandler Handler\n\tfor _, h := range handlers {\n\t\tif h.MediaType == \"default\" {\n\t\t\tdefaultHandler = h.Handler\n\t\t\tcontinue\n\t\t}\n\t\ttypes = append(types, h.MediaType)\n\t}\n\taccept := r.c.DefaultReq.Accepts(types...) //nolint:staticcheck // It is fine to ignore the static check\n\n\tif accept == \"\" {\n\t\tif defaultHandler == nil {\n\t\t\treturn r.SendStatus(StatusNotAcceptable)\n\t\t}\n\t\treturn defaultHandler(r.c)\n\t}\n\n\tfor _, h := range handlers {\n\t\tif h.MediaType == accept {\n\t\t\tr.c.fasthttp.Response.Header.SetContentType(h.MediaType)\n\t\t\treturn h.Handler(r.c)\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"%w: format: an Accept was found but no handler was called\", errUnreachable)\n}\n\n// AutoFormat performs content-negotiation on the Accept HTTP header.\n// It uses Accepts to select a proper format.\n// The supported content types are text/html, text/plain, application/json, application/xml, application/vnd.msgpack, and application/cbor.\n// For more flexible content negotiation, use Format.\n// If the header is not specified or there is no proper format, text/plain is used.\nfunc (r *DefaultRes) AutoFormat(body any) error {\n\t// Get accepted content type\n\taccept := r.c.DefaultReq.Accepts(\"html\", \"json\", \"txt\", \"xml\", \"msgpack\", \"cbor\") //nolint:staticcheck // It is fine to ignore the static check\n\n\t// Set accepted content type\n\tr.Type(accept)\n\t// Type convert provided body\n\tvar b string\n\tswitch val := body.(type) {\n\tcase string:\n\t\tb = val\n\tcase []byte:\n\t\tb = r.c.app.toString(val)\n\tdefault:\n\t\tb = fmt.Sprintf(\"%v\", val)\n\t}\n\n\t// Format based on the accept content type\n\tswitch accept {\n\tcase \"txt\":\n\t\treturn r.SendString(b)\n\tcase \"json\":\n\t\treturn r.JSON(body)\n\tcase \"xml\":\n\t\treturn r.XML(body)\n\tcase \"html\":\n\t\treturn r.SendString(\"<p>\" + b + \"</p>\")\n\tcase \"msgpack\":\n\t\treturn r.MsgPack(body)\n\tcase \"cbor\":\n\t\treturn r.CBOR(body)\n\t}\n\n\t// Default case\n\treturn r.SendString(b)\n}\n\n// Get (a.k.a. GetRespHeader) returns the HTTP response header specified by field.\n// Field names are case-insensitive\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultRes) Get(key string, defaultValue ...string) string {\n\treturn defaultString(r.c.app.toString(r.c.fasthttp.Response.Header.Peek(key)), defaultValue)\n}\n\n// GetHeaders (a.k.a GetRespHeaders) returns the HTTP response headers.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting instead.\nfunc (r *DefaultRes) GetHeaders() map[string][]string {\n\tapp := r.c.app\n\trespHeader := &r.c.fasthttp.Response.Header\n\t// Pre-allocate map with known header count to avoid reallocations\n\theaders := make(map[string][]string, respHeader.Len())\n\tfor k, v := range respHeader.All() {\n\t\tkey := app.toString(k)\n\t\theaders[key] = append(headers[key], app.toString(v))\n\t}\n\treturn headers\n}\n\n// JSON converts any interface or string to JSON.\n// Array and slice values encode as JSON arrays,\n// except that []byte encodes as a base64-encoded string,\n// and a nil slice encodes as the null JSON value.\n// If the ctype parameter is given, this method will set the\n// Content-Type header equal to ctype. If ctype is not given,\n// The Content-Type header will be set to application/json; charset=utf-8.\nfunc (r *DefaultRes) JSON(data any, ctype ...string) error {\n\traw, err := r.c.app.config.JSONEncoder(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse := &r.c.fasthttp.Response\n\tresponse.SetBodyRaw(raw)\n\tif len(ctype) > 0 {\n\t\tresponse.Header.SetContentType(ctype[0])\n\t} else {\n\t\tresponse.Header.SetContentType(MIMEApplicationJSONCharsetUTF8)\n\t}\n\treturn nil\n}\n\n// MsgPack converts any interface or string to MessagePack encoded bytes.\n// If the ctype parameter is given, this method will set the\n// Content-Type header equal to ctype. If ctype is not given,\n// The Content-Type header will be set to application/vnd.msgpack.\nfunc (r *DefaultRes) MsgPack(data any, ctype ...string) error {\n\traw, err := r.c.app.config.MsgPackEncoder(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse := &r.c.fasthttp.Response\n\tresponse.SetBodyRaw(raw)\n\tif len(ctype) > 0 {\n\t\tresponse.Header.SetContentType(ctype[0])\n\t} else {\n\t\tresponse.Header.SetContentType(MIMEApplicationMsgPack)\n\t}\n\treturn nil\n}\n\n// CBOR converts any interface or string to CBOR encoded bytes.\n// If the ctype parameter is given, this method will set the\n// Content-Type header equal to ctype. If ctype is not given,\n// The Content-Type header will be set to application/cbor.\nfunc (r *DefaultRes) CBOR(data any, ctype ...string) error {\n\traw, err := r.c.app.config.CBOREncoder(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse := &r.c.fasthttp.Response\n\tresponse.SetBodyRaw(raw)\n\tif len(ctype) > 0 {\n\t\tresponse.Header.SetContentType(ctype[0])\n\t} else {\n\t\tresponse.Header.SetContentType(MIMEApplicationCBOR)\n\t}\n\treturn nil\n}\n\n// JSONP sends a JSON response with JSONP support.\n// This method is identical to JSON, except that it opts-in to JSONP callback support.\n// By default, the callback name is simply callback.\nfunc (r *DefaultRes) JSONP(data any, callback ...string) error {\n\traw, err := r.c.app.config.JSONEncoder(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcb := \"callback\"\n\tif len(callback) > 0 {\n\t\tcb = callback[0]\n\t}\n\n\t// Build JSONP response: callback(data);\n\t// Use bytebufferpool to avoid string concatenation allocations\n\tbuf := bytebufferpool.Get()\n\tbuf.WriteString(cb)\n\tbuf.WriteByte('(')\n\tbuf.Write(raw)\n\tbuf.WriteString(\");\")\n\n\tr.setCanonical(HeaderXContentTypeOptions, \"nosniff\")\n\tr.c.fasthttp.Response.Header.SetContentType(MIMETextJavaScriptCharsetUTF8)\n\t// Use SetBody (not SetBodyRaw) to copy the bytes before returning buffer to pool\n\tr.c.fasthttp.Response.SetBody(buf.Bytes())\n\tbytebufferpool.Put(buf)\n\treturn nil\n}\n\n// XML converts any interface or string to XML.\n// This method also sets the content header to application/xml; charset=utf-8.\nfunc (r *DefaultRes) XML(data any) error {\n\traw, err := r.c.app.config.XMLEncoder(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresponse := &r.c.fasthttp.Response\n\tresponse.SetBodyRaw(raw)\n\tresponse.Header.SetContentType(MIMEApplicationXMLCharsetUTF8)\n\treturn nil\n}\n\n// Links joins the links followed by the property to populate the response's Link HTTP header field.\nfunc (r *DefaultRes) Links(link ...string) {\n\tif len(link) == 0 {\n\t\treturn\n\t}\n\tbb := bytebufferpool.Get()\n\tfor i := range link {\n\t\tif i%2 == 0 {\n\t\t\tbb.WriteByte('<')\n\t\t\tbb.WriteString(link[i])\n\t\t\tbb.WriteByte('>')\n\t\t} else {\n\t\t\tbb.WriteString(`; rel=\"`)\n\t\t\tbb.WriteString(link[i])\n\t\t\tbb.WriteString(`\",`)\n\t\t}\n\t}\n\tr.setCanonical(HeaderLink, utils.TrimRight(r.c.app.toString(bb.Bytes()), ','))\n\tbytebufferpool.Put(bb)\n}\n\n// Location sets the response Location HTTP header to the specified path parameter.\nfunc (r *DefaultRes) Location(path string) {\n\tr.setCanonical(HeaderLocation, path)\n}\n\n// OriginalURL contains the original request URL.\n// Returned value is only valid within the handler. Do not store any references.\n// Make copies or use the Immutable setting to use the value outside the Handler.\nfunc (r *DefaultRes) OriginalURL() string {\n\treturn r.c.OriginalURL()\n}\n\n// Redirect returns the Redirect reference.\n// Use Redirect().Status() to set custom redirection status code.\n// If status is not specified, status defaults to 303 See Other.\n// You can use Redirect().To(), Redirect().Route() and Redirect().Back() for redirection.\nfunc (r *DefaultRes) Redirect() *Redirect {\n\treturn r.c.Redirect()\n}\n\n// ViewBind Add vars to default view var map binding to template engine.\n// Variables are read by the Render method and may be overwritten.\nfunc (r *DefaultRes) ViewBind(vars Map) error {\n\treturn r.c.ViewBind(vars)\n}\n\n// getLocationFromRoute get URL location from route using parameters\nfunc (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) {\n\tif route == nil || route.Path == \"\" {\n\t\treturn \"\", ErrNotFound\n\t}\n\n\tapp := r.c.app\n\tbuf := bytebufferpool.Get()\n\tfor _, segment := range route.routeParser.segs {\n\t\tif !segment.IsParam {\n\t\t\t_, err := buf.WriteString(segment.Const)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to write string: %w\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tfor key, val := range params {\n\t\t\tisSame := key == segment.ParamName || (!app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName))\n\t\t\tisGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0\n\t\t\tif isSame || isGreedy {\n\t\t\t\t_, err := buf.WriteString(utils.ToString(val))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"failed to write string: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tlocation := buf.String()\n\t// release buffer\n\tbytebufferpool.Put(buf)\n\treturn location, nil\n}\n\n// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: \"/user/1831\"\nfunc (r *DefaultRes) GetRouteURL(routeName string, params Map) (string, error) {\n\troute := r.c.app.GetRoute(routeName)\n\treturn r.getLocationFromRoute(&route, params)\n}\n\n// Render a template with data and sends a text/html response.\n// We support the following engines: https://github.com/gofiber/template\nfunc (r *DefaultRes) Render(name string, bind any, layouts ...string) error {\n\t// Get new buffer from pool\n\tbuf := bytebufferpool.Get()\n\tdefer bytebufferpool.Put(buf)\n\n\t// Initialize empty bind map if bind is nil\n\tif bind == nil {\n\t\tbind = make(Map)\n\t}\n\n\t// Pass-locals-to-views, bind, appListKeys\n\tr.c.renderExtensions(bind)\n\n\trootApp := r.c.app\n\tvar rendered bool\n\tfor i := len(rootApp.mountFields.appListKeys) - 1; i >= 0; i-- {\n\t\tprefix := rootApp.mountFields.appListKeys[i]\n\t\tapp := rootApp.mountFields.appList[prefix]\n\t\tif prefix == \"\" || strings.Contains(r.c.OriginalURL(), prefix) {\n\t\t\tif len(layouts) == 0 && app.config.ViewsLayout != \"\" {\n\t\t\t\tlayouts = []string{\n\t\t\t\t\tapp.config.ViewsLayout,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Render template from Views\n\t\t\tif app.config.Views != nil {\n\t\t\t\tif err := app.config.Views.Render(buf, name, bind, layouts...); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to render: %w\", err)\n\t\t\t\t}\n\n\t\t\t\trendered = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif !rendered {\n\t\t// Render raw template using 'name' as filepath if no engine is set\n\t\tvar tmpl *template.Template\n\t\tif _, err := readContent(buf, name); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Parse template\n\t\ttmpl, err := template.New(\"\").Parse(rootApp.toString(buf.Bytes()))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse: %w\", err)\n\t\t}\n\t\tbuf.Reset()\n\t\t// Render template\n\t\tif err := tmpl.Execute(buf, bind); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute: %w\", err)\n\t\t}\n\t}\n\n\tresponse := &r.c.fasthttp.Response\n\n\t// Set Content-Type to text/html\n\tresponse.Header.SetContentType(MIMETextHTMLCharsetUTF8)\n\t// Set rendered template to body\n\tresponse.SetBody(buf.Bytes())\n\n\treturn nil\n}\n\nfunc (r *DefaultRes) renderExtensions(bind any) {\n\tr.c.renderExtensions(bind)\n}\n\n// Send sets the HTTP response body without copying it.\n// From this point onward the body argument must not be changed.\nfunc (r *DefaultRes) Send(body []byte) error {\n\t// Write response body\n\tr.c.fasthttp.Response.SetBodyRaw(body)\n\treturn nil\n}\n\n// SendEarlyHints allows the server to hint to the browser what resources a page would need\n// so the browser can preload them while waiting for the server's full response. Only Link\n// headers already written to the response will be transmitted as Early Hints.\n//\n// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.\n//\n// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.\n//\n// See: https://developer.chrome.com/docs/web-platform/early-hints and\n// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax\nfunc (r *DefaultRes) SendEarlyHints(hints []string) error {\n\tif len(hints) == 0 {\n\t\treturn nil\n\t}\n\tfor _, h := range hints {\n\t\tr.c.fasthttp.Response.Header.Add(\"Link\", h)\n\t}\n\treturn r.c.fasthttp.EarlyHints()\n}\n\n// SendFile transfers the file from the specified path.\n// By default, the file is not compressed. To enable compression, set SendFile.Compress to true.\n// The Content-Type response HTTP header field is set based on the file's extension.\n// If the file extension is missing or invalid, the Content-Type is detected from the file's format.\nfunc (r *DefaultRes) SendFile(file string, config ...SendFile) error {\n\t// Save the filename, we will need it in the error message if the file isn't found\n\tfilename := file\n\n\tvar cfg SendFile\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\n\tif cfg.CacheDuration == 0 {\n\t\tcfg.CacheDuration = 10 * time.Second\n\t}\n\n\tvar fsHandler fasthttp.RequestHandler\n\tvar cacheControlValue string\n\n\tapp := r.c.app\n\tapp.sendfilesMutex.RLock()\n\tfor _, sf := range app.sendfiles {\n\t\tif sf.configEqual(cfg) {\n\t\t\tfsHandler = sf.handler\n\t\t\tcacheControlValue = sf.cacheControlValue\n\t\t\tbreak\n\t\t}\n\t}\n\tapp.sendfilesMutex.RUnlock()\n\n\tif fsHandler == nil {\n\t\tfasthttpFS := &fasthttp.FS{\n\t\t\tRoot:                   \"\",\n\t\t\tFS:                     cfg.FS,\n\t\t\tAllowEmptyRoot:         true,\n\t\t\tGenerateIndexPages:     false,\n\t\t\tAcceptByteRange:        cfg.ByteRange,\n\t\t\tCompress:               cfg.Compress,\n\t\t\tCompressBrotli:         cfg.Compress,\n\t\t\tCompressZstd:           cfg.Compress,\n\t\t\tCompressedFileSuffixes: app.config.CompressedFileSuffixes,\n\t\t\tCacheDuration:          cfg.CacheDuration,\n\t\t\tSkipCache:              cfg.CacheDuration < 0,\n\t\t\tIndexNames:             []string{\"index.html\"},\n\t\t\tPathNotFound: func(ctx *fasthttp.RequestCtx) {\n\t\t\t\tctx.Response.SetStatusCode(StatusNotFound)\n\t\t\t},\n\t\t}\n\n\t\tif cfg.FS != nil {\n\t\t\tfasthttpFS.Root = \".\"\n\t\t}\n\n\t\tsf := &sendFileStore{\n\t\t\tconfig:  cfg,\n\t\t\thandler: fasthttpFS.NewRequestHandler(),\n\t\t}\n\n\t\tmaxAge := cfg.MaxAge\n\t\tif maxAge > 0 {\n\t\t\tsf.cacheControlValue = \"public, max-age=\" + strconv.Itoa(maxAge)\n\t\t}\n\n\t\t// set vars\n\t\tfsHandler = sf.handler\n\t\tcacheControlValue = sf.cacheControlValue\n\n\t\tapp.sendfilesMutex.Lock()\n\t\tapp.sendfiles = append(app.sendfiles, sf)\n\t\tapp.sendfilesMutex.Unlock()\n\t}\n\n\t// Keep original path for mutable params\n\tr.c.pathOriginal = utils.CopyString(r.c.pathOriginal)\n\n\trequest := &r.c.fasthttp.Request\n\n\t// Delete the Accept-Encoding header if compression is disabled\n\tif !cfg.Compress {\n\t\t// https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55\n\t\trequest.Header.Del(HeaderAcceptEncoding)\n\t}\n\n\t// copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments\n\tif file == \"\" || (!filepath.IsAbs(file) && cfg.FS == nil) {\n\t\t// extend relative path to absolute path\n\t\thasTrailingSlash := file != \"\" && (file[len(file)-1] == '/' || file[len(file)-1] == '\\\\')\n\n\t\tvar err error\n\t\tfile = filepath.FromSlash(file)\n\t\tif file, err = filepath.Abs(file); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to determine abs file path: %w\", err)\n\t\t}\n\t\tif hasTrailingSlash {\n\t\t\tfile += \"/\"\n\t\t}\n\t}\n\n\t// convert the path to forward slashes regardless the OS in order to set the URI properly\n\t// the handler will convert back to OS path separator before opening the file\n\tfile = filepath.ToSlash(file)\n\n\t// Restore the original requested URL\n\toriginalURL := utils.CopyString(r.c.OriginalURL())\n\tdefer request.SetRequestURI(originalURL)\n\n\t// Set new URI for fileHandler\n\trequest.SetRequestURI(file)\n\n\tvar (\n\t\tsendFileSize    int64\n\t\thasSendFileSize bool\n\t)\n\n\tif cfg.ByteRange && len(request.Header.Peek(HeaderRange)) > 0 {\n\t\tsizePath := file\n\t\tif cfg.FS != nil {\n\t\t\tsizePath = filepath.ToSlash(filename)\n\t\t}\n\n\t\tif size, err := sendFileContentLength(sizePath, cfg); err == nil {\n\t\t\tsendFileSize = size\n\t\t\thasSendFileSize = true\n\t\t}\n\t}\n\n\t// Save status code\n\tresponse := &r.c.fasthttp.Response\n\tstatus := response.StatusCode()\n\n\t// Serve file\n\tfsHandler(r.c.fasthttp)\n\n\t// Sets the response Content-Disposition header to attachment if the Download option is true\n\tif cfg.Download {\n\t\tr.Attachment()\n\t}\n\n\t// Get the status code which is set by fasthttp\n\tfsStatus := response.StatusCode()\n\n\t// Check for error\n\tif status != StatusNotFound && fsStatus == StatusNotFound {\n\t\treturn NewError(StatusNotFound, fmt.Sprintf(\"sendfile: file %s not found\", filename))\n\t}\n\n\t// Set the status code set by the user if it is different from the fasthttp status code and 200\n\tif status != fsStatus && status != StatusOK {\n\t\tr.Status(status)\n\t}\n\n\t// Apply cache control header\n\tif status != StatusNotFound && status != StatusForbidden {\n\t\tif cfg.ByteRange && hasSendFileSize && response.StatusCode() == StatusRequestedRangeNotSatisfiable && len(response.Header.Peek(HeaderContentRange)) == 0 {\n\t\t\tresponse.Header.Set(HeaderContentRange, \"bytes */\"+strconv.FormatInt(sendFileSize, 10))\n\t\t}\n\n\t\tif cacheControlValue != \"\" {\n\t\t\tresponse.Header.Set(HeaderCacheControl, cacheControlValue)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\nfunc sendFileContentLength(path string, cfg SendFile) (int64, error) {\n\tif cfg.FS != nil {\n\t\tcleanPath := pathpkg.Clean(utils.TrimLeft(path, '/'))\n\t\tif cleanPath == \".\" {\n\t\t\tcleanPath = \"\"\n\t\t}\n\t\tinfo, err := fs.Stat(cfg.FS, cleanPath)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"stat %q: %w\", cleanPath, err)\n\t\t}\n\t\treturn info.Size(), nil\n\t}\n\n\tinfo, err := os.Stat(filepath.FromSlash(path))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"stat %q: %w\", path, err)\n\t}\n\n\treturn info.Size(), nil\n}\n\n// SendStatus sets the HTTP status code and if the response body is empty,\n// it sets the correct status message in the body.\nfunc (r *DefaultRes) SendStatus(status int) error {\n\tr.Status(status)\n\n\tif statusDisallowsBody(status) {\n\t\tr.c.fasthttp.Response.ResetBody()\n\t\treturn nil\n\t}\n\n\t// Only set status body when there is no response body\n\tif len(r.c.fasthttp.Response.Body()) == 0 {\n\t\treturn r.SendString(utils.StatusMessage(status))\n\t}\n\n\treturn nil\n}\n\n// SendString sets the HTTP response body for string types.\n// This means no type assertion, recommended for faster performance\nfunc (r *DefaultRes) SendString(body string) error {\n\tr.c.fasthttp.Response.SetBodyString(body)\n\n\treturn nil\n}\n\n// SendStream sets response body stream and optional body size.\nfunc (r *DefaultRes) SendStream(stream io.Reader, size ...int) error {\n\tif len(size) > 0 && size[0] >= 0 {\n\t\tr.c.fasthttp.Response.SetBodyStream(stream, size[0])\n\t} else {\n\t\tr.c.fasthttp.Response.SetBodyStream(stream, -1)\n\t}\n\n\treturn nil\n}\n\n// SendStreamWriter sets response body stream writer\nfunc (r *DefaultRes) SendStreamWriter(streamWriter func(*bufio.Writer)) error {\n\tr.c.fasthttp.Response.SetBodyStreamWriter(fasthttp.StreamWriter(streamWriter))\n\n\treturn nil\n}\n\n// Set sets the response's HTTP header field to the specified key, value.\nfunc (r *DefaultRes) Set(key, val string) {\n\tr.c.fasthttp.Response.Header.Set(key, val)\n}\n\nfunc (r *DefaultRes) setCanonical(key, val string) {\n\tr.c.fasthttp.Response.Header.SetCanonical(utils.UnsafeBytes(key), utils.UnsafeBytes(val))\n}\n\n// Status sets the HTTP status for the response.\n// This method is chainable.\nfunc (r *DefaultRes) Status(status int) Ctx {\n\tr.c.fasthttp.Response.SetStatusCode(status)\n\treturn r.c\n}\n\nfunc statusDisallowsBody(status int) bool {\n\t// As per RFC 9110, 1xx (Informational) responses cannot have a body.\n\tif status >= 100 && status < 200 {\n\t\treturn true\n\t}\n\n\tswitch status {\n\tcase StatusNoContent, StatusResetContent, StatusNotModified:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Type sets the Content-Type HTTP header to the MIME type specified by the file extension.\nfunc (r *DefaultRes) Type(extension string, charset ...string) Ctx {\n\tmimeType := utils.GetMIME(extension)\n\n\tif len(charset) > 0 {\n\t\tr.c.fasthttp.Response.Header.SetContentType(mimeType + \"; charset=\" + charset[0])\n\t} else {\n\t\t// Automatically add UTF-8 charset for text-based MIME types\n\t\tif shouldIncludeCharset(mimeType) {\n\t\t\tr.c.fasthttp.Response.Header.SetContentType(mimeType + \"; charset=utf-8\")\n\t\t} else {\n\t\t\tr.c.fasthttp.Response.Header.SetContentType(mimeType)\n\t\t}\n\t}\n\treturn r.c\n}\n\n// shouldIncludeCharset determines if a MIME type should include UTF-8 charset by default\nfunc shouldIncludeCharset(mimeType string) bool {\n\t// Everything under text/ gets UTF-8 by default.\n\tif strings.HasPrefix(mimeType, \"text/\") {\n\t\treturn true\n\t}\n\n\t// Explicit application types that should default to UTF-8.\n\tswitch mimeType {\n\tcase MIMEApplicationJSON,\n\t\tMIMEApplicationJavaScript,\n\t\tMIMEApplicationXML:\n\t\treturn true\n\t}\n\n\t// Any application/*+json or application/*+xml.\n\tif strings.HasSuffix(mimeType, \"+json\") || strings.HasSuffix(mimeType, \"+xml\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// Vary adds the given header field to the Vary response header.\n// This will append the header, if not already listed; otherwise, leaves it listed in the current location.\nfunc (r *DefaultRes) Vary(fields ...string) {\n\tr.Append(HeaderVary, fields...)\n}\n\n// Write appends p into response body.\nfunc (r *DefaultRes) Write(p []byte) (int, error) {\n\tr.c.fasthttp.Response.AppendBody(p)\n\treturn len(p), nil\n}\n\n// Writef appends f & a into response body writer.\nfunc (r *DefaultRes) Writef(f string, a ...any) (int, error) {\n\t//nolint:wrapcheck // This must not be wrapped\n\treturn fmt.Fprintf(r.c.fasthttp.Response.BodyWriter(), f, a...)\n}\n\n// WriteString appends s to response body.\nfunc (r *DefaultRes) WriteString(s string) (int, error) {\n\tr.c.fasthttp.Response.AppendBodyString(s)\n\treturn len(s), nil\n}\n\n// Release is a method to reset Res fields when to use ReleaseCtx()\nfunc (r *DefaultRes) release() {\n\tr.c = nil\n}\n\n// Drop closes the underlying connection without sending any response headers or body.\n// This can be useful for silently terminating client connections, such as in DDoS mitigation\n// or when blocking access to sensitive endpoints.\nfunc (r *DefaultRes) Drop() error {\n\t//nolint:wrapcheck // error wrapping is avoided to keep the operation lightweight and focused on connection closure.\n\treturn r.c.fasthttp.Conn().Close()\n}\n\n// End immediately flushes the current response and closes the underlying connection.\n//\n// Note: End does not work when using streaming (e.g. fasthttp's HijackConn or SendStream),\n// because in streaming mode the connection is managed asynchronously and ctx.Conn() may return nil.\nfunc (r *DefaultRes) End() error {\n\tctx := r.c.fasthttp\n\tif ctx == nil {\n\t\treturn nil\n\t}\n\n\tconn := ctx.Conn()\n\tif conn == nil {\n\t\treturn nil\n\t}\n\n\tbw := bufio.NewWriter(conn)\n\tif err := ctx.Response.Write(bw); err != nil {\n\t\treturn err\n\t}\n\n\tif err := bw.Flush(); err != nil {\n\t\treturn err //nolint:wrapcheck // unnecessary to wrap it\n\t}\n\n\treturn conn.Close() //nolint:wrapcheck // unnecessary to wrap it\n}\n"
  },
  {
    "path": "res_interface_gen.go",
    "content": "// Code generated by ifacemaker; DO NOT EDIT.\n\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Res is an interface for response-related Ctx methods.\ntype Res interface {\n\t// App returns the *App reference to the instance of the Fiber application\n\tApp() *App\n\t// Append the specified value to the HTTP response header field.\n\t// If the header is not already set, it creates the header with the specified value.\n\tAppend(field string, values ...string)\n\t// Attachment sets the HTTP response Content-Disposition header field to attachment.\n\tAttachment(filename ...string)\n\t// ClearCookie expires a specific cookie by key on the client side.\n\t// If no key is provided it expires all cookies that came with the request.\n\tClearCookie(key ...string)\n\t// RequestCtx returns *fasthttp.RequestCtx that carries a deadline\n\t// a cancellation signal, and other values across API boundaries.\n\tRequestCtx() *fasthttp.RequestCtx\n\t// Cookie sets a cookie by passing a cookie struct.\n\tCookie(cookie *Cookie)\n\t// Download transfers the file from path as an attachment.\n\t// Typically, browsers will prompt the user for download.\n\t// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).\n\t// Override this default with the filename parameter.\n\tDownload(file string, filename ...string) error\n\t// Response return the *fasthttp.Response object\n\t// This allows you to use all fasthttp response methods\n\t// https://godoc.org/github.com/valyala/fasthttp#Response\n\tResponse() *fasthttp.Response\n\t// Format performs content-negotiation on the Accept HTTP header.\n\t// It uses Accepts to select a proper format and calls the matching\n\t// user-provided handler function.\n\t// If no accepted format is found, and a format with MediaType \"default\" is given,\n\t// that default handler is called. If no format is found and no default is given,\n\t// StatusNotAcceptable is sent.\n\tFormat(handlers ...ResFmt) error\n\t// AutoFormat performs content-negotiation on the Accept HTTP header.\n\t// It uses Accepts to select a proper format.\n\t// The supported content types are text/html, text/plain, application/json, application/xml, application/vnd.msgpack, and application/cbor.\n\t// For more flexible content negotiation, use Format.\n\t// If the header is not specified or there is no proper format, text/plain is used.\n\tAutoFormat(body any) error\n\t// Get (a.k.a. GetRespHeader) returns the HTTP response header specified by field.\n\t// Field names are case-insensitive\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGet(key string, defaultValue ...string) string\n\t// GetHeaders (a.k.a GetRespHeaders) returns the HTTP response headers.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting instead.\n\tGetHeaders() map[string][]string\n\t// JSON converts any interface or string to JSON.\n\t// Array and slice values encode as JSON arrays,\n\t// except that []byte encodes as a base64-encoded string,\n\t// and a nil slice encodes as the null JSON value.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/json; charset=utf-8.\n\tJSON(data any, ctype ...string) error\n\t// MsgPack converts any interface or string to MessagePack encoded bytes.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/vnd.msgpack.\n\tMsgPack(data any, ctype ...string) error\n\t// CBOR converts any interface or string to CBOR encoded bytes.\n\t// If the ctype parameter is given, this method will set the\n\t// Content-Type header equal to ctype. If ctype is not given,\n\t// The Content-Type header will be set to application/cbor.\n\tCBOR(data any, ctype ...string) error\n\t// JSONP sends a JSON response with JSONP support.\n\t// This method is identical to JSON, except that it opts-in to JSONP callback support.\n\t// By default, the callback name is simply callback.\n\tJSONP(data any, callback ...string) error\n\t// XML converts any interface or string to XML.\n\t// This method also sets the content header to application/xml; charset=utf-8.\n\tXML(data any) error\n\t// Links joins the links followed by the property to populate the response's Link HTTP header field.\n\tLinks(link ...string)\n\t// Location sets the response Location HTTP header to the specified path parameter.\n\tLocation(path string)\n\t// OriginalURL contains the original request URL.\n\t// Returned value is only valid within the handler. Do not store any references.\n\t// Make copies or use the Immutable setting to use the value outside the Handler.\n\tOriginalURL() string\n\t// Redirect returns the Redirect reference.\n\t// Use Redirect().Status() to set custom redirection status code.\n\t// If status is not specified, status defaults to 303 See Other.\n\t// You can use Redirect().To(), Redirect().Route() and Redirect().Back() for redirection.\n\tRedirect() *Redirect\n\t// ViewBind Add vars to default view var map binding to template engine.\n\t// Variables are read by the Render method and may be overwritten.\n\tViewBind(vars Map) error\n\t// getLocationFromRoute get URL location from route using parameters\n\tgetLocationFromRoute(route *Route, params Map) (string, error)\n\t// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: \"/user/1831\"\n\tGetRouteURL(routeName string, params Map) (string, error)\n\t// Render a template with data and sends a text/html response.\n\t// We support the following engines: https://github.com/gofiber/template\n\tRender(name string, bind any, layouts ...string) error\n\trenderExtensions(bind any)\n\t// Send sets the HTTP response body without copying it.\n\t// From this point onward the body argument must not be changed.\n\tSend(body []byte) error\n\t// SendEarlyHints allows the server to hint to the browser what resources a page would need\n\t// so the browser can preload them while waiting for the server's full response. Only Link\n\t// headers already written to the response will be transmitted as Early Hints.\n\t//\n\t// This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.\n\t//\n\t// NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.\n\t//\n\t// See: https://developer.chrome.com/docs/web-platform/early-hints and\n\t// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax\n\tSendEarlyHints(hints []string) error\n\t// SendFile transfers the file from the specified path.\n\t// By default, the file is not compressed. To enable compression, set SendFile.Compress to true.\n\t// The Content-Type response HTTP header field is set based on the file's extension.\n\t// If the file extension is missing or invalid, the Content-Type is detected from the file's format.\n\tSendFile(file string, config ...SendFile) error\n\t// SendStatus sets the HTTP status code and if the response body is empty,\n\t// it sets the correct status message in the body.\n\tSendStatus(status int) error\n\t// SendString sets the HTTP response body for string types.\n\t// This means no type assertion, recommended for faster performance\n\tSendString(body string) error\n\t// SendStream sets response body stream and optional body size.\n\tSendStream(stream io.Reader, size ...int) error\n\t// SendStreamWriter sets response body stream writer\n\tSendStreamWriter(streamWriter func(*bufio.Writer)) error\n\t// Set sets the response's HTTP header field to the specified key, value.\n\tSet(key, val string)\n\tsetCanonical(key, val string)\n\t// Status sets the HTTP status for the response.\n\t// This method is chainable.\n\tStatus(status int) Ctx\n\t// Type sets the Content-Type HTTP header to the MIME type specified by the file extension.\n\tType(extension string, charset ...string) Ctx\n\t// Vary adds the given header field to the Vary response header.\n\t// This will append the header, if not already listed; otherwise, leaves it listed in the current location.\n\tVary(fields ...string)\n\t// Write appends p into response body.\n\tWrite(p []byte) (int, error)\n\t// Writef appends f & a into response body writer.\n\tWritef(f string, a ...any) (int, error)\n\t// WriteString appends s to response body.\n\tWriteString(s string) (int, error)\n\t// Release is a method to reset Res fields when to use ReleaseCtx()\n\trelease()\n\t// Drop closes the underlying connection without sending any response headers or body.\n\t// This can be useful for silently terminating client connections, such as in DDoS mitigation\n\t// or when blocking access to sensitive endpoints.\n\tDrop() error\n\t// End immediately flushes the current response and closes the underlying connection.\n\tEnd() error\n}\n"
  },
  {
    "path": "router.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 🤖 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"sync/atomic\"\n\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Router defines all router handle interface, including app and group router.\ntype Router interface {\n\tUse(args ...any) Router\n\n\tGet(path string, handler any, handlers ...any) Router\n\tHead(path string, handler any, handlers ...any) Router\n\tPost(path string, handler any, handlers ...any) Router\n\tPut(path string, handler any, handlers ...any) Router\n\tDelete(path string, handler any, handlers ...any) Router\n\tConnect(path string, handler any, handlers ...any) Router\n\tOptions(path string, handler any, handlers ...any) Router\n\tTrace(path string, handler any, handlers ...any) Router\n\tPatch(path string, handler any, handlers ...any) Router\n\n\tAdd(methods []string, path string, handler any, handlers ...any) Router\n\tAll(path string, handler any, handlers ...any) Router\n\n\tGroup(prefix string, handlers ...any) Router\n\n\tRouteChain(path string) Register\n\tRoute(prefix string, fn func(router Router), name ...string) Router\n\n\tName(name string) Router\n}\n\n// Route is a struct that holds all metadata for each registered handler.\ntype Route struct {\n\t// ### important: always keep in sync with the copy method \"app.copyRoute\" and all creations of Route struct ###\n\tgroup *Group // Group instance. used for routes in groups\n\n\tpath string // Prettified path\n\n\t// Public fields\n\tMethod string `json:\"method\"` // HTTP method\n\tName   string `json:\"name\"`   // Route's name\n\t//nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine\n\tPath        string      `json:\"path\"`   // Original registered route path\n\tParams      []string    `json:\"params\"` // Case-sensitive param keys\n\tHandlers    []Handler   `json:\"-\"`      // Ctx handlers\n\trouteParser routeParser // Parameter parser\n\n\t// Data for routing\n\tuse      bool // USE matches path prefixes\n\tmount    bool // Indicated a mounted app on a specific route\n\tstar     bool // Path equals '*'\n\troot     bool // Path equals '/'\n\tautoHead bool // Automatically generated HEAD route\n}\n\nfunc (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {\n\t// root detectionPath check\n\tif r.root && len(detectionPath) == 1 && detectionPath[0] == '/' {\n\t\treturn true\n\t}\n\n\t// '*' wildcard matches any detectionPath\n\tif r.star {\n\t\tif len(path) > 1 {\n\t\t\tparams[0] = path[1:]\n\t\t} else {\n\t\t\tparams[0] = \"\"\n\t\t}\n\t\treturn true\n\t}\n\n\t// Does this route have parameters?\n\tif len(r.Params) > 0 {\n\t\t// Match params using precomputed routeParser\n\t\tif r.routeParser.getMatch(detectionPath, path, params, r.use) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Middleware route?\n\tif r.use {\n\t\t// Single slash or prefix match\n\t\tplen := len(r.path)\n\t\tif r.root {\n\t\t\t// If r.root is '/', it matches everything starting at '/'\n\t\t\tif detectionPath != \"\" && detectionPath[0] == '/' {\n\t\t\t\treturn true\n\t\t\t}\n\t\t} else if len(detectionPath) >= plen && detectionPath[:plen] == r.path {\n\t\t\tif hasPartialMatchBoundary(detectionPath, plen) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t} else if len(r.path) == len(detectionPath) && detectionPath == r.path {\n\t\t// Check exact match\n\t\treturn true\n\t}\n\n\t// No match\n\treturn false\n}\n\nfunc (app *App) next(c *DefaultCtx) (bool, error) {\n\tmethodInt := c.methodInt\n\ttreeHash := c.treePathHash\n\t// Get stack length\n\ttree, ok := app.treeStack[methodInt][treeHash]\n\tif !ok {\n\t\ttree = app.treeStack[methodInt][0]\n\t}\n\tlenr := len(tree) - 1\n\n\tindexRoute := c.indexRoute\n\n\t// Loop over the route stack starting from previous index\n\tfor indexRoute < lenr {\n\t\t// Increment route index\n\t\tindexRoute++\n\n\t\t// Get *Route\n\t\troute := tree[indexRoute]\n\n\t\tif route.mount {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if it matches the request path\n\t\tif !route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif c.skipNonUseRoutes && !route.use {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Pass route reference and param values\n\t\tc.route = route\n\t\t// Non use handler matched\n\t\tif !route.use {\n\t\t\tc.matched = true\n\t\t}\n\t\t// Execute first handler of route\n\t\tif len(route.Handlers) > 0 {\n\t\t\tc.indexHandler = 0\n\t\t\tc.indexRoute = indexRoute\n\t\t\treturn true, route.Handlers[0](c)\n\t\t}\n\n\t\treturn true, nil // Stop scanning the stack\n\t}\n\n\t// If c.Next() does not match, return 404\n\t// If no match, scan stack again if other methods match the request\n\t// Moved from app.handler because middleware may break the route chain\n\tif c.matched {\n\t\treturn false, ErrNotFound\n\t}\n\n\texists := false\n\tmethods := app.config.RequestMethods\n\tfor i := range methods {\n\t\t// Skip original method\n\t\tif methodInt == i {\n\t\t\tcontinue\n\t\t}\n\t\t// Reset stack index\n\t\tindexRoute := -1\n\n\t\ttree, ok := app.treeStack[i][treeHash]\n\t\tif !ok {\n\t\t\ttree = app.treeStack[i][0]\n\t\t}\n\t\t// Get stack length\n\t\tlenr := len(tree) - 1\n\t\t// Loop over the route stack starting from previous index\n\t\tfor indexRoute < lenr {\n\t\t\t// Increment route index\n\t\t\tindexRoute++\n\t\t\t// Get *Route\n\t\t\troute := tree[indexRoute]\n\t\t\t// Skip use routes\n\t\t\tif route.use {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Check if it matches the request path\n\t\t\t// No match, next route\n\t\t\tif route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) {\n\t\t\t\t// We matched\n\t\t\t\texists = true\n\t\t\t\t// Add method to Allow header\n\t\t\t\tc.Append(HeaderAllow, methods[i])\n\t\t\t\t// Break stack loop\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tc.indexRoute = indexRoute\n\t}\n\tif exists {\n\t\treturn false, ErrMethodNotAllowed\n\t}\n\treturn false, ErrNotFound\n}\n\nfunc (app *App) nextCustom(c CustomCtx) (bool, error) {\n\tmethodInt := c.getMethodInt()\n\ttreeHash := c.getTreePathHash()\n\t// Get stack length\n\ttree, ok := app.treeStack[methodInt][treeHash]\n\tif !ok {\n\t\ttree = app.treeStack[methodInt][0]\n\t}\n\tlenr := len(tree) - 1\n\n\tindexRoute := c.getIndexRoute()\n\n\t// Loop over the route stack starting from previous index\n\tfor indexRoute < lenr {\n\t\t// Increment route index\n\t\tindexRoute++\n\n\t\t// Get *Route\n\t\troute := tree[indexRoute]\n\n\t\tif route.mount {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if it matches the request path\n\t\tif !route.match(c.getDetectionPath(), c.Path(), c.getValues()) {\n\t\t\tcontinue\n\t\t}\n\t\tif c.getSkipNonUseRoutes() && !route.use {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Pass route reference and param values\n\t\tc.setRoute(route)\n\t\t// Non use handler matched\n\t\tif !route.use {\n\t\t\tc.setMatched(true)\n\t\t}\n\t\t// Execute first handler of route\n\t\tif len(route.Handlers) > 0 {\n\t\t\tc.setIndexHandler(0)\n\t\t\tc.setIndexRoute(indexRoute)\n\t\t\treturn true, route.Handlers[0](c)\n\t\t}\n\t\treturn true, nil // Stop scanning the stack\n\t}\n\n\t// If c.Next() does not match, return 404\n\t// If no match, scan stack again if other methods match the request\n\t// Moved from app.handler because middleware may break the route chain\n\tif c.getMatched() {\n\t\treturn false, ErrNotFound\n\t}\n\n\texists := false\n\tmethods := app.config.RequestMethods\n\tfor i := range methods {\n\t\t// Skip original method\n\t\tif methodInt == i {\n\t\t\tcontinue\n\t\t}\n\t\t// Reset stack index\n\t\tindexRoute := -1\n\n\t\ttree, ok := app.treeStack[i][treeHash]\n\t\tif !ok {\n\t\t\ttree = app.treeStack[i][0]\n\t\t}\n\t\t// Get stack length\n\t\tlenr := len(tree) - 1\n\t\t// Loop over the route stack starting from previous index\n\t\tfor indexRoute < lenr {\n\t\t\t// Increment route index\n\t\t\tindexRoute++\n\t\t\t// Get *Route\n\t\t\troute := tree[indexRoute]\n\t\t\t// Skip use routes\n\t\t\tif route.use {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Check if it matches the request path\n\t\t\t// No match, next route\n\t\t\tif route.match(c.getDetectionPath(), c.Path(), c.getValues()) {\n\t\t\t\t// We matched\n\t\t\t\texists = true\n\t\t\t\t// Add method to Allow header\n\t\t\t\tc.Append(HeaderAllow, methods[i])\n\t\t\t\t// Break stack loop\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tc.setIndexRoute(indexRoute)\n\t}\n\tif exists {\n\t\treturn false, ErrMethodNotAllowed\n\t}\n\treturn false, ErrNotFound\n}\n\nfunc (app *App) requestHandler(rctx *fasthttp.RequestCtx) {\n\t// Acquire context from the pool\n\tctx := app.AcquireCtx(rctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\tvar err error\n\t// Attempt to match a route and execute the chain\n\tif d, isDefault := ctx.(*DefaultCtx); isDefault {\n\t\t// Check if the HTTP method is valid\n\t\tif d.methodInt == -1 {\n\t\t\t_ = d.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil\n\t\t\treturn\n\t\t}\n\n\t\t// Optional: check flash messages (hot path, see hasFlashCookie).\n\t\tif hasFlashCookie(&d.Request().Header) {\n\t\t\td.Redirect().parseAndClearFlashMessages()\n\t\t}\n\t\t_, err = app.next(d)\n\t} else {\n\t\t// Check if the HTTP method is valid\n\t\tif ctx.getMethodInt() == -1 {\n\t\t\t_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil\n\t\t\treturn\n\t\t}\n\n\t\t// Optional: check flash messages (hot path, see hasFlashCookie).\n\t\tif hasFlashCookie(&ctx.Request().Header) {\n\t\t\tctx.Redirect().parseAndClearFlashMessages()\n\t\t}\n\t\t_, err = app.nextCustom(ctx)\n\t}\n\tif err != nil {\n\t\tif catch := ctx.App().ErrorHandler(ctx, err); catch != nil {\n\t\t\t_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc (app *App) addPrefixToRoute(prefix string, route *Route) *Route {\n\tprefixedPath := getGroupPath(prefix, route.Path)\n\tprettyPath := prefixedPath\n\t// Case-sensitive routing, all to lowercase\n\tif !app.config.CaseSensitive {\n\t\tprettyPath = utilsstrings.ToLower(prettyPath)\n\t}\n\t// Strict routing, remove trailing slashes\n\tif !app.config.StrictRouting && len(prettyPath) > 1 {\n\t\tprettyPath = utils.TrimRight(prettyPath, '/')\n\t}\n\n\troute.Path = prefixedPath\n\troute.path = RemoveEscapeChar(prettyPath)\n\troute.routeParser = parseRoute(prettyPath, app.customConstraints...)\n\troute.root = false\n\troute.star = false\n\n\treturn route\n}\n\nfunc (*App) copyRoute(route *Route) *Route {\n\treturn &Route{\n\t\t// Router booleans\n\t\tuse:      route.use,\n\t\tmount:    route.mount,\n\t\tstar:     route.star,\n\t\troot:     route.root,\n\t\tautoHead: route.autoHead,\n\n\t\t// Path data\n\t\tpath:        route.path,\n\t\trouteParser: route.routeParser,\n\n\t\t// Public data\n\t\tPath:     route.Path,\n\t\tParams:   route.Params,\n\t\tName:     route.Name,\n\t\tMethod:   route.Method,\n\t\tHandlers: route.Handlers,\n\t}\n}\n\nfunc (app *App) normalizePath(path string) string {\n\tif path == \"\" {\n\t\tpath = \"/\"\n\t}\n\tif path[0] != '/' {\n\t\tpath = \"/\" + path\n\t}\n\tif !app.config.CaseSensitive {\n\t\tpath = utilsstrings.ToLower(path)\n\t}\n\tif !app.config.StrictRouting && len(path) > 1 {\n\t\tpath = utils.TrimRight(path, '/')\n\t}\n\treturn RemoveEscapeChar(path)\n}\n\n// RemoveRoute is used to remove a route from the stack by path.\n// If no methods are specified, it will remove the route for all methods defined in the app.\n// You should call RebuildTree after using this to ensure consistency of the tree.\nfunc (app *App) RemoveRoute(path string, methods ...string) {\n\t// Normalize same as register uses\n\tnorm := app.normalizePath(path)\n\n\tpathMatchFunc := func(r *Route) bool {\n\t\treturn r.path == norm // compare private normalized path\n\t}\n\tapp.deleteRoute(methods, pathMatchFunc)\n}\n\n// RemoveRouteByName is used to remove a route from the stack by name.\n// If no methods are specified, it will remove the route for all methods defined in the app.\n// You should call RebuildTree after using this to ensure consistency of the tree.\nfunc (app *App) RemoveRouteByName(name string, methods ...string) {\n\tmatchFunc := func(r *Route) bool { return r.Name == name }\n\tapp.deleteRoute(methods, matchFunc)\n}\n\n// RemoveRouteFunc is used to remove a route from the stack by a custom match function.\n// If no methods are specified, it will remove the route for all methods defined in the app.\n// You should call RebuildTree after using this to ensure consistency of the tree.\n// Note: The route.Path is original path, not the normalized path.\nfunc (app *App) RemoveRouteFunc(matchFunc func(r *Route) bool, methods ...string) {\n\tapp.deleteRoute(methods, matchFunc)\n}\n\nfunc (app *App) deleteRoute(methods []string, matchFunc func(r *Route) bool) {\n\tif len(methods) == 0 {\n\t\tmethods = app.config.RequestMethods\n\t}\n\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tremovedUseRoutes := make(map[string]struct{})\n\n\tfor _, method := range methods {\n\t\t// Uppercase HTTP methods\n\t\tmethod = utilsstrings.ToUpper(method)\n\n\t\t// Get unique HTTP method identifier\n\t\tm := app.methodInt(method)\n\t\tif m == -1 {\n\t\t\tcontinue // Skip invalid HTTP methods\n\t\t}\n\n\t\tfor i := len(app.stack[m]) - 1; i >= 0; i-- {\n\t\t\troute := app.stack[m][i]\n\t\t\tif !matchFunc(route) {\n\t\t\t\tcontinue // Skip if route does not match\n\t\t\t}\n\n\t\t\tapp.stack[m] = append(app.stack[m][:i], app.stack[m][i+1:]...)\n\t\t\tapp.routesRefreshed = true\n\n\t\t\t// Decrement global handler count. In middleware routes, only decrement once\n\t\t\tif _, ok := removedUseRoutes[route.path]; (route.use && slices.Equal(methods, app.config.RequestMethods) && !ok) || !route.use {\n\t\t\t\tif route.use {\n\t\t\t\t\tremovedUseRoutes[route.path] = struct{}{}\n\t\t\t\t}\n\n\t\t\t\tatomic.AddUint32(&app.handlersCount, ^uint32(len(route.Handlers)-1)) //nolint:gosec // G115 - handler count is always small\n\t\t\t}\n\n\t\t\tif method == MethodGet && !route.use && !route.mount {\n\t\t\t\tapp.pruneAutoHeadRouteLocked(route.path)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// pruneAutoHeadRouteLocked removes an automatically generated HEAD route so a\n// later explicit registration can take its place without duplicating handler\n// chains. The caller must already hold app.mutex.\nfunc (app *App) pruneAutoHeadRouteLocked(path string) {\n\theadIndex := app.methodInt(MethodHead)\n\tif headIndex == -1 {\n\t\treturn\n\t}\n\n\tnorm := app.normalizePath(path)\n\n\theadStack := app.stack[headIndex]\n\tfor i := len(headStack) - 1; i >= 0; i-- {\n\t\theadRoute := headStack[i]\n\t\tif headRoute.path != norm || headRoute.mount || headRoute.use || !headRoute.autoHead {\n\t\t\tcontinue\n\t\t}\n\n\t\tapp.stack[headIndex] = append(headStack[:i], headStack[i+1:]...)\n\t\tapp.routesRefreshed = true\n\t\tatomic.AddUint32(&app.handlersCount, ^uint32(len(headRoute.Handlers)-1)) //nolint:gosec // G115 - handler count is always small\n\t\treturn\n\t}\n}\n\nfunc (app *App) register(methods []string, pathRaw string, group *Group, handlers ...Handler) {\n\t// A regular route requires at least one ctx handler\n\tif len(handlers) == 0 && group == nil {\n\t\tpanic(fmt.Sprintf(\"missing handler/middleware in route: %s\\n\", pathRaw))\n\t}\n\t// No nil handlers allowed\n\tfor _, h := range handlers {\n\t\tif h == nil {\n\t\t\tpanic(fmt.Sprintf(\"nil handler in route: %s\\n\", pathRaw))\n\t\t}\n\t}\n\n\t// Precompute path normalization ONCE\n\tif pathRaw == \"\" {\n\t\tpathRaw = \"/\"\n\t}\n\tif pathRaw[0] != '/' {\n\t\tpathRaw = \"/\" + pathRaw\n\t}\n\tpathPretty := pathRaw\n\tif !app.config.CaseSensitive {\n\t\tpathPretty = utilsstrings.ToLower(pathPretty)\n\t}\n\tif !app.config.StrictRouting && len(pathPretty) > 1 {\n\t\tpathPretty = utils.TrimRight(pathPretty, '/')\n\t}\n\tpathClean := RemoveEscapeChar(pathPretty)\n\n\tparsedRaw := parseRoute(pathRaw, app.customConstraints...)\n\tparsedPretty := parseRoute(pathPretty, app.customConstraints...)\n\n\tisMount := group != nil && group.app != app\n\n\tfor _, method := range methods {\n\t\tmethod = utilsstrings.ToUpper(method)\n\t\tif method != methodUse && app.methodInt(method) == -1 {\n\t\t\tpanic(fmt.Sprintf(\"add: invalid http method %s\\n\", method))\n\t\t}\n\n\t\tisUse := method == methodUse\n\t\tisStar := pathClean == \"/*\"\n\t\tisRoot := pathClean == \"/\"\n\n\t\troute := Route{\n\t\t\tuse:   isUse,\n\t\t\tmount: isMount,\n\t\t\tstar:  isStar,\n\t\t\troot:  isRoot,\n\n\t\t\tpath:        pathClean,\n\t\t\trouteParser: parsedPretty,\n\t\t\tParams:      parsedRaw.params,\n\t\t\tgroup:       group,\n\n\t\t\tPath:     pathRaw,\n\t\t\tMethod:   method,\n\t\t\tHandlers: handlers,\n\t\t}\n\n\t\t// Increment global handler count\n\t\tatomic.AddUint32(&app.handlersCount, uint32(len(handlers))) //nolint:gosec // G115 - handler count is always small\n\n\t\t// Middleware route matches all HTTP methods\n\t\tif isUse {\n\t\t\t// Add route to all HTTP methods stack\n\t\t\tfor _, m := range app.config.RequestMethods {\n\t\t\t\t// Create a route copy to avoid duplicates during compression\n\t\t\t\tr := route\n\t\t\t\tapp.addRoute(m, &r)\n\t\t\t}\n\t\t} else {\n\t\t\t// Add route to stack\n\t\t\tapp.addRoute(method, &route)\n\t\t}\n\t}\n}\n\nfunc (app *App) addRoute(method string, route *Route) {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\t// Get unique HTTP method identifier\n\tm := app.methodInt(method)\n\n\tif method == MethodHead && !route.mount && !route.use {\n\t\tapp.pruneAutoHeadRouteLocked(route.path)\n\t}\n\n\t// prevent identically route registration\n\tl := len(app.stack[m])\n\tif l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount {\n\t\tpreRoute := app.stack[m][l-1]\n\t\tpreRoute.Handlers = append(preRoute.Handlers, route.Handlers...)\n\t} else {\n\t\troute.Method = method\n\t\t// Add route to the stack\n\t\tapp.stack[m] = append(app.stack[m], route)\n\t\tapp.routesRefreshed = true\n\t}\n\n\t// Execute onRoute hooks & change latestRoute if not adding mounted route\n\tif !route.mount {\n\t\tapp.latestRoute = route\n\t\tif err := app.hooks.executeOnRouteHooks(route); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (app *App) ensureAutoHeadRoutes() {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\tapp.ensureAutoHeadRoutesLocked()\n}\n\nfunc (app *App) ensureAutoHeadRoutesLocked() {\n\tif app.config.DisableHeadAutoRegister {\n\t\treturn\n\t}\n\n\theadIndex := app.methodInt(MethodHead)\n\tgetIndex := app.methodInt(MethodGet)\n\tif headIndex == -1 || getIndex == -1 {\n\t\treturn\n\t}\n\n\theadStack := app.stack[headIndex]\n\texisting := make(map[string]struct{}, len(headStack))\n\tfor _, route := range headStack {\n\t\tif route.mount || route.use {\n\t\t\tcontinue\n\t\t}\n\t\texisting[route.path] = struct{}{}\n\t}\n\n\tif len(app.stack[getIndex]) == 0 {\n\t\treturn\n\t}\n\n\tvar added bool\n\n\tfor _, route := range app.stack[getIndex] {\n\t\tif route.mount || route.use {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := existing[route.path]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\theadRoute := app.copyRoute(route)\n\t\theadRoute.group = route.group\n\t\theadRoute.Method = MethodHead\n\t\theadRoute.autoHead = true\n\t\t// Fasthttp automatically omits response bodies when transmitting\n\t\t// HEAD responses, so the copied GET handler stack can execute\n\t\t// unchanged while still producing an empty body on the wire.\n\n\t\theadStack = append(headStack, headRoute)\n\t\texisting[route.path] = struct{}{}\n\t\tapp.routesRefreshed = true\n\t\tadded = true\n\n\t\tatomic.AddUint32(&app.handlersCount, uint32(len(headRoute.Handlers))) //nolint:gosec // G115 - handler count is always small\n\n\t\tapp.latestRoute = headRoute\n\t\tif err := app.hooks.executeOnRouteHooks(headRoute); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif added {\n\t\tapp.stack[headIndex] = headStack\n\t}\n}\n\n// RebuildTree rebuilds the prefix tree from the previously registered routes.\n// This method is useful when you want to register routes dynamically after the app has started.\n// It is not recommended to use this method on production environments because rebuilding\n// the tree is performance-intensive and not thread-safe in runtime. Since building the tree\n// is only done in the startupProcess of the app, this method does not make sure that the\n// routeTree is being safely changed, as it would add a great deal of overhead in the request.\n// Latest benchmark results showed a degradation from 82.79 ns/op to 94.48 ns/op and can be found in:\n// https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283\nfunc (app *App) RebuildTree() *App {\n\tapp.mutex.Lock()\n\tdefer app.mutex.Unlock()\n\n\treturn app.buildTree()\n}\n\n// buildTree build the prefix tree from the previously registered routes\nfunc (app *App) buildTree() *App {\n\t// If routes haven't been refreshed, nothing to do\n\tif !app.routesRefreshed {\n\t\treturn app\n\t}\n\n\t// 1) First loop: determine all possible 3-char prefixes (\"treePaths\") for each method\n\tfor method := range app.config.RequestMethods {\n\t\troutes := app.stack[method]\n\t\ttreePaths := make([]int, len(routes))\n\n\t\tglobalCount := 0\n\t\tprefixCounts := make(map[int]int, len(routes))\n\n\t\tfor i, route := range routes {\n\t\t\tif len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths {\n\t\t\t\ttreePaths[i] = int(route.routeParser.segs[0].Const[0])<<16 |\n\t\t\t\t\tint(route.routeParser.segs[0].Const[1])<<8 |\n\t\t\t\t\tint(route.routeParser.segs[0].Const[2])\n\t\t\t}\n\n\t\t\tif treePaths[i] == 0 {\n\t\t\t\tglobalCount++\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprefixCounts[treePaths[i]]++\n\t\t}\n\n\t\tprevBuckets := app.treeStack[method]\n\t\ttsMap := make(map[int][]*Route, len(prefixCounts)+1)\n\t\ttsMap[0] = reuseRouteBucket(prevBuckets, 0, globalCount)\n\t\tfor treePath, count := range prefixCounts {\n\t\t\ttsMap[treePath] = reuseRouteBucket(prevBuckets, treePath, count+globalCount)\n\t\t}\n\n\t\tfor i, route := range routes {\n\t\t\ttreePath := treePaths[i]\n\n\t\t\tif treePath == 0 {\n\t\t\t\tfor bucket := range tsMap {\n\t\t\t\t\ttsMap[bucket] = append(tsMap[bucket], route)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttsMap[treePath] = append(tsMap[treePath], route)\n\t\t}\n\n\t\tapp.treeStack[method] = tsMap\n\t}\n\n\t// reset the flag and return\n\tapp.routesRefreshed = false\n\treturn app\n}\n\nfunc reuseRouteBucket(prev map[int][]*Route, key, capHint int) []*Route {\n\tif bucket, ok := prev[key]; ok && cap(bucket) >= capHint {\n\t\treturn bucket[:0]\n\t}\n\treturn make([]*Route, 0, capHint)\n}\n"
  },
  {
    "path": "router_test.go",
    "content": "// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️\n// 📃 GitHub Repository: https://github.com/gofiber/fiber\n// 📌 API Documentation: https://docs.gofiber.io\n\npackage fiber\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp\"\n)\n\nvar routesFixture routeJSON\n\nfunc init() {\n\tdat, err := os.ReadFile(\"./.github/testdata/testRoutes.json\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif err := json.Unmarshal(dat, &routesFixture); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Test_Route_Handler_Order(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tvar order []int\n\n\thandler1 := func(c Ctx) error {\n\t\torder = append(order, 1)\n\t\treturn c.Next()\n\t}\n\thandler2 := func(c Ctx) error {\n\t\torder = append(order, 2)\n\t\treturn c.Next()\n\t}\n\thandler3 := func(c Ctx) error {\n\t\torder = append(order, 3)\n\t\treturn c.Next()\n\t}\n\n\tapp.Get(\"/test\", handler1, handler2, handler3, func(c Ctx) error {\n\t\torder = append(order, 4)\n\t\treturn c.SendStatus(200)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\texpectedOrder := []int{1, 2, 3, 4}\n\trequire.Equal(t, expectedOrder, order, \"Handler order\")\n}\n\nfunc Test_hasFlashCookieExactMatch(t *testing.T) {\n\tt.Parallel()\n\n\tbuildRequestWithCookie := func(t *testing.T, cookie string) *fasthttp.Request {\n\t\tt.Helper()\n\n\t\trawRequest := strings.NewReader(\n\t\t\t\"GET / HTTP/1.1\\r\\nHost: localhost\\r\\nCookie: \" + cookie + \"\\r\\n\\r\\n\",\n\t\t)\n\t\treq := new(fasthttp.Request)\n\t\trequire.NoError(t, req.Read(bufio.NewReader(rawRequest)))\n\t\treturn req\n\t}\n\n\treq := buildRequestWithCookie(t, FlashCookieName+\"X=not-the-flash-cookie\")\n\trequire.False(t, hasFlashCookie(&req.Header))\n\n\treq = buildRequestWithCookie(t, FlashCookieName+\"=valid\")\n\trequire.True(t, hasFlashCookie(&req.Header))\n\n\tvar syntheticReq fasthttp.Request\n\tsyntheticReq.Header.Set(HeaderCookie, FlashCookieName+\"=valid\")\n\trequire.False(t, hasFlashCookie(&syntheticReq.Header))\n}\n\nfunc Test_Route_MixedFiberAndHTTPHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tvar order []string\n\n\tfiberBefore := func(c Ctx) error {\n\t\torder = append(order, \"fiber-before\")\n\t\tc.Set(\"X-Fiber\", \"1\")\n\t\treturn c.Next()\n\t}\n\n\thttpHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\torder = append(order, \"http-final\")\n\t\tw.Header().Set(\"X-HTTP\", \"true\")\n\t\t_, err := w.Write([]byte(\"http\"))\n\t\tassert.NoError(t, err)\n\t})\n\n\tfiberAfter := func(c Ctx) error {\n\t\torder = append(order, \"fiber-after\")\n\t\treturn c.SendString(\"fiber\")\n\t}\n\n\tapp.Get(\"/mixed\", fiberBefore, httpHandler, fiberAfter)\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/mixed\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"http\", string(body))\n\trequire.Equal(t, \"true\", resp.Header.Get(\"X-HTTP\"))\n\trequire.Equal(t, \"1\", resp.Header.Get(\"X-Fiber\"))\n\n\trequire.Equal(t, []string{\"fiber-before\", \"http-final\"}, order)\n}\n\nfunc Test_Route_Group_WithHTTPHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tvar order []string\n\n\tapp.Use(\"/api\", func(c Ctx) error {\n\t\torder = append(order, \"app-use\")\n\t\treturn c.Next()\n\t})\n\n\tgrp := app.Group(\"/api\", func(c Ctx) error {\n\t\torder = append(order, \"group-middleware\")\n\t\treturn c.Next()\n\t})\n\n\tgrp.Get(\"/users\", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\torder = append(order, \"http-handler\")\n\t\t_, err := w.Write([]byte(\"users\"))\n\t\tassert.NoError(t, err)\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/api/users\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"users\", string(body))\n\n\trequire.Equal(t, []string{\"app-use\", \"group-middleware\", \"http-handler\"}, order)\n}\n\nfunc Test_RouteChain_WithHTTPHandlers(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tchain := app.RouteChain(\"/combo\")\n\tchain.Get(func(c Ctx) error {\n\t\tc.Set(\"X-Chain\", \"fiber\")\n\t\treturn c.Next()\n\t}, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t_, err := w.Write([]byte(\"combo\"))\n\t\tassert.NoError(t, err)\n\t}))\n\n\tchain.RouteChain(\"/nested\").Get(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"X-Nested\", \"true\")\n\t\t_, err := w.Write([]byte(\"nested\"))\n\t\tassert.NoError(t, err)\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/combo\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, \"fiber\", resp.Header.Get(\"X-Chain\"))\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, resp.Body.Close())\n\t})\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"combo\", string(body))\n\n\tnestedResp, err := app.Test(httptest.NewRequest(MethodGet, \"/combo/nested\", http.NoBody))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 200, nestedResp.StatusCode)\n\trequire.Equal(t, \"true\", nestedResp.Header.Get(\"X-Nested\"))\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, nestedResp.Body.Close())\n\t})\n\n\tnestedBody, err := io.ReadAll(nestedResp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"nested\", string(nestedBody))\n}\n\nfunc Test_Route_Match_SameLength(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/:param\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"param\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/:param\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \":param\", app.toString(body))\n\n\t// with param\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"test\", app.toString(body))\n}\n\nfunc Test_Route_Match_Star(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/*\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"*\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/*\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"*\", app.toString(body))\n\n\t// with param\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"test\", app.toString(body))\n\n\t// without parameter\n\troute := Route{\n\t\tstar:        true,\n\t\tpath:        \"/*\",\n\t\trouteParser: routeParser{},\n\t}\n\tparams := [maxParams]string{}\n\tmatch := route.match(\"\", \"\", &params)\n\trequire.True(t, match)\n\trequire.Equal(t, [maxParams]string{}, params)\n\n\t// with parameter\n\tmatch = route.match(\"/favicon.ico\", \"/favicon.ico\", &params)\n\trequire.True(t, match)\n\trequire.Equal(t, [maxParams]string{\"favicon.ico\"}, params)\n\n\t// without parameter again\n\tmatch = route.match(\"\", \"\", &params)\n\trequire.True(t, match)\n\trequire.Equal(t, [maxParams]string{}, params)\n}\n\nfunc Test_Route_Match_Root(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(\"root\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"root\", app.toString(body))\n}\n\nfunc Test_Route_Match_Parser(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Get(\"/foo/:ParamName\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"ParamName\"))\n\t})\n\tapp.Get(\"/Foobar/*\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"*\"))\n\t})\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/foo/bar\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"bar\", app.toString(body))\n\n\t// with star\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/Foobar/test\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"test\", app.toString(body))\n}\n\nfunc TestAutoRegisterHeadRoutes(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tname string\n\t}{\n\t\t{name: \"auto registers head for get\"},\n\t\t{name: \"disable auto register config\"},\n\t\t{name: \"explicit head overrides auto head route\"},\n\t\t{name: \"auto head for grouped routes\"},\n\t\t{name: \"static handler auto head\"},\n\t\t{name: \"head without matching route returns 404\"},\n\t\t{name: \"late explicit get keeps explicit head\"},\n\t\t{name: \"route listing includes auto head\"},\n\t\t{name: \"head mirrors status without body\"},\n\t}\n\n\trequireClose := func(tb testing.TB, closer io.Closer) {\n\t\ttb.Helper()\n\t\trequire.NoError(tb, closer.Close())\n\t}\n\n\tregisterCleanup := func(tb testing.TB, body io.ReadCloser) {\n\t\ttb.Helper()\n\t\ttb.Cleanup(func() {\n\t\t\trequireClose(tb, body)\n\t\t})\n\t}\n\n\trunners := []func(t *testing.T){\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tapp.Get(\"/\", func(c Ctx) error {\n\t\t\t\tc.Set(\"X-Test\", \"auto\")\n\t\t\t\treturn c.SendString(\"Hello\")\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusOK, respHead.StatusCode)\n\t\t\trequire.Equal(t, int64(len(\"Hello\")), respHead.ContentLength)\n\t\t\trequire.Equal(t, \"auto\", respHead.Header.Get(\"X-Test\"))\n\n\t\t\tbody, err := io.ReadAll(respHead.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, body)\n\n\t\t\trespGet, err := app.Test(httptest.NewRequest(MethodGet, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respGet.Body)\n\t\t\trequire.Equal(t, StatusOK, respGet.StatusCode)\n\t\t\trequire.Equal(t, int64(len(\"Hello\")), respGet.ContentLength)\n\t\t\tdata, err := io.ReadAll(respGet.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"Hello\", string(data))\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New(Config{DisableHeadAutoRegister: true})\n\t\t\tapp.Get(\"/\", func(c Ctx) error {\n\t\t\t\treturn c.SendString(\"Hello\")\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodHead, \"/\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, resp.Body)\n\t\t\trequire.Equal(t, StatusMethodNotAllowed, resp.StatusCode)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tvar getCalls int\n\t\t\tapp.Get(\"/override\", func(c Ctx) error {\n\t\t\t\tgetCalls++\n\t\t\t\treturn c.SendString(\"GET\")\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/override\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, StatusOK, respHead.StatusCode)\n\t\t\trequire.Equal(t, 1, getCalls)\n\t\t\trequireClose(t, respHead.Body)\n\n\t\t\tvar headCalls int\n\t\t\tapp.Head(\"/override\", func(c Ctx) error {\n\t\t\t\theadCalls++\n\t\t\t\tc.Set(\"X-Explicit\", \"true\")\n\t\t\t\treturn c.SendStatus(StatusNoContent)\n\t\t\t})\n\n\t\t\trespHead, err = app.Test(httptest.NewRequest(MethodHead, \"/override\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusNoContent, respHead.StatusCode)\n\t\t\trequire.Equal(t, \"true\", respHead.Header.Get(\"X-Explicit\"))\n\t\t\tbody, err := io.ReadAll(respHead.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, body)\n\t\t\trequire.Equal(t, 1, getCalls)\n\t\t\trequire.Equal(t, 1, headCalls)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tgroup := app.Group(\"/api\")\n\t\t\tgroup.Get(\"/users/:id\", func(c Ctx) error {\n\t\t\t\tc.Set(\"X-User\", c.Params(\"id\"))\n\t\t\t\treturn c.SendString(\"grouped\")\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/api/users/42\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusOK, respHead.StatusCode)\n\t\t\trequire.Equal(t, \"42\", respHead.Header.Get(\"X-User\"))\n\t\t\tbody, err := io.ReadAll(respHead.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, body)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tconst file = \"./.github/testdata/testRoutes.json\"\n\t\t\tcontent, err := os.ReadFile(file)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tapp := New()\n\t\t\tapp.Get(\"/file\", func(c Ctx) error {\n\t\t\t\treturn c.SendFile(file)\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/file\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusOK, respHead.StatusCode)\n\t\t\trequire.Equal(t, int64(len(content)), respHead.ContentLength)\n\t\t\tbody, err := io.ReadAll(respHead.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, body)\n\n\t\t\trespGet, err := app.Test(httptest.NewRequest(MethodGet, \"/file\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respGet.Body)\n\t\t\tdata, err := io.ReadAll(respGet.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, content, data)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodHead, \"/missing\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, resp.Body)\n\t\t\trequire.Equal(t, StatusNotFound, resp.StatusCode)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tvar headCalls int\n\t\t\tapp.Head(\"/late\", func(c Ctx) error {\n\t\t\t\theadCalls++\n\t\t\t\tc.Set(\"X-Late\", \"head\")\n\t\t\t\treturn c.SendStatus(StatusAccepted)\n\t\t\t})\n\n\t\t\tvar getCalls int\n\t\t\tapp.Get(\"/late\", func(c Ctx) error {\n\t\t\t\tgetCalls++\n\t\t\t\treturn c.SendString(\"ok\")\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/late\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusAccepted, respHead.StatusCode)\n\t\t\trequire.Equal(t, \"head\", respHead.Header.Get(\"X-Late\"))\n\t\t\trequire.Equal(t, 1, headCalls)\n\t\t\trequire.Equal(t, 0, getCalls)\n\n\t\t\trespGet, err := app.Test(httptest.NewRequest(MethodGet, \"/late\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respGet.Body)\n\t\t\trequire.Equal(t, StatusOK, respGet.StatusCode)\n\t\t\trequire.Equal(t, 1, getCalls)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tapp.Get(\"/list\", func(c Ctx) error {\n\t\t\t\treturn c.SendString(\"list\")\n\t\t\t})\n\n\t\t\tapp.startupProcess()\n\n\t\t\troutes := app.GetRoutes()\n\t\t\tvar hasGet, hasHead bool\n\t\t\tfor _, route := range routes {\n\t\t\t\tif route.Path == \"/list\" {\n\t\t\t\t\tif route.Method == MethodGet {\n\t\t\t\t\t\thasGet = true\n\t\t\t\t\t}\n\t\t\t\t\tif route.Method == MethodHead {\n\t\t\t\t\t\thasHead = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\trequire.True(t, hasGet)\n\t\t\trequire.True(t, hasHead)\n\t\t},\n\t\tfunc(t *testing.T) {\n\t\t\tt.Helper()\n\t\t\tapp := New()\n\t\t\tapp.Get(\"/nocontent\", func(c Ctx) error {\n\t\t\t\treturn c.SendStatus(StatusNoContent)\n\t\t\t})\n\n\t\t\trespHead, err := app.Test(httptest.NewRequest(MethodHead, \"/nocontent\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respHead.Body)\n\t\t\trequire.Equal(t, StatusNoContent, respHead.StatusCode)\n\t\t\tbody, err := io.ReadAll(respHead.Body)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, body)\n\n\t\t\trespGet, err := app.Test(httptest.NewRequest(MethodGet, \"/nocontent\", http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\tregisterCleanup(t, respGet.Body)\n\t\t\trequire.Equal(t, StatusNoContent, respGet.StatusCode)\n\t\t},\n\t}\n\n\trequire.Len(t, runners, len(cases))\n\n\tfor i, tc := range cases {\n\t\trunner := runners[i]\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trunner(t)\n\t\t})\n\t}\n}\n\nfunc Test_Route_Match_Middleware(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Use(\"/foo/*\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"*\"))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/foo/*\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"*\", app.toString(body))\n\n\t// with param\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/foo/bar/fasel\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"bar/fasel\", app.toString(body))\n}\n\nfunc Test_Route_Match_UnescapedPath(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New(Config{UnescapePath: true})\n\n\tapp.Use(\"/créer\", func(c Ctx) error {\n\t\treturn c.SendString(\"test\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/cr%C3%A9er\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"test\", app.toString(body))\n\t// without special chars\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/créer\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\t// check deactivated behavior\n\tapp.config.UnescapePath = false\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/cr%C3%A9er\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Route_Match_WithEscapeChar(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\t// static route and escaped part\n\tapp.Get(\"/v1/some/resource/name\\\\:customVerb\", func(c Ctx) error {\n\t\treturn c.SendString(\"static\")\n\t})\n\t// group route\n\tgroup := app.Group(\"/v2/\\\\:firstVerb\")\n\tgroup.Get(\"/\\\\:customVerb\", func(c Ctx) error {\n\t\treturn c.SendString(\"group\")\n\t})\n\t// route with resource param and escaped part\n\tapp.Get(\"/v3/:resource/name\\\\:customVerb\", func(c Ctx) error {\n\t\treturn c.SendString(c.Params(\"resource\"))\n\t})\n\n\t// check static route\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/v1/some/resource/name:customVerb\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"static\", app.toString(body))\n\n\t// check group route\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/v2/:firstVerb/:customVerb\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"group\", app.toString(body))\n\n\t// check param route\n\tresp, err = app.Test(httptest.NewRequest(MethodGet, \"/v3/awesome/name:customVerb\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusOK, resp.StatusCode, \"Status code\")\n\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"awesome\", app.toString(body))\n}\n\nfunc Test_Route_Match_Middleware_HasPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Use(\"/foo\", func(c Ctx) error {\n\t\treturn c.SendString(\"middleware\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/foo/bar\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"middleware\", app.toString(body))\n}\n\nfunc Test_Route_Match_Middleware_NoBoundary(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Use(\"/foo\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/foobar\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, StatusNotFound, resp.StatusCode, \"Status code\")\n}\n\nfunc Test_Route_Match_Middleware_Root(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tapp.Use(\"/\", func(c Ctx) error {\n\t\treturn c.SendString(\"middleware\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, \"/everything\", http.NoBody))\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, 200, resp.StatusCode, \"Status code\")\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"middleware\", app.toString(body))\n}\n\nfunc Test_Router_Register_Missing_Handler(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\n\tt.Run(\"No Handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.PanicsWithValue(t, \"missing handler/middleware in route: /doe\\n\", func() {\n\t\t\tapp.register([]string{\"USE\"}, \"/doe\", nil)\n\t\t})\n\t})\n\n\tt.Run(\"Nil Handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trequire.PanicsWithValue(t, \"nil handler in route: /doe\\n\", func() {\n\t\t\tapp.register([]string{\"USE\"}, \"/doe\", nil, nil)\n\t\t})\n\t})\n}\n\nfunc Test_Ensure_Router_Interface_Implementation(t *testing.T) {\n\tt.Parallel()\n\n\tvar app any = (*App)(nil)\n\t_, ok := app.(Router)\n\trequire.True(t, ok)\n\n\tvar group any = (*Group)(nil)\n\t_, ok = group.(Router)\n\trequire.True(t, ok)\n}\n\nfunc Test_Router_Handler_Catch_Error(t *testing.T) {\n\tt.Parallel()\n\n\tapp := New()\n\tapp.config.ErrorHandler = func(_ Ctx, _ error) error {\n\t\treturn errors.New(\"fake error\")\n\t}\n\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn ErrForbidden\n\t})\n\n\tc := &fasthttp.RequestCtx{}\n\n\tapp.Handler()(c)\n\n\trequire.Equal(t, StatusInternalServerError, c.Response.Header.StatusCode())\n}\n\nfunc Test_Router_NotFound(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/this/route/does/not/exist\")\n\n\tappHandler(c)\n\n\trequire.Equal(t, 404, c.Response.StatusCode())\n\trequire.Equal(t, \"Not Found\", string(c.Response.Body()))\n}\n\nfunc Test_Router_NotFound_HTML_Inject(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/does/not/exist<script>alert('foo');</script>\")\n\n\tappHandler(c)\n\n\trequire.Equal(t, 404, c.Response.StatusCode())\n\trequire.Equal(t, \"Not Found\", string(c.Response.Body()))\n}\n\nfunc registerTreeManipulationRoutes(app *App, middleware ...func(Ctx) error) {\n\tconverted := make([]any, len(middleware))\n\tfor i, h := range middleware {\n\t\tconverted[i] = h\n\t}\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tapp.Get(\"/dynamically-defined\", func(c Ctx) error {\n\t\t\treturn c.SendStatus(StatusOK)\n\t\t})\n\n\t\tapp.RebuildTree()\n\n\t\treturn c.SendStatus(StatusOK)\n\t}, converted...)\n}\n\nfunc verifyRequest(tb testing.TB, app *App, path string, expectedStatus int) *http.Response {\n\ttb.Helper()\n\n\tresp, err := app.Test(httptest.NewRequest(MethodGet, path, http.NoBody))\n\trequire.NoError(tb, err, \"app.Test(req)\")\n\trequire.Equal(tb, expectedStatus, resp.StatusCode, \"Status code\")\n\n\treturn resp\n}\n\nfunc verifyRouteHandlerCounts(tb testing.TB, app *App, expectedRoutesCount int) {\n\ttb.Helper()\n\n\t//  this is taken from listen.go's printRoutesMessage app method\n\tvar routes []RouteMessage\n\tfor _, routeStack := range app.stack {\n\t\tfor _, route := range routeStack {\n\t\t\trouteMsg := RouteMessage{\n\t\t\t\tname:   route.Name,\n\t\t\t\tmethod: route.Method,\n\t\t\t\tpath:   route.Path,\n\t\t\t}\n\n\t\t\tfor _, handler := range route.Handlers {\n\t\t\t\trouteMsg.handlers += runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() + \" \"\n\t\t\t}\n\n\t\t\troutes = append(routes, routeMsg)\n\t\t}\n\t}\n\n\tfor _, route := range routes {\n\t\trequire.Equal(tb, expectedRoutesCount, strings.Count(route.handlers, \" \"))\n\t}\n}\n\nfunc verifyThereAreNoRoutes(tb testing.TB, app *App) {\n\ttb.Helper()\n\n\trequire.Equal(tb, uint32(0), app.handlersCount)\n\tverifyRouteHandlerCounts(tb, app, 0)\n}\n\nfunc Test_App_Rebuild_Tree(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tregisterTreeManipulationRoutes(app)\n\n\tverifyRequest(t, app, \"/dynamically-defined\", StatusNotFound)\n\tverifyRequest(t, app, \"/test\", StatusOK)\n\tverifyRequest(t, app, \"/dynamically-defined\", StatusOK)\n}\n\nfunc Test_App_Remove_Route_A_B_Feature_Testing(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/api/feature-a\", func(c Ctx) error {\n\t\tapp.RemoveRoute(\"/api/feature\", MethodGet)\n\t\tapp.RebuildTree()\n\t\t// Redefine route\n\t\tapp.Get(\"/api/feature\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"Testing feature-a\")\n\t\t})\n\n\t\tapp.RebuildTree()\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\tapp.Get(\"/api/feature-b\", func(c Ctx) error {\n\t\tapp.RemoveRoute(\"/api/feature\", MethodGet)\n\t\tapp.RebuildTree()\n\t\t// Redefine route\n\t\tapp.Get(\"/api/feature\", func(c Ctx) error {\n\t\t\treturn c.SendString(\"Testing feature-b\")\n\t\t})\n\n\t\tapp.RebuildTree()\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tverifyRequest(t, app, \"/api/feature-a\", StatusOK)\n\n\tresp := verifyRequest(t, app, \"/api/feature\", StatusOK)\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\n\trequire.Equal(t, \"Testing feature-a\", string(body), \"Response Message\")\n\n\tverifyRequest(t, app, \"/api/feature-b\", StatusOK)\n\n\tresp = verifyRequest(t, app, \"/api/feature\", StatusOK)\n\tbody, err = io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"app.Test(req)\")\n\trequire.Equal(t, \"Testing feature-b\", string(body), \"Response Message\")\n}\n\nfunc Test_App_Remove_Route_By_Name(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/api/test\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t}).Name(\"test\")\n\n\tapp.RemoveRouteByName(\"test\", MethodGet)\n\tapp.RebuildTree()\n\n\tverifyRequest(t, app, \"/test\", StatusNotFound)\n\tverifyThereAreNoRoutes(t, app)\n}\n\nfunc Test_App_Remove_Route_By_Name_Non_Existing_Route(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.RemoveRouteByName(\"test\", MethodGet)\n\tapp.RebuildTree()\n\n\tverifyThereAreNoRoutes(t, app)\n}\n\nfunc Test_App_Remove_Route_Nested(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapi := app.Group(\"/api\")\n\n\tv1 := api.Group(\"/v1\")\n\tv1.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tverifyRequest(t, app, \"/api/v1/test\", StatusOK)\n\tapp.RemoveRoute(\"/api/v1/test\", MethodGet)\n\n\tverifyThereAreNoRoutes(t, app)\n}\n\nfunc Test_App_Remove_Route_Parameterized(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/test/:id\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\tverifyRequest(t, app, \"/test/:id\", StatusOK)\n\tapp.RemoveRoute(\"/test/:id\", MethodGet)\n\n\tverifyThereAreNoRoutes(t, app)\n}\n\nfunc Test_App_Remove_Route(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp.RemoveRoute(\"/test\", MethodGet)\n\tapp.RebuildTree()\n\n\tverifyRequest(t, app, \"/test\", StatusNotFound)\n}\n\nfunc Test_App_Remove_Route_Non_Existing_Route(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tapp.RemoveRoute(\"/test\", MethodGet, MethodHead)\n\tapp.RebuildTree()\n\n\tverifyThereAreNoRoutes(t, app)\n}\n\nfunc Test_App_Use_StrictRoutingBoundary(t *testing.T) {\n\ttype testCase struct {\n\t\tname           string\n\t\tpath           string\n\t\texpectedStatus int\n\t\tstrictRouting  bool\n\t\texpectMatched  bool\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tname:           \"Strict exact match\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict trailing slash partial\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict nested partial\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/users\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict disallows sibling prefix\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/apiv1\",\n\t\t\texpectMatched:  false,\n\t\t\texpectedStatus: StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict exact match\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict trailing slash partial\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict nested partial\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/users\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict disallows sibling prefix\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/apiv1\",\n\t\t\texpectMatched:  false,\n\t\t\texpectedStatus: StatusNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapp := New(Config{StrictRouting: tt.strictRouting})\n\n\t\t\tmatched := false\n\t\t\tapp.Use(\"/api\", func(c Ctx) error {\n\t\t\t\tmatched = true\n\t\t\t\treturn c.SendStatus(StatusOK)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, tt.path, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedStatus, resp.StatusCode)\n\t\t\trequire.Equal(t, tt.expectMatched, matched)\n\t\t})\n\t}\n}\n\nfunc Test_Group_Use_StrictRoutingBoundary(t *testing.T) {\n\ttype testCase struct {\n\t\tname           string\n\t\tpath           string\n\t\texpectedStatus int\n\t\tstrictRouting  bool\n\t\texpectMatched  bool\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tname:           \"Strict group exact match\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/v1\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict group trailing slash partial\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/v1/\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict group nested partial\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/v1/users\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Strict group disallows sibling prefix\",\n\t\t\tstrictRouting:  true,\n\t\t\tpath:           \"/api/v1beta\",\n\t\t\texpectMatched:  false,\n\t\t\texpectedStatus: StatusNotFound,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict group exact match\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/v1\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict group trailing slash partial\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/v1/\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict group nested partial\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/v1/users\",\n\t\t\texpectMatched:  true,\n\t\t\texpectedStatus: StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:           \"Non-strict group disallows sibling prefix\",\n\t\t\tstrictRouting:  false,\n\t\t\tpath:           \"/api/v1beta\",\n\t\t\texpectMatched:  false,\n\t\t\texpectedStatus: StatusNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapp := New(Config{StrictRouting: tt.strictRouting})\n\n\t\t\tgrp := app.Group(\"/api\")\n\t\t\tmatched := false\n\t\t\tgrp.Use(\"/v1\", func(c Ctx) error {\n\t\t\t\tmatched = true\n\t\t\t\treturn c.Next()\n\t\t\t})\n\t\t\tgrp.Get(\"/v1\", func(c Ctx) error {\n\t\t\t\treturn c.SendStatus(StatusOK)\n\t\t\t})\n\t\t\tgrp.Get(\"/v1/*\", func(c Ctx) error {\n\t\t\t\treturn c.SendStatus(StatusOK)\n\t\t\t})\n\n\t\t\tresp, err := app.Test(httptest.NewRequest(MethodGet, tt.path, http.NoBody))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedStatus, resp.StatusCode)\n\t\t\trequire.Equal(t, tt.expectMatched, matched)\n\t\t})\n\t}\n}\n\nfunc Test_App_Remove_Route_Concurrent(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\t// Add test route\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\t// Concurrently remove and add routes\n\tvar wg sync.WaitGroup\n\tfor range 10 {\n\t\twg.Go(func() {\n\t\t\tapp.RemoveRoute(\"/test\", MethodGet)\n\t\t\tapp.Get(\"/test\", func(c Ctx) error {\n\t\t\t\treturn c.SendStatus(StatusOK)\n\t\t\t})\n\t\t})\n\t}\n\twg.Wait()\n\n\t// Verify final state\n\tapp.RebuildTree()\n\tverifyRequest(t, app, \"/test\", StatusOK)\n}\n\nfunc Test_Route_Registration_Prevent_Duplicate_With_Middleware(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\n\tmiddleware := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\n\tregisterTreeManipulationRoutes(app, middleware)\n\tregisterTreeManipulationRoutes(app)\n\n\tverifyRequest(t, app, \"/dynamically-defined\", StatusNotFound)\n\trequire.Equal(t, uint32(6), app.handlersCount)\n\n\tverifyRequest(t, app, \"/test\", StatusOK)\n\trequire.Equal(t, uint32(7), app.handlersCount)\n\n\tverifyRequest(t, app, \"/dynamically-defined\", StatusOK)\n\trequire.Equal(t, uint32(8), app.handlersCount)\n\n\tverifyRequest(t, app, \"/test\", StatusOK)\n\trequire.Equal(t, uint32(9), app.handlersCount)\n\n\tverifyRequest(t, app, \"/dynamically-defined\", StatusOK)\n\trequire.Equal(t, uint32(9), app.handlersCount)\n}\n\nfunc TestNormalizePath(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tpath          string\n\t\texpected      string\n\t\tcaseSensitive bool\n\t\tstrictRouting bool\n\t}{\n\t\t{\n\t\t\tname:          \"Empty path\",\n\t\t\tpath:          \"\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/\",\n\t\t},\n\t\t{\n\t\t\tname:          \"No leading slash\",\n\t\t\tpath:          \"users\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/users\",\n\t\t},\n\t\t{\n\t\t\tname:          \"With trailing slash and strict routing\",\n\t\t\tpath:          \"/users/\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/users/\",\n\t\t},\n\t\t{\n\t\t\tname:          \"With trailing slash and non-strict routing\",\n\t\t\tpath:          \"/users/\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: false,\n\t\t\texpected:      \"/users\",\n\t\t},\n\t\t{\n\t\t\tname:          \"Case sensitive\",\n\t\t\tpath:          \"/Users\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/Users\",\n\t\t},\n\t\t{\n\t\t\tname:          \"Case insensitive\",\n\t\t\tpath:          \"/Users\",\n\t\t\tcaseSensitive: false,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/users\",\n\t\t},\n\t\t{\n\t\t\tname:          \"With escape characters\",\n\t\t\tpath:          \"/users\\\\/profile\",\n\t\t\tcaseSensitive: true,\n\t\t\tstrictRouting: true,\n\t\t\texpected:      \"/users/profile\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tapp := &App{\n\t\t\t\tconfig: Config{\n\t\t\t\t\tCaseSensitive: tt.caseSensitive,\n\t\t\t\t\tStrictRouting: tt.strictRouting,\n\t\t\t\t},\n\t\t\t}\n\t\t\tresult := app.normalizePath(tt.path)\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestRemoveRoute(t *testing.T) {\n\tapp := New()\n\n\tvar buf strings.Builder\n\n\tapp.Use(func(c Ctx) error {\n\t\tbuf.WriteString(\"1\") //nolint:errcheck // not needed\n\t\treturn c.Next()\n\t})\n\n\tapp.Post(\"/\", func(c Ctx) error {\n\t\tbuf.WriteString(\"2\") //nolint:errcheck // not needed\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp.Use(\"/test\", func(c Ctx) error {\n\t\tbuf.WriteString(\"3\") //nolint:errcheck // not needed\n\t\treturn c.Next()\n\t})\n\n\tapp.Get(\"/test\", func(c Ctx) error {\n\t\tbuf.WriteString(\"4\") //nolint:errcheck // not needed\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp.Post(\"/test\", func(c Ctx) error {\n\t\tbuf.WriteString(\"5\") //nolint:errcheck // not needed\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\tapp.startupProcess()\n\n\trequire.Equal(t, uint32(6), app.handlersCount)\n\n\treq, err := http.NewRequestWithContext(context.Background(), MethodPost, \"/\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, \"12\", buf.String())\n\n\tbuf.Reset()\n\n\treq, err = http.NewRequestWithContext(context.Background(), MethodGet, \"/test\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 200, resp.StatusCode)\n\trequire.Equal(t, \"134\", buf.String())\n\n\tbuf.Reset()\n\n\trequire.Equal(t, uint32(6), app.handlersCount)\n\n\tapp.RemoveRoute(\"/test\", MethodGet)\n\tapp.RebuildTree()\n\n\tapp.RemoveRoute(\"/test\", \"TEST\")\n\tapp.RebuildTree()\n\n\tapp.RemoveRouteFunc(func(_ *Route) bool {\n\t\treturn false\n\t}, MethodGet)\n\n\treq, err = http.NewRequestWithContext(context.Background(), MethodGet, \"/test\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\tbuf.Reset()\n\n\trequire.Equal(t, StatusMethodNotAllowed, resp.StatusCode)\n\n\trequire.Equal(t, uint32(4), app.handlersCount)\n\n\tapp.RemoveRoute(\"/test\", MethodPost)\n\tapp.RebuildTree()\n\n\trequire.Equal(t, uint32(3), app.handlersCount)\n\n\treq, err = http.NewRequestWithContext(context.Background(), MethodPost, \"/test\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 404, resp.StatusCode)\n\trequire.Equal(t, \"1\", buf.String())\n\n\tbuf.Reset()\n\n\treq, err = http.NewRequestWithContext(context.Background(), MethodGet, \"/test\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 404, resp.StatusCode)\n\trequire.Equal(t, \"1\", buf.String())\n\n\tbuf.Reset()\n\n\tapp.RemoveRoute(\"/\", MethodGet, MethodPost)\n\n\trequire.Equal(t, uint32(2), app.handlersCount)\n\n\treq, err = http.NewRequestWithContext(context.Background(), MethodGet, \"/\", http.NoBody)\n\trequire.NoError(t, err)\n\n\tresp, err = app.Test(req)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 404, resp.StatusCode)\n\trequire.Empty(t, buf.String())\n\n\tbuf.Reset()\n\n\tapp.RemoveRoute(\"/test\", MethodGet, MethodPost)\n\n\trequire.Equal(t, uint32(2), app.handlersCount)\n\n\tapp.RemoveRoute(\"/test\", app.config.RequestMethods...)\n\n\trequire.Equal(t, uint32(1), app.handlersCount)\n\n\tapp.Patch(\"/test\", func(c Ctx) error {\n\t\tbuf.WriteString(\"6\") //nolint:errcheck // not needed\n\t\treturn c.SendStatus(StatusOK)\n\t})\n\n\trequire.Equal(t, uint32(2), app.handlersCount)\n\n\tapp.RemoveRoute(\"/test\")\n\tapp.RemoveRoute(\"/\")\n\tapp.RebuildTree()\n\n\trequire.Equal(t, uint32(0), app.handlersCount)\n}\n\n//////////////////////////////////////////////\n///////////////// BENCHMARKS /////////////////\n//////////////////////////////////////////////\n\nfunc registerDummyRoutes(app *App) {\n\th := func(_ Ctx) error {\n\t\treturn nil\n\t}\n\tfor _, r := range routesFixture.GitHubAPI {\n\t\tapp.Add([]string{r.Method}, r.Path, h)\n\t}\n}\n\nfunc acquireDefaultCtxForRouterBenchmark(b *testing.B, app *App, fctx *fasthttp.RequestCtx) *DefaultCtx {\n\tb.Helper()\n\n\tctx := app.AcquireCtx(fctx)\n\tdefaultCtx, ok := ctx.(*DefaultCtx)\n\tif !ok {\n\t\tb.Fatal(\"AcquireCtx did not return *DefaultCtx\")\n\t}\n\treturn defaultCtx\n}\n\n// go test -v -run=^$ -bench=Benchmark_App_RebuildTree -benchmem -count=4\nfunc Benchmark_App_RebuildTree(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\tapp.routesRefreshed = true\n\t\tapp.RebuildTree()\n\t}\n}\n\n// go test -v -run=^$ -bench=Benchmark_App_MethodNotAllowed -benchmem -count=4\nfunc Benchmark_App_MethodNotAllowed(b *testing.B) {\n\tapp := New()\n\th := func(c Ctx) error {\n\t\treturn c.SendString(\"Hello World!\")\n\t}\n\tapp.All(\"/this/is/a/\", h)\n\tapp.Get(\"/this/is/a/dummy/route/oke\", h)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/this/is/a/dummy/route/oke\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n\trequire.Equal(b, 405, c.Response.StatusCode())\n\trequire.Equal(b, MethodGet+\", \"+MethodHead, string(c.Response.Header.Peek(\"Allow\")))\n\trequire.Equal(b, utils.StatusMessage(StatusMethodNotAllowed), string(c.Response.Body()))\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_NotFound -benchmem -count=4\nfunc Benchmark_Router_NotFound(b *testing.B) {\n\tb.ReportAllocs()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/this/route/does/not/exist\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n\trequire.Equal(b, 404, c.Response.StatusCode())\n\trequire.Equal(b, \"Not Found\", string(c.Response.Body()))\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler -benchmem -count=4\nfunc Benchmark_Router_Handler(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\nfunc Benchmark_Router_Handler_Strict_Case(b *testing.B) {\n\tapp := New(Config{\n\t\tStrictRouting: true,\n\t\tCaseSensitive: true,\n\t})\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Chain -benchmem -count=4\nfunc Benchmark_Router_Chain(b *testing.B) {\n\tapp := New()\n\thandler := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\tapp.Get(\"/\", handler, handler, handler, handler, handler, handler)\n\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(MethodGet)\n\tc.URI().SetPath(\"/\")\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_WithCompression -benchmem -count=4\nfunc Benchmark_Router_WithCompression(b *testing.B) {\n\tapp := New()\n\thandler := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(MethodGet)\n\tc.URI().SetPath(\"/\")\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\n// go test -run=^$ -bench=Benchmark_Startup_Process -benchmem -count=9\nfunc Benchmark_Startup_Process(b *testing.B) {\n\tfor b.Loop() {\n\t\tapp := New()\n\t\tregisterDummyRoutes(app)\n\t\tapp.startupProcess()\n\t}\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Next -benchmem -count=4\nfunc Benchmark_Router_Next(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tapp.startupProcess()\n\n\trequest := &fasthttp.RequestCtx{}\n\n\trequest.Request.Header.SetMethod(\"DELETE\")\n\trequest.URI().SetPath(\"/user/keys/1337\")\n\tvar res bool\n\tvar err error\n\n\tc := app.AcquireCtx(request).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\tfor b.Loop() {\n\t\tc.indexRoute = -1\n\t\tres, err = app.next(c)\n\t}\n\trequire.NoError(b, err)\n\trequire.True(b, res)\n\trequire.Equal(b, 4, c.indexRoute)\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default -benchmem -count=4\nfunc Benchmark_Router_Next_Default(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n}\n\n// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel$ github.com/gofiber/fiber/v3 -count=1\nfunc Benchmark_Router_Next_Default_Parallel(b *testing.B) {\n\tapp := New()\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.Header.SetMethod(MethodGet)\n\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default_Immutable -benchmem -count=4\nfunc Benchmark_Router_Next_Default_Immutable(b *testing.B) {\n\tapp := New(Config{Immutable: true})\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\th(fctx)\n\t}\n}\n\n// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel_Immutable$ github.com/gofiber/fiber/v3 -count=1\nfunc Benchmark_Router_Next_Default_Parallel_Immutable(b *testing.B) {\n\tapp := New(Config{Immutable: true})\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\th := app.Handler()\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfctx := &fasthttp.RequestCtx{}\n\t\tfctx.Request.Header.SetMethod(MethodGet)\n\t\tfctx.Request.SetRequestURI(\"/\")\n\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4\nfunc Benchmark_Route_Match(b *testing.B) {\n\tvar match bool\n\tvar params [maxParams]string\n\n\tparsed := parseRoute(\"/user/keys/:id\")\n\troute := &Route{\n\t\tuse:         false,\n\t\troot:        false,\n\t\tstar:        false,\n\t\trouteParser: parsed,\n\t\tParams:      parsed.params,\n\t\tpath:        \"/user/keys/:id\",\n\n\t\tPath:   \"/user/keys/:id\",\n\t\tMethod: \"DELETE\",\n\t}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tfor b.Loop() {\n\t\tmatch = route.match(\"/user/keys/1337\", \"/user/keys/1337\", &params)\n\t}\n\n\trequire.True(b, match)\n\trequire.Equal(b, []string{\"1337\"}, params[0:len(parsed.params)])\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Star -benchmem -count=4\nfunc Benchmark_Route_Match_Star(b *testing.B) {\n\tvar match bool\n\tvar params [maxParams]string\n\n\tparsed := parseRoute(\"/*\")\n\troute := &Route{\n\t\tuse:         false,\n\t\troot:        false,\n\t\tstar:        true,\n\t\trouteParser: parsed,\n\t\tParams:      parsed.params,\n\t\tpath:        \"/user/keys/bla\",\n\n\t\tPath:   \"/user/keys/bla\",\n\t\tMethod: \"DELETE\",\n\t}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\tfor b.Loop() {\n\t\tmatch = route.match(\"/user/keys/bla\", \"/user/keys/bla\", &params)\n\t}\n\n\trequire.True(b, match)\n\trequire.Equal(b, []string{\"user/keys/bla\"}, params[0:len(parsed.params)])\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Root -benchmem -count=4\nfunc Benchmark_Route_Match_Root(b *testing.B) {\n\tvar match bool\n\tvar params [maxParams]string\n\n\tparsed := parseRoute(\"/\")\n\troute := &Route{\n\t\tuse:         false,\n\t\troot:        true,\n\t\tstar:        false,\n\t\tpath:        \"/\",\n\t\trouteParser: parsed,\n\t\tParams:      parsed.params,\n\n\t\tPath:   \"/\",\n\t\tMethod: \"DELETE\",\n\t}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\tfor b.Loop() {\n\t\tmatch = route.match(\"/\", \"/\", &params)\n\t}\n\n\trequire.True(b, match)\n\trequire.Equal(b, []string{}, params[0:len(parsed.params)])\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_CaseSensitive -benchmem -count=4\nfunc Benchmark_Router_Handler_CaseSensitive(b *testing.B) {\n\tapp := New()\n\tapp.config.CaseSensitive = true\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\n// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_Unescape -benchmem -count=4\nfunc Benchmark_Router_Handler_Unescape(b *testing.B) {\n\tapp := New()\n\tapp.config.UnescapePath = true\n\tregisterDummyRoutes(app)\n\tapp.Delete(\"/créer\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(MethodDelete)\n\tc.URI().SetPath(\"/cr%C3%A9er\")\n\n\tfor b.Loop() {\n\t\tc.URI().SetPath(\"/cr%C3%A9er\")\n\t\tappHandler(c)\n\t}\n}\n\n// go test -run=^$ -bench=Benchmark_Router_Handler_StrictRouting -benchmem -count=4\nfunc Benchmark_Router_Handler_StrictRouting(b *testing.B) {\n\tapp := New()\n\tapp.config.CaseSensitive = true\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\n\tc := &fasthttp.RequestCtx{}\n\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\n\tfor b.Loop() {\n\t\tappHandler(c)\n\t}\n}\n\n// go test -run=^$ -bench=Benchmark_Router_GitHub_API -benchmem -count=16\nfunc Benchmark_Router_GitHub_API(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tapp.startupProcess()\n\n\tc := &fasthttp.RequestCtx{}\n\tvar match bool\n\tvar err error\n\n\tb.ResetTimer()\n\tfor i := range routesFixture.TestRoutes {\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\tc.Request.Header.SetMethod(routesFixture.TestRoutes[i].Method)\n\t\t\tfor pb.Next() {\n\t\t\t\tc.URI().SetPath(routesFixture.TestRoutes[i].Path)\n\n\t\t\t\tctx := app.AcquireCtx(c).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed\n\n\t\t\t\tmatch, err = app.next(ctx)\n\t\t\t\tapp.ReleaseCtx(ctx)\n\t\t\t}\n\t\t})\n\n\t\trequire.NoError(b, err)\n\t\trequire.True(b, match)\n\t}\n}\n\ntype testRoute struct {\n\tMethod string `json:\"method\"`\n\tPath   string `json:\"path\"`\n}\n\ntype routeJSON struct {\n\tTestRoutes []testRoute `json:\"test_routes\"`\n\tGitHubAPI  []testRoute `json:\"github_api\"`\n}\n\nfunc newCustomApp() *App {\n\treturn NewWithCustomCtx(func(app *App) CustomCtx {\n\t\treturn &customCtx{DefaultCtx: *NewDefaultCtx(app)}\n\t})\n}\n\nfunc Test_NextCustom_MethodNotAllowed(t *testing.T) {\n\tt.Parallel()\n\tapp := newCustomApp()\n\tapp.Get(\"/foo\", func(c Ctx) error { return c.SendStatus(StatusOK) })\n\tuseRoute := &Route{use: true, path: \"/foo\", Path: \"/foo\", routeParser: parseRoute(\"/foo\")}\n\tm := app.methodInt(MethodGet)\n\tapp.stack[m] = append([]*Route{useRoute}, app.stack[m]...)\n\tapp.routesRefreshed = true\n\tapp.ensureAutoHeadRoutes()\n\tapp.RebuildTree()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodPost)\n\tfctx.Request.SetRequestURI(\"/foo\")\n\n\tctx := app.AcquireCtx(fctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\tmatched, err := app.nextCustom(ctx)\n\trequire.False(t, matched)\n\trequire.ErrorIs(t, err, ErrMethodNotAllowed)\n\tallow := string(ctx.Response().Header.Peek(HeaderAllow))\n\trequire.Equal(t, \"GET, HEAD\", allow)\n}\n\nfunc Test_NextCustom_NotFound(t *testing.T) {\n\tt.Parallel()\n\tapp := newCustomApp()\n\tapp.RebuildTree()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/not-exist\")\n\n\tctx := app.AcquireCtx(fctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\tmatched, err := app.nextCustom(ctx)\n\trequire.False(t, matched)\n\tvar e *Error\n\trequire.ErrorAs(t, err, &e)\n\trequire.Equal(t, StatusNotFound, e.Code)\n}\n\nfunc Test_RequestHandler_CustomCtx_NotImplemented(t *testing.T) {\n\tt.Parallel()\n\tapp := newCustomApp()\n\n\th := app.Handler()\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(\"UNKNOWN\")\n\tfctx.Request.SetRequestURI(\"/\")\n\n\th(fctx)\n\trequire.Equal(t, StatusNotImplemented, fctx.Response.StatusCode())\n}\n\nfunc Test_NextCustom_Matched404(t *testing.T) {\n\tt.Parallel()\n\tapp := newCustomApp()\n\tapp.RebuildTree()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/none\")\n\n\tctx := app.AcquireCtx(fctx)\n\tctx.setMatched(true)\n\tdefer app.ReleaseCtx(ctx)\n\n\tmatched, err := app.nextCustom(ctx)\n\trequire.False(t, matched)\n\tvar e *Error\n\trequire.ErrorAs(t, err, &e)\n\trequire.Equal(t, StatusNotFound, e.Code)\n}\n\nfunc Test_NextCustom_SkipMountAndNoHandlers(t *testing.T) {\n\tt.Parallel()\n\tapp := newCustomApp()\n\tm := app.methodInt(MethodGet)\n\tmountR := &Route{path: \"/skip\", Path: \"/skip\", routeParser: parseRoute(\"/skip\"), mount: true}\n\tempty := &Route{path: \"/foo\", Path: \"/foo\", routeParser: parseRoute(\"/foo\")}\n\tapp.stack[m] = []*Route{mountR, empty}\n\tapp.routesRefreshed = true\n\tapp.RebuildTree()\n\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/foo\")\n\n\tctx := app.AcquireCtx(fctx)\n\tdefer app.ReleaseCtx(ctx)\n\n\tmatched, err := app.nextCustom(ctx)\n\trequire.True(t, matched)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"/foo\", ctx.Route().Path)\n}\n\nfunc Test_AddRoute_MergeHandlers(t *testing.T) {\n\tt.Parallel()\n\tapp := New()\n\tcount := func(_ Ctx) error { return nil }\n\tapp.Get(\"/merge\", count)\n\tapp.Get(\"/merge\", count)\n\n\trequire.Len(t, app.stack[app.methodInt(MethodGet)], 1)\n\trequire.Len(t, app.stack[app.methodInt(MethodGet)][0].Handlers, 2)\n}\n\nfunc Benchmark_App_RebuildTree_Parallel(b *testing.B) {\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\t// Each worker gets its own App instance to avoid data races on shared state\n\t\tlocalApp := New()\n\t\tregisterDummyRoutes(localApp)\n\t\tfor pb.Next() {\n\t\t\tlocalApp.routesRefreshed = true\n\t\t\tlocalApp.RebuildTree()\n\t\t}\n\t})\n}\n\nfunc Benchmark_App_MethodNotAllowed_Parallel(b *testing.B) {\n\tapp := New()\n\th := func(c Ctx) error {\n\t\treturn c.SendString(\"Hello World!\")\n\t}\n\tapp.All(\"/this/is/a/\", h)\n\tapp.Get(\"/this/is/a/dummy/route/oke\", h)\n\tappHandler := app.Handler()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\t// Each worker gets its own RequestCtx to avoid data races\n\t\tc := &fasthttp.RequestCtx{}\n\t\tc.Request.Header.SetMethod(\"DELETE\")\n\t\tc.URI().SetPath(\"/this/is/a/dummy/route/oke\")\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n\n\t// Single-threaded verification on a fresh context to preserve correctness checks\n\tverifyCtx := &fasthttp.RequestCtx{}\n\tverifyCtx.Request.Header.SetMethod(\"DELETE\")\n\tverifyCtx.URI().SetPath(\"/this/is/a/dummy/route/oke\")\n\tappHandler(verifyCtx)\n\trequire.Equal(b, 405, verifyCtx.Response.StatusCode())\n\trequire.Equal(b, MethodGet+\", \"+MethodHead, string(verifyCtx.Response.Header.Peek(\"Allow\")))\n\trequire.Equal(b, utils.StatusMessage(StatusMethodNotAllowed), string(verifyCtx.Response.Body()))\n}\n\nfunc Benchmark_Router_NotFound_Parallel(b *testing.B) {\n\tb.ReportAllocs()\n\tapp := New()\n\tapp.Use(func(c Ctx) error {\n\t\treturn c.Next()\n\t})\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/this/route/does/not/exist\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n\trequire.Equal(b, 404, c.Response.StatusCode())\n\trequire.Equal(b, \"Not Found\", string(c.Response.Body()))\n}\n\nfunc Benchmark_Router_Handler_Parallel(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_Handler_Strict_Case_Parallel(b *testing.B) {\n\tapp := New(Config{StrictRouting: true, CaseSensitive: true})\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_Chain_Parallel(b *testing.B) {\n\tapp := New()\n\thandler := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\tapp.Get(\"/\", handler, handler, handler, handler, handler, handler)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(MethodGet)\n\tc.URI().SetPath(\"/\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_WithCompression_Parallel(b *testing.B) {\n\tapp := New()\n\thandler := func(c Ctx) error {\n\t\treturn c.Next()\n\t}\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tapp.Get(\"/\", handler)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(MethodGet)\n\tc.URI().SetPath(\"/\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Startup_Process_Parallel(b *testing.B) {\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tapp := New()\n\t\t\tregisterDummyRoutes(app)\n\t\t\tapp.startupProcess()\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_Next_Parallel(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tapp.startupProcess()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\t// Each worker gets its own request and context to avoid data races.\n\t\trequest := &fasthttp.RequestCtx{}\n\t\trequest.Request.Header.SetMethod(\"DELETE\")\n\t\trequest.URI().SetPath(\"/user/keys/1337\")\n\t\tc := acquireDefaultCtxForRouterBenchmark(b, app, request)\n\t\tfor pb.Next() {\n\t\t\tc.indexRoute = -1\n\t\t\t//nolint:errcheck // Benchmark hot path - error checked in verification\n\t\t\t_, _ = app.next(c)\n\t\t}\n\t})\n\n\t// Single-threaded verification on a fresh context to preserve correctness checks.\n\tverifyRequest := &fasthttp.RequestCtx{}\n\tverifyRequest.Request.Header.SetMethod(\"DELETE\")\n\tverifyRequest.URI().SetPath(\"/user/keys/1337\")\n\tverifyCtx := acquireDefaultCtxForRouterBenchmark(b, app, verifyRequest)\n\tverifyCtx.indexRoute = -1\n\tres, err := app.next(verifyCtx)\n\trequire.NoError(b, err)\n\trequire.True(b, res)\n\trequire.Equal(b, 4, verifyCtx.indexRoute)\n}\n\nfunc Benchmark_Router_Next_Default_Immutable_Parallel(b *testing.B) {\n\tapp := New(Config{Immutable: true})\n\tapp.Get(\"/\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\th := app.Handler()\n\tfctx := &fasthttp.RequestCtx{}\n\tfctx.Request.Header.SetMethod(MethodGet)\n\tfctx.Request.SetRequestURI(\"/\")\n\tb.ReportAllocs()\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Route_Match_Parallel(b *testing.B) {\n\tparsed := parseRoute(\"/user/keys/:id\")\n\troute := &Route{use: false, root: false, star: false, routeParser: parsed, Params: parsed.params, path: \"/user/keys/:id\", Path: \"/user/keys/:id\", Method: \"DELETE\"}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tb.RunParallel(func(pb *testing.PB) {\n\t\t// Each worker gets its own local variables to avoid data races\n\t\tvar params [maxParams]string\n\t\tfor pb.Next() {\n\t\t\t_ = route.match(\"/user/keys/1337\", \"/user/keys/1337\", &params)\n\t\t}\n\t})\n\n\t// Single-threaded verification to preserve correctness checks\n\tvar verifyParams [maxParams]string\n\tmatch := route.match(\"/user/keys/1337\", \"/user/keys/1337\", &verifyParams)\n\trequire.True(b, match)\n\trequire.Equal(b, []string{\"1337\"}, verifyParams[0:len(parsed.params)])\n}\n\nfunc Benchmark_Route_Match_Star_Parallel(b *testing.B) {\n\tvar match bool\n\tvar params [maxParams]string\n\tparsed := parseRoute(\"/*\")\n\troute := &Route{use: false, root: false, star: true, routeParser: parsed, Params: parsed.params, path: \"/user/keys/bla\", Path: \"/user/keys/bla\", Method: \"DELETE\"}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tmatch = route.match(\"/user/keys/bla\", \"/user/keys/bla\", &params)\n\t\t}\n\t})\n\trequire.True(b, match)\n\trequire.Equal(b, []string{\"user/keys/bla\"}, params[0:len(parsed.params)])\n}\n\nfunc Benchmark_Route_Match_Root_Parallel(b *testing.B) {\n\tvar match bool\n\tvar params [maxParams]string\n\tparsed := parseRoute(\"/\")\n\troute := &Route{use: false, root: true, star: false, path: \"/\", routeParser: parsed, Params: parsed.params, Path: \"/\", Method: \"DELETE\"}\n\troute.Handlers = append(route.Handlers, func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tmatch = route.match(\"/\", \"/\", &params)\n\t\t}\n\t})\n\trequire.True(b, match)\n\trequire.Equal(b, []string{}, params[0:len(parsed.params)])\n}\n\nfunc Benchmark_Router_Handler_CaseSensitive_Parallel(b *testing.B) {\n\tapp := New()\n\tapp.config.CaseSensitive = true\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_Handler_Unescape_Parallel(b *testing.B) {\n\tapp := New()\n\tapp.config.UnescapePath = true\n\tregisterDummyRoutes(app)\n\tapp.Delete(\"/créer\", func(_ Ctx) error {\n\t\treturn nil\n\t})\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(MethodDelete)\n\tc.URI().SetPath(\"/cr%C3%A9er\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tc.URI().SetPath(\"/cr%C3%A9er\")\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_Handler_StrictRouting_Parallel(b *testing.B) {\n\tapp := New()\n\tapp.config.CaseSensitive = true\n\tregisterDummyRoutes(app)\n\tappHandler := app.Handler()\n\tc := &fasthttp.RequestCtx{}\n\tc.Request.Header.SetMethod(\"DELETE\")\n\tc.URI().SetPath(\"/user/keys/1337\")\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\tappHandler(c)\n\t\t}\n\t})\n}\n\nfunc Benchmark_Router_GitHub_API_Parallel(b *testing.B) {\n\tapp := New()\n\tregisterDummyRoutes(app)\n\tapp.startupProcess()\n\tb.ResetTimer()\n\tfor i := range routesFixture.TestRoutes {\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\t// Each worker gets its own RequestCtx and local variables to avoid data races\n\t\t\tc := &fasthttp.RequestCtx{}\n\t\t\tc.Request.Header.SetMethod(routesFixture.TestRoutes[i].Method)\n\t\t\tfor pb.Next() {\n\t\t\t\tc.URI().SetPath(routesFixture.TestRoutes[i].Path)\n\t\t\t\tctx := acquireDefaultCtxForRouterBenchmark(b, app, c)\n\t\t\t\t//nolint:errcheck // Benchmark hot path - error checked in verification\n\t\t\t\t_, _ = app.next(ctx)\n\t\t\t\tapp.ReleaseCtx(ctx)\n\t\t\t}\n\t\t})\n\n\t\t// Single-threaded verification on a fresh context to preserve correctness checks\n\t\tverifyC := &fasthttp.RequestCtx{}\n\t\tverifyC.Request.Header.SetMethod(routesFixture.TestRoutes[i].Method)\n\t\tverifyC.URI().SetPath(routesFixture.TestRoutes[i].Path)\n\t\tverifyCtx := acquireDefaultCtxForRouterBenchmark(b, app, verifyC)\n\t\tmatch, err := app.next(verifyCtx)\n\t\tapp.ReleaseCtx(verifyCtx)\n\t\trequire.NoError(b, err)\n\t\trequire.True(b, match)\n\t}\n}\n"
  },
  {
    "path": "services.go",
    "content": "package fiber\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n)\n\n// Service is an interface that defines the methods for a service.\ntype Service interface {\n\t// Start starts the service, returning an error if it fails.\n\tStart(ctx context.Context) error\n\n\t// String returns a string representation of the service.\n\t// It is used to print a human-readable name of the service in the startup message.\n\tString() string\n\n\t// State returns the current state of the service.\n\tState(ctx context.Context) (string, error)\n\n\t// Terminate terminates the service, returning an error if it fails.\n\tTerminate(ctx context.Context) error\n}\n\n// hasConfiguredServices Checks if there are any services for the current application.\nfunc (app *App) hasConfiguredServices() bool {\n\treturn len(app.configured.Services) > 0\n}\n\nfunc (app *App) validateConfiguredServices() error {\n\treturn validateServicesSlice(app.configured.Services)\n}\n\nfunc validateServicesSlice(services []Service) error {\n\tfor idx, srv := range services {\n\t\tif srv == nil {\n\t\t\treturn fmt.Errorf(\"fiber: service at index %d is nil\", idx)\n\t\t}\n\t}\n\treturn nil\n}\n\n// initServices If the app is configured to use services, this function registers\n// a post shutdown hook to shutdown them after the server is closed.\n// This function panics if there is an error starting the services.\nfunc (app *App) initServices() {\n\tif !app.hasConfiguredServices() {\n\t\treturn\n\t}\n\n\tif err := app.startServices(app.servicesStartupCtx()); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// servicesStartupCtx Returns the context for the services startup.\n// If the ServicesStartupContextProvider is not set, it returns a new background context.\nfunc (app *App) servicesStartupCtx() context.Context {\n\tif app.configured.ServicesStartupContextProvider != nil {\n\t\treturn app.configured.ServicesStartupContextProvider()\n\t}\n\n\treturn context.Background()\n}\n\n// servicesShutdownCtx Returns the context for the services shutdown.\n// If the ServicesShutdownContextProvider is not set, it returns a new background context.\nfunc (app *App) servicesShutdownCtx() context.Context {\n\tif app.configured.ServicesShutdownContextProvider != nil {\n\t\treturn app.configured.ServicesShutdownContextProvider()\n\t}\n\n\treturn context.Background()\n}\n\n// startServices Handles the start process of services for the current application.\n// Iterates over all configured services and tries to start them, returning an error if any error occurs.\nfunc (app *App) startServices(ctx context.Context) error {\n\tif !app.hasConfiguredServices() {\n\t\treturn nil\n\t}\n\n\tvar errs []error\n\tfor idx, srv := range app.configured.Services {\n\t\tif srv == nil {\n\t\t\treturn fmt.Errorf(\"fiber: service at index %d is nil\", idx)\n\t\t}\n\t\tif err := ctx.Err(); err != nil {\n\t\t\t// Context is canceled, return an error the soonest possible, so that\n\t\t\t// the user can see the context cancellation error and act on it.\n\t\t\treturn fmt.Errorf(\"context canceled while starting service %s: %w\", srv.String(), err)\n\t\t}\n\n\t\terr := srv.Start(ctx)\n\t\tif err == nil {\n\t\t\t// mark the service as started\n\t\t\tapp.state.setService(srv)\n\t\t\tcontinue\n\t\t}\n\n\t\tif errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n\t\t\treturn fmt.Errorf(\"service %s start: %w\", srv.String(), err)\n\t\t}\n\n\t\terrs = append(errs, fmt.Errorf(\"service %s start: %w\", srv.String(), err))\n\t}\n\treturn errors.Join(errs...)\n}\n\n// shutdownServices Handles the shutdown process of services for the current application.\n// Iterates over all the started services in reverse order and tries to terminate them,\n// returning an error if any error occurs.\nfunc (app *App) shutdownServices(ctx context.Context) error {\n\tif app.state.ServicesLen() == 0 {\n\t\treturn nil\n\t}\n\n\tvar errs []error\n\tfor key, srv := range app.state.Services() {\n\t\tif srv == nil {\n\t\t\treturn fmt.Errorf(\"fiber: service %q is nil\", key)\n\t\t}\n\t\tif err := ctx.Err(); err != nil {\n\t\t\t// Context is canceled, do a best effort to terminate the services.\n\t\t\terrs = append(errs, fmt.Errorf(\"service %s terminate: %w\", srv.String(), err))\n\t\t\tcontinue\n\t\t}\n\n\t\terr := srv.Terminate(ctx)\n\t\tif err != nil {\n\t\t\t// Best effort to terminate the services.\n\t\t\terrs = append(errs, fmt.Errorf(\"service %s terminate: %w\", srv.String(), err))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Remove the service from the State\n\t\tapp.state.deleteService(srv)\n\t}\n\treturn errors.Join(errs...)\n}\n\n// logServices logs information about services and returns an error\n// if any configured service is nil.\nfunc (app *App) logServices(ctx context.Context, out io.Writer, colors *Colors) error {\n\tif !app.hasConfiguredServices() {\n\t\treturn nil\n\t}\n\n\tscheme := colors\n\tif scheme == nil {\n\t\tscheme = &DefaultColors\n\t}\n\n\tfmt.Fprintf(out,\n\t\t\"%sINFO%s Services: \\t%s%d%s\\n\",\n\t\tscheme.Green, scheme.Reset, scheme.Blue, app.state.ServicesLen(), scheme.Reset)\n\tfor key, srv := range app.state.Services() {\n\t\tif srv == nil {\n\t\t\treturn fmt.Errorf(\"fiber: service %q is nil\", key)\n\t\t}\n\t\tvar state string\n\t\tvar stateColor string\n\t\tstate, err := srv.State(ctx)\n\t\tif err != nil {\n\t\t\tstate = errString\n\t\t\tstateColor = scheme.Red\n\t\t} else {\n\t\t\tstateColor = scheme.Blue\n\t\t}\n\t\tfmt.Fprintf(out, \"%sINFO%s    🧩 %s[ %s ] %s%s\\n\", scheme.Green, scheme.Reset, stateColor, utilsstrings.ToUpper(state), srv.String(), scheme.Reset)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "services_test.go",
    "content": "package fiber\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tterminateErrorMessage = \"terminate error\"\n\tstartErrorMessage     = \"start error\"\n)\n\n// mockService implements Service interface for testing\ntype mockService struct {\n\tstartError     error\n\tterminateError error\n\tstateError     error\n\tname           string\n\tstarted        bool\n\tterminated     bool\n\tstartDelay     time.Duration\n\tterminateDelay time.Duration\n}\n\nfunc (m *mockService) Start(ctx context.Context) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn fmt.Errorf(\"context canceled: %w\", ctx.Err())\n\tdefault:\n\t}\n\n\tif m.startDelay > 0 {\n\t\ttimer := time.NewTimer(m.startDelay)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\ttimer.Stop()\n\t\t\treturn fmt.Errorf(\"context canceled: %w\", ctx.Err())\n\t\tcase <-timer.C:\n\t\t\t// Continue after delay\n\t\t}\n\t}\n\n\tif m.startError != nil {\n\t\tm.started = false\n\t\treturn m.startError\n\t}\n\n\tm.started = true\n\treturn nil\n}\n\nfunc (m *mockService) String() string {\n\treturn m.name\n}\n\nfunc (m *mockService) State(ctx context.Context) (string, error) {\n\tif ctx.Err() != nil {\n\t\treturn \"\", fmt.Errorf(\"context canceled: %w\", ctx.Err())\n\t}\n\n\tif m.stateError != nil {\n\t\treturn \"error\", m.stateError\n\t}\n\n\tif m.started {\n\t\treturn \"running\", nil\n\t}\n\n\tif m.terminated {\n\t\treturn \"stopped\", nil\n\t}\n\n\treturn \"unknown\", nil\n}\n\nfunc (m *mockService) Terminate(ctx context.Context) error {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn fmt.Errorf(\"context canceled: %w\", ctx.Err())\n\tdefault:\n\t}\n\n\tif m.terminateDelay > 0 {\n\t\ttimer := time.NewTimer(m.terminateDelay)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\ttimer.Stop()\n\t\t\treturn fmt.Errorf(\"context canceled: %w\", ctx.Err())\n\t\tcase <-timer.C:\n\t\t\t// Continue after delay\n\t\t}\n\t}\n\n\tif m.terminateError != nil {\n\t\tm.terminated = false\n\t\treturn m.terminateError\n\t}\n\n\tm.started = false\n\tm.terminated = true\n\treturn nil\n}\n\nfunc Test_HasConfiguredServices(t *testing.T) {\n\ttestHasConfiguredServicesFn := func(t *testing.T, app *App, expected bool) {\n\t\tt.Helper()\n\n\t\tresult := app.hasConfiguredServices()\n\t\trequire.Equal(t, expected, result)\n\t}\n\n\tt.Run(\"no-services\", func(t *testing.T) {\n\t\ttestHasConfiguredServicesFn(t, &App{configured: Config{}}, false)\n\t})\n\n\tt.Run(\"has-services\", func(t *testing.T) {\n\t\ttestHasConfiguredServicesFn(t, &App{configured: Config{Services: []Service{&mockService{name: \"test-dep\"}}}}, true)\n\t})\n}\n\nfunc Test_InitServices(t *testing.T) {\n\tt.Run(\"no-services\", func(t *testing.T) {\n\t\tapp := &App{configured: Config{}}\n\t\trequire.NotPanics(t, app.initServices)\n\t})\n\n\tt.Run(\"start/success\", func(t *testing.T) {\n\t\t// Initialize the app using the struct and defining the state and hooks manually,\n\t\t// because we are not checking the shutdown hooks in this test.\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\"},\n\t\t\t\t\t&mockService{name: \"dep2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\tapp.hooks = newHooks(app)\n\n\t\trequire.NotPanics(t, app.initServices)\n\t})\n\n\tt.Run(\"start/error\", func(t *testing.T) {\n\t\t// Initialize the app using the struct and defining the state and hooks manually,\n\t\t// because we are not checking the shutdown hooks in this test.\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\", startError: errors.New(startErrorMessage + \" 1\")},\n\t\t\t\t\t&mockService{name: \"dep2\", startError: errors.New(startErrorMessage + \" 2\")},\n\t\t\t\t\t&mockService{name: \"dep3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\tapp.hooks = newHooks(app)\n\n\t\trequire.Panics(t, app.initServices)\n\t})\n\n\tt.Run(\"shutdown-hooks/success\", func(t *testing.T) {\n\t\t// Initialize the app using the New function to verify that the shutdown hooks are registered\n\t\t// and the app mutex is not causing a deadlock.\n\t\tapp := New(Config{\n\t\t\tServices: []Service{&mockService{name: \"dep1\"}},\n\t\t})\n\n\t\trequire.NotPanics(t, app.initServices)\n\n\t\ttype stringsLogger struct {\n\t\t\tstrings.Builder\n\t\t}\n\n\t\tvar buf stringsLogger\n\t\tlog.SetOutput(&buf)\n\n\t\tapp.Hooks().executeOnPostShutdownHooks(nil)\n\n\t\trequire.NotContains(t, buf.String(), \"failed to call post shutdown hook:\")\n\t})\n\n\tt.Run(\"shutdown-hooks/error\", func(t *testing.T) {\n\t\t// Initialize the app using the New function to verify that the shutdown hooks are registered\n\t\t// and the app mutex is not causing a deadlock.\n\t\tapp := New(Config{\n\t\t\tServices: []Service{\n\t\t\t\t&mockService{name: \"dep1\"},\n\t\t\t\t&mockService{name: \"dep2\", terminateError: errors.New(terminateErrorMessage + \" 2\")},\n\t\t\t},\n\t\t})\n\n\t\trequire.NotPanics(t, app.initServices)\n\n\t\ttype stringsLogger struct {\n\t\t\tstrings.Builder\n\t\t}\n\n\t\tvar buf stringsLogger\n\t\tlog.SetOutput(&buf)\n\n\t\tapp.Hooks().executeOnPostShutdownHooks(nil)\n\n\t\trequire.Contains(t, buf.String(), \"failed to shutdown services: service dep2 terminate: terminate error 2\")\n\t})\n}\n\nfunc Test_StartServices(t *testing.T) {\n\tt.Run(\"no-services\", func(t *testing.T) {\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\terr := app.startServices(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Zero(t, app.state.ServicesLen())\n\t})\n\n\tt.Run(\"successful-start\", func(t *testing.T) {\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\"},\n\t\t\t\t\t&mockService{name: \"dep2\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\terr := app.startServices(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 2, app.state.ServicesLen())\n\t})\n\n\tt.Run(\"failed-start\", func(t *testing.T) {\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\", startError: errors.New(startErrorMessage + \" 1\")},\n\t\t\t\t\t&mockService{name: \"dep2\", startError: errors.New(startErrorMessage + \" 2\")},\n\t\t\t\t\t&mockService{name: \"dep3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\terr := app.startServices(context.Background())\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), startErrorMessage+\" 1\")\n\t\trequire.Contains(t, err.Error(), startErrorMessage+\" 2\")\n\t\trequire.Equal(t, 1, app.state.ServicesLen())\n\t})\n\n\tt.Run(\"context\", func(t *testing.T) {\n\t\tt.Run(\"already-canceled\", func(t *testing.T) {\n\t\t\tapp := &App{\n\t\t\t\tconfigured: Config{\n\t\t\t\t\tServices: []Service{\n\t\t\t\t\t\t&mockService{name: \"dep1\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstate: newState(),\n\t\t\t}\n\n\t\t\t// Create a context that is already canceled\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tcancel()\n\n\t\t\terr := app.startServices(ctx)\n\t\t\trequire.ErrorIs(t, err, context.Canceled)\n\t\t\trequire.Zero(t, app.state.ServicesLen())\n\t\t})\n\n\t\tt.Run(\"cancellation\", func(t *testing.T) {\n\t\t\t// Create a service that takes some time to start\n\t\t\tslowDep := &mockService{name: \"slow-dep\", startDelay: 200 * time.Millisecond}\n\n\t\t\tapp := &App{\n\t\t\t\tconfigured: Config{\n\t\t\t\t\tServices: []Service{slowDep},\n\t\t\t\t},\n\t\t\t\tstate: newState(),\n\t\t\t}\n\n\t\t\t// Create a context that will be canceled immediately\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\t\t\tdefer cancel()\n\n\t\t\t// Start services with a delay that is longer than the timeout\n\t\t\terr := app.startServices(ctx)\n\t\t\trequire.ErrorIs(t, err, context.DeadlineExceeded)\n\t\t\trequire.Zero(t, app.state.ServicesLen())\n\t\t})\n\t})\n}\n\nfunc Test_ShutdownServices(t *testing.T) {\n\tt.Run(\"no-services\", func(t *testing.T) {\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\terr := app.shutdownServices(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Zero(t, app.state.ServicesLen())\n\t})\n\n\tt.Run(\"successful-shutdown\", func(t *testing.T) {\n\t\tsrv1 := &mockService{name: \"dep1\"}\n\t\tsrv2 := &mockService{name: \"dep2\"}\n\n\t\t// Expected state, including the two started services\n\t\texpectedState := newState()\n\t\texpectedState.setService(srv1)\n\t\texpectedState.setService(srv2)\n\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{srv1, srv2},\n\t\t\t},\n\t\t\tstate: expectedState,\n\t\t}\n\n\t\terr := app.shutdownServices(context.Background())\n\t\trequire.NoError(t, err)\n\t\trequire.Zero(t, app.state.ServicesLen())\n\t})\n\n\tt.Run(\"failed-shutdown\", func(t *testing.T) {\n\t\tsrv1 := &mockService{name: \"dep1\", terminateError: errors.New(terminateErrorMessage + \" 1\")}\n\t\tsrv2 := &mockService{name: \"dep2\", terminateError: errors.New(terminateErrorMessage + \" 2\")}\n\t\tsrv3 := &mockService{name: \"dep3\"}\n\n\t\t// Expected state, including the two started services\n\t\texpectedState := newState()\n\t\texpectedState.setService(srv1)\n\t\texpectedState.setService(srv2)\n\t\texpectedState.setService(srv3)\n\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServices: []Service{srv1, srv2, srv3},\n\t\t\t},\n\t\t\tstate: expectedState,\n\t\t}\n\n\t\terr := app.shutdownServices(context.Background())\n\t\trequire.Error(t, err)\n\t\trequire.Contains(t, err.Error(), terminateErrorMessage+\" 1\")\n\t\trequire.Contains(t, err.Error(), terminateErrorMessage+\" 2\")\n\t\trequire.Equal(t, 2, app.state.ServicesLen()) // 2 services failed to terminate\n\t})\n\n\tt.Run(\"context\", func(t *testing.T) {\n\t\tt.Run(\"already-canceled\", func(t *testing.T) {\n\t\t\tsrv1 := &mockService{name: \"dep1\"}\n\n\t\t\t// Expected state, including the two started services\n\t\t\texpectedState := newState()\n\t\t\texpectedState.setService(srv1)\n\n\t\t\tapp := &App{\n\t\t\t\tconfigured: Config{\n\t\t\t\t\tServices: []Service{srv1},\n\t\t\t\t},\n\t\t\t\tstate: expectedState,\n\t\t\t}\n\n\t\t\t// Create a context that is already canceled\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tcancel()\n\n\t\t\terr := app.shutdownServices(ctx)\n\t\t\trequire.Error(t, err)\n\t\t\trequire.ErrorIs(t, err, context.Canceled)\n\t\t\trequire.Contains(t, err.Error(), \"service dep1 terminate\")\n\t\t\trequire.Equal(t, 1, app.state.ServicesLen())\n\t\t})\n\n\t\tt.Run(\"cancellation\", func(t *testing.T) {\n\t\t\t// Create a service that takes some time to terminate\n\t\t\tslowDep := &mockService{name: \"slow-dep\", terminateDelay: 200 * time.Millisecond}\n\n\t\t\t// Expected state, including the two started services\n\t\t\texpectedState := newState()\n\t\t\texpectedState.setService(slowDep)\n\n\t\t\tapp := &App{\n\t\t\t\tconfigured: Config{\n\t\t\t\t\tServices: []Service{slowDep},\n\t\t\t\t},\n\t\t\t\tstate: expectedState,\n\t\t\t}\n\n\t\t\t// Create a new context for shutdown\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)\n\t\t\tdefer cancel()\n\n\t\t\t// Shutdown services with canceled context\n\t\t\terr := app.shutdownServices(ctx)\n\t\t\trequire.ErrorIs(t, err, context.DeadlineExceeded)\n\t\t\trequire.Equal(t, 1, app.state.ServicesLen())\n\t\t})\n\t})\n}\n\nfunc Test_LogServices(t *testing.T) {\n\tt.Parallel()\n\t// Service with successful State\n\trunningService := &mockService{name: \"running\", started: true}\n\t// Service with State error\n\terrorService := &mockService{name: \"error\", stateError: errors.New(\"state error\")}\n\n\texpectedState := newState()\n\texpectedState.setService(runningService)\n\texpectedState.setService(errorService)\n\n\tapp := &App{\n\t\tconfigured: Config{\n\t\t\tServices: []Service{runningService, errorService},\n\t\t},\n\t\tstate: expectedState,\n\t}\n\n\tvar buf bytes.Buffer\n\n\tcolors := Colors{\n\t\tGreen: \"\\033[32m\",\n\t\tReset: \"\\033[0m\",\n\t\tBlue:  \"\\033[34m\",\n\t\tRed:   \"\\033[31m\",\n\t}\n\n\terr := app.logServices(context.Background(), &buf, &colors)\n\trequire.NoError(t, err)\n\n\toutput := buf.String()\n\n\tfor _, srv := range app.state.Services() {\n\t\tstateColor := colors.Blue\n\t\tstate := \"RUNNING\"\n\t\tif _, err := srv.State(context.Background()); err != nil {\n\t\t\tstateColor = colors.Red\n\t\t\tstate = \"ERROR\"\n\t\t}\n\n\t\texpected := fmt.Sprintf(\"%sINFO%s    🧩 %s[ %s ] %s%s\\n\", colors.Green, colors.Reset, stateColor, strings.ToUpper(state), srv.String(), colors.Reset)\n\t\trequire.Contains(t, output, expected)\n\t}\n}\n\nfunc Test_NewConfiguredServicesNil(t *testing.T) {\n\tt.Parallel()\n\n\trequire.PanicsWithError(t, \"fiber: service at index 0 is nil\", func() {\n\t\tNew(Config{\n\t\t\tServices: []Service{nil},\n\t\t})\n\t})\n}\n\nfunc Test_ServiceContextProviders(t *testing.T) {\n\tt.Run(\"no-provider\", func(t *testing.T) {\n\t\tapp := &App{\n\t\t\tconfigured: Config{},\n\t\t\tstate:      newState(),\n\t\t}\n\t\trequire.Equal(t, context.Background(), app.servicesStartupCtx())\n\t\trequire.Equal(t, context.Background(), app.servicesShutdownCtx())\n\t})\n\n\tt.Run(\"with-provider\", func(t *testing.T) {\n\t\tctx := context.TODO()\n\t\tapp := &App{\n\t\t\tconfigured: Config{\n\t\t\t\tServicesStartupContextProvider: func() context.Context {\n\t\t\t\t\treturn ctx\n\t\t\t\t},\n\t\t\t\tServicesShutdownContextProvider: func() context.Context {\n\t\t\t\t\treturn ctx\n\t\t\t\t},\n\t\t\t},\n\t\t\tstate: newState(),\n\t\t}\n\n\t\trequire.Equal(t, ctx, app.servicesStartupCtx())\n\t\trequire.Equal(t, ctx, app.servicesShutdownCtx())\n\t})\n}\n\nfunc Benchmark_StartServices(b *testing.B) {\n\tbenchmarkFn := func(b *testing.B, services []Service) {\n\t\tb.Helper()\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: services,\n\t\t\t})\n\n\t\t\tctx := context.Background()\n\t\t\tif err := app.startServices(ctx); err != nil {\n\t\t\t\tb.Fatal(\"Expected no error but got\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tb.Run(\"no-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{})\n\t})\n\n\tb.Run(\"single-service\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t})\n\t})\n\n\tb.Run(\"multiple-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t\t&mockService{name: \"dep2\"},\n\t\t\t&mockService{name: \"dep3\"},\n\t\t})\n\t})\n\n\tb.Run(\"multiple-services-with-delays\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", startDelay: 1 * time.Millisecond},\n\t\t\t&mockService{name: \"dep2\", startDelay: 2 * time.Millisecond},\n\t\t\t&mockService{name: \"dep3\", startDelay: 3 * time.Millisecond},\n\t\t})\n\t})\n}\n\nfunc Benchmark_ShutdownServices(b *testing.B) {\n\tbenchmarkFn := func(b *testing.B, services []Service) {\n\t\tb.Helper()\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: services,\n\t\t\t})\n\n\t\t\tctx := context.Background()\n\t\t\tif err := app.shutdownServices(ctx); err != nil {\n\t\t\t\tb.Fatal(\"Expected no error but got\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tb.Run(\"no-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{})\n\t})\n\n\tb.Run(\"single-service\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t})\n\t})\n\n\tb.Run(\"multiple-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t\t&mockService{name: \"dep2\"},\n\t\t\t&mockService{name: \"dep3\"},\n\t\t})\n\t})\n\n\tb.Run(\"multiple-services-with-delays\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", terminateDelay: 1 * time.Millisecond},\n\t\t\t&mockService{name: \"dep2\", terminateDelay: 2 * time.Millisecond},\n\t\t\t&mockService{name: \"dep3\", terminateDelay: 3 * time.Millisecond},\n\t\t})\n\t})\n}\n\nfunc Benchmark_StartServices_withContextCancellation(b *testing.B) {\n\tbenchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {\n\t\tb.Helper()\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: services,\n\t\t\t})\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\terr := app.startServices(ctx)\n\t\t\t// We expect an error here due to the short timeout\n\t\t\tif err == nil && timeout < time.Second {\n\t\t\t\tb.Fatal(\"Expected error due to context cancellation but got none\")\n\t\t\t}\n\t\t\tcancel()\n\t\t}\n\t}\n\n\tb.Run(\"single-service/immediate-cancellation\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", startDelay: 100 * time.Millisecond},\n\t\t}, 10*time.Millisecond)\n\t})\n\n\tb.Run(\"multiple-services/immediate-cancellation\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", startDelay: 100 * time.Millisecond},\n\t\t\t&mockService{name: \"dep2\", startDelay: 200 * time.Millisecond},\n\t\t\t&mockService{name: \"dep3\", startDelay: 300 * time.Millisecond},\n\t\t}, 10*time.Millisecond)\n\t})\n\n\tb.Run(\"multiple-services/successful-completion\", func(b *testing.B) {\n\t\tconst timeout = 500 * time.Millisecond\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\", startDelay: 10 * time.Millisecond},\n\t\t\t\t\t&mockService{name: \"dep2\", startDelay: 20 * time.Millisecond},\n\t\t\t\t\t&mockService{name: \"dep3\", startDelay: 30 * time.Millisecond},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\terr := app.startServices(ctx)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(\"Expected no error but got\", err)\n\t\t\t}\n\t\t\tcancel()\n\t\t}\n\t})\n}\n\nfunc Benchmark_ShutdownServices_withContextCancellation(b *testing.B) {\n\tbenchmarkFn := func(b *testing.B, services []Service, timeout time.Duration) {\n\t\tb.Helper()\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: services,\n\t\t\t})\n\n\t\t\terr := app.startServices(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(\"Expected no error during startup but got\", err)\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\terr = app.shutdownServices(ctx)\n\t\t\t// We expect an error here due to the short timeout\n\t\t\tif err == nil && timeout < time.Second {\n\t\t\t\tb.Fatal(\"Expected error due to context cancellation but got none\")\n\t\t\t}\n\t\t\tcancel()\n\t\t}\n\t}\n\n\tb.Run(\"single-service/immediate-cancellation\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", terminateDelay: 100 * time.Millisecond},\n\t\t}, 10*time.Millisecond)\n\t})\n\n\tb.Run(\"multiple-services/immediate-cancellation\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\", terminateDelay: 100 * time.Millisecond},\n\t\t\t&mockService{name: \"dep2\", terminateDelay: 200 * time.Millisecond},\n\t\t\t&mockService{name: \"dep3\", terminateDelay: 300 * time.Millisecond},\n\t\t}, 10*time.Millisecond)\n\t})\n\n\tb.Run(\"multiple-services/successful-completion\", func(b *testing.B) {\n\t\tconst timeout = 500 * time.Millisecond\n\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: []Service{\n\t\t\t\t\t&mockService{name: \"dep1\", terminateDelay: 10 * time.Millisecond},\n\t\t\t\t\t&mockService{name: \"dep2\", terminateDelay: 20 * time.Millisecond},\n\t\t\t\t\t&mockService{name: \"dep3\", terminateDelay: 30 * time.Millisecond},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\terr := app.startServices(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(\"Expected no error but got\", err)\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\t\terr = app.shutdownServices(ctx)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(\"Expected no error but got\", err)\n\t\t\t}\n\t\t\tcancel()\n\t\t}\n\t})\n}\n\nfunc Benchmark_ServicesMemory(b *testing.B) {\n\tbenchmarkFn := func(b *testing.B, services []Service) {\n\t\tb.Helper()\n\n\t\tb.ReportAllocs()\n\n\t\tvar err error\n\t\tfor b.Loop() {\n\t\t\tapp := New(Config{\n\t\t\t\tServices: services,\n\t\t\t})\n\n\t\t\tctx := context.Background()\n\t\t\terr = app.startServices(ctx)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\terr = app.shutdownServices(ctx)\n\t\t}\n\t\trequire.NoError(b, err)\n\t}\n\n\tb.Run(\"no-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{})\n\t})\n\n\tb.Run(\"single-service\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t})\n\t})\n\n\tb.Run(\"multiple-services\", func(b *testing.B) {\n\t\tbenchmarkFn(b, []Service{\n\t\t\t&mockService{name: \"dep1\"},\n\t\t\t&mockService{name: \"dep2\"},\n\t\t\t&mockService{name: \"dep3\"},\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "state.go",
    "content": "package fiber\n\nimport (\n\t\"encoding/hex\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n)\n\nconst servicesStatePrefix = \"gofiber-services-\"\n\nvar servicesStatePrefixHash string\n\nfunc init() {\n\tservicesStatePrefixHash = hex.EncodeToString([]byte(servicesStatePrefix + uuid.New().String()))\n}\n\n// State is a key-value store for Fiber's app in order to be used as a global storage for the app's dependencies.\n// It's a thread-safe implementation of a map[string]any, using sync.Map.\ntype State struct {\n\tdependencies  sync.Map\n\tservicePrefix string\n}\n\n// NewState creates a new instance of State.\nfunc newState() *State {\n\t// Initialize the services state prefix using a hashed random string\n\treturn &State{\n\t\tdependencies:  sync.Map{},\n\t\tservicePrefix: servicesStatePrefixHash,\n\t}\n}\n\n// Set sets a key-value pair in the State.\nfunc (s *State) Set(key string, value any) {\n\ts.dependencies.Store(key, value)\n}\n\n// Get retrieves a value from the State.\nfunc (s *State) Get(key string) (any, bool) {\n\treturn s.dependencies.Load(key)\n}\n\n// MustGet retrieves a value from the State and panics if the key is not found.\nfunc (s *State) MustGet(key string) any {\n\tif dep, ok := s.Get(key); ok {\n\t\treturn dep\n\t}\n\n\tpanic(\"state: dependency not found!\")\n}\n\n// Has checks if a key is present in the State.\n// It returns a boolean indicating if the key is present.\nfunc (s *State) Has(key string) bool {\n\t_, ok := s.Get(key)\n\treturn ok\n}\n\n// Delete removes a key-value pair from the State.\nfunc (s *State) Delete(key string) {\n\ts.dependencies.Delete(key)\n}\n\n// Reset resets the State by removing all keys.\nfunc (s *State) Reset() {\n\ts.dependencies.Clear()\n}\n\n// Keys returns a slice containing all keys present in the State.\nfunc (s *State) Keys() []string {\n\t// Pre-allocate with estimated capacity to reduce allocations\n\tkeys := make([]string, 0, 8)\n\ts.dependencies.Range(func(key, _ any) bool {\n\t\tkeyStr, ok := key.(string)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\n\t\tkeys = append(keys, keyStr)\n\t\treturn true\n\t})\n\n\treturn keys\n}\n\n// Len returns the number of keys in the State.\nfunc (s *State) Len() int {\n\tlength := 0\n\ts.dependencies.Range(func(_, _ any) bool {\n\t\tlength++\n\t\treturn true\n\t})\n\n\treturn length\n}\n\n// GetState retrieves a value from the State and casts it to the desired type.\n// It returns the casted value and a boolean indicating if the cast was successful.\nfunc GetState[T any](s *State, key string) (T, bool) {\n\tdep, ok := s.Get(key)\n\n\tif ok {\n\t\tdepT, okCast := dep.(T)\n\t\treturn depT, okCast\n\t}\n\n\tvar zeroVal T\n\treturn zeroVal, false\n}\n\n// MustGetState retrieves a value from the State and casts it to the desired type.\n// It panics if the key is not found or if the type assertion fails.\nfunc MustGetState[T any](s *State, key string) T {\n\tdep, ok := GetState[T](s, key)\n\tif !ok {\n\t\tpanic(\"state: dependency not found!\")\n\t}\n\n\treturn dep\n}\n\n// GetStateWithDefault retrieves a value from the State,\n// casting it to the desired type. If the key is not present,\n// it returns the provided default value.\nfunc GetStateWithDefault[T any](s *State, key string, defaultVal T) T {\n\tdep, ok := GetState[T](s, key)\n\tif !ok {\n\t\treturn defaultVal\n\t}\n\n\treturn dep\n}\n\n// GetString retrieves a string value from the State.\n// It returns the string and a boolean indicating successful type assertion.\nfunc (s *State) GetString(key string) (string, bool) {\n\treturn GetState[string](s, key)\n}\n\n// GetInt retrieves an integer value from the State.\n// It returns the int and a boolean indicating successful type assertion.\nfunc (s *State) GetInt(key string) (int, bool) {\n\treturn GetState[int](s, key)\n}\n\n// GetBool retrieves a boolean value from the State.\n// It returns the bool and a boolean indicating successful type assertion.\nfunc (s *State) GetBool(key string) (value, ok bool) { //nolint:nonamedreturns // Better idea to use named returns here\n\treturn GetState[bool](s, key)\n}\n\n// GetFloat64 retrieves a float64 value from the State.\n// It returns the float64 and a boolean indicating successful type assertion.\nfunc (s *State) GetFloat64(key string) (float64, bool) {\n\treturn GetState[float64](s, key)\n}\n\n// GetUint retrieves a uint value from the State.\n// It returns the uint and a boolean indicating successful type assertion.\nfunc (s *State) GetUint(key string) (uint, bool) {\n\treturn GetState[uint](s, key)\n}\n\n// GetInt8 retrieves an int8 value from the State.\n// It returns the int8 and a boolean indicating successful type assertion.\nfunc (s *State) GetInt8(key string) (int8, bool) {\n\treturn GetState[int8](s, key)\n}\n\n// GetInt16 retrieves an int16 value from the State.\n// It returns the int16 and a boolean indicating successful type assertion.\nfunc (s *State) GetInt16(key string) (int16, bool) {\n\treturn GetState[int16](s, key)\n}\n\n// GetInt32 retrieves an int32 value from the State.\n// It returns the int32 and a boolean indicating successful type assertion.\nfunc (s *State) GetInt32(key string) (int32, bool) {\n\treturn GetState[int32](s, key)\n}\n\n// GetInt64 retrieves an int64 value from the State.\n// It returns the int64 and a boolean indicating successful type assertion.\nfunc (s *State) GetInt64(key string) (int64, bool) {\n\treturn GetState[int64](s, key)\n}\n\n// GetUint8 retrieves a uint8 value from the State.\n// It returns the uint8 and a boolean indicating successful type assertion.\nfunc (s *State) GetUint8(key string) (uint8, bool) {\n\treturn GetState[uint8](s, key)\n}\n\n// GetUint16 retrieves a uint16 value from the State.\n// It returns the uint16 and a boolean indicating successful type assertion.\nfunc (s *State) GetUint16(key string) (uint16, bool) {\n\treturn GetState[uint16](s, key)\n}\n\n// GetUint32 retrieves a uint32 value from the State.\n// It returns the uint32 and a boolean indicating successful type assertion.\nfunc (s *State) GetUint32(key string) (uint32, bool) {\n\treturn GetState[uint32](s, key)\n}\n\n// GetUint64 retrieves a uint64 value from the State.\n// It returns the uint64 and a boolean indicating successful type assertion.\nfunc (s *State) GetUint64(key string) (uint64, bool) {\n\treturn GetState[uint64](s, key)\n}\n\n// GetUintptr retrieves a uintptr value from the State.\n// It returns the uintptr and a boolean indicating successful type assertion.\nfunc (s *State) GetUintptr(key string) (uintptr, bool) {\n\treturn GetState[uintptr](s, key)\n}\n\n// GetFloat32 retrieves a float32 value from the State.\n// It returns the float32 and a boolean indicating successful type assertion.\nfunc (s *State) GetFloat32(key string) (float32, bool) {\n\treturn GetState[float32](s, key)\n}\n\n// GetComplex64 retrieves a complex64 value from the State.\n// It returns the complex64 and a boolean indicating successful type assertion.\nfunc (s *State) GetComplex64(key string) (complex64, bool) {\n\treturn GetState[complex64](s, key)\n}\n\n// GetComplex128 retrieves a complex128 value from the State.\n// It returns the complex128 and a boolean indicating successful type assertion.\nfunc (s *State) GetComplex128(key string) (complex128, bool) {\n\treturn GetState[complex128](s, key)\n}\n\n// serviceKey returns a key for a service in the State.\n// A key is composed of the State's servicePrefix (hashed) and the hash of the service string.\n// This way we can avoid collisions and have a unique key for each service.\nfunc (s *State) serviceKey(key string) string {\n\t// hash the service string to avoid collisions\n\treturn s.servicePrefix + hex.EncodeToString([]byte(key))\n}\n\n// setService sets a service in the State.\nfunc (s *State) setService(srv Service) {\n\t// Always prepend the service key with the servicesStateKey to avoid collisions\n\ts.Set(s.serviceKey(srv.String()), srv)\n}\n\n// Delete removes a key-value pair from the State.\nfunc (s *State) deleteService(srv Service) {\n\ts.Delete(s.serviceKey(srv.String()))\n}\n\n// serviceKeys returns a slice containing all keys present for services in the application's State.\nfunc (s *State) serviceKeys() []string {\n\t// Pre-allocate with estimated capacity to reduce allocations\n\tkeys := make([]string, 0, 8)\n\ts.dependencies.Range(func(key, _ any) bool {\n\t\tkeyStr, ok := key.(string)\n\t\tif !ok {\n\t\t\treturn true\n\t\t}\n\n\t\tif !strings.HasPrefix(keyStr, s.servicePrefix) {\n\t\t\treturn true // Continue iterating if key doesn't have service prefix\n\t\t}\n\n\t\tkeys = append(keys, keyStr)\n\t\treturn true\n\t})\n\n\treturn keys\n}\n\n// Services returns a map containing all services present in the State.\n// The key is the hash of the service String() value and the value is the service itself.\nfunc (s *State) Services() map[string]Service {\n\tkeys := s.serviceKeys()\n\tservices := make(map[string]Service, len(keys))\n\n\tfor _, key := range keys {\n\t\tservices[key] = MustGetState[Service](s, key)\n\t}\n\n\treturn services\n}\n\n// ServicesLen returns the number of keys for services in the State.\nfunc (s *State) ServicesLen() int {\n\tlength := 0\n\ts.dependencies.Range(func(key, _ any) bool {\n\t\tif str, ok := key.(string); ok && strings.HasPrefix(str, s.servicePrefix) {\n\t\t\tlength++\n\t\t}\n\t\treturn true\n\t})\n\n\treturn length\n}\n\n// GetService returns a service present in the application's State.\nfunc GetService[T Service](s *State, key string) (T, bool) {\n\tsrv, ok := GetState[T](s, s.serviceKey(key))\n\treturn srv, ok\n}\n\n// MustGetService returns a service present in the application's State.\n// It panics if the service is not found.\nfunc MustGetService[T Service](s *State, key string) T {\n\tsrv, ok := GetService[T](s, key)\n\tif !ok {\n\t\tpanic(\"state: service not found!\")\n\t}\n\n\treturn srv\n}\n"
  },
  {
    "path": "state_test.go",
    "content": "package fiber\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestState_SetAndGet_WithApp(t *testing.T) {\n\tt.Parallel()\n\t// Create app\n\tapp := New()\n\n\t// test setting and getting a value\n\tapp.State().Set(\"foo\", \"bar\")\n\tval, ok := app.State().Get(\"foo\")\n\trequire.True(t, ok)\n\trequire.Equal(t, \"bar\", val)\n\n\t// test key not found\n\t_, ok = app.State().Get(\"unknown\")\n\trequire.False(t, ok)\n}\n\nfunc TestState_SetAndGet(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\t// test setting and getting a value\n\tst.Set(\"foo\", \"bar\")\n\tval, ok := st.Get(\"foo\")\n\trequire.True(t, ok)\n\trequire.Equal(t, \"bar\", val)\n\n\t// test key not found\n\t_, ok = st.Get(\"unknown\")\n\trequire.False(t, ok)\n}\n\nfunc TestState_GetString(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"str\", \"hello\")\n\ts, ok := st.GetString(\"str\")\n\trequire.True(t, ok)\n\trequire.Equal(t, \"hello\", s)\n\n\t// wrong type should return false\n\tst.Set(\"num\", 123)\n\ts, ok = st.GetString(\"num\")\n\trequire.False(t, ok)\n\trequire.Empty(t, s)\n\n\t// missing key should return false\n\ts, ok = st.GetString(\"missing\")\n\trequire.False(t, ok)\n\trequire.Empty(t, s)\n}\n\nfunc TestState_GetInt(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"num\", 456)\n\ti, ok := st.GetInt(\"num\")\n\trequire.True(t, ok)\n\trequire.Equal(t, 456, i)\n\n\t// wrong type should return zero value\n\tst.Set(\"str\", \"abc\")\n\ti, ok = st.GetInt(\"str\")\n\trequire.False(t, ok)\n\trequire.Equal(t, 0, i)\n\n\t// missing key should return zero value\n\ti, ok = st.GetInt(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, 0, i)\n}\n\nfunc TestState_GetBool(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"flag\", true)\n\tb, ok := st.GetBool(\"flag\")\n\trequire.True(t, ok)\n\trequire.True(t, b)\n\n\t// wrong type\n\tst.Set(\"num\", 1)\n\tb, ok = st.GetBool(\"num\")\n\trequire.False(t, ok)\n\trequire.False(t, b)\n\n\t// missing key should return false\n\tb, ok = st.GetBool(\"missing\")\n\trequire.False(t, ok)\n\trequire.False(t, b)\n}\n\nfunc TestState_GetFloat64(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"pi\", 3.14)\n\tf, ok := st.GetFloat64(\"pi\")\n\trequire.True(t, ok)\n\trequire.InDelta(t, 3.14, f, 0.0001)\n\n\t// wrong type should return zero value\n\tst.Set(\"int\", 10)\n\tf, ok = st.GetFloat64(\"int\")\n\trequire.False(t, ok)\n\trequire.InDelta(t, 0.0, f, 0.0001)\n\n\t// missing key should return zero value\n\tf, ok = st.GetFloat64(\"missing\")\n\trequire.False(t, ok)\n\trequire.InDelta(t, 0.0, f, 0.0001)\n}\n\nfunc TestState_GetUint(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"uint\", uint(100))\n\tu, ok := st.GetUint(\"uint\")\n\trequire.True(t, ok)\n\trequire.Equal(t, uint(100), u)\n\n\tst.Set(\"wrong\", \"not uint\")\n\tu, ok = st.GetUint(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint(0), u)\n\n\tu, ok = st.GetUint(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint(0), u)\n}\n\nfunc TestState_GetInt8(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"int8\", int8(10))\n\ti, ok := st.GetInt8(\"int8\")\n\trequire.True(t, ok)\n\trequire.Equal(t, int8(10), i)\n\n\tst.Set(\"wrong\", \"not int8\")\n\ti, ok = st.GetInt8(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int8(0), i)\n\n\ti, ok = st.GetInt8(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int8(0), i)\n}\n\nfunc TestState_GetInt16(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"int16\", int16(200))\n\ti, ok := st.GetInt16(\"int16\")\n\trequire.True(t, ok)\n\trequire.Equal(t, int16(200), i)\n\n\tst.Set(\"wrong\", \"not int16\")\n\ti, ok = st.GetInt16(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int16(0), i)\n\n\ti, ok = st.GetInt16(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int16(0), i)\n}\n\nfunc TestState_GetInt32(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"int32\", int32(3000))\n\ti, ok := st.GetInt32(\"int32\")\n\trequire.True(t, ok)\n\trequire.Equal(t, int32(3000), i)\n\n\tst.Set(\"wrong\", \"not int32\")\n\ti, ok = st.GetInt32(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int32(0), i)\n\n\ti, ok = st.GetInt32(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int32(0), i)\n}\n\nfunc TestState_GetInt64(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"int64\", int64(4000))\n\ti, ok := st.GetInt64(\"int64\")\n\trequire.True(t, ok)\n\trequire.Equal(t, int64(4000), i)\n\n\tst.Set(\"wrong\", \"not int64\")\n\ti, ok = st.GetInt64(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int64(0), i)\n\n\ti, ok = st.GetInt64(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, int64(0), i)\n}\n\nfunc TestState_GetUint8(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"uint8\", uint8(20))\n\tu, ok := st.GetUint8(\"uint8\")\n\trequire.True(t, ok)\n\trequire.Equal(t, uint8(20), u)\n\n\tst.Set(\"wrong\", \"not uint8\")\n\tu, ok = st.GetUint8(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint8(0), u)\n\n\tu, ok = st.GetUint8(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint8(0), u)\n}\n\nfunc TestState_GetUint16(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"uint16\", uint16(300))\n\tu, ok := st.GetUint16(\"uint16\")\n\trequire.True(t, ok)\n\trequire.Equal(t, uint16(300), u)\n\n\tst.Set(\"wrong\", \"not uint16\")\n\tu, ok = st.GetUint16(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint16(0), u)\n\n\tu, ok = st.GetUint16(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint16(0), u)\n}\n\nfunc TestState_GetUint32(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"uint32\", uint32(400000))\n\tu, ok := st.GetUint32(\"uint32\")\n\trequire.True(t, ok)\n\trequire.Equal(t, uint32(400000), u)\n\n\tst.Set(\"wrong\", \"not uint32\")\n\tu, ok = st.GetUint32(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint32(0), u)\n\n\tu, ok = st.GetUint32(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint32(0), u)\n}\n\nfunc TestState_GetUint64(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"uint64\", uint64(5000000))\n\tu, ok := st.GetUint64(\"uint64\")\n\trequire.True(t, ok)\n\trequire.Equal(t, uint64(5000000), u)\n\n\tst.Set(\"wrong\", \"not uint64\")\n\tu, ok = st.GetUint64(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint64(0), u)\n\n\tu, ok = st.GetUint64(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uint64(0), u)\n}\n\nfunc TestState_GetUintptr(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tvar ptr uintptr = 12345\n\tst.Set(\"uintptr\", ptr)\n\tu, ok := st.GetUintptr(\"uintptr\")\n\trequire.True(t, ok)\n\trequire.Equal(t, ptr, u)\n\n\tst.Set(\"wrong\", \"not uintptr\")\n\tu, ok = st.GetUintptr(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uintptr(0), u)\n\n\tu, ok = st.GetUintptr(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, uintptr(0), u)\n}\n\nfunc TestState_GetFloat32(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"float32\", float32(3.14))\n\tf, ok := st.GetFloat32(\"float32\")\n\trequire.True(t, ok)\n\trequire.InDelta(t, float32(3.14), f, 0.0001)\n\n\tst.Set(\"wrong\", \"not float32\")\n\tf, ok = st.GetFloat32(\"wrong\")\n\trequire.False(t, ok)\n\trequire.InDelta(t, float32(0), f, 0.0001)\n\n\tf, ok = st.GetFloat32(\"missing\")\n\trequire.False(t, ok)\n\trequire.InDelta(t, float32(0), f, 0.0001)\n}\n\nfunc TestState_GetComplex64(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tvar c complex64 = complex(2, 3)\n\tst.Set(\"complex64\", c)\n\tcRes, ok := st.GetComplex64(\"complex64\")\n\trequire.True(t, ok)\n\trequire.Equal(t, c, cRes)\n\n\tst.Set(\"wrong\", \"not complex64\")\n\tcRes, ok = st.GetComplex64(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, complex64(0), cRes)\n\n\tcRes, ok = st.GetComplex64(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, complex64(0), cRes)\n}\n\nfunc TestState_GetComplex128(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tc := complex(4, 5)\n\tst.Set(\"complex128\", c)\n\tcRes, ok := st.GetComplex128(\"complex128\")\n\trequire.True(t, ok)\n\trequire.Equal(t, c, cRes)\n\n\tst.Set(\"wrong\", \"not complex128\")\n\tcRes, ok = st.GetComplex128(\"wrong\")\n\trequire.False(t, ok)\n\trequire.Equal(t, complex128(0), cRes)\n\n\tcRes, ok = st.GetComplex128(\"missing\")\n\trequire.False(t, ok)\n\trequire.Equal(t, complex128(0), cRes)\n}\n\nfunc TestState_MustGet(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"exists\", \"value\")\n\tval := st.MustGet(\"exists\")\n\trequire.Equal(t, \"value\", val)\n\n\t// must-get on missing key should panic\n\trequire.Panics(t, func() {\n\t\t_ = st.MustGet(\"missing\")\n\t})\n}\n\nfunc TestState_Has(t *testing.T) {\n\tt.Parallel()\n\n\tst := newState()\n\n\tst.Set(\"key\", \"value\")\n\trequire.True(t, st.Has(\"key\"))\n}\n\nfunc TestState_Delete(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"key\", \"value\")\n\tst.Delete(\"key\")\n\t_, ok := st.Get(\"key\")\n\trequire.False(t, ok)\n}\n\nfunc TestState_Reset(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"a\", 1)\n\tst.Set(\"b\", 2)\n\tst.Reset()\n\trequire.Equal(t, 0, st.Len())\n\trequire.Empty(t, st.Keys())\n}\n\nfunc TestState_Keys(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tkeys := []string{\"one\", \"two\", \"three\"}\n\tfor _, k := range keys {\n\t\tst.Set(k, k)\n\t}\n\n\treturnedKeys := st.Keys()\n\trequire.ElementsMatch(t, keys, returnedKeys)\n}\n\nfunc TestState_Keys_SkipsNonStringKeys(t *testing.T) {\n\tt.Parallel()\n\n\tst := newState()\n\tst.Set(\"one\", \"one\")\n\tst.Set(\"two\", \"two\")\n\tst.dependencies.Store(42, \"value\")\n\tst.dependencies.Store(struct{}{}, \"value\")\n\n\treturnedKeys := st.Keys()\n\trequire.ElementsMatch(t, []string{\"one\", \"two\"}, returnedKeys)\n}\n\nfunc TestState_Keys_SkipsNonStringKeys_WithMixedOrder(t *testing.T) {\n\tt.Parallel()\n\n\tst := newState()\n\tst.Set(\"before\", \"value\")\n\tst.dependencies.Store(42, \"value\")\n\tst.Set(\"after\", \"value\")\n\n\treturnedKeys := st.Keys()\n\trequire.ElementsMatch(t, []string{\"before\", \"after\"}, returnedKeys)\n}\n\nfunc TestState_Len(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\trequire.Equal(t, 0, st.Len())\n\n\tst.Set(\"a\", \"a\")\n\trequire.Equal(t, 1, st.Len())\n\n\tst.Set(\"b\", \"b\")\n\trequire.Equal(t, 2, st.Len())\n\n\tst.Delete(\"a\")\n\trequire.Equal(t, 1, st.Len())\n}\n\ntype testCase[T any] struct {\n\tvalue    any\n\texpected T\n\tname     string\n\tkey      string\n\tok       bool\n}\n\nfunc runGenericTest[T any](t *testing.T, getter func(*State, string) (T, bool), tests []testCase[T]) {\n\tt.Helper()\n\n\tst := newState()\n\tfor _, tc := range tests {\n\t\tst.Set(tc.key, tc.value)\n\t\tgot, ok := getter(st, tc.key)\n\t\trequire.Equal(t, tc.ok, ok, tc.name)\n\t\trequire.Equal(t, tc.expected, got, tc.name)\n\t}\n}\n\nfunc TestState_GetGeneric(t *testing.T) {\n\tt.Parallel()\n\n\trunGenericTest(t, GetState[int], []testCase[int]{\n\t\t{name: \"int correct conversion\", key: \"num\", value: 42, expected: 42, ok: true},\n\t\t{name: \"int wrong conversion from string\", key: \"str\", value: \"abc\", expected: 0, ok: false},\n\t})\n\n\trunGenericTest(t, GetState[string], []testCase[string]{\n\t\t{name: \"string correct conversion\", key: \"strVal\", value: \"hello\", expected: \"hello\", ok: true},\n\t\t{name: \"string wrong conversion from int\", key: \"intVal\", value: 100, expected: \"\", ok: false},\n\t})\n\n\trunGenericTest(t, GetState[bool], []testCase[bool]{\n\t\t{name: \"bool correct conversion\", key: \"flag\", value: true, expected: true, ok: true},\n\t\t{name: \"bool wrong conversion from int\", key: \"intFlag\", value: 1, expected: false, ok: false},\n\t})\n\n\trunGenericTest(t, GetState[float64], []testCase[float64]{\n\t\t{name: \"float64 correct conversion\", key: \"pi\", value: 3.14, expected: 3.14, ok: true},\n\t\t{name: \"float64 wrong conversion from int\", key: \"intVal\", value: 10, expected: 0.0, ok: false},\n\t})\n}\n\nfunc Test_MustGetStateGeneric(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"flag\", true)\n\tflag := MustGetState[bool](st, \"flag\")\n\trequire.True(t, flag)\n\n\t// mismatched type should panic\n\trequire.Panics(t, func() {\n\t\t_ = MustGetState[string](st, \"flag\")\n\t})\n\n\t// missing key should also panic\n\trequire.Panics(t, func() {\n\t\t_ = MustGetState[string](st, \"missing\")\n\t})\n}\n\nfunc Test_GetStateWithDefault(t *testing.T) {\n\tt.Parallel()\n\tst := newState()\n\n\tst.Set(\"flag\", true)\n\tflag := GetStateWithDefault(st, \"flag\", false)\n\trequire.True(t, flag)\n\n\t// mismatched type should return the default value\n\tstr := GetStateWithDefault(st, \"flag\", \"default\")\n\trequire.Equal(t, \"default\", str)\n\n\t// missing key should return the default value\n\tflag = GetStateWithDefault(st, \"missing\", false)\n\trequire.False(t, flag)\n}\n\nfunc TestState_Service(t *testing.T) {\n\tt.Parallel()\n\n\tsrv1 := &mockService{name: \"test1\"}\n\t// service 2 is using a very subtle name to check it is not picked up\n\tsrv2 := &mockService{name: \"test1 \"}\n\n\tt.Run(\"set/get/ok\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tst := newState()\n\t\tst.setService(srv1)\n\n\t\tgot, ok := st.Get(st.serviceKey(srv1.String()))\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, srv1, got)\n\t})\n\n\tt.Run(\"set/get/ko\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tst := newState()\n\t\tst.setService(srv1)\n\n\t\tkoSrv := &mockService{name: \"ko\"}\n\n\t\tgot, ok := st.Get(st.serviceKey(koSrv.String()))\n\t\trequire.False(t, ok)\n\t\trequire.Nil(t, got)\n\t})\n\n\tt.Run(\"len\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"empty\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\trequire.Equal(t, 0, st.Len())\n\t\t\trequire.Empty(t, st.serviceKeys())\n\t\t})\n\n\t\tt.Run(\"with-services\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\n\t\t\trequire.Equal(t, 2, st.Len())\n\t\t\trequire.Equal(t, 2, st.ServicesLen())\n\t\t})\n\n\t\tt.Run(\"with-services/with-keys\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\t\t\tst.Set(\"key1\", \"value1\")\n\t\t\tst.Set(\"key2\", \"value2\")\n\n\t\t\tservicesLen := st.ServicesLen()\n\t\t\trequire.Equal(t, 4, st.Len())\n\t\t\trequire.Equal(t, 2, servicesLen)\n\t\t})\n\t})\n\n\tt.Run(\"keys\", func(t *testing.T) {\n\t\tt.Run(\"empty\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\t// adding more keys to check they are not included\n\t\t\tst.Set(\"key1\", \"value1\")\n\t\t\tst.Set(\"key2\", \"value2\")\n\n\t\t\trequire.Empty(t, st.serviceKeys())\n\t\t})\n\n\t\tt.Run(\"with-services\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\n\t\t\tkeys := st.serviceKeys()\n\t\t\trequire.Len(t, keys, 2)\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv1.String()))\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv2.String()))\n\t\t})\n\n\t\tt.Run(\"with-services/with-keys\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\t\t\tst.Set(\"key1\", \"value1\")\n\t\t\tst.Set(\"key2\", \"value2\")\n\n\t\t\tkeys := st.serviceKeys()\n\t\t\trequire.Len(t, keys, 2)\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv1.String()))\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv2.String()))\n\t\t\trequire.NotContains(t, keys, \"key1\")\n\t\t\trequire.NotContains(t, keys, \"key2\")\n\t\t})\n\n\t\tt.Run(\"with-non-string-key\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\t\t\tst.dependencies.Store(42, \"value\")\n\t\t\tst.dependencies.Store(struct{}{}, \"value\")\n\n\t\t\tkeys := st.serviceKeys()\n\t\t\trequire.Len(t, keys, 2)\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv1.String()))\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv2.String()))\n\t\t})\n\n\t\tt.Run(\"with-non-service-keys\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\t\t\tst.setService(srv2)\n\t\t\tst.Set(\"other\", \"value\")\n\t\t\tst.dependencies.Store(42, \"value\")\n\n\t\t\tkeys := st.serviceKeys()\n\t\t\trequire.Len(t, keys, 2)\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv1.String()))\n\t\t\trequire.Contains(t, keys, st.serviceKey(srv2.String()))\n\t\t})\n\t})\n\n\tt.Run(\"delete\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"ok\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\n\t\t\tst.setService(srv1)\n\t\t\tst.deleteService(srv1)\n\n\t\t\t_, ok := st.Get(st.serviceKey(srv1.String()))\n\t\t\trequire.False(t, ok)\n\t\t})\n\n\t\tt.Run(\"missing\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tst := newState()\n\t\t\tst.setService(srv1)\n\n\t\t\tst.deleteService(srv2)\n\n\t\t\t_, ok := st.Get(st.serviceKey(srv1.String()))\n\t\t\trequire.True(t, ok)\n\n\t\t\t_, ok = st.Get(st.serviceKey(srv2.String()))\n\t\t\trequire.False(t, ok)\n\t\t})\n\t})\n}\n\nfunc TestState_GetService(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"ok\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsrv1 := &mockService{name: \"test1\"}\n\n\t\tst := newState()\n\t\tst.setService(srv1)\n\n\t\tgot, ok := GetService[*mockService](st, srv1.String())\n\t\trequire.True(t, ok)\n\t\trequire.Equal(t, srv1, got)\n\t})\n\n\tt.Run(\"ko\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsrv1 := &mockService{name: \"test1\"}\n\n\t\tst := newState()\n\n\t\tgot, ok := GetService[*mockService](st, srv1.String())\n\t\trequire.False(t, ok)\n\t\trequire.Nil(t, got)\n\t})\n}\n\nfunc TestState_MustGetService(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"ok\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsrv1 := &mockService{name: \"test1\"}\n\n\t\tst := newState()\n\t\tst.setService(srv1)\n\n\t\tgot := MustGetService[*mockService](st, srv1.String())\n\t\trequire.Equal(t, srv1, got)\n\t})\n\n\tt.Run(\"panics\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsrv1 := &mockService{name: \"test1\"}\n\n\t\tst := newState()\n\n\t\trequire.Panics(t, func() {\n\t\t\t_ = MustGetService[*mockService](st, srv1.String())\n\t\t})\n\t})\n}\n\nfunc BenchmarkState_Set(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n}\n\nfunc BenchmarkState_Get(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.Get(key)\n\t}\n}\n\nfunc BenchmarkState_GetString(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, strconv.Itoa(i))\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetString(key)\n\t}\n}\n\nfunc BenchmarkState_GetInt(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetInt(key)\n\t}\n}\n\nfunc BenchmarkState_GetBool(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i%2 == 0)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetBool(key)\n\t}\n}\n\nfunc BenchmarkState_GetFloat64(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, float64(i))\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetFloat64(key)\n\t}\n}\n\nfunc BenchmarkState_MustGet(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.MustGet(key)\n\t}\n}\n\nfunc BenchmarkState_GetStateGeneric(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tGetState[int](st, key)\n\t}\n}\n\nfunc BenchmarkState_MustGetStateGeneric(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tMustGetState[int](st, key)\n\t}\n}\n\nfunc BenchmarkState_GetStateWithDefault(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tGetStateWithDefault(st, key, 0)\n\t}\n}\n\nfunc BenchmarkState_Has(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\t// prepopulate the state\n\tfor i := range n {\n\t\tst.Set(\"key\"+strconv.Itoa(i), i)\n\t}\n\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tst.Has(\"key\" + strconv.Itoa(i%n))\n\t}\n}\n\nfunc BenchmarkState_Delete(b *testing.B) {\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tst := newState()\n\t\tst.Set(\"a\", 1)\n\t\tst.Delete(\"a\")\n\t}\n}\n\nfunc BenchmarkState_Reset(b *testing.B) {\n\tb.ReportAllocs()\n\n\tfor b.Loop() {\n\t\tst := newState()\n\t\t// add a fixed number of keys before clearing\n\t\tfor j := range 100 {\n\t\t\tst.Set(\"key\"+strconv.Itoa(j), j)\n\t\t}\n\t\tst.Reset()\n\t}\n}\n\nfunc BenchmarkState_Keys(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\tfor i := range n {\n\t\tst.Set(\"key\"+strconv.Itoa(i), i)\n\t}\n\n\tfor b.Loop() {\n\t\t_ = st.Keys()\n\t}\n}\n\nfunc BenchmarkState_Len(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tn := 1000\n\tfor i := range n {\n\t\tst.Set(\"key\"+strconv.Itoa(i), i)\n\t}\n\n\tfor b.Loop() {\n\t\t_ = st.Len()\n\t}\n}\n\nfunc BenchmarkState_GetUint(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uint values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uint(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUint(key)\n\t}\n}\n\nfunc BenchmarkState_GetInt8(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with int8 values (using modulo to stay in range).\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, int8(i%128)) //nolint:gosec // G115 - modulo keeps value in range\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetInt8(key)\n\t}\n}\n\nfunc BenchmarkState_GetInt16(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with int16 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, int16(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetInt16(key)\n\t}\n}\n\nfunc BenchmarkState_GetInt32(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with int32 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, int32(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetInt32(key)\n\t}\n}\n\nfunc BenchmarkState_GetInt64(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with int64 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, int64(i))\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetInt64(key)\n\t}\n}\n\nfunc BenchmarkState_GetUint8(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uint8 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uint8(i%256)) //nolint:gosec // G115 - modulo keeps value in range\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUint8(key)\n\t}\n}\n\nfunc BenchmarkState_GetUint16(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uint16 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uint16(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUint16(key)\n\t}\n}\n\nfunc BenchmarkState_GetUint32(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uint32 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uint32(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUint32(key)\n\t}\n}\n\nfunc BenchmarkState_GetUint64(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uint64 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uint64(i)) //nolint:gosec // G115 - test values are small\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUint64(key)\n\t}\n}\n\nfunc BenchmarkState_GetUintptr(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with uintptr values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, uintptr(i))\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetUintptr(key)\n\t}\n}\n\nfunc BenchmarkState_GetFloat32(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with float32 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\tst.Set(key, float32(i))\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetFloat32(key)\n\t}\n}\n\nfunc BenchmarkState_GetComplex64(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with complex64 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\t// Create a complex64 value with both real and imaginary parts.\n\t\tst.Set(key, complex(float32(i), float32(i)))\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetComplex64(key)\n\t}\n}\n\nfunc BenchmarkState_GetComplex128(b *testing.B) {\n\tb.ReportAllocs()\n\tst := newState()\n\tn := 1000\n\t// Prepopulate the state with complex128 values.\n\tfor i := range n {\n\t\tkey := \"key\" + strconv.Itoa(i)\n\t\t// Create a complex128 value with both real and imaginary parts.\n\t\tst.Set(key, complex(float64(i), float64(i)))\n\t}\n\ti := 0\n\tfor b.Loop() {\n\t\ti++\n\t\tkey := \"key\" + strconv.Itoa(i%n)\n\t\tst.GetComplex128(key)\n\t}\n}\n\nfunc BenchmarkState_GetService(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tsrv := &mockService{name: \"benchService\"}\n\tst.setService(srv)\n\n\tfor b.Loop() {\n\t\t_, _ = GetService[*mockService](st, srv.String())\n\t}\n}\n\nfunc BenchmarkState_MustGetService(b *testing.B) {\n\tb.ReportAllocs()\n\n\tst := newState()\n\tsrv := &mockService{name: \"benchService\"}\n\tst.setService(srv)\n\n\tfor b.Loop() {\n\t\t_ = MustGetService[*mockService](st, srv.String())\n\t}\n}\n"
  },
  {
    "path": "storage_interface.go",
    "content": "package fiber\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// Storage interface for communicating with different database/key-value\n// providers\ntype Storage interface {\n\t// GetWithContext gets the value for the given key with a context.\n\t// `nil, nil` is returned when the key does not exist\n\tGetWithContext(ctx context.Context, key string) ([]byte, error)\n\n\t// Get gets the value for the given key.\n\t// `nil, nil` is returned when the key does not exist\n\tGet(key string) ([]byte, error)\n\n\t// SetWithContext stores the given value for the given key\n\t// with an expiration value, 0 means no expiration.\n\t// Empty key or value will be ignored without an error.\n\tSetWithContext(ctx context.Context, key string, val []byte, exp time.Duration) error\n\n\t// Set stores the given value for the given key along\n\t// with an expiration value, 0 means no expiration.\n\t// Empty key or value will be ignored without an error.\n\tSet(key string, val []byte, exp time.Duration) error\n\n\t// DeleteWithContext deletes the value for the given key with a context.\n\t// It returns no error if the storage does not contain the key,\n\tDeleteWithContext(ctx context.Context, key string) error\n\n\t// Delete deletes the value for the given key.\n\t// It returns no error if the storage does not contain the key,\n\tDelete(key string) error\n\n\t// ResetWithContext resets the storage and deletes all keys with a context.\n\tResetWithContext(ctx context.Context) error\n\n\t// Reset resets the storage and delete all keys.\n\tReset() error\n\n\t// Close closes the storage and will stop any running garbage\n\t// collectors and open connections.\n\tClose() error\n}\n"
  }
]